From 0048c3310157170287d34fd56ed96c9c4bafbc8e Mon Sep 17 00:00:00 2001
From: Goulven Champenois
Date: Wed, 26 Feb 2025 15:29:53 +0100
Subject: [PATCH 01/12] Remove parenthesis in application dsfr_table helper for
readability
---
app/helpers/application_helper.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 5be70be..fbc9e8d 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -24,7 +24,7 @@ def head_title
end
def dsfr_table(caption:, size: :md, scroll: true, border: false, **html_attributes, &block)
- render(Dsfr::TableComponent.new(caption:, size:, scroll:, border:, html_attributes:), &block)
+ render Dsfr::TableComponent.new(caption:, size:, scroll:, border:, html_attributes:), &block
end
def root? = request.path == "/"
From 8cc6854a8e1743ae3c944dc4ac5e396cc44bb43d Mon Sep 17 00:00:00 2001
From: Goulven Champenois
Date: Wed, 26 Feb 2025 15:17:49 +0100
Subject: [PATCH 02/12] Add a Dsfr Sidemenu component
---
.../dsfr/sidemenu_component.html.erb | 20 ++++
app/components/dsfr/sidemenu_component.rb | 30 ++++++
.../dsfr/sidemenu_item_component.rb | 24 +++++
app/helpers/application_helper.rb | 6 ++
.../dsfr/sidemenu_component_spec.rb | 91 +++++++++++++++++++
.../dsfr/sidemenu_item_component_spec.rb | 81 +++++++++++++++++
6 files changed, 252 insertions(+)
create mode 100644 app/components/dsfr/sidemenu_component.html.erb
create mode 100644 app/components/dsfr/sidemenu_component.rb
create mode 100644 app/components/dsfr/sidemenu_item_component.rb
create mode 100644 spec/components/dsfr/sidemenu_component_spec.rb
create mode 100644 spec/components/dsfr/sidemenu_item_component_spec.rb
diff --git a/app/components/dsfr/sidemenu_component.html.erb b/app/components/dsfr/sidemenu_component.html.erb
new file mode 100644
index 0000000..0c030e2
--- /dev/null
+++ b/app/components/dsfr/sidemenu_component.html.erb
@@ -0,0 +1,20 @@
+
diff --git a/app/components/dsfr/sidemenu_component.rb b/app/components/dsfr/sidemenu_component.rb
new file mode 100644
index 0000000..00fd526
--- /dev/null
+++ b/app/components/dsfr/sidemenu_component.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Dsfr
+ class SidemenuComponent < ApplicationComponent
+ DEFAULT_BUTTON_TEXT = "Dans cette rubrique".freeze
+
+ renders_many :items, "Dsfr::SidemenuItemComponent"
+
+ attr_reader :title, :button, :sticky, :full_height, :right
+
+ def initialize(title:, button: DEFAULT_BUTTON_TEXT, sticky: false, full_height: false, right: false)
+ @title = title
+ @button = button
+ @full_height = full_height
+ @sticky = full_height || sticky
+ @right = right
+ end
+
+ def css_classes
+ token_list(
+ "fr-sidemenu",
+ "fr-sidemenu--sticky" => sticky,
+ "fr-sidemenu--sticky-full-height" => full_height,
+ "fr-sidemenu--right" => right
+ )
+ end
+
+ def render? = items.any?
+ end
+end
diff --git a/app/components/dsfr/sidemenu_item_component.rb b/app/components/dsfr/sidemenu_item_component.rb
new file mode 100644
index 0000000..3bb5047
--- /dev/null
+++ b/app/components/dsfr/sidemenu_item_component.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Dsfr
+ # This class is used by the Sidemenu component via renders_many
+ class SidemenuItemComponent < ApplicationComponent
+ attr_reader :href, :text, :active
+
+ def initialize(href:, text:, active: nil)
+ @href = href
+ @text = text
+ @active = active
+ end
+
+ def active
+ @active.nil? ? helpers.current_page?(href) : @active
+ end
+
+ def call
+ content_tag :li, class: token_list("fr-sidemenu__item", "fr-sidemenu__item--active" => active) do
+ content_tag :a, href:, class: "fr-sidemenu__link", "aria-current": active ? :page : nil do text end
+ end
+ end
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index fbc9e8d..dfef2cf 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -27,6 +27,12 @@ def dsfr_table(caption:, size: :md, scroll: true, border: false, **html_attribut
render Dsfr::TableComponent.new(caption:, size:, scroll:, border:, html_attributes:), &block
end
+ def dsfr_sidemenu(title:, button: nil, sticky: false, full_height: false, right: false, &block)
+ component = Dsfr::SidemenuComponent.new(title:, button:, sticky:, full_height:, right:)
+ yield(component) if block_given?
+ render component
+ end
+
def root? = request.path == "/"
def time_ago(datetime)
diff --git a/spec/components/dsfr/sidemenu_component_spec.rb b/spec/components/dsfr/sidemenu_component_spec.rb
new file mode 100644
index 0000000..e8c84bc
--- /dev/null
+++ b/spec/components/dsfr/sidemenu_component_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe Dsfr::SidemenuComponent, type: :component do
+ let(:title) { "Title" }
+ let(:component) { described_class.new(title:) }
+ let(:rendered_component) { render_inline(component) }
+
+ describe "rendering" do
+ context "with no items" do
+ it "renders nothing" do
+ expect(rendered_component.to_s).to be_empty
+ end
+ end
+
+ context "with items" do
+ before do
+ component.with_item(href: "/page1", text: "Page 1")
+ component.with_item(href: "/page2", text: "Page 2")
+ end
+
+ it "renders the component" do
+ expect(rendered_component.css(".fr-sidemenu")).to be_present
+ end
+
+ it "renders the title" do
+ expect(rendered_component.css("#fr-sidemenu-title").text).to eq(title)
+ end
+
+ it "renders the default button text" do
+ expect(rendered_component.css(".fr-sidemenu__btn").text.strip).to eq(Dsfr::SidemenuComponent::DEFAULT_BUTTON_TEXT)
+ end
+
+ it "renders the items" do
+ expect(rendered_component.css(".fr-sidemenu__item").count).to eq(2)
+ expect(rendered_component.css(".fr-sidemenu__link").first.text).to eq("Page 1")
+ expect(rendered_component.css(".fr-sidemenu__link").last.text).to eq("Page 2")
+ end
+ end
+ end
+
+ describe "CSS classes" do
+ before do
+ component.with_item(href: "/page", text: "Page")
+ end
+
+ context "with default options" do
+ it "renders with the base class" do
+ expect(rendered_component.css(".fr-sidemenu")).to be_present
+ expect(rendered_component.css(".fr-sidemenu--sticky")).not_to be_present
+ expect(rendered_component.css(".fr-sidemenu--sticky-full-height")).not_to be_present
+ expect(rendered_component.css(".fr-sidemenu--right")).not_to be_present
+ end
+ end
+
+ context "with sticky option" do
+ let(:component) { described_class.new(title: title, sticky: true) }
+
+ it "adds the sticky class" do
+ expect(rendered_component.css(".fr-sidemenu--sticky")).to be_present
+ end
+ end
+
+ context "with full_height option" do
+ let(:component) { described_class.new(title: title, full_height: true) }
+
+ it "adds both sticky and full-height classes" do
+ expect(rendered_component.css(".fr-sidemenu--sticky")).to be_present
+ expect(rendered_component.css(".fr-sidemenu--sticky-full-height")).to be_present
+ end
+ end
+
+ context "with right option" do
+ let(:component) { described_class.new(title: title, right: true) }
+
+ it "adds the right class" do
+ expect(rendered_component.css(".fr-sidemenu--right")).to be_present
+ end
+ end
+
+ context "with custom button text" do
+ let(:button_text) { "Custom Button" }
+ let(:component) { described_class.new(title: title, button: button_text) }
+
+ it "renders the custom button text" do
+ expect(rendered_component.css(".fr-sidemenu__btn").text.strip).to eq(button_text)
+ end
+ end
+ end
+end
diff --git a/spec/components/dsfr/sidemenu_item_component_spec.rb b/spec/components/dsfr/sidemenu_item_component_spec.rb
new file mode 100644
index 0000000..d34e4ca
--- /dev/null
+++ b/spec/components/dsfr/sidemenu_item_component_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+RSpec.describe Dsfr::SidemenuItemComponent, type: :component do
+ let(:href) { "/test-page" }
+ let(:text) { "Test Page" }
+ let(:active) { nil }
+ let(:component) { described_class.new(href:, text:, active:) }
+ let(:rendered_component) { render_inline(component) }
+
+ describe "rendering" do
+ it "renders a list item with the proper classes" do
+ expect(rendered_component.css("li.fr-sidemenu__item")).to be_present
+ end
+
+ it "renders a link with the proper attributes" do
+ link = rendered_component.css("a.fr-sidemenu__link").first
+ expect(link).to be_present
+ expect(link["href"]).to eq(href)
+ expect(link.text).to eq(text)
+ end
+ end
+
+ describe "active state" do
+ context "when active is explicitly set to true" do
+ let(:active) { true }
+
+ it "adds the active class to the list item" do
+ expect(rendered_component.css("li.fr-sidemenu__item--active")).to be_present
+ end
+
+ it "sets aria-current attribute on the link" do
+ expect(rendered_component.css("a[aria-current='page']")).to be_present
+ end
+ end
+
+ context "when active is explicitly set to false" do
+ let(:active) { false }
+
+ it "does not add the active class to the list item" do
+ expect(rendered_component.css("li.fr-sidemenu__item--active")).not_to be_present
+ end
+
+ it "does not set aria-current attribute on the link" do
+ expect(rendered_component.css("a[aria-current]")).not_to be_present
+ end
+ end
+
+ context "when active is not set" do
+ before do
+ helpers = instance_double(ActionView::Helpers::UrlHelper, current_page?: current_page)
+ allow(component).to receive(:helpers).and_return(helpers)
+ end
+
+ context "and the current page matches the href" do
+ let(:current_page) { true }
+
+ it "adds the active class to the list item" do
+ expect(rendered_component.css("li.fr-sidemenu__item--active")).to be_present
+ end
+
+ it "sets aria-current attribute on the link" do
+ expect(rendered_component.css("a[aria-current='page']")).to be_present
+ end
+ end
+
+ context "and the current page does not match the href" do
+ let(:current_page) { false }
+
+ it "does not add the active class to the list item" do
+ expect(rendered_component.css("li.fr-sidemenu__item--active")).not_to be_present
+ end
+
+ it "does not set aria-current attribute on the link" do
+ expect(rendered_component.css("a[aria-current]")).not_to be_present
+ end
+ end
+ end
+ end
+end
From 289a12d9ec17fe7ed8504a9452fdf9e1973ea285 Mon Sep 17 00:00:00 2001
From: Goulven Champenois
Date: Wed, 26 Feb 2025 11:11:41 +0100
Subject: [PATCH 03/12] Add audits_count to Site model
---
app/models/audit.rb | 2 +-
db/migrate/20250226100820_add_audits_count_to_site.rb | 11 +++++++++++
db/schema.rb | 3 ++-
3 files changed, 14 insertions(+), 2 deletions(-)
create mode 100644 db/migrate/20250226100820_add_audits_count_to_site.rb
diff --git a/app/models/audit.rb b/app/models/audit.rb
index e919644..523f13d 100644
--- a/app/models/audit.rb
+++ b/app/models/audit.rb
@@ -1,5 +1,5 @@
class Audit < ApplicationRecord
- belongs_to :site, touch: true
+ belongs_to :site, touch: true, counter_cache: true
Check.types.each do |name, klass|
has_one name, class_name: klass.name, dependent: :destroy
end
diff --git a/db/migrate/20250226100820_add_audits_count_to_site.rb b/db/migrate/20250226100820_add_audits_count_to_site.rb
new file mode 100644
index 0000000..f220bb1
--- /dev/null
+++ b/db/migrate/20250226100820_add_audits_count_to_site.rb
@@ -0,0 +1,11 @@
+class AddAuditsCountToSite < ActiveRecord::Migration[8.0]
+ def change
+ add_column :sites, :audits_count, :integer, null: false, default: 0
+ unless reverting?
+ # Set counter for all objects in one query, without instantiating all models
+ execute <<-SQL.squish
+ UPDATE sites SET audits_count = (SELECT count(1) FROM audits WHERE audits.site_id = sites.id)
+ SQL
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 2bd4b1c..f2701ff 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.0].define(version: 2025_02_17_085742) do
+ActiveRecord::Schema[8.0].define(version: 2025_02_26_100820) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
@@ -60,6 +60,7 @@
t.string "slug", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.integer "audits_count", default: 0, null: false
t.index ["slug"], name: "index_sites_on_slug", unique: true
end
From cc661d57fee25009265cac8549a94145ef1f51e9 Mon Sep 17 00:00:00 2001
From: Goulven Champenois
Date: Wed, 26 Feb 2025 15:18:04 +0100
Subject: [PATCH 04/12] Relax Rubocop rules
---
.rubocop.yml | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/.rubocop.yml b/.rubocop.yml
index 9f0026b..3780795 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -57,6 +57,15 @@ RSpec/MultipleExpectations:
RSpec/MultipleMemoizedHelpers:
AllowSubject: true
Max: 10
+RSpec/NestedGroups:
+ Max: 4
+RSpec/ContextWording:
+ Prefixes:
+ - when
+ - and
+ - with
+ - without
+ - for
Style/StringLiterals:
EnforcedStyle: "double_quotes"
From 56bddc31f4cc3d6a3dc23046f8171c667da840e6 Mon Sep 17 00:00:00 2001
From: Goulven Champenois
Date: Wed, 26 Feb 2025 15:29:11 +0100
Subject: [PATCH 05/12] Use audit scope in Site.has_one_of_many for readability
---
app/models/site.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/models/site.rb b/app/models/site.rb
index a1cabd3..4e158a2 100644
--- a/app/models/site.rb
+++ b/app/models/site.rb
@@ -2,7 +2,7 @@ class Site < ApplicationRecord
extend FriendlyId
has_many :audits, dependent: :destroy
- has_one_of_many :audit, -> { past.order("audits.created_at DESC") }, dependent: :destroy
+ has_one_of_many :audit, -> { past.sort_by_newest }, dependent: :destroy
friendly_id :url_without_scheme, use: [:slugged, :history]
From 877c88b44751f660a757e743c79c4e5528d49956 Mon Sep 17 00:00:00 2001
From: Goulven Champenois
Date: Wed, 26 Feb 2025 15:28:31 +0100
Subject: [PATCH 06/12] Allow displaying an arbitrary audit on site page
---
app/controllers/sites_controller.rb | 4 ++-
app/views/sites/index.html.erb | 2 ++
app/views/sites/show.html.erb | 48 +++++++++++++++++++----------
config/locales/fr.yml | 10 ++++++
4 files changed, 46 insertions(+), 18 deletions(-)
diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb
index 62e8197..9fe383a 100644
--- a/app/controllers/sites_controller.rb
+++ b/app/controllers/sites_controller.rb
@@ -8,7 +8,9 @@ def index
end
# GET /sites/1
- def show; end
+ def show
+ @audit = @site.audit
+ end
# GET /sites/new
def new; end
diff --git a/app/views/sites/index.html.erb b/app/views/sites/index.html.erb
index 048c3dc..5104b73 100644
--- a/app/views/sites/index.html.erb
+++ b/app/views/sites/index.html.erb
@@ -22,6 +22,8 @@
<%= l site.audit.checked_at, format: :compact %>
(<%= time_ago site.audit.checked_at %>)
+ <% else %>
+ <%= Audit.human("audit/status.pending") %>
<% end %>
diff --git a/app/views/sites/show.html.erb b/app/views/sites/show.html.erb
index 7e9cffc..6eb4ed4 100644
--- a/app/views/sites/show.html.erb
+++ b/app/views/sites/show.html.erb
@@ -1,20 +1,34 @@
-
- <%= Site.human :url %> :
- <%= link_to @site.url, @site.url, target: :_blank %>
-
-<% if @site.audit.checked_at %>
-
- <%= Site.human :last_audit_at %> :
- <%= l @site.audit.checked_at, format: :long %>
- (<%= time_ago @site.audit.checked_at %>).
-
-<% end %>
-<% @site.audit.all_checks.each do |check| %>
-
- <%= check.human_type %> :
- <%= badge check.to_badge %>
-
-<% end %>
+
+
+
+ <%= Site.human :url %> :
+ <%= link_to @site.url, @site.url, target: :_blank %>
+
+
+ <%= Audit.human :created_at %> :
+ <%= l @audit.created_at, format: :long %>
+ (<%= time_ago @audit.created_at %>).
+
+ <% if @audit.checked_at %>
+
+ <%= Audit.human :checked_at %> :
+ <%= l @audit.checked_at, format: :long %>
+ (<%= time_ago @audit.checked_at %>).
+
+ <% @audit.all_checks.each do |check| %>
+
+ <%= check.human_type %> :
+ <%= badge check.to_badge %>
+
+ <% end %>
+ <% else %>
+
+ <%= Audit.human(:status) %> :
+ <%= Audit.human("audit/status.pending") %>
+
+ <% end %>
+
+
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index 0f2be50..d6026f4 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -21,6 +21,16 @@ fr:
activerecord:
attributes:
+ audit:
+ created_at: Enregistré le
+ checked_at: Executé le
+ status: État
+ audit/status:
+ pending: Planifié
+ running: En cours
+ passed: Effectué
+ retryable: À retenter
+ failed: Échoué
check:
type: Type de contrôle
status: État
From 9bc3e7216cc91a625b5e8b7640f9e780fc94897d Mon Sep 17 00:00:00 2001
From: Goulven Champenois
Date: Wed, 26 Feb 2025 15:41:26 +0100
Subject: [PATCH 07/12] Allow viewing previous audits
---
app/controllers/audits_controller.rb | 26 ++++++++++++++++++++++++++
app/views/sites/show.html.erb | 9 ++++++++-
config/locales/fr.yml | 15 +++++++++++----
config/routes.rb | 4 +++-
4 files changed, 48 insertions(+), 6 deletions(-)
create mode 100644 app/controllers/audits_controller.rb
diff --git a/app/controllers/audits_controller.rb b/app/controllers/audits_controller.rb
new file mode 100644
index 0000000..2759e75
--- /dev/null
+++ b/app/controllers/audits_controller.rb
@@ -0,0 +1,26 @@
+class AuditsController < ApplicationController
+ before_action :set_site
+
+ # POST /sites/1/audits
+ def create
+ @audit = @site.audit!
+ if @audit.persisted?
+ redirect_to @site, notice: t(".notice")
+ else
+ render "sites/show", status: :unprocessable_entity
+ end
+ end
+
+ # GET /sites/1/audits/1
+ def show
+ @audit = @site.audits.find(params[:id])
+ @title = @site.to_title
+ render "sites/show"
+ end
+
+ private
+
+ def set_site
+ @site = Site.friendly.find(params[:site_id])
+ end
+end
diff --git a/app/views/sites/show.html.erb b/app/views/sites/show.html.erb
index 6eb4ed4..bc73788 100644
--- a/app/views/sites/show.html.erb
+++ b/app/views/sites/show.html.erb
@@ -28,11 +28,18 @@
<% end %>
+
+ <%= dsfr_sidemenu(button: Site.human(:audits), title: Site.human(:audit_history, total: @site.audits_count), right: true) do |sidemenu| %>
+ <% @site.audits.past.sort_by_newest.each do |audit| %>
+ <% sidemenu.with_item text: l(audit.created_at, format: :long).upcase_first, href: url_for([@site, audit]), active: @audit == audit %>
+ <% end %>
+ <% end %>
+
<%= button_to "Supprimer ce site", @site, method: :delete, class: "fr-btn fr-btn--tertiary", form: { data: { turbo_confirm: t("shared.confirm") } } %>
- <%= link_to "Revenir à la liste", { action: :index }, class: "fr-btn fr-btn--tertiary-no-outline" %>
+ <%= link_to "Revenir à la liste", { controller: :sites, action: :index }, class: "fr-btn fr-btn--tertiary-no-outline" %>
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index d6026f4..ae374ac 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -25,22 +25,23 @@ fr:
created_at: Enregistré le
checked_at: Executé le
status: État
+ count:
+ zero: Aucun audit
+ one: Un audit
+ other: "%{count} audits"
audit/status:
pending: Planifié
running: En cours
passed: Effectué
retryable: À retenter
failed: Échoué
+
check:
type: Type de contrôle
status: État
run_at: Contrôle prévu le
checked_at: Contrôle effectué le
attempts: Tentatives
- count:
- zero: Aucun contrôle
- one: Un contrôle
- other: "%{count} contrôles"
check/status:
pending: Planifié
running: En cours
@@ -70,6 +71,9 @@ fr:
created_at: Date de création
updated_at: Date de mise à jour
url: Adresse du site
+ audits: Tous les audits
+ audit_history: Historique des audits (%{total})
+ audits_count: Audits
last_audit_at: Dernier audit
detail: Détails
view: Voir la fiche
@@ -112,3 +116,6 @@ fr:
notice: Site modifié
destroy:
notice: Site supprimé
+ audits:
+ create:
+ notice: Audit créé
diff --git a/config/routes.rb b/config/routes.rb
index ccfe971..95ad781 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,6 +1,8 @@
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
Rails.application.routes.draw do
- resources :sites
+ resources :sites do
+ resources :audits, only: [:create, :show]
+ end
# Static pages
scope controller: :pages do
From f4afe56bd10d831df2ab680dfc6bfa775f8733a8 Mon Sep 17 00:00:00 2001
From: Goulven Champenois
Date: Wed, 26 Feb 2025 15:44:24 +0100
Subject: [PATCH 08/12] Use I18n for site actions
---
app/views/sites/show.html.erb | 4 ++--
config/locales/fr.yml | 2 ++
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/app/views/sites/show.html.erb b/app/views/sites/show.html.erb
index bc73788..7db5138 100644
--- a/app/views/sites/show.html.erb
+++ b/app/views/sites/show.html.erb
@@ -40,6 +40,6 @@
- <%= button_to "Supprimer ce site", @site, method: :delete, class: "fr-btn fr-btn--tertiary", form: { data: { turbo_confirm: t("shared.confirm") } } %>
- <%= link_to "Revenir à la liste", { controller: :sites, action: :index }, class: "fr-btn fr-btn--tertiary-no-outline" %>
+ <%= button_to Site.human(:delete), @site, method: :delete, class: "fr-btn fr-btn--tertiary", form: { data: { turbo_confirm: t("shared.confirm") } } %>
+ <%= link_to t("shared.back_to_list"), { controller: :sites, action: :index }, class: "fr-btn fr-btn--tertiary-no-outline" %>
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index ae374ac..ccd016b 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -9,6 +9,7 @@ fr:
actions: "Actions"
confirm: "Êtes-vous sûr ? Cette action ne peut pas être annulée."
time_ago: "il y a %{time}"
+ back_to_list: "Revenir à la liste"
time:
formats:
@@ -78,6 +79,7 @@ fr:
detail: Détails
view: Voir la fiche
view_name: Voir la fiche de %{name}
+ delete: Supprimer ce site
count:
zero: Aucun site
one: Un site
From 5baa463dc5dee40a9cb9c6f05e5d59d4b2163f28 Mon Sep 17 00:00:00 2001
From: Goulven Champenois
Date: Wed, 26 Feb 2025 15:47:40 +0100
Subject: [PATCH 09/12] Allow creating new audits
---
app/models/site.rb | 1 +
app/views/sites/show.html.erb | 1 +
config/locales/fr.yml | 1 +
3 files changed, 3 insertions(+)
diff --git a/app/models/site.rb b/app/models/site.rb
index 4e158a2..df04e4d 100644
--- a/app/models/site.rb
+++ b/app/models/site.rb
@@ -33,5 +33,6 @@ def url=(new_url)
def name = super.presence || url_without_scheme
alias to_title name
def audit = super || audits.last || audits.build
+ def audit! = audits.create(url:)
def should_generate_new_friendly_id? = new_record? || (audit && slug != url_without_scheme.parameterize)
end
diff --git a/app/views/sites/show.html.erb b/app/views/sites/show.html.erb
index 7db5138..321b5bf 100644
--- a/app/views/sites/show.html.erb
+++ b/app/views/sites/show.html.erb
@@ -40,6 +40,7 @@
+ <%= button_to Audit.human(:new), [@site, :audits], method: :post, class: "fr-btn" %>
<%= button_to Site.human(:delete), @site, method: :delete, class: "fr-btn fr-btn--tertiary", form: { data: { turbo_confirm: t("shared.confirm") } } %>
<%= link_to t("shared.back_to_list"), { controller: :sites, action: :index }, class: "fr-btn fr-btn--tertiary-no-outline" %>
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index ccd016b..3b585a0 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -26,6 +26,7 @@ fr:
created_at: Enregistré le
checked_at: Executé le
status: État
+ new: "Nouvel audit"
count:
zero: Aucun audit
one: Un audit
From 7bbcad0cfd57726592aaa66f16c0dd20ab3842f3 Mon Sep 17 00:00:00 2001
From: Goulven Champenois
Date: Wed, 26 Feb 2025 16:09:41 +0100
Subject: [PATCH 10/12] Refactor setting audit checked_at
---
app/jobs/run_check_job.rb | 4 +---
app/jobs/update_audit_job.rb | 8 ++++++++
app/jobs/update_audit_status_job.rb | 5 -----
app/models/audit.rb | 5 +++++
4 files changed, 14 insertions(+), 8 deletions(-)
create mode 100644 app/jobs/update_audit_job.rb
delete mode 100644 app/jobs/update_audit_status_job.rb
diff --git a/app/jobs/run_check_job.rb b/app/jobs/run_check_job.rb
index 16289bf..b0b1ee0 100644
--- a/app/jobs/run_check_job.rb
+++ b/app/jobs/run_check_job.rb
@@ -4,8 +4,6 @@ def perform(check)
check.run
- check.audit.update(checked_at: Time.zone.now)
-
- UpdateAuditStatusJob.perform_later(check.audit)
+ UpdateAuditJob.perform_later(check.audit)
end
end
diff --git a/app/jobs/update_audit_job.rb b/app/jobs/update_audit_job.rb
new file mode 100644
index 0000000..7e7fca1
--- /dev/null
+++ b/app/jobs/update_audit_job.rb
@@ -0,0 +1,8 @@
+class UpdateAuditJob < ApplicationJob
+ def perform(audit)
+ Audit.transaction do
+ audit.derive_status_from_checks
+ audit.set_checked_at
+ end
+ end
+end
diff --git a/app/jobs/update_audit_status_job.rb b/app/jobs/update_audit_status_job.rb
deleted file mode 100644
index 349dcb8..0000000
--- a/app/jobs/update_audit_status_job.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class UpdateAuditStatusJob < ApplicationJob
- def perform(audit)
- audit.derive_status_from_checks
- end
-end
diff --git a/app/models/audit.rb b/app/models/audit.rb
index 523f13d..08c8361 100644
--- a/app/models/audit.rb
+++ b/app/models/audit.rb
@@ -43,4 +43,9 @@ def derive_status_from_checks
end
update(status: new_status)
end
+
+ def set_checked_at
+ latest_checked_at = all_checks.collect(&:checked_at).sort.last
+ update(checked_at: latest_checked_at)
+ end
end
From c105ee14761e0cca6c7190cb6bba42b55ac610ce Mon Sep 17 00:00:00 2001
From: Goulven Champenois
Date: Wed, 26 Feb 2025 16:10:25 +0100
Subject: [PATCH 11/12] Run newly-created audit immediately
---
app/controllers/sites_controller.rb | 1 +
app/models/audit.rb | 6 ++++++
app/models/site.rb | 5 ++++-
3 files changed, 11 insertions(+), 1 deletion(-)
diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb
index 9fe383a..bde7e3e 100644
--- a/app/controllers/sites_controller.rb
+++ b/app/controllers/sites_controller.rb
@@ -22,6 +22,7 @@ def edit; end
def create
@site = Site.find_or_create_by_url(site_params)
if @site.persisted?
+ @site.audit.run! if @site.audit.pending?
redirect_to @site, notice: t(".notice")
else
render :new, status: :unprocessable_entity
diff --git a/app/models/audit.rb b/app/models/audit.rb
index 08c8361..4dba6c2 100644
--- a/app/models/audit.rb
+++ b/app/models/audit.rb
@@ -33,6 +33,12 @@ def create_checks
Check.names.map { |name| send(name) || send(:"create_#{name}") }
end
+ def run!
+ all_checks.each(&:run)
+ derive_status_from_checks
+ set_checked_at
+ end
+
def derive_status_from_checks
new_status = if all_checks.any?(&:new_record?)
:pending
diff --git a/app/models/site.rb b/app/models/site.rb
index df04e4d..ed6220f 100644
--- a/app/models/site.rb
+++ b/app/models/site.rb
@@ -33,6 +33,9 @@ def url=(new_url)
def name = super.presence || url_without_scheme
alias to_title name
def audit = super || audits.last || audits.build
- def audit! = audits.create(url:)
def should_generate_new_friendly_id? = new_record? || (audit && slug != url_without_scheme.parameterize)
+
+ def audit!
+ audits.create(url:).tap(&:run!).tap(&:persisted?)
+ end
end
From 8bb1dbb08aae562f231af8d0ea8e240bc4533ecc Mon Sep 17 00:00:00 2001
From: Goulven Champenois
Date: Wed, 26 Feb 2025 17:12:34 +0100
Subject: [PATCH 12/12] Silence brakeman warning (URLs are parsed before being
displayed)
---
config/brakeman.ignore | 39 +++++++++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
create mode 100644 config/brakeman.ignore
diff --git a/config/brakeman.ignore b/config/brakeman.ignore
new file mode 100644
index 0000000..bbb167d
--- /dev/null
+++ b/config/brakeman.ignore
@@ -0,0 +1,39 @@
+{
+ "ignored_warnings": [
+ {
+ "warning_type": "Cross-Site Scripting",
+ "warning_code": 4,
+ "fingerprint": "cb8384ab051ae32f84795a297fae3519a66a98325b1b7aa214dd5657e4df9e47",
+ "check_name": "LinkToHref",
+ "message": "Potentially unsafe model attribute in `link_to` href",
+ "file": "app/views/sites/show.html.erb",
+ "line": 5,
+ "link": "https://brakemanscanner.org/docs/warning_types/link_to_href",
+ "code": "link_to(Site.friendly.find(params[:site_id]).url, Site.friendly.find(params[:site_id]).url, :target => :_blank)",
+ "render_path": [
+ {
+ "type": "controller",
+ "class": "AuditsController",
+ "method": "create",
+ "line": 10,
+ "file": "app/controllers/audits_controller.rb",
+ "rendered": {
+ "name": "sites/show",
+ "file": "app/views/sites/show.html.erb"
+ }
+ }
+ ],
+ "location": {
+ "type": "template",
+ "template": "sites/show"
+ },
+ "user_input": "Site.friendly.find(params[:site_id]).url",
+ "confidence": "Weak",
+ "cwe_id": [
+ 79
+ ],
+ "note": ""
+ }
+ ],
+ "brakeman_version": "7.0.0"
+}