Skip to content

Commit

Permalink
Add audit model
Browse files Browse the repository at this point in the history
  • Loading branch information
goulvench committed Feb 14, 2025
1 parent 6705c4a commit 9f62867
Show file tree
Hide file tree
Showing 10 changed files with 458 additions and 8 deletions.
6 changes: 3 additions & 3 deletions app/controllers/sites_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ class SitesController < ApplicationController

# GET /sites
def index
@sites = Site.all
@pagy, @sites = pagy Site.includes(:audit)
end

# GET /sites/1
Expand All @@ -18,7 +18,7 @@ def edit; end

# POST /sites
def create
@site = Site.new(site_params)
@site = Site.find_or_create_by_url(site_params)
if @site.save
redirect_to @site, notice: t(".notice")
else
Expand All @@ -44,7 +44,7 @@ def destroy
private

def set_site
@site = params[:id].present? ? Site.friendly.find(params.expect(:id)) : Site.new
@site = params[:id].present? ? Site.friendly.find(params.expect(:id)) : Site.new_with_audit
end

def redirect_old_slugs
Expand Down
32 changes: 32 additions & 0 deletions app/models/audit.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class Audit < ApplicationRecord
MAX_ATTEMPTS = 3
MAX_RUNTIME = 1.hour.freeze

belongs_to :site, touch: true

validates :url, presence: true, url: true
normalizes :url, with: ->(url) { URI.parse(url).normalize.to_s }

enum :status, ["pending", "running", "passed", "retryable", "failed"].index_by(&:itself), validate: true, default: :pending

scope :sort_by_newest, -> { order(created_at: :desc) }
scope :sort_by_url, -> { order(Arel.sql("REGEXP_REPLACE(url, '^https?://(www\.)?', '') ASC")) }
scope :due, -> { pending.where("run_at <= now()") }
scope :past, -> { where(status: [:passed, :failed]) }
scope :scheduled, -> { where("run_at > now()") }
scope :retryable, -> { failed.where(attempts: ...MAX_ATTEMPTS) }
scope :to_run, -> { due.or(retryable) }
scope :clean, -> { passed.where(attempts: 0) }
scope :late, -> { pending.where("run_at <= ?", 1.hour.ago) }
scope :retried, -> { passed.where(attempts: 1..) }
scope :stalled, -> { running.where("run_at < ?", MAX_RUNTIME.ago) }
scope :crashed, -> { failed.where(attempts: MAX_ATTEMPTS) }

delegate :hostname, :path, to: :parsed_url

def run_at = super || Time.zone.now
def due? = pending? && run_at <= Time.zone.now
def runnable? = due? || retryable?
def parsed_url = @parsed_url ||= URI.parse(url).normalize
def url_without_scheme = @url_without_scheme ||= [hostname, path == "/" ? nil : path].compact.join(nil)
end
46 changes: 44 additions & 2 deletions app/models/site.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,49 @@
class Site < ApplicationRecord
extend FriendlyId

friendly_id :url, use: [:slugged, :history]
has_many :audits, dependent: :destroy
has_one_of_many :audit, -> { order("audits.created_at DESC") }, class_name: "Audit"

attribute :url, :string
friendly_id :url_without_scheme, use: [:slugged, :history]

delegate :url, :url_without_scheme, to: :audit

class << self
def find_or_create_by_url(attributes)
url = attributes.to_h.delete(:url)
attributes.to_h.delete(:name) if attributes[:name].blank?
# Ignore http/https duplicates when searching
normalized_url = [url, url.sub(/^https?/, url.start_with?("https") ? "http" : "https")]
joins(:audits).find_by(audits: { url: normalized_url })&.tap { it.update(attributes) } \
|| create_with_audit(url:, **attributes)
end

def create_with_audit(url: nil, **attributes)
new_with_audit(url:, **attributes).tap(&:save)
end

def new_with_audit(url: nil, **attributes)
new(attributes).tap { |site| site.audits.build(url:) }
end
end

def new(attributes = nil)
return super if attributes.nil?

attributes = attributes.to_h.symbolize_keys

if url = attributes.delete(:url)
self.class.find_or_create_by(url:, **attributes)
else
super
end
end

def url=(new_url)
audit = audits.build(url: new_url)
end

def to_title = url_without_scheme
def audit = super || audits.last || audits.build
def should_generate_new_friendly_id? = new_record? || (audit && slug != url_without_scheme.parameterize)
end
2 changes: 1 addition & 1 deletion app/views/sites/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<% t.with_body do %>
<% @sites.each do |site| %>
<tr>
<td><%= link_to site.name, site %></td>
<td><%= link_to site.url_without_scheme, site %></td>
<td>
<%= l site.updated_at, format: :compact %>
<br>
Expand Down
18 changes: 18 additions & 0 deletions db/migrate/20250213152147_create_audits.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class CreateAudits < ActiveRecord::Migration[8.0]
def change
create_table :audits do |t|
t.belongs_to :site, null: false, foreign_key: true
t.string :url, null: false
t.string :status, null: false
t.integer :attempts, null: false, default: 0
t.datetime :run_at, null: false, default: -> { "CURRENT_TIMESTAMP" }

t.timestamps

t.index :url
t.index [:status, :run_at], name: "index_audits_on_status_and_run_at"
t.index "REGEXP_REPLACE(url, '^https?://(www\.)?', '')", name: "index_audits_on_normalized_url"
t.index :attempts, where: "status = 'failed' AND attempts > 0", name: "index_audits_on_retryable"
end
end
end
19 changes: 18 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions spec/factories/audits.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FactoryBot.define do
factory :audit do
site { association :site, url:, strategy: :build }
url { "https://example.com" }

trait :passed do
status { "passed" }
end

trait :failed do
status { "failed" }
end

trait :pending do
status { "pending" }
end

trait :running do
status { "running" }
end
end
end
2 changes: 1 addition & 1 deletion spec/factories/sites.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
FactoryBot.define do
factory :site do
sequence(:slug) { |n| "www.example-#{n}.com" }
sequence(:url) { |n| "https://www.example-#{n}.com/" }
end
end
Loading

0 comments on commit 9f62867

Please sign in to comment.