diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-17 22:48:36 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-17 22:48:36 +0200 |
| commit | 2748dc23be86eeb1e4f5e155b8f2191245bb5f5c (patch) | |
| tree | 58a3d0953732eef3cc2f2f64b411a396246d26ee /app/models/node.rb | |
| parent | 24d61c40786497d864c7c8843e68cf2af880e5f7 (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.rb | 81 |
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("/") |
