diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-18 04:33:53 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-18 04:33:53 +0200 |
| commit | 49de2e14972030a7ef0ca36a94ab55a23a2f3596 (patch) | |
| tree | 7b05419a805e9bac01a4acea3ba6d0348af50ee6 | |
| parent | 10bd6ba5cc640f0a85a3adb3641a65ce5edf831d (diff) | |
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.
| -rw-r--r-- | app/controllers/nodes_controller.rb | 3 | ||||
| -rw-r--r-- | app/helpers/node_actions_helper.rb | 26 | ||||
| -rw-r--r-- | app/views/nodes/show.html.erb | 5 | ||||
| -rw-r--r-- | config/locales/de.yml | 3 | ||||
| -rw-r--r-- | config/locales/en.yml | 3 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 5 | ||||
| -rw-r--r-- | test/controllers/nodes_controller_test.rb | 12 | ||||
| -rw-r--r-- | test/models/helpers/node_actions_helper_test.rb | 19 |
8 files changed, 75 insertions, 1 deletions
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 | |||
| 62 | def show | 62 | def show |
| 63 | @page = @node.draft || @node.head | 63 | @page = @node.draft || @node.head |
| 64 | @translations = @page.translation_summary | 64 | @translations = @page.translation_summary |
| 65 | @page_actions = NodeAction.where(:page_id => @node.pages.select(:id)) | ||
| 66 | .order(:occurred_at, :id) | ||
| 67 | .group_by(&:page_id) | ||
| 65 | end | 68 | end |
| 66 | 69 | ||
| 67 | def edit | 70 | 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 | |||
| 59 | end | 59 | end |
| 60 | end | 60 | end |
| 61 | 61 | ||
| 62 | def revision_lifecycle_badges actions | ||
| 63 | return "" if actions.blank? | ||
| 64 | |||
| 65 | badges = actions.map do |action| | ||
| 66 | key = case action.action | ||
| 67 | when "create" then "node_actions.revision_created" | ||
| 68 | when "publish" then action.metadata["via"] == "revision" ? | ||
| 69 | "node_actions.revision_restored" : | ||
| 70 | "node_actions.revision_published" | ||
| 71 | end | ||
| 72 | next unless key | ||
| 73 | |||
| 74 | badge = h(t(key, :date => action.occurred_at.strftime("%Y-%m-%d"), | ||
| 75 | :actor => action.actor_name)) | ||
| 76 | if action.inferred_from | ||
| 77 | badge = safe_join([badge, content_tag(:span, t("node_actions.backfilled"), | ||
| 78 | :class => "node_action_inferred", | ||
| 79 | :title => action.inferred_from)], " ") | ||
| 80 | end | ||
| 81 | badge | ||
| 82 | end.compact | ||
| 83 | |||
| 84 | return "" if badges.empty? | ||
| 85 | safe_join(["— ", safe_join(badges, " · ")]) | ||
| 86 | end | ||
| 87 | |||
| 62 | private | 88 | private |
| 63 | 89 | ||
| 64 | def revision_ref action, key | 90 | 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 @@ | |||
| 229 | </summary> | 229 | </summary> |
| 230 | <ul> | 230 | <ul> |
| 231 | <% @node.pages.order(:revision).each do |page| %> | 231 | <% @node.pages.order(:revision).each do |page| %> |
| 232 | <li><%= link_to "##{page.revision} — #{page.title} (#{page.editor.try(:login)}, #{page.updated_at})", node_revision_path(@node, page) %></li> | 232 | <li> |
| 233 | <%= link_to "##{page.revision} — #{page.title} (#{page.editor.try(:login)}, #{page.updated_at})", node_revision_path(@node, page) %> | ||
| 234 | <span class="revision_lifecycle"><%= revision_lifecycle_badges(@page_actions[page.id]) %></span> | ||
| 235 | </li> | ||
| 233 | <% end %> | 236 | <% end %> |
| 234 | </ul> | 237 | </ul> |
| 235 | </details> | 238 | </details> |
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: | |||
| 113 | trash: "%{actor} hat %{subject} in den Papierkorb verschoben (vorher unter %{from})" | 113 | trash: "%{actor} hat %{subject} in den Papierkorb verschoben (vorher unter %{from})" |
| 114 | restore_from_trash: "%{actor} hat %{subject} aus dem Papierkorb nach %{to} wiederhergestellt" | 114 | restore_from_trash: "%{actor} hat %{subject} aus dem Papierkorb nach %{to} wiederhergestellt" |
| 115 | destroy: "%{actor} hat %{subject} endgültig gelöscht (zuletzt unter %{path})" | 115 | destroy: "%{actor} hat %{subject} endgültig gelöscht (zuletzt unter %{path})" |
| 116 | revision_created: "angelegt am %{date} von %{actor}" | ||
| 117 | revision_published: "veröffentlicht am %{date} von %{actor}" | ||
| 118 | 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: | |||
| 65 | trash: "%{actor} moved %{subject} to the Trash (was at %{from})" | 65 | trash: "%{actor} moved %{subject} to the Trash (was at %{from})" |
| 66 | restore_from_trash: "%{actor} restored %{subject} from the Trash to %{to}" | 66 | restore_from_trash: "%{actor} restored %{subject} from the Trash to %{to}" |
| 67 | destroy: "%{actor} permanently deleted %{subject} (last at %{path})" | 67 | destroy: "%{actor} permanently deleted %{subject} (last at %{path})" |
| 68 | revision_created: "created %{date} by %{actor}" | ||
| 69 | revision_published: "published %{date} by %{actor}" | ||
| 70 | 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 { | |||
| 1544 | color: #777; | 1544 | color: #777; |
| 1545 | } | 1545 | } |
| 1546 | 1546 | ||
| 1547 | .revision_lifecycle { | ||
| 1548 | font-size: 0.85rem; | ||
| 1549 | color: #777; | ||
| 1550 | } | ||
| 1551 | |||
| 1547 | /* ============================================================ | 1552 | /* ============================================================ |
| 1548 | Trash section | 1553 | Trash section |
| 1549 | ============================================================ */ | 1554 | ============================================================ */ |
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 | |||
| 745 | assert_select "td", /quentin/ | 745 | assert_select "td", /quentin/ |
| 746 | assert_select "form[action=?]", node_path(node), count: 1 | 746 | assert_select "form[action=?]", node_path(node), count: 1 |
| 747 | end | 747 | end |
| 748 | |||
| 749 | test "show annotates history rows with their lifecycle" do | ||
| 750 | login_as :quentin | ||
| 751 | node = Node.root.children.create!(:slug => "history_annotation_test") | ||
| 752 | Globalize.with_locale(I18n.default_locale) { node.draft.update!(:title => "Annotated") } | ||
| 753 | node.publish_draft!(users(:quentin)) | ||
| 754 | |||
| 755 | get :show, params: { :id => node.id } | ||
| 756 | |||
| 757 | assert_response :success | ||
| 758 | assert_select "span.revision_lifecycle", /quentin/ | ||
| 759 | end | ||
| 748 | end | 760 | 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 | |||
| 114 | assert_includes action_summary(restore), "club/new" | 114 | assert_includes action_summary(restore), "club/new" |
| 115 | assert_includes action_summary(doomed), "trash/old" | 115 | assert_includes action_summary(doomed), "trash/old" |
| 116 | end | 116 | end |
| 117 | |||
| 118 | test "revision lifecycle badges cover create, publish, rollback, and inference" do | ||
| 119 | created = entry("create", { "title" => "x", "path" => "a/b" }) | ||
| 120 | published = entry("publish", { "via" => "draft", "title" => { "from" => nil, "to" => "x" } }) | ||
| 121 | restored = entry("publish", { "via" => "revision", "title" => { "from" => "y", "to" => "x" } }) | ||
| 122 | restored.update!(:inferred_from => "from_page_revision") | ||
| 123 | |||
| 124 | out = revision_lifecycle_badges([created, published, restored]) | ||
| 125 | |||
| 126 | assert_includes out, "created" | ||
| 127 | assert_includes out, "published" | ||
| 128 | assert_includes out, "restored" | ||
| 129 | assert_includes out, "quentin" | ||
| 130 | assert_includes out, "node_action_inferred" | ||
| 131 | assert_includes out, "<span" | ||
| 132 | assert_not_includes out, "<span" | ||
| 133 | |||
| 134 | assert_equal "", revision_lifecycle_badges(nil) | ||
| 135 | end | ||
| 117 | end | 136 | end |
