diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-08-09 15:45:56 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-08-09 15:45:56 +0200 |
| commit | 9d32458491d79f8e06a706375030c78282f4427b (patch) | |
| tree | 5eee208b844df39dfd66d9f0d1618fb5fa437fdb | |
| parent | ed3905b5409190c1e11c2c49c2163ac967c6d8b9 (diff) | |
Report every editorial attribute in the revision diff
diff_against gained external_url, published_at and user. The view renders
each metadata section only when that attribute changed, and says so once
when none did.
Metadata now precedes the locale pointer, which speaks only of translations:
above the metadata it read as "nothing changed" on a revision that had moved
the page.
| -rw-r--r-- | app/models/page.rb | 6 | ||||
| -rw-r--r-- | app/views/revisions/diff.html.erb | 133 | ||||
| -rw-r--r-- | config/locales/de.yml | 5 | ||||
| -rw-r--r-- | config/locales/en.yml | 5 | ||||
| -rw-r--r-- | public/stylesheets/admin.css | 6 | ||||
| -rw-r--r-- | test/controllers/revisions_controller_test.rb | 22 | ||||
| -rw-r--r-- | test/models/page_test.rb | 28 |
7 files changed, 148 insertions, 57 deletions
diff --git a/app/models/page.rb b/app/models/page.rb index 3240057f..c2dc227b 100644 --- a/app/models/page.rb +++ b/app/models/page.rb | |||
| @@ -277,6 +277,12 @@ class Page < ApplicationRecord | |||
| 277 | 277 | ||
| 278 | text_diffs.merge( | 278 | text_diffs.merge( |
| 279 | address: address_diff_against(other), | 279 | address: address_diff_against(other), |
| 280 | external_url: { from: other.external_url, to: external_url, | ||
| 281 | changed: external_url.presence != other.external_url.presence }, | ||
| 282 | published_at: { from: other.published_at, to: published_at, | ||
| 283 | changed: published_at != other.published_at }, | ||
| 284 | user: { from: other.user, to: user, | ||
| 285 | changed: user_id != other.user_id }, | ||
| 280 | tags: { added: tag_list.to_a - other.tag_list.to_a, removed: other.tag_list.to_a - tag_list.to_a }, | 286 | tags: { added: tag_list.to_a - other.tag_list.to_a, removed: other.tag_list.to_a - tag_list.to_a }, |
| 281 | template_name: { from: other.template_name, to: template_name, changed: template_name != other.template_name }, | 287 | template_name: { from: other.template_name, to: template_name, changed: template_name != other.template_name }, |
| 282 | assets: { added: assets.to_a - other.assets.to_a, removed: other.assets.to_a - assets.to_a } | 288 | assets: { added: assets.to_a - other.assets.to_a, removed: other.assets.to_a - assets.to_a } |
diff --git a/app/views/revisions/diff.html.erb b/app/views/revisions/diff.html.erb index 0fd9ccb9..25f5ad04 100644 --- a/app/views/revisions/diff.html.erb +++ b/app/views/revisions/diff.html.erb | |||
| @@ -48,7 +48,6 @@ | |||
| 48 | </p> | 48 | </p> |
| 49 | <% end %> | 49 | <% end %> |
| 50 | 50 | ||
| 51 | |||
| 52 | <% if @available_layer_pairs.present? %> | 51 | <% if @available_layer_pairs.present? %> |
| 53 | <div class="node_action_bar standalone_action_bar"> | 52 | <div class="node_action_bar standalone_action_bar"> |
| 54 | <% @available_layer_pairs.each do |pair| %> | 53 | <% @available_layer_pairs.each do |pair| %> |
| @@ -71,21 +70,88 @@ | |||
| 71 | <% end %> | 70 | <% end %> |
| 72 | 71 | ||
| 73 | <div id="diffview"> | 72 | <div id="diffview"> |
| 74 | <% current_summary = @locale_summary.find { |s| s[:locale] == @translation_locale } %> | 73 | <% meta_changed = @diff[:address][:changed] || @diff[:external_url][:changed] || |
| 75 | <% if current_summary && !current_summary[:changed] %> | 74 | @diff[:template_name][:changed] || |
| 76 | <% elsewhere = @locale_summary.select { |s| s[:changed] }.map { |s| | 75 | @diff[:published_at][:changed] || @diff[:user][:changed] || |
| 77 | link_to s[:locale].to_s.upcase, diff_node_revisions_path(@node, | 76 | @diff[:tags][:added].any? || @diff[:tags][:removed].any? || |
| 78 | start_revision: params[:start_revision], end_revision: params[:end_revision], | 77 | @diff[:assets][:added].any? || @diff[:assets][:removed].any? %> |
| 79 | view: @diff_view, translation_locale: s[:locale]) } %> | 78 | |
| 80 | <p class="diff_unchanged diff_locale_pointer"> | 79 | <div class="diff_preamble"> |
| 81 | <% if elsewhere.any? %> | 80 | <div class="diff_meta"> |
| 82 | <%= t(".unchanged_here_html", :lang => @translation_locale.to_s.upcase, | 81 | <% unless meta_changed %> |
| 83 | :others => safe_join(elsewhere, ", ")) %> | 82 | <p class="diff_unchanged"><%= t(".no_metadata_change") %></p> |
| 84 | <% else %> | 83 | <% end %> |
| 85 | <%= t(".unchanged_anywhere") %> | 84 | |
| 85 | <% if @diff[:address][:changed] %> | ||
| 86 | <h3><%= t(".address") %></h3> | ||
| 87 | <p> | ||
| 88 | <del><%= @diff[:address][:from] || t(".none_marker") %></del> | ||
| 89 | <ins><%= @diff[:address][:to] || t(".none_marker") %></ins> | ||
| 90 | </p> | ||
| 91 | <% end %> | ||
| 92 | |||
| 93 | <% if @diff[:external_url][:changed] %> | ||
| 94 | <h3><%= Page.human_attribute_name(:external_url) %></h3> | ||
| 95 | <p> | ||
| 96 | <del><%= @diff[:external_url][:from].presence || t(".none_marker") %></del> | ||
| 97 | <ins><%= @diff[:external_url][:to].presence || t(".none_marker") %></ins> | ||
| 98 | </p> | ||
| 99 | <% end %> | ||
| 100 | |||
| 101 | <% if @diff[:published_at][:changed] %> | ||
| 102 | <h3><%= Page.human_attribute_name(:published_at) %></h3> | ||
| 103 | <p> | ||
| 104 | <del><%= @diff[:published_at][:from] ? admin_datetime(@diff[:published_at][:from]) : t(".none_marker") %></del> | ||
| 105 | <ins><%= @diff[:published_at][:to] ? admin_datetime(@diff[:published_at][:to]) : t(".none_marker") %></ins> | ||
| 106 | </p> | ||
| 86 | <% end %> | 107 | <% end %> |
| 87 | </p> | 108 | |
| 88 | <% end %> | 109 | <% if @diff[:user][:changed] %> |
| 110 | <h3><%= Page.human_attribute_name(:user) %></h3> | ||
| 111 | <p> | ||
| 112 | <del><%= @diff[:user][:from]&.login || t(".none_marker") %></del> | ||
| 113 | <ins><%= @diff[:user][:to]&.login || t(".none_marker") %></ins> | ||
| 114 | </p> | ||
| 115 | <% end %> | ||
| 116 | |||
| 117 | <% if @diff[:tags][:added].any? || @diff[:tags][:removed].any? %> | ||
| 118 | <h3><%= Page.human_attribute_name(:tag_list) %></h3> | ||
| 119 | <ul class="diff_set_list"> | ||
| 120 | <% @diff[:tags][:added].each do |tag| %><li><ins><%= tag %></ins></li><% end %> | ||
| 121 | <% @diff[:tags][:removed].each do |tag| %><li><del><%= tag %></del></li><% end %> | ||
| 122 | </ul> | ||
| 123 | <% end %> | ||
| 124 | |||
| 125 | <% if @diff[:template_name][:changed] %> | ||
| 126 | <h3><%= Page.human_attribute_name(:template_name) %></h3> | ||
| 127 | <p><del><%= @diff[:template_name][:from] || t(".none_marker") %></del> <ins><%= @diff[:template_name][:to] || t(".none_marker") %></ins></p> | ||
| 128 | <% end %> | ||
| 129 | |||
| 130 | <% if @diff[:assets][:added].any? || @diff[:assets][:removed].any? %> | ||
| 131 | <h3><%= Page.human_attribute_name(:assets) %></h3> | ||
| 132 | <ul class="diff_set_list"> | ||
| 133 | <% @diff[:assets][:added].each do |asset| %><li><ins><%= asset.upload_file_name %></ins></li><% end %> | ||
| 134 | <% @diff[:assets][:removed].each do |asset| %><li><del><%= asset.upload_file_name %></del></li><% end %> | ||
| 135 | </ul> | ||
| 136 | <% end %> | ||
| 137 | </div> | ||
| 138 | |||
| 139 | <% current_summary = @locale_summary.find { |s| s[:locale] == @translation_locale } %> | ||
| 140 | <% if current_summary && !current_summary[:changed] %> | ||
| 141 | <% elsewhere = @locale_summary.select { |s| s[:changed] }.map { |s| | ||
| 142 | link_to s[:locale].to_s.upcase, diff_node_revisions_path(@node, | ||
| 143 | start_revision: params[:start_revision], end_revision: params[:end_revision], | ||
| 144 | view: @diff_view, translation_locale: s[:locale]) } %> | ||
| 145 | <p class="diff_unchanged diff_locale_pointer"> | ||
| 146 | <% if elsewhere.any? %> | ||
| 147 | <%= t(".unchanged_here_html", :lang => @translation_locale.to_s.upcase, | ||
| 148 | :others => safe_join(elsewhere, ", ")) %> | ||
| 149 | <% else %> | ||
| 150 | <%= t(".unchanged_anywhere") %> | ||
| 151 | <% end %> | ||
| 152 | </p> | ||
| 153 | <% end %> | ||
| 154 | </div> | ||
| 89 | 155 | ||
| 90 | <% if @diff_view == :side_by_side %> | 156 | <% if @diff_view == :side_by_side %> |
| 91 | <div class="diff_side_by_side"> | 157 | <div class="diff_side_by_side"> |
| @@ -114,41 +180,4 @@ | |||
| 114 | <h3><%= Page.human_attribute_name(:body) %></h3> | 180 | <h3><%= Page.human_attribute_name(:body) %></h3> |
| 115 | <%= raw @diff[:body] %> | 181 | <%= raw @diff[:body] %> |
| 116 | <% end %> | 182 | <% end %> |
| 117 | |||
| 118 | <h3><%= t(".address") %></h3> | ||
| 119 | <% if @diff[:address][:changed] %> | ||
| 120 | <p> | ||
| 121 | <del><%= @diff[:address][:from] || t(".none_marker") %></del> | ||
| 122 | <ins><%= @diff[:address][:to] || t(".none_marker") %></ins> | ||
| 123 | </p> | ||
| 124 | <% else %> | ||
| 125 | <p class="diff_unchanged"><%= t(".no_change") %></p> | ||
| 126 | <% end %> | ||
| 127 | |||
| 128 | <h3><%= Page.human_attribute_name(:tag_list) %></h3> | ||
| 129 | <% if @diff[:tags][:added].empty? && @diff[:tags][:removed].empty? %> | ||
| 130 | <p class="diff_unchanged"><%= t(".no_change") %></p> | ||
| 131 | <% else %> | ||
| 132 | <ul class="diff_set_list"> | ||
| 133 | <% @diff[:tags][:added].each do |tag| %><li><ins><%= tag %></ins></li><% end %> | ||
| 134 | <% @diff[:tags][:removed].each do |tag| %><li><del><%= tag %></del></li><% end %> | ||
| 135 | </ul> | ||
| 136 | <% end %> | ||
| 137 | |||
| 138 | <h3><%= Page.human_attribute_name(:template_name) %></h3> | ||
| 139 | <% if @diff[:template_name][:changed] %> | ||
| 140 | <p><del><%= @diff[:template_name][:from] || t(".none_marker") %></del> <ins><%= @diff[:template_name][:to] || t(".none_marker") %></ins></p> | ||
| 141 | <% else %> | ||
| 142 | <p class="diff_unchanged"><%= t(".no_change") %></p> | ||
| 143 | <% end %> | ||
| 144 | |||
| 145 | <h3><%= Page.human_attribute_name(:assets) %></h3> | ||
| 146 | <% if @diff[:assets][:added].empty? && @diff[:assets][:removed].empty? %> | ||
| 147 | <p class="diff_unchanged"><%= t(".no_change") %></p> | ||
| 148 | <% else %> | ||
| 149 | <ul class="diff_set_list"> | ||
| 150 | <% @diff[:assets][:added].each do |asset| %><li><ins><%= asset.upload_file_name %></ins></li><% end %> | ||
| 151 | <% @diff[:assets][:removed].each do |asset| %><li><del><%= asset.upload_file_name %></del></li><% end %> | ||
| 152 | </ul> | ||
| 153 | <% end %> | ||
| 154 | </div> | 183 | </div> |
diff --git a/config/locales/de.yml b/config/locales/de.yml index 2aec7bfa..3f2eb0ab 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml | |||
| @@ -106,6 +106,9 @@ de: | |||
| 106 | tag_list: "Tags" | 106 | tag_list: "Tags" |
| 107 | template_name: "Template" | 107 | template_name: "Template" |
| 108 | assets: "Anhänge" | 108 | assets: "Anhänge" |
| 109 | external_url: "Externe Homepage" | ||
| 110 | published_at: "Veröffentlichungsdatum" | ||
| 111 | user: "Autor" | ||
| 109 | menu_item: | 112 | menu_item: |
| 110 | node_id: "Node-ID" | 113 | node_id: "Node-ID" |
| 111 | path: "Pfad" | 114 | path: "Pfad" |
| @@ -744,7 +747,7 @@ de: | |||
| 744 | revisions_link: "Alle Revisionen" | 747 | revisions_link: "Alle Revisionen" |
| 745 | compare_numbered: "Stattdessen zwei nummerierte Revisionen vergleichen" | 748 | compare_numbered: "Stattdessen zwei nummerierte Revisionen vergleichen" |
| 746 | none_marker: "(keins)" | 749 | none_marker: "(keins)" |
| 747 | no_change: "Keine Änderung." | 750 | no_metadata_change: "Keine Änderungen an den Metadaten." |
| 748 | unchanged_here_html: "Keine Änderung in der Übersetzung %{lang} zwischen diesen Revisionen — geändert wurde %{others}." | 751 | unchanged_here_html: "Keine Änderung in der Übersetzung %{lang} zwischen diesen Revisionen — geändert wurde %{others}." |
| 749 | unchanged_anywhere: "Zwischen diesen Revisionen wurde keine Übersetzung geändert." | 752 | unchanged_anywhere: "Zwischen diesen Revisionen wurde keine Übersetzung geändert." |
| 750 | address: "Adresse" | 753 | address: "Adresse" |
diff --git a/config/locales/en.yml b/config/locales/en.yml index 965906df..e8428f69 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml | |||
| @@ -57,6 +57,9 @@ en: | |||
| 57 | tag_list: "Tags" | 57 | tag_list: "Tags" |
| 58 | template_name: "Template" | 58 | template_name: "Template" |
| 59 | assets: "Assets" | 59 | assets: "Assets" |
| 60 | external_url: "External homepage" | ||
| 61 | published_at: "Publication date" | ||
| 62 | user: "Author" | ||
| 60 | menu_item: | 63 | menu_item: |
| 61 | node_id: "Node Id" | 64 | node_id: "Node Id" |
| 62 | path: "Path" | 65 | path: "Path" |
| @@ -712,7 +715,7 @@ en: | |||
| 712 | revisions_link: "All revisions" | 715 | revisions_link: "All revisions" |
| 713 | compare_numbered: "Compare two numbered revisions instead" | 716 | compare_numbered: "Compare two numbered revisions instead" |
| 714 | none_marker: "(none)" | 717 | none_marker: "(none)" |
| 715 | no_change: "No change." | 718 | no_metadata_change: "No metadata changes." |
| 716 | unchanged_here_html: "No change in the %{lang} translation between these revisions — %{others} changed." | 719 | unchanged_here_html: "No change in the %{lang} translation between these revisions — %{others} changed." |
| 717 | unchanged_anywhere: "No translation changed between these revisions." | 720 | unchanged_anywhere: "No translation changed between these revisions." |
| 718 | address: "Address" | 721 | address: "Address" |
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 7951f667..b8a01112 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css | |||
| @@ -799,6 +799,12 @@ table.revisions_table tr:hover { | |||
| 799 | margin: 0; | 799 | margin: 0; |
| 800 | } | 800 | } |
| 801 | 801 | ||
| 802 | .diff_preamble { | ||
| 803 | border-bottom: 1px solid var(--hairline); | ||
| 804 | margin-bottom: 1.5rem; | ||
| 805 | padding-bottom: 0.5rem; | ||
| 806 | } | ||
| 807 | |||
| 802 | .user_table { | 808 | .user_table { |
| 803 | width: 100%; | 809 | width: 100%; |
| 804 | max-width: 44rem; | 810 | max-width: 44rem; |
diff --git a/test/controllers/revisions_controller_test.rb b/test/controllers/revisions_controller_test.rb index 34f00a4f..d9f49e58 100644 --- a/test/controllers/revisions_controller_test.rb +++ b/test/controllers/revisions_controller_test.rb | |||
| @@ -151,19 +151,35 @@ class RevisionsControllerTest < ActionController::TestCase | |||
| 151 | assert_select "input[type='hidden'][name='end_revision'][value='draft']" | 151 | assert_select "input[type='hidden'][name='end_revision'][value='draft']" |
| 152 | end | 152 | end |
| 153 | 153 | ||
| 154 | test "diffing two revisions also shows tag, template, and asset changes" do | 154 | test "diffing shows tag, template, external URL and asset changes" do |
| 155 | login_as :quentin | 155 | login_as :quentin |
| 156 | find_or_create_draft(@node, @user) | 156 | find_or_create_draft(@node, @user) |
| 157 | @node.draft.tag_list = "update" | 157 | draft = @node.draft |
| 158 | @node.draft.save! | 158 | draft.tag_list = "update" |
| 159 | draft.template_name = "title_only" | ||
| 160 | draft.external_url = "https://example.org/" | ||
| 161 | draft.save! | ||
| 162 | draft.related_assets.create!(:asset => Asset.create!(:name => "diffed", | ||
| 163 | :upload_file_name => "diffed.png", | ||
| 164 | :upload_content_type => "image/png")) | ||
| 159 | 165 | ||
| 160 | post(:diff, params: { :node_id => @node.id, :start_revision => @node.pages.first.revision, :end_revision => @node.pages.last.revision }) | 166 | post(:diff, params: { :node_id => @node.id, :start_revision => @node.pages.first.revision, :end_revision => @node.pages.last.revision }) |
| 161 | assert_response :success | 167 | assert_response :success |
| 162 | assert_select "h3", Page.human_attribute_name(:tag_list) | 168 | assert_select "h3", Page.human_attribute_name(:tag_list) |
| 163 | assert_select "h3", Page.human_attribute_name(:template_name) | 169 | assert_select "h3", Page.human_attribute_name(:template_name) |
| 170 | assert_select "h3", Page.human_attribute_name(:external_url) | ||
| 164 | assert_select "h3", Page.human_attribute_name(:assets) | 171 | assert_select "h3", Page.human_attribute_name(:assets) |
| 165 | end | 172 | end |
| 166 | 173 | ||
| 174 | test "a diff with no metadata changes says so once" do | ||
| 175 | login_as :quentin | ||
| 176 | |||
| 177 | post(:diff, params: { :node_id => @node.id, :start_revision => @node.pages.first.revision, :end_revision => @node.pages.last.revision }) | ||
| 178 | assert_response :success | ||
| 179 | assert_select ".diff_meta .diff_unchanged" | ||
| 180 | assert_select ".diff_meta h3", false | ||
| 181 | end | ||
| 182 | |||
| 167 | test "revisions#index links back to the node" do | 183 | test "revisions#index links back to the node" do |
| 168 | login_as :quentin | 184 | login_as :quentin |
| 169 | get :index, params: { :node_id => @node.id } | 185 | get :index, params: { :node_id => @node.id } |
diff --git a/test/models/page_test.rb b/test/models/page_test.rb index b737e8b0..f095a7e1 100644 --- a/test/models/page_test.rb +++ b/test/models/page_test.rb | |||
| @@ -302,6 +302,34 @@ class PageTest < ActiveSupport::TestCase | |||
| 302 | assert_equal "title_only", diff[:template_name][:to] | 302 | assert_equal "title_only", diff[:template_name][:to] |
| 303 | end | 303 | end |
| 304 | 304 | ||
| 305 | test "diff_against reports external URL, publication date and author changes" do | ||
| 306 | n = Node.root.children.create! :slug => "meta_diff_test" | ||
| 307 | d = find_or_create_draft(n, @user1) | ||
| 308 | d.external_url = "https://old.example.org/" | ||
| 309 | d.published_at = Time.utc(2026, 1, 1, 12, 0, 0) | ||
| 310 | d.save! | ||
| 311 | n.publish_draft! | ||
| 312 | |||
| 313 | new_author = User.where.not(:id => n.head.user_id).first | ||
| 314 | d2 = find_or_create_draft(n, @user1) | ||
| 315 | d2.external_url = "https://new.example.org/" | ||
| 316 | d2.published_at = Time.utc(2026, 3, 1, 12, 0, 0) | ||
| 317 | d2.user = new_author | ||
| 318 | d2.save! | ||
| 319 | |||
| 320 | diff = d2.diff_against(n.head) | ||
| 321 | |||
| 322 | assert diff[:external_url][:changed] | ||
| 323 | assert_equal "https://old.example.org/", diff[:external_url][:from] | ||
| 324 | assert_equal "https://new.example.org/", diff[:external_url][:to] | ||
| 325 | |||
| 326 | assert diff[:published_at][:changed] | ||
| 327 | assert_equal Time.utc(2026, 3, 1, 12, 0, 0).to_i, diff[:published_at][:to].to_i | ||
| 328 | |||
| 329 | assert diff[:user][:changed] | ||
| 330 | assert_equal new_author.login, diff[:user][:to].login | ||
| 331 | end | ||
| 332 | |||
| 305 | test "diff_against reports added and removed assets by filename" do | 333 | test "diff_against reports added and removed assets by filename" do |
| 306 | n = Node.root.children.create! :slug => "asset_diff_test" | 334 | n = Node.root.children.create! :slug => "asset_diff_test" |
| 307 | d = find_or_create_draft(n, @user1) | 335 | d = find_or_create_draft(n, @user1) |
