From a25d90335ad3738b6831288190132c2f7498465c Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 10 Jul 2026 00:35:24 +0200 Subject: Retire vendored cacycle_diff.js for a diff-lcs-based diff view Revisions#diff now computes an inline or side-by-side word diff server-side (Page#diff_against, lib/html_word_diff.rb) instead of shipping raw/escaped content to the browser for a 2008-era vendored JS differ to mangle. View mode is picked via a `view` param, settable either on the diff page itself or directly from the revisions#index sticky bar next to "Diff revisions". Also fixes a pre-existing bug in RevisionsController#diff's single- revision branch, which set params[:start]/params[:end] instead of params[:start_revision]/params[:end_revision]. --- test/controllers/revisions_controller_test.rb | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) (limited to 'test/controllers/revisions_controller_test.rb') diff --git a/test/controllers/revisions_controller_test.rb b/test/controllers/revisions_controller_test.rb index b4dcd8f..e2fc976 100644 --- a/test/controllers/revisions_controller_test.rb +++ b/test/controllers/revisions_controller_test.rb @@ -59,4 +59,33 @@ class RevisionsControllerTest < ActionController::TestCase assert_equal @node.head, @node.pages.first assert_equal "first", @node.head.reload.body end + + test "diffing two revisions renders real markup with only the changed words marked" do + login_as :quentin + post( + :diff, params: { + :node_id => @node.id, + :start_revision => @node.pages.first.revision, + :end_revision => @node.pages.last.revision + } + ) + assert_response :success + assert_select "del", "first" + assert_select "ins", "second" + assert_no_match /</, response.body + end + + test "diffing two revisions in side by side view renders two columns" do + login_as :quentin + post( + :diff, params: { + :node_id => @node.id, + :start_revision => @node.pages.first.revision, + :end_revision => @node.pages.last.revision, + :view => "side_by_side" + } + ) + assert_response :success + assert_select ".diff_column", 2 + end end -- cgit v1.3 From cdf861a23233afb955b3c722a73d9e6ac8de3942 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 10 Jul 2026 00:44:22 +0200 Subject: Fix stale assertions and escaped body in revisions#show MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The table-based markup revisions#show's own test was asserting against (, ) was retired when this view moved to the node_description/node_content pattern; updated the assertions to match. Found in the same pass: @page.body had no raw(), so any real markup in a revision's body rendered as escaped entities on this page — same bug class as revisions#diff, just not yet fixed here. --- app/views/revisions/show.html.erb | 2 +- test/controllers/revisions_controller_test.rb | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) (limited to 'test/controllers/revisions_controller_test.rb') diff --git a/app/views/revisions/show.html.erb b/app/views/revisions/show.html.erb index 4148096..92d959b 100644 --- a/app/views/revisions/show.html.erb +++ b/app/views/revisions/show.html.erb @@ -33,6 +33,6 @@
<%= @page.abstract %>
Body
-
<%= @page.body %>
+
<%= raw @page.body %>
diff --git a/test/controllers/revisions_controller_test.rb b/test/controllers/revisions_controller_test.rb index e2fc976..46fc220 100644 --- a/test/controllers/revisions_controller_test.rb +++ b/test/controllers/revisions_controller_test.rb @@ -28,21 +28,33 @@ class RevisionsControllerTest < ActionController::TestCase assert_response :success assert_select ".revision", 2 end - - test "showing one revision" do + + test "showing one revision" do login_as :quentin get :show, params: { :node_id => @node.id, :id => @node.pages.last.id } assert_response :success - assert_select "strong", "Body" - assert_select "td", {:count => 1, :text => "second"} + assert_select ".node_description", "Body" + assert_select ".node_content", {:count => 1, :text => "second"} end - + + test "showing a revision renders real markup in the body, not escaped entities" do + login_as :quentin + node = Node.root.children.create!(:slug => "show_markup_test") + draft = node.draft + draft.body = "

Hello

" + node.publish_draft! + + get :show, params: { :node_id => node.id, :id => node.head.id } + assert_response :success + assert_select ".node_content h3", "Hello" + end + test "diffing two revisions" do login_as :quentin post( :diff, params: { :node_id => @node.id, - :start_revision => @node.pages.first.revision, + :start_revision => @node.pages.first.revision, :end_revision => @node.pages.last.revision } ) -- cgit v1.3 From 81769557804ce59ce3e4603e8f47e731b61b180d Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 10 Jul 2026 00:44:41 +0200 Subject: Whitespace fixes --- test/controllers/revisions_controller_test.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'test/controllers/revisions_controller_test.rb') diff --git a/test/controllers/revisions_controller_test.rb b/test/controllers/revisions_controller_test.rb index 46fc220..caca6bf 100644 --- a/test/controllers/revisions_controller_test.rb +++ b/test/controllers/revisions_controller_test.rb @@ -1,12 +1,12 @@ require 'test_helper' class RevisionsControllerTest < ActionController::TestCase - + def setup Node.root.descendants.destroy_all @user = User.find_by_login("aaron") @node = Node.root.children.create!( :slug => "version_me" ) - + draft = @node.draft draft.body = "first" @node.publish_draft! @@ -15,13 +15,13 @@ class RevisionsControllerTest < ActionController::TestCase draft.update(:body => "second") @node.publish_draft! end - + test "setup" do assert_equal 2, Node.count assert_equal 2, @node.pages.count assert_equal ["first", "second"], @node.pages.map {|p| p.body} end - + test "get list of revisions for a given node" do login_as :quentin get :index, params: { :node_id => @node.id } @@ -60,13 +60,13 @@ class RevisionsControllerTest < ActionController::TestCase ) assert_response :success end - + test "restoring a revision" do assert_equal "second", @node.head.body - + login_as :aaron put( :restore, params: { :node_id => @node.id, :id => @node.pages.first.id } ) - + @node.reload assert_equal @node.head, @node.pages.first assert_equal "first", @node.head.reload.body -- cgit v1.3 From 205e6216fc7850fe717122c189e5003d1f9e8afe Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 10 Jul 2026 02:03:19 +0200 Subject: Add head/draft/autosave layer comparison at three UI entry points Node#resolve_page_reference and #available_layer_pairs let Page#diff_against compare named layers (head/draft/autosave), not just numbered revisions -- autosave was never part of Node#pages, so this was the missing piece. Wired into nodes#show's Status section, nodes#edit right after an autosave gets resurrected ("What changed?"), and the admin wizard's current-drafts table, which now also lists autosave-only nodes it previously never showed. revisions#diff hides the numbered-revision picker when comparing named layers (it can't represent them), shows a plain label instead, and offers buttons to switch between whichever other pairs make sense for the node's current state. Destroying the topmost layer is available directly from the diff view, reusing the existing revert! path. "Discard changes" is renamed "Discard Autosave" everywhere it appears, to match "Destroy Draft". --- app/controllers/admin_controller.rb | 4 +-- app/controllers/nodes_controller.rb | 2 +- app/controllers/revisions_controller.rb | 16 ++++++++--- app/helpers/revisions_helper.rb | 5 ++++ app/models/node.rb | 21 ++++++++++++++ app/views/admin/index.html.erb | 13 +++++++-- app/views/nodes/edit.html.erb | 9 +++++- app/views/nodes/show.html.erb | 12 +++++++- app/views/revisions/diff.html.erb | 40 +++++++++++++++++++++++---- config/routes.rb | 1 + test/controllers/admin_controller_test.rb | 15 ++++++++-- test/controllers/revisions_controller_test.rb | 39 ++++++++++++++++++++++++++ test/models/node_test.rb | 22 +++++++++++++++ 13 files changed, 179 insertions(+), 20 deletions(-) (limited to 'test/controllers/revisions_controller_test.rb') diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 3fa0519..6ab2135 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -5,10 +5,10 @@ class AdminController < ApplicationController before_action :login_required def index - @drafts = Node.where("draft_id IS NOT NULL") + @drafts = Node.where("draft_id IS NOT NULL OR autosave_id IS NOT NULL") .limit(50).order("updated_at desc") - @drafts_count = Node.where("draft_id IS NOT NULL").count + @drafts_count = Node.where("draft_id IS NOT NULL OR autosave_id IS NOT NULL").count @recent_changes = Node.where( "updated_at < ? AND updated_at > ? AND parent_id IS NOT NULL", diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb index 38d42d9..d1538e1 100644 --- a/app/controllers/nodes_controller.rb +++ b/app/controllers/nodes_controller.rb @@ -72,7 +72,7 @@ class NodesController < ApplicationController if @node.autosave flash.now[:notice] = "This page has unsaved changes from a previous session, shown below. " \ - "Save to keep them, or use \"Discard changes\" below to go back to the last saved version." + "Save to keep them, or use \"Discard Autosave\" below to go back to the last saved version." elsif freshly_locked flash.now[:notice] = "Node locked and ready to edit" end diff --git a/app/controllers/revisions_controller.rb b/app/controllers/revisions_controller.rb index 9acb26f..4b0c549 100644 --- a/app/controllers/revisions_controller.rb +++ b/app/controllers/revisions_controller.rb @@ -21,10 +21,18 @@ class RevisionsController < ApplicationController params[:start_revision], params[:end_revision] = 1, 1 end - @start = @node.pages.find_by_revision( params[:start_revision] ) - @end = @node.pages.find_by_revision( params[:end_revision] ) - @diff_view = params[:view] == "side_by_side" ? :side_by_side : :inline - @diff = @end.diff_against( @start, view: @diff_view ) + @start = @node.resolve_page_reference(params[:start_revision]) + @end = @node.resolve_page_reference(params[:end_revision]) + + if @start.nil? || @end.nil? + flash[:error] = "That comparison is no longer available." + redirect_to(node_path(@node)) and return + end + + @diff_view = params[:view] == "side_by_side" ? :side_by_side : :inline + @diff = @end.diff_against(@start, view: @diff_view) + @available_layer_pairs = @node.available_layer_pairs + @locked_by_other = @node.locked? && @node.lock_owner != current_user end def show diff --git a/app/helpers/revisions_helper.rb b/app/helpers/revisions_helper.rb index fdb51f8..a629013 100644 --- a/app/helpers/revisions_helper.rb +++ b/app/helpers/revisions_helper.rb @@ -1,2 +1,7 @@ module RevisionsHelper + # Human-readable label for a diff endpoint -- "head"/"draft"/"autosave" + # get their name; anything else is a revision number. + def describe_page_reference(ref) + %w[head draft autosave].include?(ref.to_s) ? ref.to_s.capitalize : "revision #{ref}" + end end diff --git a/app/models/node.rb b/app/models/node.rb index 7675ab6..82d9954 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -123,6 +123,27 @@ class Node < ApplicationRecord self.draft.reload end + def resolve_page_reference ref + case ref.to_s + when "head" then head + when "draft" then draft + when "autosave" then autosave + else pages.find_by_revision(ref) + end + end + + # Which layer-pairs are meaningful to compare right now, given this + # node's actual state. Head vs autosave only shows up when no draft + # sits between them -- with a draft present, autosave is compared + # against the draft, never past it straight to head. + def available_layer_pairs + pairs = [] + pairs << [:head, :draft] if head && draft + pairs << [:draft, :autosave] if draft && autosave + pairs << [:head, :autosave] if head && autosave && !draft + pairs + end + def find_or_create_draft current_user self.wipe_draft! if draft && self.lock_owner == current_user diff --git a/app/views/admin/index.html.erb b/app/views/admin/index.html.erb index 77b45f4..c67ccb3 100644 --- a/app/views/admin/index.html.erb +++ b/app/views/admin/index.html.erb @@ -82,9 +82,9 @@
- -

Current Drafts (<%= @drafts_count %>)

- + +

Current Drafts & Autosaves (<%= @drafts_count %>)

+ @@ -92,6 +92,7 @@ + <% @drafts.each do |node| %> "> @@ -103,6 +104,9 @@ + <% end %>
IDActions Locked by Rev.Autosave
<%= link_to 'show', node_path(node) %> <%= link_to 'Revisions', node_revisions_path(node) %> + <% if pair = node.available_layer_pairs.last %> + <%= link_to 'diff', diff_node_revisions_path(node, start_revision: pair.first, end_revision: pair.last) %> + <% end %> <%= node.lock_owner.login if node.lock_owner %> @@ -110,6 +114,9 @@ <%= link_to ( node.draft ? node.draft.revision : (node.head ? node.head.revision : "EMPTY" ) ), node_revisions_path(node) %> + <%= node.autosave ? "unsaved changes" : "" %> +
diff --git a/app/views/nodes/edit.html.erb b/app/views/nodes/edit.html.erb index cdc9b36..1c19410 100644 --- a/app/views/nodes/edit.html.erb +++ b/app/views/nodes/edit.html.erb @@ -6,9 +6,16 @@ disabled: @node.autosave.present? %> <% if @node.autosave || (@node.draft && @node.head) %> - <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard changes'), + <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard Autosave'), revert_node_path(@node), method: :put, form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } %> + <% if pair = @node.available_layer_pairs.find { |p| p.include?(:autosave) } %> + <%= button_to 'What changed?', + diff_node_revisions_path(@node), + method: :get, + params: { start_revision: pair.first, end_revision: pair.last }, + form: { class: 'button_to computation' } %> + <% end %> <% end %> <%= submit_tag "Save Draft", form: dom_id(@node, :edit) %> diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb index 036caf2..2469310 100644 --- a/app/views/nodes/show.html.erb +++ b/app/views/nodes/show.html.erb @@ -32,6 +32,16 @@ <% end %>
+ <% @node.available_layer_pairs.each do |pair| %> +
+ <%= button_to "Diff #{pair.first.to_s.capitalize} vs. #{pair.last.to_s.capitalize}", + diff_node_revisions_path(@node), + method: :get, + params: { start_revision: pair.first, end_revision: pair.last }, + form: { class: 'button_to computation' } %> +
+ <% end %> + <% unless locked_by_other %> <% if @node.draft && !@node.autosave %>
@@ -41,7 +51,7 @@ <% end %> <% if @node.draft || @node.autosave %>
- <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard changes'), + <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard Autosave'), revert_node_path(@node), method: :put, form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } %>
diff --git a/app/views/revisions/diff.html.erb b/app/views/revisions/diff.html.erb index d7bb528..3157dca 100644 --- a/app/views/revisions/diff.html.erb +++ b/app/views/revisions/diff.html.erb @@ -4,11 +4,41 @@ <%= link_to 'Revisions', node_revisions_path(@node) %>

-<%= form_tag diff_node_revisions_path do %> - <%= select_tag :start_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:start_revision].to_i) %> - <%= select_tag :end_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:end_revision].to_i) %> - <%= select_tag :view, options_for_select([['Inline', 'inline'], ['Side by side', 'side_by_side']], @diff_view) %> - <%= submit_tag 'Diff' %> +

+ Comparing <%= describe_page_reference(params[:start_revision]) %> + against <%= describe_page_reference(params[:end_revision]) %> +

+ +<% numeric_comparison = params[:start_revision].to_s =~ /\A\d+\z/ && params[:end_revision].to_s =~ /\A\d+\z/ %> + +<% if numeric_comparison %> + <%= form_tag diff_node_revisions_path do %> + <%= select_tag :start_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:start_revision].to_i) %> + <%= select_tag :end_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:end_revision].to_i) %> + <%= select_tag :view, options_for_select([['Inline', 'inline'], ['Side by side', 'side_by_side']], @diff_view) %> + <%= submit_tag 'Diff' %> + <% end %> +<% else %> +

<%= link_to 'Compare two numbered revisions instead', node_revisions_path(@node) %>

+<% end %> + +<% if @available_layer_pairs.present? %> +

+ <% @available_layer_pairs.each do |pair| %> + <% next if [params[:start_revision].to_s, params[:end_revision].to_s].sort == pair.map(&:to_s).sort %> + <%= button_to "Diff #{pair.first.to_s.capitalize} vs. #{pair.last.to_s.capitalize}", + diff_node_revisions_path(@node), + method: :get, + params: { start_revision: pair.first, end_revision: pair.last, view: @diff_view }, + form: { class: 'button_to computation' } %> + <% end %> + + <% if !@locked_by_other && (@node.autosave || @node.draft) %> + <%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard Autosave'), + revert_node_path(@node), method: :put, + form: { data: { confirm: "This cannot be undone. Continue?" }, class: 'button_to destructive' } %> + <% end %> +

<% end %>
diff --git a/config/routes.rb b/config/routes.rb index c0aef2f..da6b626 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -47,6 +47,7 @@ Cccms::Application.routes.draw do resources :revisions do collection do post :diff + get :diff end member do put :restore diff --git a/test/controllers/admin_controller_test.rb b/test/controllers/admin_controller_test.rb index 9bbf29b..d6005ba 100644 --- a/test/controllers/admin_controller_test.rb +++ b/test/controllers/admin_controller_test.rb @@ -1,8 +1,17 @@ require 'test_helper' class AdminControllerTest < ActionController::TestCase - # Replace this with your real tests. - test "the truth" do - assert true + test "current drafts includes nodes with only an autosave" do + node = Node.root.children.create!(:slug => "admin_autosave_only") + node.lock_for_editing!(User.find_by_login("aaron")) + node.autosave!({title: "in progress"}, User.find_by_login("aaron")) + node.save_draft!(User.find_by_login("aaron")) + node.publish_draft! + node.lock_for_editing!(User.find_by_login("aaron")) + node.autosave!({title: "editing again"}, User.find_by_login("aaron")) + + login_as :quentin + get :index + assert_includes assigns(:drafts), node end end diff --git a/test/controllers/revisions_controller_test.rb b/test/controllers/revisions_controller_test.rb index caca6bf..162e6f1 100644 --- a/test/controllers/revisions_controller_test.rb +++ b/test/controllers/revisions_controller_test.rb @@ -100,4 +100,43 @@ class RevisionsControllerTest < ActionController::TestCase assert_response :success assert_select ".diff_column", 2 end + + test "diffing head against draft by name" do + login_as :quentin + @node.find_or_create_draft(@user) + @node.draft.update(:body => "draft body") + + post(:diff, params: { :node_id => @node.id, :start_revision => "head", :end_revision => "draft" }) + assert_response :success + end + + test "diffing a layer pair that no longer exists redirects with a flash" do + login_as :quentin + post(:diff, params: { :node_id => @node.id, :start_revision => "draft", :end_revision => "autosave" }) + assert_redirected_to node_path(@node) + assert flash[:error].present? + end + + test "diffing by name shows a clear comparison label instead of a misleading revision picker" do + login_as :quentin + @node.find_or_create_draft(@user) + + post(:diff, params: { :node_id => @node.id, :start_revision => "head", :end_revision => "draft" }) + assert_response :success + assert_select "strong", "Head" + assert_select "strong", "Draft" + assert_select "select[name=?]", "start_revision", :count => 0 + end + + test "pair-switcher buttons carry their params as real hidden fields, not a query string" do + login_as :quentin + @node.find_or_create_draft(@user) + @node.lock_for_editing!(@user) + @node.autosave!({ :body => "unsaved" }, @user) + + post(:diff, params: { :node_id => @node.id, :start_revision => "head", :end_revision => "draft" }) + assert_response :success + assert_select "form.computation input[type=hidden][name=start_revision]" + assert_select "form.computation input[type=hidden][name=end_revision]" + end end diff --git a/test/models/node_test.rb b/test/models/node_test.rb index 2138c19..9e71dec 100644 --- a/test/models/node_test.rb +++ b/test/models/node_test.rb @@ -473,4 +473,26 @@ class NodeTest < ActiveSupport::TestCase node.publish_draft! end end + + test "available_layer_pairs matches the six-state table" do + node = Node.root.children.create!(:slug => "layer_pairs_test") + user = @user1 || User.find_by_login("aaron") + + assert_equal [[:draft, :autosave]], (node.lock_for_editing!(user); node.autosave!({title: "v1"}, user); node.available_layer_pairs) # state F + + node.save_draft!(user) + node.publish_draft! + assert_equal [], node.available_layer_pairs # state A + + node.lock_for_editing!(user) + node.autosave!({title: "v2"}, user) + assert_equal [[:head, :autosave]], node.available_layer_pairs # state B + + node.save_draft!(user) + assert_equal [[:head, :draft]], node.available_layer_pairs # state C + + node.lock_for_editing!(user) + node.autosave!({title: "v3"}, user) + assert_equal [[:head, :draft], [:draft, :autosave]], node.available_layer_pairs # state D + end end -- cgit v1.3 From cf730b751b590ca80695492f5d62c4fb43efb4d3 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 10 Jul 2026 03:40:48 +0200 Subject: Ensure that comparison view toggle is there for all diff modes --- app/views/revisions/diff.html.erb | 17 ++++++++++++++++- public/stylesheets/admin.css | 1 + test/controllers/revisions_controller_test.rb | 9 +++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) (limited to 'test/controllers/revisions_controller_test.rb') diff --git a/app/views/revisions/diff.html.erb b/app/views/revisions/diff.html.erb index 490cf17..d70503c 100644 --- a/app/views/revisions/diff.html.erb +++ b/app/views/revisions/diff.html.erb @@ -9,13 +9,28 @@ against <%= describe_page_reference(params[:end_revision]) %>

+

+ View: + <% if @diff_view == :inline %> + Inline + <% else %> + <%= link_to 'Inline', diff_node_revisions_path(@node, start_revision: params[:start_revision], end_revision: params[:end_revision], view: 'inline') %> + <% end %> + · + <% if @diff_view == :side_by_side %> + Side by side + <% else %> + <%= link_to 'Side by side', diff_node_revisions_path(@node, start_revision: params[:start_revision], end_revision: params[:end_revision], view: 'side_by_side') %> + <% end %> +

+ <% numeric_comparison = params[:start_revision].to_s =~ /\A\d+\z/ && params[:end_revision].to_s =~ /\A\d+\z/ %> <% if numeric_comparison %> <%= form_tag diff_node_revisions_path do %> <%= select_tag :start_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:start_revision].to_i) %> <%= select_tag :end_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:end_revision].to_i) %> - <%= select_tag :view, options_for_select([['Inline', 'inline'], ['Side by side', 'side_by_side']], @diff_view) %> + <%= hidden_field_tag :view, @diff_view %> <%= submit_tag 'Diff' %> <% end %> <% else %> diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index 56470bc..510e3cc 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -32,6 +32,7 @@ table.user_table a, table.revisions_table a, .add_child_links a, .standalone_action_bar a, +.diff_view_toggle a, div#draft_list table td.actions a { text-decoration: underline; -webkit-text-decoration-style: wavy; diff --git a/test/controllers/revisions_controller_test.rb b/test/controllers/revisions_controller_test.rb index 162e6f1..bf92c5b 100644 --- a/test/controllers/revisions_controller_test.rb +++ b/test/controllers/revisions_controller_test.rb @@ -139,4 +139,13 @@ class RevisionsControllerTest < ActionController::TestCase assert_select "form.computation input[type=hidden][name=start_revision]" assert_select "form.computation input[type=hidden][name=end_revision]" end + + test "the view toggle is available even when comparing named layers" do + login_as :quentin + @node.find_or_create_draft(@user) + + post(:diff, params: { :node_id => @node.id, :start_revision => "head", :end_revision => "draft" }) + assert_response :success + assert_select "a", "Side by side" + end end -- cgit v1.3 From 30ed9fd9a8bffb44e6ab91dfedb8c0e33837769a Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 10 Jul 2026 04:41:54 +0200 Subject: Include assets, tags and template in diff between revisions --- app/models/page.rb | 33 ++++++++++++-------- app/views/revisions/diff.html.erb | 27 +++++++++++++++++ public/stylesheets/admin.css | 11 +++++++ test/controllers/revisions_controller_test.rb | 13 ++++++++ test/models/page_test.rb | 43 +++++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 13 deletions(-) (limited to 'test/controllers/revisions_controller_test.rb') diff --git a/app/models/page.rb b/app/models/page.rb index ea04cd6..740d42e 100644 --- a/app/models/page.rb +++ b/app/models/page.rb @@ -176,19 +176,26 @@ class Page < ApplicationRecord end def diff_against other, view: :inline - if view == :side_by_side - { - title: HtmlWordDiff.side_by_side(other.title.to_s, title.to_s), - abstract: HtmlWordDiff.side_by_side(other.abstract.to_s, abstract.to_s), - body: HtmlWordDiff.side_by_side(other.body.to_s, body.to_s) - } - else - { - title: HtmlWordDiff.inline(other.title.to_s, title.to_s), - abstract: HtmlWordDiff.inline(other.abstract.to_s, abstract.to_s), - body: HtmlWordDiff.inline(other.body.to_s, body.to_s) - } - end + text_diffs = + if view == :side_by_side + { + title: HtmlWordDiff.side_by_side(other.title.to_s, title.to_s), + abstract: HtmlWordDiff.side_by_side(other.abstract.to_s, abstract.to_s), + body: HtmlWordDiff.side_by_side(other.body.to_s, body.to_s) + } + else + { + title: HtmlWordDiff.inline(other.title.to_s, title.to_s), + abstract: HtmlWordDiff.inline(other.abstract.to_s, abstract.to_s), + body: HtmlWordDiff.inline(other.body.to_s, body.to_s) + } + end + + text_diffs.merge( + tags: { added: tag_list.to_a - other.tag_list.to_a, removed: other.tag_list.to_a - tag_list.to_a }, + template_name: { from: other.template_name, to: template_name, changed: template_name != other.template_name }, + assets: { added: assets.to_a - other.assets.to_a, removed: other.assets.to_a - assets.to_a } + ) end def public? diff --git a/app/views/revisions/diff.html.erb b/app/views/revisions/diff.html.erb index d70503c..b9ce6bd 100644 --- a/app/views/revisions/diff.html.erb +++ b/app/views/revisions/diff.html.erb @@ -85,4 +85,31 @@

Body

<%= raw @diff[:body] %> <% end %> + +

Tags

+ <% if @diff[:tags][:added].empty? && @diff[:tags][:removed].empty? %> +

No change.

+ <% else %> +
    + <% @diff[:tags][:added].each do |tag| %>
  • <%= tag %>
  • <% end %> + <% @diff[:tags][:removed].each do |tag| %>
  • <%= tag %>
  • <% end %> +
+ <% end %> + +

Template

+ <% if @diff[:template_name][:changed] %> +

<%= @diff[:template_name][:from] || '(none)' %> <%= @diff[:template_name][:to] || '(none)' %>

+ <% else %> +

No change.

+ <% end %> + +

Assets

+ <% if @diff[:assets][:added].empty? && @diff[:assets][:removed].empty? %> +

No change.

+ <% else %> +
    + <% @diff[:assets][:added].each do |asset| %>
  • <%= asset.upload_file_name %>
  • <% end %> + <% @diff[:assets][:removed].each do |asset| %>
  • <%= asset.upload_file_name %>
  • <% end %> +
+ <% end %>
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css index e04499d..7b39c72 100644 --- a/public/stylesheets/admin.css +++ b/public/stylesheets/admin.css @@ -549,6 +549,17 @@ table.revisions_table tr:hover { cursor: help; } +.diff_unchanged { + color: #969696; + font-style: italic; +} + +.diff_set_list { + list-style: none; + padding-left: 0; + margin: 0; +} + table.user_table td.user_login { padding-right: 30px; } diff --git a/test/controllers/revisions_controller_test.rb b/test/controllers/revisions_controller_test.rb index bf92c5b..53927b4 100644 --- a/test/controllers/revisions_controller_test.rb +++ b/test/controllers/revisions_controller_test.rb @@ -148,4 +148,17 @@ class RevisionsControllerTest < ActionController::TestCase assert_response :success assert_select "a", "Side by side" end + + test "diffing two revisions also shows tag, template, and asset changes" do + login_as :quentin + @node.find_or_create_draft(@user) + @node.draft.tag_list = "update" + @node.draft.save! + + post(:diff, params: { :node_id => @node.id, :start_revision => @node.pages.first.revision, :end_revision => @node.pages.last.revision }) + assert_response :success + assert_select "h3", "Tags" + assert_select "h3", "Template" + assert_select "h3", "Assets" + end end diff --git a/test/models/page_test.rb b/test/models/page_test.rb index 3868a53..bcddc89 100644 --- a/test/models/page_test.rb +++ b/test/models/page_test.rb @@ -228,4 +228,47 @@ class PageTest < ActiveSupport::TestCase assert_equal 2, fragment.css('ins.diff_structural').length assert_match "der Zugang erfolgt über den Hinterhof.", fragment.text end + + test "diff_against reports tag and template changes" do + n = Node.root.children.create! :slug => "field_diff_test" + d = n.find_or_create_draft @user1 + d.tag_list = "update" + d.template_name = "standard_template" + d.save! + n.publish_draft! + + d2 = n.find_or_create_draft @user1 + d2.tag_list = "update, pressemitteilung" + d2.template_name = "title_only" + d2.save! + + diff = d2.diff_against(n.head) + + assert_equal ["pressemitteilung"], diff[:tags][:added] + assert_equal [], diff[:tags][:removed] + assert diff[:template_name][:changed] + assert_equal "standard_template", diff[:template_name][:from] + assert_equal "title_only", diff[:template_name][:to] + end + + test "diff_against reports added and removed assets by filename" do + n = Node.root.children.create! :slug => "asset_diff_test" + d = n.find_or_create_draft @user1 + d.save! + n.publish_draft! + + kept_asset = Asset.create!(:upload_file_name => "kept.png", :upload_content_type => "image/png", :upload_file_size => 1) + removed_asset = Asset.create!(:upload_file_name => "removed.pdf", :upload_content_type => "application/pdf", :upload_file_size => 1) + n.head.update_assets([kept_asset.id, removed_asset.id]) + + d2 = n.find_or_create_draft @user1 + added_asset = Asset.create!(:upload_file_name => "added.png", :upload_content_type => "image/png", :upload_file_size => 1) + d2.update_assets([kept_asset.id, added_asset.id]) + d2.save! + + diff = d2.diff_against(n.head) + + assert_equal [added_asset], diff[:assets][:added] + assert_equal [removed_asset], diff[:assets][:removed] + end end -- cgit v1.3 From d12598f17547d6b6ad817f32bc6419556ecdf019 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 10 Jul 2026 04:44:58 +0200 Subject: Add test for backlink in revisions#index --- test/controllers/revisions_controller_test.rb | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'test/controllers/revisions_controller_test.rb') diff --git a/test/controllers/revisions_controller_test.rb b/test/controllers/revisions_controller_test.rb index 53927b4..76d4e8c 100644 --- a/test/controllers/revisions_controller_test.rb +++ b/test/controllers/revisions_controller_test.rb @@ -161,4 +161,11 @@ class RevisionsControllerTest < ActionController::TestCase assert_select "h3", "Template" assert_select "h3", "Assets" end + + test "revisions#index links back to the node" do + login_as :quentin + get :index, params: { :node_id => @node.id } + assert_response :success + assert_select "a[href=?]", node_path(@node) + end end -- cgit v1.3 From ba789678f9e8ed189413afb1446ad210df34e488 Mon Sep 17 00:00:00 2001 From: erdgeist Date: Mon, 13 Jul 2026 19:12:56 +0200 Subject: Make revision diffs locale-aware, defaulting to whichever locale changed diff_against compared title/abstract/body under whatever I18n.locale happened to be ambient, with no concept of 'diff this translation specifically' -- so a change confined to one locale was invisible to Diff Head vs. Draft regardless of which locale you were looking at when you clicked it, exactly the 'yields nothing' complaint from earlier this session. Page#diff_against gains a locale: keyword, additive only -- nil preserves the exact original ambient-locale behavior every existing caller and test already depends on; passing a locale switches to reading each side's actual PageTranslation row directly, same fallback-free reasoning as Page#translation_summary. Page#locale_diff_summary reports one entry per locale present on either side, so an added or removed translation counts as a change even where content matches everywhere it exists on both. RevisionsController#diff now resolves a real locale before diffing -- defaulting to whichever locale actually changed, falling back to the default locale only when nothing did -- and the view carries that locale through every existing control (view toggle, layer-pair buttons, the revision-select form) so it and the view/layer-pair axis stay independently selectable rather than resetting each other. --- app/controllers/revisions_controller.rb | 7 +++++- app/views/revisions/diff.html.erb | 25 +++++++++++++++++---- test/controllers/revisions_controller_test.rb | 32 +++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) (limited to 'test/controllers/revisions_controller_test.rb') diff --git a/app/controllers/revisions_controller.rb b/app/controllers/revisions_controller.rb index 4b0c549..0aed243 100644 --- a/app/controllers/revisions_controller.rb +++ b/app/controllers/revisions_controller.rb @@ -29,8 +29,13 @@ class RevisionsController < ApplicationController redirect_to(node_path(@node)) and return end + @locale_summary = @end.locale_diff_summary(@start) + requested_locale = params[:locale].presence&.to_sym + default_locale = @locale_summary.find { |s| s[:changed] }&.dig(:locale) || I18n.default_locale + @locale = @locale_summary.any? { |s| s[:locale] == requested_locale } ? requested_locale : default_locale + @diff_view = params[:view] == "side_by_side" ? :side_by_side : :inline - @diff = @end.diff_against(@start, view: @diff_view) + @diff = @end.diff_against(@start, view: @diff_view, locale: @locale) @available_layer_pairs = @node.available_layer_pairs @locked_by_other = @node.locked? && @node.lock_owner != current_user end diff --git a/app/views/revisions/diff.html.erb b/app/views/revisions/diff.html.erb index 5638b7a..149b17c 100644 --- a/app/views/revisions/diff.html.erb +++ b/app/views/revisions/diff.html.erb @@ -7,20 +7,37 @@

Comparing <%= describe_page_reference(params[:start_revision]) %> against <%= describe_page_reference(params[:end_revision]) %> + — showing the <%= @locale.to_s.upcase %> translation

+<% if @locale_summary.size > 1 %> +

+ Locale: + <% @locale_summary.each_with_index do |s, i| %> + <%= " · ".html_safe if i > 0 %> + <% label = "#{s[:locale].to_s.upcase}#{' *' if s[:changed]}" %> + <% if s[:locale] == @locale %> + <%= label %> + <% else %> + <%= link_to label, diff_node_revisions_path(@node, start_revision: params[:start_revision], end_revision: params[:end_revision], view: @diff_view, locale: s[:locale]) %> + <% end %> + <% end %> +

+

* has a difference between these two versions.

+<% end %> +

View: <% if @diff_view == :inline %> Inline <% else %> - <%= link_to 'Inline', diff_node_revisions_path(@node, start_revision: params[:start_revision], end_revision: params[:end_revision], view: 'inline') %> + <%= link_to 'Inline', diff_node_revisions_path(@node, start_revision: params[:start_revision], end_revision: params[:end_revision], view: 'inline', locale: @locale) %> <% end %> · <% if @diff_view == :side_by_side %> Side by side <% else %> - <%= link_to 'Side by side', diff_node_revisions_path(@node, start_revision: params[:start_revision], end_revision: params[:end_revision], view: 'side_by_side') %> + <%= link_to 'Side by side', diff_node_revisions_path(@node, start_revision: params[:start_revision], end_revision: params[:end_revision], view: 'side_by_side', locale: @locale) %> <% end %>

@@ -31,6 +48,7 @@ <%= select_tag :start_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:start_revision].to_i) %> <%= select_tag :end_revision, options_for_select(@node.pages.map{|x| x.revision}, params[:end_revision].to_i) %> <%= hidden_field_tag :view, @diff_view %> + <%= hidden_field_tag :locale, @locale %> <%= submit_tag 'Diff' %> <% end %> <% else %> @@ -44,10 +62,9 @@ <%= button_to "Diff #{pair.first.to_s.capitalize} vs. #{pair.last.to_s.capitalize}", diff_node_revisions_path(@node), method: :get, - params: { start_revision: pair.first, end_revision: pair.last, view: @diff_view }, + params: { start_revision: pair.first, end_revision: pair.last, view: @diff_view, locale: @locale }, form: { class: 'button_to computation' } %> <% end %> - <% if !@locked_by_other && (@node.autosave || @node.draft) %> <%= button_to revert_node_path(@node), method: :put, params: { return_to: request.fullpath }, diff --git a/test/controllers/revisions_controller_test.rb b/test/controllers/revisions_controller_test.rb index 76d4e8c..e5726f7 100644 --- a/test/controllers/revisions_controller_test.rb +++ b/test/controllers/revisions_controller_test.rb @@ -168,4 +168,36 @@ class RevisionsControllerTest < ActionController::TestCase assert_response :success assert_select "a[href=?]", node_path(@node) end + + test "diff defaults to a locale that actually changed, not the ambient default" do + login_as :quentin + node = Node.root.children.create!(:slug => "diff_default_locale_test") + node.draft.save! + node.publish_draft! + + node.find_or_create_draft(@user) + Globalize.with_locale(:en) { node.draft.update!(:title => "Changed in English only") } + node.draft.save! + + post(:diff, params: { :node_id => node.id, :start_revision => "head", :end_revision => "draft" }) + + assert_response :success + assert_match(/Changed/, response.body) + end + + test "diff respects an explicitly requested locale over the auto-detected one" do + login_as :quentin + node = Node.root.children.create!(:slug => "diff_explicit_locale_test") + node.draft.save! + node.publish_draft! + + node.find_or_create_draft(@user) + Globalize.with_locale(:en) { node.draft.update!(:title => "English changed") } + node.draft.save! + + post(:diff, params: { :node_id => node.id, :start_revision => "head", :end_revision => "draft", :locale => "de" }) + + assert_response :success + assert_no_match(/English changed/, response.body) + end end -- cgit v1.3