summaryrefslogtreecommitdiff
path: root/app/models/node.rb
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-17 22:48:36 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-17 22:48:36 +0200
commit2748dc23be86eeb1e4f5e155b8f2191245bb5f5c (patch)
tree58a3d0953732eef3cc2f2f64b411a396246d26ee /app/models/node.rb
parent24d61c40786497d864c7c8843e68cf2af880e5f7 (diff)
Add trash!, restore_from_trash!, and destroy_from_trash! with log entries
Also update the node action contract to include the trash related verbs.
Diffstat (limited to 'app/models/node.rb')
-rw-r--r--app/models/node.rb81
1 files changed, 81 insertions, 0 deletions
diff --git a/app/models/node.rb b/app/models/node.rb
index a2e9981..4dae287 100644
--- a/app/models/node.rb
+++ b/app/models/node.rb
@@ -283,6 +283,87 @@ class Node < ApplicationRecord
283 end 283 end
284 end 284 end
285 285
286
287 # Moves this node and its subtree into the Trash. Demotes every head
288 # in the subtree first (aggregators and search operate on heads
289 # regardless of tree position); where a node has no draft, the former
290 # head becomes its draft so content stays editable and restorable --
291 # otherwise the former head remains a plain revision. One log entry,
292 # at the root, carrying the leaving-public-view snapshot.
293 def trash! current_user = nil
294 return nil if in_trash?
295 raise ActiveRecord::RecordInvalid.new(self), "The Trash node itself cannot be trashed" if trash_node?
296
297 ActiveRecord::Base.transaction do
298 path_before = unique_name
299 was_published = head_id.present?
300 final_published_at = head&.published_at
301
302 demoted = 0
303 ([self] + descendants.to_a).each do |node|
304 next unless node.head_id
305 former = node.head
306 node.head = nil
307 node.draft_id = former.id if node.draft_id.nil?
308 node.save!
309 demoted += 1
310 end
311
312 self.reload
313 move_to_child_of(Node.trash)
314 self.reload
315 update_unique_name
316 send(:update_unique_names_of_children)
317 unlock!
318
319 metadata = { :path => { "from" => path_before, "to" => unique_name } }
320 metadata[:was_published] = true if was_published
321 metadata[:final_published_at] = final_published_at.iso8601 if final_published_at
322 metadata[:demoted_heads] = demoted if demoted > 0
323
324 NodeAction.record!(:node => self, :user => current_user, :action => "trash", **metadata)
325 self
326 end
327 end
328
329 # Returns the node to the living tree under a chosen parent. The
330 # subtree comes back exactly as it sits in the Trash: all drafts,
331 # nothing published. Republication is a separate, witnessed act
332 # per node.
333 def restore_from_trash! new_parent, current_user = nil
334 return nil unless in_trash?
335
336 if new_parent.nil? || new_parent == self || descendants.include?(new_parent) ||
337 new_parent.trash_node? || new_parent.in_trash?
338 raise ActiveRecord::RecordInvalid.new(self), "Restore target must be a living node"
339 end
340
341 ActiveRecord::Base.transaction do
342 path_before = unique_name
343 move_to_child_of(new_parent)
344 self.reload
345 update_unique_name
346 send(:update_unique_names_of_children)
347
348 NodeAction.record!(:node => self, :user => current_user, :action => "restore_from_trash",
349 :path => { "from" => path_before, "to" => unique_name })
350 self
351 end
352 end
353
354 # Final deletion. Only from inside the Trash, never with children.
355 # The log entry is written first, in the same transaction; node_id
356 # nullifies when the row dies, and the metadata carries what remains.
357 def destroy_from_trash! current_user = nil
358 raise ActiveRecord::RecordInvalid.new(self), "Nodes are only destroyed from the Trash" unless in_trash?
359
360 ActiveRecord::Base.transaction do
361 NodeAction.record!(:node => self, :user => current_user, :action => "destroy",
362 :path => unique_name)
363 destroy!
364 end
365 end
366
286 # returns an array with all parts of a unique_name rather than a string 367 # returns an array with all parts of a unique_name rather than a string
287 def unique_path 368 def unique_path
288 unique_name.to_s.split("/") 369 unique_name.to_s.split("/")