From 206dc5c50a73c5402b90d7fdc8945d3ba9356758 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Sat, 4 Jul 2026 01:24:55 +0200
Subject: Add self-service event creation from nodes#show
nodes#show's events table now renders unconditionally (previously
hidden entirely for a zero-event node) and gains an "add event" link.
The suggested tag_list is derived from the page's own category tags
via NodesHelper::DEFAULT_EVENT_TAG_BY_PAGE_TAG (erfa-detail/
chaostreff-detail -> open-day) rather than hardcoded, and degrades to
a blank field for any node whose tags don't match - deliberately
universal, not chapter-specific, since Updates have historically
carried event dates the same way.
events#new surfaces *why* the tag was pre-filled via flash.now (not
flash - this is a same-request render, not a redirect), using an
explicit auto_tag_source param passed alongside tag_list rather than
having the controller re-derive the reason from node_id.
No destroy link added to nodes#show's events list - deliberate, per
existing subnav-semantics convention (destructive actions live on the
resource's own views). Covered directly by test.
Adds real coverage for EventsController, previously 100% commented-out
scaffold, plus unit tests for the new tag-mapping helper and the
weekday-abbreviation helpers from the prior commit.
---
test/controllers/nodes_controller_test.rb | 33 +++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
(limited to 'test/controllers/nodes_controller_test.rb')
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index 53799f1..f14e27c 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -379,4 +379,37 @@ class NodesControllerTest < ActionController::TestCase
get :index
end
+ test "show renders events row and add-link for zero-event chapter node" do
+ login_as :quentin
+ node = create_node_with_published_page
+ node.head.tag_list = "erfa-detail"
+ node.head.save!
+
+ get :show, params: { id: node.id }
+ assert_response :success
+ assert_select "a", text: "add event"
+ assert_select "a[href*='tag_list=open-day']"
+ assert_select "a[href*='auto_tag_source=erfa-detail']"
+ end
+
+ test "show renders events row without a tag default for untagged node" do
+ login_as :quentin
+ node = create_node_with_published_page
+
+ get :show, params: { id: node.id }
+ assert_response :success
+ assert_select "a", text: "add event"
+ assert_select "a[href*='tag_list=']", count: 0
+ end
+
+ test "show never renders a destroy link for events" do
+ login_as :quentin
+ node = create_node_with_published_page
+ Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour)
+
+ get :show, params: { id: node.id }
+ assert_response :success
+ assert_select "form.button_to.destructive", count: 0
+ end
+
end
--
cgit v1.3
From 394a2b890686cadd7a3ecb0038ce5d0b744431f4 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Mon, 6 Jul 2026 04:34:04 +0200
Subject: Strengthen event/occurrence cascade test with real assertions
test_can_remove_a_node_with_an_event previously just called node.destroy
and get :index with no assertions at all - would pass whether or not
occurrences were actually cleaned up, or even if index rendered
correctly afterward. Now confirms occurrences genuinely exist before
destroy (otherwise a passing post-destroy count of zero is meaningless -
indistinguishable from "nothing to cascade in the first place"), scopes
the count to this event specifically rather than a global Occurrence.count
that could coincidentally pass regardless of whether this cascade works,
and checks the trailing index request actually succeeds rather than just
not raising.
First real test of the occurrences.event_id FK constraint added earlier
this session, not just the application-level dependent: :destroy.
---
test/controllers/nodes_controller_test.rb | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
(limited to 'test/controllers/nodes_controller_test.rb')
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index f14e27c..99e06ae 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -364,7 +364,7 @@ class NodesControllerTest < ActionController::TestCase
test "can remove a node with an event" do
node = create_node_with_published_page
- Event.create!(
+ event = Event.create!(
:start_time => "2009-01-01T15:23:42".to_time,
:end_time => "2009-01-01T20:05:23".to_time,
:url => "http://events.ccc.de/congress/2082",
@@ -373,10 +373,16 @@ class NodesControllerTest < ActionController::TestCase
:allday => true,
:node_id => node.id
)
+ event_id = event.id
+ assert_operator Occurrence.where(event_id: event_id).count, :>, 0, "expected the event to have generated at least one occurrence before destroy"
+
node.destroy
+ assert_equal 0, Occurrence.where(event_id: event_id).count
+
login_as :quentin
get :index
+ assert_response :success
end
test "show renders events row and add-link for zero-event chapter node" do
--
cgit v1.3
From 1393b3de31d95b1aa5122d6da37bd3259830bdb6 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Mon, 6 Jul 2026 16:51:27 +0200
Subject: Fix test to match the current layout
---
test/controllers/nodes_controller_test.rb | 126 +++++++++++++++---------------
1 file changed, 63 insertions(+), 63 deletions(-)
(limited to 'test/controllers/nodes_controller_test.rb')
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index 99e06ae..61f7db4 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -9,13 +9,13 @@ class NodesControllerTest < ActionController::TestCase
get :index
assert_response :success
end
-
+
def test_new
login_as :quentin
get :new
assert_response :success
end
-
+
test "create generic node with parent_id provided" do
login_as :quentin
before_count = Node.count
@@ -33,7 +33,7 @@ class NodesControllerTest < ActionController::TestCase
assert_equal Node.last.parent_id, Node.root.id
assert_equal 1, Node.last.level
end
-
+
test "create update node" do
login_as :quentin
post(
@@ -45,7 +45,7 @@ class NodesControllerTest < ActionController::TestCase
)
assert_response :redirect
end
-
+
test "create top level node" do
login_as :quentin
before_count = Node.count
@@ -62,64 +62,64 @@ class NodesControllerTest < ActionController::TestCase
assert_equal expected, Node.last.unique_name
assert_equal 1, Node.last.level
end
-
+
test "creating a top_level node without a title should not work" do
login_as :quentin
-
+
assert_no_difference "Node.count" do
post(:create, params: { :kind => "top_level" } )
end
end
-
+
test "creating a generic node without a parent_id should not work" do
login_as :quentin
-
+
assert_no_difference "Node.count" do
post(:create, params: { :kind => "generic" } )
end
end
-
+
test "editing a node" do
login_as :quentin
-
+
node = Node.find_by_unique_name("fourth_child")
node.pages.create
node.draft = node.pages.last
node.save
-
+
assert_equal 1, node.pages.length
-
+
draft = node.find_or_create_draft( User.first )
draft.title = "Hello"
draft.body = "World"
draft.save
node.publish_draft!
-
+
get :edit, params: { :id => node.id }
assert_response :success
assert_select("#page_title[value='Hello']")
assert_select("#page_body", "World")
-
+
node.reload
assert_equal 2, node.pages.length
assert_equal "Hello", node.find_or_create_draft( User.first ).title
assert_equal "World", node.find_or_create_draft( User.first ).body
end
-
+
test "editing a locked node raises LockedByAnotherUser Exception" do
login_as :quentin
-
+
node = create_node_with_draft
node.lock_owner = User.last
node.save
-
+
assert node.locked?
-
+
get :edit, params: { :id => node.id }
assert_response :redirect
assert flash[:error] =~ /Page is locked by another user/
end
-
+
def test_update_a_draft
test_node = Node.root.children.create! :slug => "test_node"
login_as :quentin
@@ -128,7 +128,7 @@ class NodesControllerTest < ActionController::TestCase
assert_equal "Hello", test_node.draft.title
assert_equal "There", test_node.draft.body
end
-
+
def test_update_a_draft_with_changing_the_template
test_node = Node.root.children.create! :slug => "test_node"
@@ -148,19 +148,19 @@ class NodesControllerTest < ActionController::TestCase
assert_equal "There", test_node.head.body
assert_equal "Foobar", test_node.head.template_name
end
-
+
test "publish draft with staged_slug unqueal slug" do
login_as :quentin
-
+
test_node = Node.root.children.create! :slug => "test_node", :staged_slug => "peter_pan"
-
+
put :publish, params: { :id => test_node.id }
-
+
test_node.reload
assert_equal "peter_pan", test_node.slug
assert_equal "peter_pan", test_node.unique_name
end
-
+
test "publish draft with staged_slug with more levels of nodes" do
login_as :quentin
@@ -168,12 +168,12 @@ class NodesControllerTest < ActionController::TestCase
test_node2 = test_node.children.create! :slug => "test_node2"
put :publish, params: { :id => test_node.id }
-
+
test_node.reload; test_node2.reload
assert_equal "peter_pan/test_node2", test_node2.unique_name
assert_equal "peter_pan", test_node.unique_name
end
-
+
test "publish draft with staged_parent_id" do
login_as :quentin
@@ -187,77 +187,77 @@ class NodesControllerTest < ActionController::TestCase
assert_equal "parent/test_node", test_node.unique_name
assert_equal "parent/test_node/test_node2", test_node2.unique_name
end
-
+
test "publish draft with staged_parent_id and staged_slug" do
login_as :quentin
-
+
parent = Node.root.children.create! :slug => "parent"
-
+
test_node = Node.root.children.create!(
- :slug => "test_node",
+ :slug => "test_node",
:staged_parent_id => parent.id,
:staged_slug => "peter_pan"
)
-
+
test_node2 = test_node.children.create! :slug => "test_node2"
-
+
put :publish, params: { :id => test_node.id }
-
+
test_node.reload; test_node2.reload
assert_equal "parent/peter_pan", test_node.unique_name
assert_equal "parent/peter_pan/test_node2", test_node2.unique_name
end
-
+
test "show node with empty draft" do
login_as :quentin
assert_not_nil node = create_node_with_draft
get :show, params: { :id => node.id }
assert_response :success
end
-
+
test "show node with published draft" do
login_as :quentin
node = create_node_with_published_page
get :show, params: { :id => node.id }
assert_response :success
- assert_select "td", :text => "Test", :count => 3
+ assert_select "div.node_content", :text => "Test", :count => 3
end
-
+
test "unlocking a locked node" do
login_as :quentin
node = create_node_with_published_page
node.find_or_create_draft User.first
-
+
assert node.locked?
-
+
put :unlock, params: { :id => node.id }
assert_response :redirect
assert !node.reload.locked?
end
-
+
test "unlocking an already unlocked node" do
login_as :quentin
node = create_node_with_published_page
-
+
put :unlock, params: { :id => node.id }
assert_response :redirect
assert_equal "Already unlocked", flash[:notice]
end
-
+
test "updating a node by changing its parent" do
Node.root.descendants.destroy_all
login_as :quentin
node = create_node_with_published_page
node.find_or_create_draft User.first
-
+
other_node = Node.root.children.create( :slug => "other" )
-
+
node.staged_parent_id = other_node.id
node.publish_draft!
assert Node.valid?
end
-
+
test "editing the initial draft sets the author to current_user" do
login_as :quentin
Node.root.descendants.destroy_all
@@ -266,7 +266,7 @@ class NodesControllerTest < ActionController::TestCase
node.reload
assert_equal "quentin", node.draft.user.login
end
-
+
test "updating the author of a node with existing head" do
login_as :quentin
Node.root.descendants.destroy_all
@@ -275,70 +275,70 @@ class NodesControllerTest < ActionController::TestCase
node.find_or_create_draft users(:quentin)
assert node.draft.valid?
assert node.valid?
-
+
put :update, params: { :id => node.id, :page => {:user_id => users(:aaron).id} }
assert_response :redirect
assert_equal "aaron", node.reload.draft.user.login
end
-
+
test "updating an existing page should not modify published_at" do
login_as :quentin
Node.root.descendants.destroy_all
node = create_node_with_published_page
-
+
get :edit, params: { :id => node.id }
assert_response :success
-
+
put :publish, params: { :id => node.id }
-
+
node.reload
assert_equal node.pages[0].published_at, node.pages[1].published_at
end
-
+
test "updating an exisiting page should not alter the author" do
login_as :aaron
Node.root.descendants.destroy_all
node = create_node_with_published_page
get :edit, params: { :id => node.id }
-
+
put :publish, params: { :id => node.id }
-
+
node.reload
assert_equal node.pages[0].user, node.pages[1].user
end
-
+
test "editor and author are the same on a new node" do
login_as :quentin
node = create_node_with_draft
get :edit, params: { :id => node.id }
-
+
node.reload
assert_equal "quentin", node.draft.user.login
assert_equal "quentin", node.draft.editor.login
end
-
+
test "creating new draft alters the editor but keeps the author" do
node = create_node_with_published_page
assert_equal "quentin", node.head.user.login
-
+
login_as :aaron
get :edit, params: {:id => node.id }
-
+
node.reload
assert_equal "quentin", node.head.user.login
assert_equal "aaron", node.draft.editor.login
end
-
+
test "unlocking and relocking changes editor if done by another user" do
node = create_node_with_published_page
draft = node.find_or_create_draft users(:quentin)
assert_equal draft.user.login, draft.editor.login
assert node.locked?
node.unlock!
-
+
login_as :aaron
get :edit, params: { :id => node.id }
-
+
node.reload
assert_equal "quentin", node.draft.user.login
assert_equal "aaron", node.draft.editor.login
--
cgit v1.3
From c6bf63a82007c275d13e9e9e0857434b3b7890c0 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Wed, 8 Jul 2026 16:23:01 +0200
Subject: Fix tests for the new autosave routing
Route Save and autosave through the new Node methods
update now promotes the current autosave into the draft via
save_draft! rather than writing submitted params directly; autosave
gets its own PUT member route so PATCH update can mean "deliberate
save" specifically, matching every other custom action on this
resource. Both now require the lock to already be held rather than
acquiring it themselves, which is a deliberate narrowing: through the
real UI this is always true, since neither can fire before edit has
loaded. Two existing tests called update directly with no prior
edit, which the old code tolerated by acquiring the lock as a side
effect; updated to call edit first instead of loosening the
guarantee back open.
NodesController#edit itself is unchanged and still calls the old
find_or_create_draft, so drafts are still created eagerly on entering
edit for now -- switching that over is the next step, not this one.
---
app/controllers/nodes_controller.rb | 39 +++++++++++++++++++++++--------
test/controllers/nodes_controller_test.rb | 2 ++
2 files changed, 31 insertions(+), 10 deletions(-)
(limited to 'test/controllers/nodes_controller_test.rb')
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb
index a72be68..042963b 100644
--- a/app/controllers/nodes_controller.rb
+++ b/app/controllers/nodes_controller.rb
@@ -11,7 +11,8 @@ class NodesController < ApplicationController
:update,
:destroy,
:publish,
- :unlock
+ :unlock,
+ :autosave
]
def index
@@ -72,17 +73,35 @@ class NodesController < ApplicationController
def update
@node.update(node_params)
- @draft = @node.find_or_create_draft current_user
- @draft.tag_list = params[:tag_list]
- if @draft.update( page_params )
- flash[:notice] = "Draft has been saved: #{Time.now}"
- respond_to do |format|
- format.html { redirect_to edit_node_path(@node) }
- format.js
- end
+ @node.autosave!( page_params.merge(:tag_list => params[:tag_list]), current_user )
+ @node.save_draft!(current_user)
+
+ flash[:notice] = "Draft has been saved: #{Time.now}"
+
+ if params[:commit] == "Save + Unlock + Exit"
+ @node.unlock!
+ redirect_to node_path(@node)
else
- render :action => :edit
+ redirect_to edit_node_path(@node)
end
+ rescue LockedByAnotherUser => e
+ flash[:error] = e.message
+ redirect_to node_path(@node)
+ rescue ActiveRecord::RecordInvalid
+ @page = @node.autosave || @node.draft || @node.head
+ render :action => :edit
+ end
+
+ def autosave
+ @node.update(node_params)
+ @node.autosave!( page_params.merge(:tag_list => params[:tag_list]), current_user )
+ head :ok
+ rescue LockedByAnotherUser => e
+ render plain: e.message, status: :locked
+ rescue ActiveRecord::RecordInvalid => e
+ render plain: e.message, status: :unprocessable_entity
+ rescue StandardError => e
+ render plain: "Autosave failed", status: :internal_server_error
end
def destroy
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index 61f7db4..f0be8c9 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -123,6 +123,7 @@ class NodesControllerTest < ActionController::TestCase
def test_update_a_draft
test_node = Node.root.children.create! :slug => "test_node"
login_as :quentin
+ get :edit, params: { :id => test_node.id }
put :update, params: { :id => test_node.id, :page => {:title => "Hello", :body => "There"} }
test_node.reload
assert_equal "Hello", test_node.draft.title
@@ -133,6 +134,7 @@ class NodesControllerTest < ActionController::TestCase
test_node = Node.root.children.create! :slug => "test_node"
login_as :quentin
+ get :edit, params: { :id => test_node.id }
put :update, params: {
:id => test_node.id,
:page => {
--
cgit v1.3
From a5d7fb9730aa629b3eb8244ee1b3dd863fbfbed5 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Wed, 8 Jul 2026 16:24:52 +0200
Subject: Fix node_content count in show-with-published-draft test
Fallout from the earlier nodes#show heading change, which replaced the
Title row with an
-- unrelated to the autosave work in the
preceding commits.
---
test/controllers/nodes_controller_test.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
(limited to 'test/controllers/nodes_controller_test.rb')
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index f0be8c9..030cff0 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -222,7 +222,7 @@ class NodesControllerTest < ActionController::TestCase
node = create_node_with_published_page
get :show, params: { :id => node.id }
assert_response :success
- assert_select "div.node_content", :text => "Test", :count => 3
+ assert_select "div.node_content", :text => "Test", :count => 2
end
test "unlocking a locked node" do
--
cgit v1.3
From 6ad96c44d04df01e6abde097c681e824dd5fe745 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Wed, 8 Jul 2026 17:31:15 +0200
Subject: Fix authorship and published_at loss in autosave promotion
Two real bugs surfaced by the full test suite, not by the new tests
written for this feature -- both were latent the moment lock_for_editing!
and save_draft! replaced find_or_create_draft, and only visible once an
existing test exercised the exact path each one lived in.
lock_for_editing! never stamped user/editor onto a draft that already
existed when the lock was acquired. find_or_create_draft used to do this
as part of acquiring the lock; splitting lock acquisition from draft
creation dropped it entirely, since it looked like draft-creation logic
rather than locking logic. Restored as an explicit step: claim authorship
only if none is set yet, editorship unconditionally, matching the old
behavior exactly.
save_draft!'s "no draft yet" branch set user/editor before calling
clone_attributes_from, whose first line is an unconditional self.reload
-- silently discarding both, since neither had been persisted yet.
clone_attributes_from also always copies published_at from its source
without the ||= guard used for template_name, and the source here is
always the autosave, whose published_at is never anything but nil --
meaning every single promotion, not just the first, was quietly
resetting a published page's published_at, which publish_draft!'s own
||= Time.now would then treat as never-published and re-stamp. Fixed by
running clone_attributes_from first on both branches, then applying
user/editor/published_at afterward, exactly as the existing-draft branch
already happened to do by accident.
Four controller tests updated to insert a real put :update between
get :edit and assertions that used to be true immediately after
visiting edit -- deferred draft creation means edit alone no longer
produces one, which is the intended consequence of this whole
redesign, not something these tests were meant to catch.
---
app/models/node.rb | 36 ++++++++++++++++++++++++++++---
test/controllers/nodes_controller_test.rb | 7 ++++--
test/models/node_test.rb | 3 +++
3 files changed, 41 insertions(+), 5 deletions(-)
(limited to 'test/controllers/nodes_controller_test.rb')
diff --git a/app/models/node.rb b/app/models/node.rb
index 9eb0fe4..f15c908 100644
--- a/app/models/node.rb
+++ b/app/models/node.rb
@@ -56,6 +56,11 @@ class Node < ApplicationRecord
def lock_for_editing! current_user
if self.lock_owner.nil? || self.lock_owner == current_user
lock_for! current_user
+ if self.draft
+ self.draft.user = current_user if self.draft.user.nil?
+ self.draft.editor = current_user
+ self.draft.save!
+ end
self
else
raise(
@@ -95,15 +100,18 @@ class Node < ApplicationRecord
return unless self.autosave
if self.draft
+ preserved_published_at = self.draft.published_at
self.draft.clone_attributes_from self.autosave
+ self.draft.published_at = preserved_published_at
self.draft.user_id = self.autosave.user_id if self.autosave.user_id
self.draft.editor = current_user
self.draft.save!
else
- empty_page = self.pages.create!
- empty_page.user = self.autosave.user_id ? self.autosave.user : (self.head ? self.head.user : current_user)
- empty_page.editor = current_user
+ empty_page = self.pages.create!
empty_page.clone_attributes_from self.autosave
+ empty_page.user = self.autosave.user_id ? self.autosave.user : (self.head ? self.head.user : current_user)
+ empty_page.editor = current_user
+ empty_page.published_at = self.head.published_at if self.head
empty_page.save!
self.draft = empty_page
self.save!
@@ -150,6 +158,28 @@ class Node < ApplicationRecord
self.draft.reload
end
+ # Discards exactly the topmost non-empty layer -- autosave if present,
+ # else draft -- and reveals whatever's beneath it. Releases the lock
+ # only once nothing is left to protect (no draft survives); leaves it
+ # alone whenever a draft remains, since #edit still has real content
+ # open.
+ def revert! current_user
+ assert_locked_by! current_user
+
+ if self.autosave
+ self.autosave.destroy
+ self.autosave_id = nil
+ self.save!
+ elsif self.draft && self.head
+ self.draft.destroy
+ self.draft_id = nil
+ self.save!
+ end
+
+ self.unlock! unless self.draft
+ self.reload
+ end
+
def staged_slug=(value)
if head.blank?
self.slug = value
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index 030cff0..f3e04c2 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -101,7 +101,7 @@ class NodesControllerTest < ActionController::TestCase
assert_select("#page_body", "World")
node.reload
- assert_equal 2, node.pages.length
+ assert_equal 1, node.pages.length
assert_equal "Hello", node.find_or_create_draft( User.first ).title
assert_equal "World", node.find_or_create_draft( User.first ).body
end
@@ -291,6 +291,7 @@ class NodesControllerTest < ActionController::TestCase
get :edit, params: { :id => node.id }
assert_response :success
+ put :update, params: { :id => node.id, :page => { :title => "updated" } }
put :publish, params: { :id => node.id }
node.reload
@@ -303,6 +304,7 @@ class NodesControllerTest < ActionController::TestCase
node = create_node_with_published_page
get :edit, params: { :id => node.id }
+ put :update, params: { :id => node.id, :page => { :title => "updated" } }
put :publish, params: { :id => node.id }
node.reload
@@ -324,7 +326,8 @@ class NodesControllerTest < ActionController::TestCase
assert_equal "quentin", node.head.user.login
login_as :aaron
- get :edit, params: {:id => node.id }
+ get :edit, params: { :id => node.id }
+ put :update, params: { :id => node.id, :page => { :title => "updated" } }
node.reload
assert_equal "quentin", node.head.user.login
diff --git a/test/models/node_test.rb b/test/models/node_test.rb
index 2953f8f..bdf556d 100644
--- a/test/models/node_test.rb
+++ b/test/models/node_test.rb
@@ -339,6 +339,9 @@ class NodeTest < ActiveSupport::TestCase
assert_equal head_revision, node.head.revision
assert_nil node.autosave
assert_equal 2, node.pages.count
+ assert_equal node.head.user, node.draft.user
+ assert_equal @user1, node.draft.editor
+ assert_equal node.head.published_at, node.draft.published_at
end
test "autosave!, save_draft!, and lock_for_editing! raise LockedByAnotherUser for a second user" do
--
cgit v1.3
From c2b2648d327e1c1749c37fe2e58cd051ed871547 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Fri, 10 Jul 2026 02:13:02 +0200
Subject: Destroying Draft or Discarding Autosave drops you where you left
---
app/controllers/nodes_controller.rb | 5 +++-
app/views/nodes/show.html.erb | 1 +
app/views/revisions/diff.html.erb | 1 +
test/controllers/nodes_controller_test.rb | 39 +++++++++++++++++++++++++++++++
4 files changed, 45 insertions(+), 1 deletion(-)
(limited to 'test/controllers/nodes_controller_test.rb')
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb
index d1538e1..6fcd930 100644
--- a/app/controllers/nodes_controller.rb
+++ b/app/controllers/nodes_controller.rb
@@ -128,7 +128,10 @@ class NodesController < ApplicationController
def revert
@node.lock_for_editing!(current_user)
@node.revert!(current_user)
- if @node.draft
+
+ if params[:return_to].present?
+ redirect_to safe_return_to(params[:return_to])
+ elsif @node.draft
redirect_to edit_node_path(@node)
else
redirect_to node_path(@node)
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index 2469310..8b9e98b 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -53,6 +53,7 @@
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index f3e04c2..ce3419c 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -423,4 +423,43 @@ class NodesControllerTest < ActionController::TestCase
assert_select "form.button_to.destructive", count: 0
end
+ test "reverting from nodes#show returns to the show page, not the editor, even if a draft remains" do
+ user = User.find_by_login("aaron")
+ node = Node.root.children.create!(:slug => "revert_return_to_test")
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v1"}, user)
+ node.save_draft!(user)
+ node.publish_draft!
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v2"}, user)
+ node.save_draft!(user)
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v3"}, user)
+ # state D: head, draft, and autosave all present, locked by aaron
+
+ login_as :aaron
+ put :revert, params: { :id => node.id, :return_to => node_path(node) }
+ assert_redirected_to node_path(node)
+ node.reload
+ assert node.draft.present?
+ assert node.autosave.blank?
+ end
+
+ test "reverting from nodes#edit without return_to still lands back in the editor when a draft remains" do
+ user = User.find_by_login("aaron")
+ node = Node.root.children.create!(:slug => "revert_default_test")
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v1"}, user)
+ node.save_draft!(user)
+ node.publish_draft!
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v2"}, user)
+ node.save_draft!(user)
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v3"}, user)
+
+ login_as :aaron
+ put :revert, params: { :id => node.id }
+ assert_redirected_to edit_node_path(node)
+ end
end
--
cgit v1.3
From 48eed5f35ea798c91f1e0a77c13e751145acfc48 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Fri, 10 Jul 2026 18:10:57 +0200
Subject: Add the missing assertions two tests were silently running without
Both ran real code and checked nothing -- Minitest's "missing
assertions" warning has been firing on every run this session.
test_find_or_create_draft_if_draft_exists_and_is_owned_by_user called
the method twice but never checked what came back; now confirms the
second call returns the same draft rather than creating a new one,
and leaves the lock and page count untouched.
test_destroy_a_published_node destroyed a node and loaded the admin
index but never checked the response, unlike its two neighbors
covering the same destroy path (dangling pages, orphaned
occurrences). Added the assert_response :success its own shape
already implied.
No behavior changed -- both were passing before for the same reason
they're passing now, they just weren't proving anything.
---
test/controllers/nodes_controller_test.rb | 1 +
test/models/node_test.rb | 14 +++++++++-----
2 files changed, 10 insertions(+), 5 deletions(-)
(limited to 'test/controllers/nodes_controller_test.rb')
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index ce3419c..37091d5 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -355,6 +355,7 @@ class NodesControllerTest < ActionController::TestCase
login_as :quentin
get :index
+ assert_response :success
end
test "no dangling pages remain after node removal" do
diff --git a/test/models/node_test.rb b/test/models/node_test.rb
index 9e71dec..280614e 100644
--- a/test/models/node_test.rb
+++ b/test/models/node_test.rb
@@ -111,15 +111,19 @@ class NodeTest < ActiveSupport::TestCase
node.publish_draft!
assert_not_nil node.find_or_create_draft( @user1 )
end
-
+
def test_find_or_create_draft_if_draft_exists_and_is_owned_by_user
node = Node.root.children.create :slug => "xyz"
node.publish_draft!
-
- node.find_or_create_draft @user1
- node.find_or_create_draft @user1
+
+ first_call = node.find_or_create_draft @user1
+ second_call = node.find_or_create_draft @user1
+
+ assert_equal first_call, second_call
+ assert_equal 2, node.pages.count
+ assert_equal @user1, node.lock_owner
end
-
+
def test_exception_if_draft_exists_but_locked_by_another_user
node = Node.root.children.create :slug => "xyz"
node.publish_draft!
--
cgit v1.3
From 15aff3eff51809d1ce21caab406a0ef8b13624b6 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Sat, 11 Jul 2026 01:33:07 +0200
Subject: Do not offer to destroy the only draft of a never-published node
---
app/views/nodes/show.html.erb | 2 +-
test/controllers/nodes_controller_test.rb | 8 ++++++++
2 files changed, 9 insertions(+), 1 deletion(-)
(limited to 'test/controllers/nodes_controller_test.rb')
diff --git a/app/views/nodes/show.html.erb b/app/views/nodes/show.html.erb
index 5756649..07cb5d1 100644
--- a/app/views/nodes/show.html.erb
+++ b/app/views/nodes/show.html.erb
@@ -49,7 +49,7 @@
form: { data: { confirm: "Publish this draft?" }, class: 'button_to state_changing' } %>
<% end %>
- <% if @node.draft || @node.autosave %>
+ <% if @node.autosave || (@node.draft && @node.head) %>
<%= button_to (@node.draft && !@node.autosave ? 'Destroy Draft' : 'Discard Autosave'),
revert_node_path(@node), method: :put,
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index 37091d5..b563d4d 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -463,4 +463,12 @@ class NodesControllerTest < ActionController::TestCase
put :revert, params: { :id => node.id }
assert_redirected_to edit_node_path(node)
end
+
+ test "nodes#show does not offer to destroy the only draft of a never-published node" do
+ node = Node.root.children.create!(:slug => "draft_only_test")
+ login_as :quentin
+ get :show, params: { :id => node.id }
+ assert_response :success
+ assert_select "form.destructive", :count => 0
+ end
end
--
cgit v1.3
From 92c394b43a0603743b914c6298aab986805ca990 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Sat, 11 Jul 2026 23:43:02 +0200
Subject: Add drafts/recent/mine/chapters admin node views
Four NodesController actions -- drafts, recent, mine, chapters --
each building its own base scope, sharing one private method
(index_matching) for search narrowing and pagination. Wizard rewrite
to link into these instead of rendering its own tables is a separate,
later step.
Node.editor_search backs the shared "q" narrowing: an ILIKE substring
match against title/abstract on whichever of head or draft is
present, splitting the term on whitespace and requiring every word to
match somewhere independently, not as one phrase, since real words can
end up separated by markup in the underlying HTML. Deliberately
separate from Node.search, the public content search, which stays
tsvector-based and head-only.
chapters generalizes into /admin/nodes/tags/:tags for an arbitrary
tag list (OR'd, not AND'd), sharing the controller action but
rendering its own template rather than branching inside one view.
---
app/controllers/nodes_controller.rb | 45 +++++++++++++
app/models/node.rb | 22 +++++++
app/views/nodes/_node_list.html.erb | 39 +++++++++++
app/views/nodes/chapters.html.erb | 9 +++
app/views/nodes/drafts.html.erb | 3 +
app/views/nodes/mine.html.erb | 3 +
app/views/nodes/recent.html.erb | 3 +
app/views/nodes/tags.html.erb | 3 +
config/routes.rb | 5 ++
public/stylesheets/admin.css | 15 +++++
test/controllers/nodes_controller_test.rb | 104 ++++++++++++++++++++++++++++++
test/models/node_test.rb | 26 ++++++++
12 files changed, 277 insertions(+)
create mode 100644 app/views/nodes/_node_list.html.erb
create mode 100644 app/views/nodes/chapters.html.erb
create mode 100644 app/views/nodes/drafts.html.erb
create mode 100644 app/views/nodes/mine.html.erb
create mode 100644 app/views/nodes/recent.html.erb
create mode 100644 app/views/nodes/tags.html.erb
(limited to 'test/controllers/nodes_controller_test.rb')
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb
index 6fcd930..ede91ad 100644
--- a/app/controllers/nodes_controller.rb
+++ b/app/controllers/nodes_controller.rb
@@ -183,6 +183,39 @@ class NodesController < ApplicationController
render plain: slug_for(params[:title])
end
+ # Filter functions for admin views
+ def drafts
+ base = Node.where("draft_id IS NOT NULL OR autosave_id IS NOT NULL")
+ @nodes = index_matching(base)
+ end
+
+ def recent
+ base = Node.where(
+ "nodes.updated_at < ? AND nodes.updated_at > ? AND nodes.parent_id IS NOT NULL",
+ Time.now, Time.now - 14.days
+ )
+ @nodes = index_matching(base)
+ end
+
+ def mine
+ base = Node.joins(:pages)
+ .where("pages.user_id = ? or pages.editor_id = ?", current_user, current_user)
+ .distinct
+ @nodes = index_matching(base)
+ end
+
+ def chapters
+ @kind_keys = Array(params[:kinds]) & %w[erfa chaostreff]
+ @kind_keys = %w[erfa chaostreff] if @kind_keys.empty?
+ tags = @kind_keys.flat_map { |key| CccConventions::NODE_KINDS[key][:tags] }
+ @nodes = nodes_matching_tags(tags)
+ end
+
+ def tags
+ tags = params[:tags].to_s.split(',').map(&:strip).reject(&:blank?)
+ @nodes = nodes_matching_tags(tags)
+ end
+
private
def slug_for(title)
@@ -214,4 +247,16 @@ class NodesController < ApplicationController
config && config[:parent] ? config[:parent].call.id : nil
end
end
+
+ def nodes_matching_tags(tags)
+ matching_pages = Page.tagged_with(tags, any: true).reselect(:id)
+ base = Node.where(head_id: matching_pages).or(Node.where(draft_id: matching_pages))
+ index_matching(base)
+ end
+
+ def index_matching(base_scope)
+ scope = base_scope.includes(:head, :draft)
+ scope = scope.merge(Node.editor_search(params[:q])) if params[:q].present?
+ scope.order("nodes.updated_at desc").paginate(page: params[:page], per_page: 25)
+ end
end
diff --git a/app/models/node.rb b/app/models/node.rb
index 9cb4840..24f3cd0 100644
--- a/app/models/node.rb
+++ b/app/models/node.rb
@@ -345,6 +345,28 @@ class Node < ApplicationRecord
.distinct
end
+ # This one is for admin-only views, where finding a draft is the point.
+ # Substring match on whichever of head/draft is present.
+ def self.editor_search(term)
+ words = term.to_s.split(/\s+/).reject(&:blank?)
+ return none if words.empty?
+
+ conditions = []
+ binds = {}
+
+ words.each_with_index do |word, i|
+ key = "term#{i}"
+ binds[key.to_sym] = "%#{sanitize_sql_like(word)}%"
+ conditions << "(head_translations.title ILIKE :#{key} OR head_translations.abstract ILIKE :#{key} " \
+ "OR draft_translations.title ILIKE :#{key} OR draft_translations.abstract ILIKE :#{key})"
+ end
+
+ joins("LEFT JOIN page_translations head_translations ON head_translations.page_id = nodes.head_id")
+ .joins("LEFT JOIN page_translations draft_translations ON draft_translations.page_id = nodes.draft_id")
+ .where(conditions.join(" AND "), binds)
+ .distinct
+ end
+
protected
def lock_for! current_user
self.lock_owner = current_user
diff --git a/app/views/nodes/_node_list.html.erb b/app/views/nodes/_node_list.html.erb
new file mode 100644
index 0000000..03f38b4
--- /dev/null
+++ b/app/views/nodes/_node_list.html.erb
@@ -0,0 +1,39 @@
+<%= form_tag url_for(controller: params[:controller], action: params[:action]), method: :get, class: "node_search_form" do %>
+ <% Array(params[:kinds]).each do |kind| %>
+ <%= hidden_field_tag "kinds[]", kind %>
+ <% end %>
+ <%= hidden_field_tag :tags, params[:tags] if params[:tags].present? %>
+ <%= text_field_tag :q, params[:q], placeholder: "Search title, abstract, body…" %>
+ <%= submit_tag "Search", class: "action_button" %>
+ <% if params[:q].present? || params[:kinds].present? || params[:tags].present? %>
+ <%= link_to "Reset", url_for(controller: params[:controller], action: params[:action]) %>
+ <% end %>
+<% end %>
+
+<%= will_paginate @nodes %>
+
+
+<%= render 'node_list' %>
diff --git a/config/routes.rb b/config/routes.rb
index aebce90..2c165d2 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -37,7 +37,12 @@ Cccms::Application.routes.draw do
resources :nodes do
collection do
+ get 'tags/:tags', action: :tags, as: :tags, constraints: { tags: /[^\/]+/ }
get :parameterize_preview
+ get :drafts
+ get :recent
+ get :mine
+ get :chapters
end
member do
diff --git a/public/stylesheets/admin.css b/public/stylesheets/admin.css
index 28b8494..aa8b288 100644
--- a/public/stylesheets/admin.css
+++ b/public/stylesheets/admin.css
@@ -748,6 +748,21 @@ form.button_to button[type="submit"] {
margin-bottom: 0;
}
+.node_search_form {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.75rem;
+}
+
+.node_search_form input[type=text] {
+ padding: 4px 12px;
+ box-sizing: border-box;
+ height: 2.25rem;
+ width: 22rem;
+ border-radius: 2px;
+}
+
/* Layout only -- the at-rest visibility (wavy underline) for these
links comes from the scoped rule in Base elements above. */
.add_child_links {
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index b563d4d..05cb195 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -471,4 +471,108 @@ class NodesControllerTest < ActionController::TestCase
assert_response :success
assert_select "form.destructive", :count => 0
end
+
+ test "drafts includes a never-published node with only a draft" do
+ node = Node.root.children.create!(:slug => "drafts_action_test")
+ login_as :quentin
+ get :drafts
+ assert_includes assigns(:nodes), node
+ end
+
+ test "chapters filters by kind, matching head or draft, and shows both by default" do
+ erfa_node = Node.root.children.create!(:slug => "chapters_erfa_test")
+ erfa_node.find_or_create_draft(@user1)
+ erfa_node.draft.tag_list = "erfa-detail"
+ erfa_node.draft.save!
+ erfa_node.publish_draft!
+
+ chaostreff_node = Node.root.children.create!(:slug => "chapters_chaostreff_test")
+ chaostreff_node.find_or_create_draft(@user1)
+ chaostreff_node.draft.tag_list = "chaostreff-detail"
+ chaostreff_node.draft.save!
+ chaostreff_node.publish_draft!
+
+ login_as :quentin
+
+ get :chapters, params: { :kinds => "erfa" }
+ assert_includes assigns(:nodes), erfa_node
+ assert_not_includes assigns(:nodes), chaostreff_node
+
+ get :chapters
+ assert_includes assigns(:nodes), erfa_node
+ assert_includes assigns(:nodes), chaostreff_node
+ end
+
+ test "recent combined with a search term does not raise an ambiguous column error" do
+ login_as :quentin
+ get :recent, params: { :q => "Zombies" }
+ assert_response :success
+ end
+
+ test "drafts combined with a search term does not raise an ambiguous column error" do
+ login_as :quentin
+ get :drafts, params: { :q => "Zombies" }
+ assert_response :success
+ end
+
+ test "mine combined with a search term does not raise an ambiguous column error" do
+ login_as :quentin
+ get :mine, params: { :q => "Zombies" }
+ assert_response :success
+ end
+
+ test "chapters combined with a search term does not raise an ambiguous column error" do
+ login_as :quentin
+ get :chapters, params: { :q => "Zombies" }
+ assert_response :success
+ end
+
+ test "tags path filters by an arbitrary raw tag, generalizing chapters" do
+ presse_node = Node.root.children.create!(:slug => "tags_path_presse_test")
+ presse_node.find_or_create_draft(@user1)
+ presse_node.draft.tag_list = "pressemitteilung"
+ presse_node.draft.save!
+ presse_node.publish_draft!
+
+ erfa_node = Node.root.children.create!(:slug => "tags_path_erfa_test")
+ erfa_node.find_or_create_draft(@user1)
+ erfa_node.draft.tag_list = "erfa-detail"
+ erfa_node.draft.save!
+ erfa_node.publish_draft!
+
+ login_as :quentin
+ get :tags, params: { :tags => "pressemitteilung" }
+
+ assert_includes assigns(:nodes), presse_node
+ assert_not_includes assigns(:nodes), erfa_node
+
+ assert_select "h1", "Nodes tagged: pressemitteilung"
+ assert_select "h1", :text => "Chapters", :count => 0
+ end
+
+ test "tags path with multiple tags matches any of them, not all" do
+ erfa_node = Node.root.children.create!(:slug => "tags_path_multi_erfa_test")
+ erfa_node.find_or_create_draft(@user1)
+ erfa_node.draft.tag_list = "erfa-detail"
+ erfa_node.draft.save!
+ erfa_node.publish_draft!
+
+ chaostreff_node = Node.root.children.create!(:slug => "tags_path_multi_chaostreff_test")
+ chaostreff_node.find_or_create_draft(@user1)
+ chaostreff_node.draft.tag_list = "chaostreff-detail"
+ chaostreff_node.draft.save!
+ chaostreff_node.publish_draft!
+
+ login_as :quentin
+ get :tags, params: {:tags => "erfa-detail,chaostreff-detail" }
+
+ assert_includes assigns(:nodes), erfa_node
+ assert_includes assigns(:nodes), chaostreff_node
+ end
+
+ test "chapters renders the curated heading" do
+ login_as :quentin
+ get :chapters
+ assert_select "h1", "Chapters"
+ end
end
diff --git a/test/models/node_test.rb b/test/models/node_test.rb
index 5626384..15e908b 100644
--- a/test/models/node_test.rb
+++ b/test/models/node_test.rb
@@ -511,4 +511,30 @@ class NodeTest < ActiveSupport::TestCase
a.reload
assert_equal Node.root.id, a.parent_id
end
+
+ test "editor_search matches a partial substring, not just a whole word" do
+ node = Node.root.children.create!(:slug => "editor_search_substring_test")
+ node.find_or_create_draft(@user1)
+ node.draft.update(:title => "Biometrics Conference")
+ node.publish_draft!
+
+ assert_includes Node.editor_search("bio"), node
+ assert_includes Node.editor_search("Conf"), node
+ end
+
+ test "editor_search returns an empty relation for a blank term, not every node" do
+ assert_equal 0, Node.editor_search("").count
+ assert_equal 0, Node.editor_search(nil).count
+ assert_equal 0, Node.editor_search(" ").count
+ end
+
+ test "editor_search requires every word to match, but each word can match a different field" do
+ node = Node.root.children.create!(:slug => "editor_search_multiword_test")
+ node.find_or_create_draft(@user1)
+ node.draft.update(:title => "Backspace e.V. Bamberg", :abstract => "Spiegelgraben 41, 96052 Bamberg")
+ node.publish_draft!
+
+ assert_includes Node.editor_search("Backspace Spiegelgraben"), node
+ assert_equal 0, Node.editor_search("Backspace Nonexistentstreet").count
+ end
end
--
cgit v1.3
From 45bf65d04d046c0ea4a1150096b2a9b846d6c0b8 Mon Sep 17 00:00:00 2001
From: erdgeist
Date: Sun, 12 Jul 2026 02:15:44 +0200
Subject: Give the sitemap its own view, with collapse and descendant counts
Extracted from admin#index's inline table into NodesController#sitemap.
Nested / per branch, one linear pass over the
existing flat [node, level] list (no added queries) -- each node's own
descendant count computed the same way, via a small stack rather than
re-walking the tree per node. Branches under updates/, club/erfas,
club/chaostreffs, and disclosure start collapsed by default
(CccConventions::SITEMAP_COLLAPSED_PATHS); any branch currently
collapsed, whether by that default or because someone just closed it,
is highlighted via a plain :not([open]) selector -- no state tracked
outside the DOM itself.
Dropped the update?-post exclusion this view used to rely on -- no
longer needed now that updates/ collapses instead of being filtered
out, so its real children (previously silently absent) now show up
correctly. admin#index's own, separate @sitemap query is unchanged;
that view has no collapse mechanism to compensate and wasn't part of
this.
---
app/controllers/nodes_controller.rb | 23 ++++++++++++++
app/helpers/nodes_helper.rb | 5 ++-
app/views/nodes/sitemap.html.erb | 32 +++++++++++++++++++
config/routes.rb | 1 +
lib/ccc_conventions.rb | 1 +
public/stylesheets/admin.css | 40 ++++++++++++++++++++++++
test/controllers/nodes_controller_test.rb | 52 +++++++++++++++++++++++++++++++
test/models/helpers/nodes_helper_test.rb | 11 +++++++
8 files changed, 164 insertions(+), 1 deletion(-)
create mode 100644 app/views/nodes/sitemap.html.erb
(limited to 'test/controllers/nodes_controller_test.rb')
diff --git a/app/controllers/nodes_controller.rb b/app/controllers/nodes_controller.rb
index ede91ad..772bf4b 100644
--- a/app/controllers/nodes_controller.rb
+++ b/app/controllers/nodes_controller.rb
@@ -216,6 +216,11 @@ class NodesController < ApplicationController
@nodes = nodes_matching_tags(tags)
end
+ def sitemap
+ @sitemap = Node.root.self_and_descendants_ordered_with_level
+ @sitemap_descendant_counts = descendant_counts_for(@sitemap)
+ end
+
private
def slug_for(title)
@@ -248,6 +253,24 @@ class NodesController < ApplicationController
end
end
+ def descendant_counts_for(ordered_with_level)
+ counts = Hash.new(0)
+ stack = [] # [node, level, index]
+ ordered_with_level.each_with_index do |(node, level), index|
+ while stack.any? && stack.last[1] >= level
+ ancestor_node, _ancestor_level, ancestor_index = stack.pop
+ counts[ancestor_node.id] = index - ancestor_index - 1
+ end
+ stack << [node, level, index]
+ end
+ total = ordered_with_level.length
+ while stack.any?
+ ancestor_node, _ancestor_level, ancestor_index = stack.pop
+ counts[ancestor_node.id] = total - ancestor_index - 1
+ end
+ counts
+ end
+
def nodes_matching_tags(tags)
matching_pages = Page.tagged_with(tags, any: true).reselect(:id)
base = Node.where(head_id: matching_pages).or(Node.where(draft_id: matching_pages))
diff --git a/app/helpers/nodes_helper.rb b/app/helpers/nodes_helper.rb
index a89f879..1268b63 100644
--- a/app/helpers/nodes_helper.rb
+++ b/app/helpers/nodes_helper.rb
@@ -12,7 +12,6 @@ module NodesHelper
end
end
-
def truncated_title_for_node node
if (title = title_for_node node) && title.size > 20
"#{truncate(title, 40)}"
@@ -63,4 +62,8 @@ module NodesHelper
path = node.unique_path
CccConventions::NODE_KINDS.select { |_, config| config[:parent_match]&.call(path) }
end
+
+ def sitemap_node_open?(node)
+ !CccConventions::SITEMAP_COLLAPSED_PATHS.include?(node.unique_name)
+ end
end
diff --git a/app/views/nodes/sitemap.html.erb b/app/views/nodes/sitemap.html.erb
new file mode 100644
index 0000000..a799c17
--- /dev/null
+++ b/app/views/nodes/sitemap.html.erb
@@ -0,0 +1,32 @@
+
Sitemap
+
+
+<%
+ open_details = [] # levels with a currently-open
+%>
+<% @sitemap.each_with_index do |(node, level), index| %>
+ <% while open_details.any? && open_details.last >= level %>
+
+ <% open_details.pop %>
+ <% end %>
+
+
diff --git a/test/controllers/admin_controller_test.rb b/test/controllers/admin_controller_test.rb
index 9beaf58..00a51e2 100644
--- a/test/controllers/admin_controller_test.rb
+++ b/test/controllers/admin_controller_test.rb
@@ -15,26 +15,6 @@ class AdminControllerTest < ActionController::TestCase
assert_includes assigns(:drafts), node
end
- test "my work list shows each matching node only once, even with several revisions by the same user" do
- login_as :quentin
- user = User.find_by_login("quentin")
- node = Node.root.children.create!(:slug => "dedup_test")
- node.lock_for_editing!(user)
- node.autosave!({:title => "v1"}, user)
- node.save_draft!(user)
- node.publish_draft!
- node.lock_for_editing!(user)
- node.autosave!({:title => "v2"}, user)
- node.save_draft!(user)
- node.publish_draft!
- # three pages now exist on this node, all touched by quentin --
- # without DISTINCT, the join would return this node three times
-
- get :index
- matches = assigns(:mynodes).select { |n| n.id == node.id }
- assert_equal 1, matches.length
- end
-
test "dashboard_search returns matching tags and nodes grouped separately" do
node = Node.root.children.create!(:slug => "dashboard_search_test")
node.find_or_create_draft(User.find_by_login("aaron"))
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb
index b43d2de..81e3f45 100644
--- a/test/controllers/nodes_controller_test.rb
+++ b/test/controllers/nodes_controller_test.rb
@@ -521,6 +521,26 @@ class NodesControllerTest < ActionController::TestCase
assert_response :success
end
+ test "mine shows each matching node only once, even with several revisions by the same user" do
+ login_as :quentin
+ user = User.find_by_login("quentin")
+ node = Node.root.children.create!(:slug => "dedup_test")
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v1"}, user)
+ node.save_draft!(user)
+ node.publish_draft!
+ node.lock_for_editing!(user)
+ node.autosave!({:title => "v2"}, user)
+ node.save_draft!(user)
+ node.publish_draft!
+ # three pages now exist on this node, all touched by quentin --
+ # without DISTINCT, the join would return this node three times
+
+ get :mine
+ matches = assigns(:nodes).select { |n| n.id == node.id }
+ assert_equal 1, matches.length
+ end
+
test "chapters combined with a search term does not raise an ambiguous column error" do
login_as :quentin
get :chapters, params: { :q => "Zombies" }
diff --git a/test/models/node_test.rb b/test/models/node_test.rb
index de540f8..0bce222 100644
--- a/test/models/node_test.rb
+++ b/test/models/node_test.rb
@@ -561,4 +561,28 @@ class NodeTest < ActiveSupport::TestCase
result = Node.drafts_and_autosaves(current_user_id: @user1.id).to_a
assert result.index(mine) < result.index(someone_elses_newer)
end
+
+ test "recently_changed includes a node whose head was recently published" do
+ node = Node.root.children.create!(:slug => "recent_changed_published")
+ node.find_or_create_draft(@user1)
+ node.autosave!({:title => "v1"}, @user1)
+ node.save_draft!(@user1)
+ node.publish_draft!
+
+ assert_includes Node.recently_changed, node
+ end
+
+ test "recently_changed excludes a node only touched by locking or unlocking after an old publish" do
+ node = Node.root.children.create!(:slug => "recent_changed_lock_only")
+ node.find_or_create_draft(@user1)
+ node.autosave!({:title => "v1"}, @user1)
+ node.save_draft!(@user1)
+ node.publish_draft!
+ node.head.update_column(:updated_at, 20.days.ago)
+
+ node.lock_for_editing!(@user1)
+ node.unlock!
+
+ assert_not_includes Node.recently_changed, node
+ end
end
--
cgit v1.3