From 49de2e14972030a7ef0ca36a94ab55a23a2f3596 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Sat, 18 Jul 2026 04:33:53 +0200 Subject: Annotate the History list with each revision's lifecycle Each row in nodes#show's History section now carries terse badges from the action log: created, published, and restored (rollback re-promotion), with date and actor, rendered from entry metadata alone. Backfilled entries wear the inferred marker, so reconstructed provenance stays distinguishable from witnessed history. A revision that was published and later restored shows both badges chronologically -- its true biography. Only create and publish entries carry page_id; trash, restore, and destroy annotate the node's own log zoom instead of any single revision, by design. --- app/controllers/nodes_controller.rb | 3 +++ app/helpers/node_actions_helper.rb | 26 +++++++++++++++++++++++++ app/views/nodes/show.html.erb | 5 ++++- config/locales/de.yml | 3 +++ config/locales/en.yml | 3 +++ public/stylesheets/admin.css | 5 +++++ test/controllers/nodes_controller_test.rb | 12 ++++++++++++ test/models/helpers/node_actions_helper_test.rb | 19 ++++++++++++++++++ 8 files changed, 75 insertions(+), 1 deletion(-) diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 021f8ff..6caa827 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb @@ -62,6 +62,9 @@ class NodesController < ApplicationController def show @page = @node.draft || @node.head @translations = @page.translation_summary + @page_actions = NodeAction.where(:page_id => @node.pages.select(:id)) + .order(:occurred_at, :id) + .group_by(&:page_id) end def edit diff --git a/app/helpers/node_actions_helper.rb b/app/helpers/node_actions_helper.rb index c829dff..eb761e3 100644 --- a/app/helpers/node_actions_helper.rb +++ b/app/helpers/node_actions_helper.rb @@ -59,6 +59,32 @@ module NodeActionsHelper end end + def revision_lifecycle_badges actions + return "" if actions.blank? + + badges = actions.map do |action| + key = case action.action + when "create" then "node_actions.revision_created" + when "publish" then action.metadata["via"] == "revision" ? + "node_actions.revision_restored" : + "node_actions.revision_published" + end + next unless key + + badge = h(t(key, :date => action.occurred_at.strftime("%Y-%m-%d"), + :actor => action.actor_name)) + if action.inferred_from + badge = safe_join([badge, content_tag(:span, t("node_actions.backfilled"), + :class => "node_action_inferred", + :title => action.inferred_from)], " ") + end + badge + end.compact + + return "" if badges.empty? + safe_join(["— ", safe_join(badges, " · ")]) + end + private def revision_ref action, key diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index 4d4f6d5..af05778 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -229,7 +229,10 @@ diff --git a/config/locales/de.yml b/config/locales/de.yml index 289e5cc..f64f6cd 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -113,3 +113,6 @@ de: trash: "%{actor} hat %{subject} in den Papierkorb verschoben (vorher unter %{from})" restore_from_trash: "%{actor} hat %{subject} aus dem Papierkorb nach %{to} wiederhergestellt" destroy: "%{actor} hat %{subject} endgültig gelöscht (zuletzt unter %{path})" + revision_created: "angelegt am %{date} von %{actor}" + revision_published: "veröffentlicht am %{date} von %{actor}" + revision_restored: "wiederhergestellt am %{date} von %{actor}" diff --git a/config/locales/en.yml b/config/locales/en.yml index b356cc1..8a8acc1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -65,3 +65,6 @@ en: trash: "%{actor} moved %{subject} to the Trash (was at %{from})" restore_from_trash: "%{actor} restored %{subject} from the Trash to %{to}" destroy: "%{actor} permanently deleted %{subject} (last at %{path})" + revision_created: "created %{date} by %{actor}" + revision_published: "published %{date} by %{actor}" + revision_restored: "restored %{date} by %{actor}" diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 88fd84b..15e753d 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -1544,6 +1544,11 @@ div#image_browser ul li { color: #777; } +.revision_lifecycle { + font-size: 0.85rem; + color: #777; +} + /* ============================================================ Trash section ============================================================ */ diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb index 6e2ddb3..ddc4565 100644 --- a/test/controllers/nodes_controller_test.rb +++ b/test/controllers/nodes_controller_test.rb @@ -745,4 +745,16 @@ class NodesControllerTest < ActionController::TestCase assert_select "td", /quentin/ assert_select "form[action=?]", node_path(node), count: 1 end + +test "show annotates history rows with their lifecycle" do + login_as :quentin + node = Node.root.children.create!(:slug => "history_annotation_test") + Globalize.with_locale(I18n.default_locale) { node.draft.update!(:title => "Annotated") } + node.publish_draft!(users(:quentin)) + + get :show, params: { :id => node.id } + + assert_response :success + assert_select "span.revision_lifecycle", /quentin/ + end end diff --git a/test/models/helpers/node_actions_helper_test.rb b/test/models/helpers/node_actions_helper_test.rb index 16c833b..ef6035d 100644 --- a/test/models/helpers/node_actions_helper_test.rb +++ b/test/models/helpers/node_actions_helper_test.rb @@ -114,4 +114,23 @@ class NodeActionsHelperTest < ActionView::TestCase assert_includes action_summary(restore), "club/new" assert_includes action_summary(doomed), "trash/old" end + + test "revision lifecycle badges cover create, publish, rollback, and inference" do + created = entry("create", { "title" => "x", "path" => "a/b" }) + published = entry("publish", { "via" => "draft", "title" => { "from" => nil, "to" => "x" } }) + restored = entry("publish", { "via" => "revision", "title" => { "from" => "y", "to" => "x" } }) + restored.update!(:inferred_from => "from_page_revision") + + out = revision_lifecycle_badges([created, published, restored]) + + assert_includes out, "created" + assert_includes out, "published" + assert_includes out, "restored" + assert_includes out, "quentin" + assert_includes out, "node_action_inferred" + assert_includes out, "