diff options
Diffstat (limited to 'app/models/node.rb')
| -rw-r--r-- | app/models/node.rb | 348 |
1 files changed, 263 insertions, 85 deletions
diff --git a/app/models/node.rb b/app/models/node.rb index 73eefd1..a440c2f 100644 --- a/app/models/node.rb +++ b/app/models/node.rb | |||
| @@ -4,17 +4,23 @@ class Node < ApplicationRecord | |||
| 4 | 4 | ||
| 5 | # Associations | 5 | # Associations |
| 6 | has_many :pages, -> { order("revision ASC") }, :dependent => :destroy | 6 | has_many :pages, -> { order("revision ASC") }, :dependent => :destroy |
| 7 | belongs_to :head, :class_name => "Page", :foreign_key => :head_id, :dependent => :destroy, optional: true | 7 | has_many :node_actions, :dependent => :nullify |
| 8 | belongs_to :draft, :class_name => "Page", :foreign_key => :draft_id, :dependent => :destroy, optional: true | 8 | belongs_to :head, :class_name => "Page", :foreign_key => :head_id, optional: true |
| 9 | belongs_to :draft, :class_name => "Page", :foreign_key => :draft_id, optional: true | ||
| 10 | # Autosave pages carry no node_id, so has_many :pages does not cover | ||
| 11 | # them -- this dependent: :destroy is their only cleanup on node destroy. | ||
| 9 | belongs_to :autosave, :class_name => "Page", :foreign_key => :autosave_id, :dependent => :destroy, optional: true | 12 | belongs_to :autosave, :class_name => "Page", :foreign_key => :autosave_id, :dependent => :destroy, optional: true |
| 13 | |||
| 10 | has_many :permissions, :dependent => :destroy | 14 | has_many :permissions, :dependent => :destroy |
| 11 | has_many :events, :dependent => :destroy | 15 | has_many :events, :dependent => :destroy |
| 12 | belongs_to :lock_owner, :class_name => "User", :foreign_key => :locking_user_id, optional: true | 16 | belongs_to :lock_owner, :class_name => "User", :foreign_key => :locking_user_id, optional: true |
| 13 | 17 | ||
| 14 | # Callbacks | 18 | # Callbacks |
| 15 | after_create :initialize_empty_page | 19 | after_create :initialize_empty_page |
| 16 | before_save :check_for_changed_slug | 20 | before_save :check_for_changed_slug |
| 17 | after_save :update_unique_names_of_children | 21 | after_save :update_unique_names_of_children |
| 22 | before_destroy :refuse_destroy_with_children | ||
| 23 | before_destroy :refuse_destroying_trash_node | ||
| 18 | 24 | ||
| 19 | # Validations | 25 | # Validations |
| 20 | validates_length_of :slug, :within => 1..255, :unless => -> { parent_id.nil? || slug.blank? } | 26 | validates_length_of :slug, :within => 1..255, :unless => -> { parent_id.nil? || slug.blank? } |
| @@ -22,6 +28,21 @@ class Node < ApplicationRecord | |||
| 22 | validates_uniqueness_of :slug, :scope => :parent_id, :unless => -> { parent_id.nil? } | 28 | validates_uniqueness_of :slug, :scope => :parent_id, :unless => -> { parent_id.nil? } |
| 23 | validates_presence_of :parent_id, :unless => -> { Node.root.nil? } | 29 | validates_presence_of :parent_id, :unless => -> { Node.root.nil? } |
| 24 | 30 | ||
| 31 | validate :reserved_slug_stays_reserved | ||
| 32 | validate :no_head_inside_trash | ||
| 33 | validates :default_template_name, | ||
| 34 | :inclusion => { :in => ->(_) { Page.custom_templates } }, | ||
| 35 | :allow_blank => true, | ||
| 36 | :if => :default_template_name_changed? | ||
| 37 | |||
| 38 | # Everything outside the Trash subtree, the Trash node included. | ||
| 39 | # Relies on unique_name being authoritative for tree position -- | ||
| 40 | # the same trust public routing places in it. | ||
| 41 | scope :not_in_trash, -> { | ||
| 42 | where.not(:unique_name => CccConventions::TRASH_SLUG) | ||
| 43 | .where("unique_name NOT LIKE ?", "#{CccConventions::TRASH_SLUG}/%") | ||
| 44 | } | ||
| 45 | |||
| 25 | # Class methods | 46 | # Class methods |
| 26 | 47 | ||
| 27 | # Returns a page for a given node. If no revision is supplied, it returns | 48 | # Returns a page for a given node. If no revision is supplied, it returns |
| @@ -48,6 +69,19 @@ class Node < ApplicationRecord | |||
| 48 | nil | 69 | nil |
| 49 | end | 70 | end |
| 50 | 71 | ||
| 72 | # The Trash container node. Lazily self-creating and idempotent, so | ||
| 73 | # every environment acquires it on first touch. | ||
| 74 | # Never call from validations. the positional predicates below | ||
| 75 | # exist for that. | ||
| 76 | def self.trash | ||
| 77 | root.children.find_by(:slug => CccConventions::TRASH_SLUG) || | ||
| 78 | root.children.create!(:slug => CccConventions::TRASH_SLUG).tap do |node| | ||
| 79 | Globalize.with_locale(I18n.default_locale) do | ||
| 80 | node.draft.update!(:title => "Trash") | ||
| 81 | end | ||
| 82 | end | ||
| 83 | end | ||
| 84 | |||
| 51 | # Instance Methods | 85 | # Instance Methods |
| 52 | 86 | ||
| 53 | # Acquires (or reaffirms) the editing lock without creating a draft or | 87 | # Acquires (or reaffirms) the editing lock without creating a draft or |
| @@ -145,28 +179,6 @@ class Node < ApplicationRecord | |||
| 145 | pairs | 179 | pairs |
| 146 | end | 180 | end |
| 147 | 181 | ||
| 148 | def find_or_create_draft current_user | ||
| 149 | self.wipe_draft! | ||
| 150 | if draft && self.lock_owner == current_user | ||
| 151 | draft | ||
| 152 | elsif draft && self.lock_owner.nil? | ||
| 153 | lock_for! current_user | ||
| 154 | draft.user = current_user if draft.user.nil? | ||
| 155 | draft.editor = current_user | ||
| 156 | draft.save | ||
| 157 | draft | ||
| 158 | elsif draft && self.lock_owner != current_user | ||
| 159 | raise( | ||
| 160 | LockedByAnotherUser, | ||
| 161 | "Page is locked by another user who is working on it! " \ | ||
| 162 | "Last modification: #{draft.updated_at.to_fs(:db)}" | ||
| 163 | ) | ||
| 164 | else | ||
| 165 | lock_for! current_user | ||
| 166 | create_new_draft current_user | ||
| 167 | end | ||
| 168 | end | ||
| 169 | |||
| 170 | def create_new_draft user | 182 | def create_new_draft user |
| 171 | empty_page = self.pages.create! | 183 | empty_page = self.pages.create! |
| 172 | empty_page.user = (self.head ? self.head.user : user) | 184 | empty_page.user = (self.head ? self.head.user : user) |
| @@ -192,10 +204,12 @@ class Node < ApplicationRecord | |||
| 192 | self.autosave.destroy | 204 | self.autosave.destroy |
| 193 | self.autosave_id = nil | 205 | self.autosave_id = nil |
| 194 | self.save! | 206 | self.save! |
| 207 | NodeAction.record!(:node => self, :user => current_user, :action => "discard_autosave") | ||
| 195 | elsif self.draft && self.head | 208 | elsif self.draft && self.head |
| 196 | self.draft.destroy | 209 | self.draft.destroy |
| 197 | self.draft_id = nil | 210 | self.draft_id = nil |
| 198 | self.save! | 211 | self.save! |
| 212 | NodeAction.record!(:node => self, :user => current_user, :action => "destroy_draft") | ||
| 199 | end | 213 | end |
| 200 | 214 | ||
| 201 | self.unlock! unless self.draft | 215 | self.unlock! unless self.draft |
| @@ -210,81 +224,187 @@ class Node < ApplicationRecord | |||
| 210 | end | 224 | end |
| 211 | end | 225 | end |
| 212 | 226 | ||
| 213 | def publish_draft! | 227 | def publish_draft! current_user = nil |
| 214 | # Return nil if nothing to publish and no staged changes | 228 | # Return nil if nothing to publish and no staged changes |
| 215 | return nil unless self.draft || staged_slug || staged_parent_id | 229 | return nil unless self.draft || staged_slug || staged_parent_id |
| 216 | 230 | ||
| 217 | if self.draft | 231 | if in_trash? || trash_node? |
| 218 | self.head = self.draft | 232 | raise ActiveRecord::RecordInvalid.new(self), "Cannot publish a node in the Trash" |
| 219 | self.head.published_at ||= Time.now | ||
| 220 | self.head.save! | ||
| 221 | self.draft = nil | ||
| 222 | end | 233 | end |
| 223 | 234 | ||
| 224 | if staged_slug && (staged_slug != slug) | 235 | path_before = self.unique_name |
| 225 | self.slug = staged_slug | ||
| 226 | self.staged_slug = nil | ||
| 227 | end | ||
| 228 | 236 | ||
| 229 | if staged_parent_id && (staged_parent_id != parent_id) | 237 | ActiveRecord::Base.transaction do |
| 230 | new_parent = Node.find(staged_parent_id) | 238 | if self.draft |
| 239 | outgoing_head = self.head | ||
| 240 | self.head = self.draft | ||
| 241 | self.head.published_at ||= Time.now | ||
| 242 | self.head.save! | ||
| 243 | self.draft = nil | ||
| 231 | 244 | ||
| 232 | if new_parent == self || self.descendants.include?(new_parent) | 245 | NodeAction.record!(:node => self, :page => self.head, :user => current_user, |
| 233 | raise ActiveRecord::RecordInvalid.new(self), "Cannot move a node under itself or one of its own descendants" | 246 | :action => "publish", :via => "draft", |
| 247 | **NodeAction.head_diff(outgoing_head, self.head)) | ||
| 234 | end | 248 | end |
| 235 | 249 | ||
| 236 | self.staged_parent_id = nil | 250 | if staged_slug && (staged_slug != slug) |
| 237 | self.save! | 251 | self.slug = staged_slug |
| 238 | self.move_to_child_of(new_parent) | 252 | self.staged_slug = nil |
| 239 | else | ||
| 240 | unless self.save | ||
| 241 | raise ActiveRecord::RecordInvalid.new(self) | ||
| 242 | end | 253 | end |
| 243 | end | ||
| 244 | 254 | ||
| 245 | self.reload | 255 | if staged_parent_id && (staged_parent_id != parent_id) |
| 246 | self.update_unique_name | 256 | new_parent = Node.find(staged_parent_id) |
| 247 | self.send(:update_unique_names_of_children) | ||
| 248 | self.unlock! | ||
| 249 | self | ||
| 250 | end | ||
| 251 | 257 | ||
| 252 | # Releases whatever's stale and abandoned; never anything actively in | 258 | if new_parent == self || self.descendants.include?(new_parent) |
| 253 | # use. Three independent cases share one rule -- nothing is touched | 259 | raise ActiveRecord::RecordInvalid.new(self), "Cannot move a node under itself or one of its own descendants" |
| 254 | # unless it's been sitting untouched for over a day: | 260 | end |
| 255 | # - a lock held with no draft or autosave at all (the editor opened | ||
| 256 | # the page and never actually wrote anything) | ||
| 257 | # - a fresh autosave older than a day, never promoted to a draft | ||
| 258 | # - a draft older than a day that's still identical to head | ||
| 259 | def wipe_draft! | ||
| 260 | return if self.autosave && self.autosave.updated_at > 1.day.ago | ||
| 261 | 261 | ||
| 262 | unless self.draft | 262 | self.staged_parent_id = nil |
| 263 | return if self.autosave.nil? && self.locked? && self.updated_at > 1.day.ago | 263 | self.save! |
| 264 | self.autosave&.destroy | 264 | self.move_to_child_of(new_parent) |
| 265 | self.autosave_id = nil | 265 | else |
| 266 | unless self.save | ||
| 267 | raise ActiveRecord::RecordInvalid.new(self) | ||
| 268 | end | ||
| 269 | end | ||
| 270 | |||
| 271 | self.reload | ||
| 272 | self.update_unique_name | ||
| 273 | self.send(:update_unique_names_of_children) | ||
| 274 | if self.unique_name != path_before | ||
| 275 | NodeAction.record!(:node => self, :user => current_user, :action => "move", | ||
| 276 | :path => { "from" => path_before, "to" => self.unique_name }) | ||
| 277 | end | ||
| 266 | self.unlock! | 278 | self.unlock! |
| 279 | self | ||
| 280 | end | ||
| 281 | end | ||
| 282 | |||
| 283 | def restore_revision! revision, current_user = nil | ||
| 284 | page = self.pages.find_by_revision(revision) | ||
| 285 | return nil unless page | ||
| 286 | |||
| 287 | ActiveRecord::Base.transaction do | ||
| 288 | outgoing_head = self.head | ||
| 289 | self.head = page | ||
| 267 | self.save! | 290 | self.save! |
| 268 | return | 291 | |
| 292 | NodeAction.record!(:node => self, :page => page, :user => current_user, | ||
| 293 | :action => "publish", :via => "revision", | ||
| 294 | **NodeAction.head_diff(outgoing_head, page)) | ||
| 295 | self | ||
| 269 | end | 296 | end |
| 270 | return unless self.head | 297 | end |
| 271 | return unless self.draft.updated_at < 1.day.ago | ||
| 272 | return if self.head.has_changes_to? self.draft | ||
| 273 | 298 | ||
| 274 | self.draft.destroy | 299 | |
| 275 | self.draft_id = nil | 300 | # Moves this node and its subtree into the Trash. Demotes every head |
| 276 | self.unlock! | 301 | # in the subtree first (aggregators and search operate on heads |
| 277 | self.save! | 302 | # regardless of tree position); where a node has no draft, the former |
| 278 | self.reload | 303 | # head becomes its draft so content stays editable and restorable -- |
| 304 | # otherwise the former head remains a plain revision. One log entry, | ||
| 305 | # at the root, carrying the leaving-public-view snapshot. | ||
| 306 | def trash! current_user = nil | ||
| 307 | return nil if in_trash? | ||
| 308 | raise ActiveRecord::RecordInvalid.new(self), "The Trash node itself cannot be trashed" if trash_node? | ||
| 309 | |||
| 310 | ActiveRecord::Base.transaction do | ||
| 311 | path_before = unique_name | ||
| 312 | was_published = head_id.present? | ||
| 313 | final_published_at = head&.published_at | ||
| 314 | |||
| 315 | demoted = 0 | ||
| 316 | ([self] + descendants.to_a).each do |node| | ||
| 317 | next unless node.head_id | ||
| 318 | former = node.head | ||
| 319 | node.head = nil | ||
| 320 | node.draft_id = former.id if node.draft_id.nil? | ||
| 321 | node.save! | ||
| 322 | demoted += 1 | ||
| 323 | end | ||
| 324 | |||
| 325 | self.reload | ||
| 326 | move_to_child_of(Node.trash) | ||
| 327 | self.reload | ||
| 328 | update_unique_name | ||
| 329 | send(:update_unique_names_of_children) | ||
| 330 | unlock! | ||
| 331 | |||
| 332 | metadata = { :path => { "from" => path_before, "to" => unique_name } } | ||
| 333 | metadata[:was_published] = true if was_published | ||
| 334 | metadata[:final_published_at] = final_published_at.iso8601 if final_published_at | ||
| 335 | metadata[:demoted_heads] = demoted if demoted > 0 | ||
| 336 | |||
| 337 | NodeAction.record!(:node => self, :user => current_user, :action => "trash", **metadata) | ||
| 338 | self | ||
| 339 | end | ||
| 279 | end | 340 | end |
| 280 | 341 | ||
| 281 | def restore_revision! revision | 342 | # Returns the node to the living tree under a chosen parent. The |
| 282 | if page = self.pages.find_by_revision(revision) | 343 | # subtree comes back exactly as it sits in the Trash: all drafts, |
| 283 | self.head = page | 344 | # nothing published. Republication is a separate, witnessed act |
| 284 | self.save | 345 | # per node. |
| 285 | else | 346 | def restore_from_trash! new_parent, current_user = nil |
| 286 | nil | 347 | return nil unless in_trash? |
| 348 | |||
| 349 | if new_parent.nil? || new_parent == self || descendants.include?(new_parent) || | ||
| 350 | new_parent.trash_node? || new_parent.in_trash? | ||
| 351 | raise ActiveRecord::RecordInvalid.new(self), "Restore target must be a living node" | ||
| 287 | end | 352 | end |
| 353 | |||
| 354 | ActiveRecord::Base.transaction do | ||
| 355 | path_before = unique_name | ||
| 356 | move_to_child_of(new_parent) | ||
| 357 | self.reload | ||
| 358 | update_unique_name | ||
| 359 | send(:update_unique_names_of_children) | ||
| 360 | |||
| 361 | NodeAction.record!(:node => self, :user => current_user, :action => "restore_from_trash", | ||
| 362 | :path => { "from" => path_before, "to" => unique_name }) | ||
| 363 | self | ||
| 364 | end | ||
| 365 | end | ||
| 366 | |||
| 367 | # Final deletion -- only from inside the Trash. Removes the whole | ||
| 368 | # subtree, deepest first, each node through a real destroy! so every | ||
| 369 | # per-node cascade runs (the categorical difference from the old | ||
| 370 | # delete_all nuke). refuse_destroy_with_children on bare destroy is | ||
| 371 | # untouched | ||
| 372 | # One log entry at the root, per the subtree rule, written before the | ||
| 373 | # rows die. | ||
| 374 | def destroy_from_trash! current_user = nil | ||
| 375 | raise ActiveRecord::RecordInvalid.new(self), "Nodes are only destroyed from the Trash" unless in_trash? | ||
| 376 | |||
| 377 | ActiveRecord::Base.transaction do | ||
| 378 | doomed = self_and_descendants_ordered_with_level | ||
| 379 | .sort_by { |_, level| -level } | ||
| 380 | .map(&:first) | ||
| 381 | |||
| 382 | metadata = { :path => unique_name } | ||
| 383 | metadata[:destroyed_descendants] = doomed.size - 1 if doomed.size > 1 | ||
| 384 | |||
| 385 | NodeAction.record!(:node => self, :user => current_user, :action => "destroy", **metadata) | ||
| 386 | doomed.each(&:destroy!) | ||
| 387 | end | ||
| 388 | end | ||
| 389 | |||
| 390 | # The most recent trash entry. Its path.from is the restore hint. | ||
| 391 | def last_trash_entry | ||
| 392 | node_actions.where(:action => "trash").order(:occurred_at => :desc, :id => :desc).first | ||
| 393 | end | ||
| 394 | |||
| 395 | # The node's pre-trash parent, if a living node still answers to | ||
| 396 | # that path. Nil when the parent was itself trashed (its unique_name | ||
| 397 | # changed) or deleted; a different node that has since taken over | ||
| 398 | # the path is a legitimate suggestion. | ||
| 399 | def suggested_restore_parent | ||
| 400 | from = last_trash_entry&.metadata&.dig("path", "from") | ||
| 401 | return nil unless from | ||
| 402 | |||
| 403 | parent_path = from.rpartition("/").first | ||
| 404 | return Node.root if parent_path.empty? | ||
| 405 | |||
| 406 | candidate = Node.find_by(:unique_name => parent_path) | ||
| 407 | candidate unless candidate.nil? || candidate.in_trash? || candidate.trash_node? | ||
| 288 | end | 408 | end |
| 289 | 409 | ||
| 290 | # returns an array with all parts of a unique_name rather than a string | 410 | # returns an array with all parts of a unique_name rather than a string |
| @@ -297,9 +417,12 @@ class Node < ApplicationRecord | |||
| 297 | parent.nil? ? [slug] : parent.path_to_root.push(slug) | 417 | parent.nil? ? [slug] : parent.path_to_root.push(slug) |
| 298 | end | 418 | end |
| 299 | 419 | ||
| 420 | def computed_unique_name | ||
| 421 | path_to_root[1..-1].join("/") # excluding root | ||
| 422 | end | ||
| 423 | |||
| 300 | def current_unique_name | 424 | def current_unique_name |
| 301 | path = path_to_root[1..-1] # excluding root | 425 | self.unique_name = computed_unique_name |
| 302 | self.unique_name = path.join("/") | ||
| 303 | end | 426 | end |
| 304 | 427 | ||
| 305 | def update_unique_name | 428 | def update_unique_name |
| @@ -339,6 +462,20 @@ class Node < ApplicationRecord | |||
| 339 | autosave || draft || head | 462 | autosave || draft || head |
| 340 | end | 463 | end |
| 341 | 464 | ||
| 465 | def trash_node? | ||
| 466 | parent&.root? && slug == CccConventions::TRASH_SLUG | ||
| 467 | end | ||
| 468 | |||
| 469 | # Inside the Trash subtree. False for the Trash node itself. | ||
| 470 | def in_trash? | ||
| 471 | current = parent | ||
| 472 | while current | ||
| 473 | return true if current.trash_node? | ||
| 474 | current = current.parent | ||
| 475 | end | ||
| 476 | false | ||
| 477 | end | ||
| 478 | |||
| 342 | # Returns immutable node id for all new nodes so that the atom feed entry ids | 479 | # Returns immutable node id for all new nodes so that the atom feed entry ids |
| 343 | # stay the same eventhough the slug or positions changes. | 480 | # stay the same eventhough the slug or positions changes. |
| 344 | # Can be removed after a year or so ;) | 481 | # Can be removed after a year or so ;) |
| @@ -379,7 +516,7 @@ class Node < ApplicationRecord | |||
| 379 | end | 516 | end |
| 380 | 517 | ||
| 381 | def self.drafts_and_autosaves(current_user_id: nil) | 518 | def self.drafts_and_autosaves(current_user_id: nil) |
| 382 | scope = where("draft_id IS NOT NULL OR autosave_id IS NOT NULL") | 519 | scope = where("draft_id IS NOT NULL OR autosave_id IS NOT NULL").not_in_trash |
| 383 | return scope.order("updated_at DESC") unless current_user_id | 520 | return scope.order("updated_at DESC") unless current_user_id |
| 384 | 521 | ||
| 385 | scope.order( | 522 | scope.order( |
| @@ -387,6 +524,15 @@ class Node < ApplicationRecord | |||
| 387 | ) | 524 | ) |
| 388 | end | 525 | end |
| 389 | 526 | ||
| 527 | # Nodes are never destroyed recursively | ||
| 528 | # Descendants must be removed or reparented individually first. | ||
| 529 | # The Trash feature will be the ordinary path to deletion. | ||
| 530 | def refuse_destroy_with_children | ||
| 531 | return unless children.exists? | ||
| 532 | errors.add(:base, "Cannot destroy a node that still has children") | ||
| 533 | throw :abort | ||
| 534 | end | ||
| 535 | |||
| 390 | def self.recently_changed | 536 | def self.recently_changed |
| 391 | includes(:head).where( | 537 | includes(:head).where( |
| 392 | "pages.updated_at < ? AND pages.updated_at > ? AND nodes.parent_id IS NOT NULL", | 538 | "pages.updated_at < ? AND pages.updated_at > ? AND nodes.parent_id IS NOT NULL", |
| @@ -443,4 +589,36 @@ class Node < ApplicationRecord | |||
| 443 | end | 589 | end |
| 444 | end | 590 | end |
| 445 | end | 591 | end |
| 592 | |||
| 593 | private | ||
| 594 | |||
| 595 | def reserved_slug_stays_reserved | ||
| 596 | if parent&.root? && !trash_node_already_me? | ||
| 597 | errors.add(:slug, "is reserved for the Trash") if slug == CccConventions::TRASH_SLUG | ||
| 598 | errors.add(:staged_slug, "is reserved for the Trash") if staged_slug == CccConventions::TRASH_SLUG | ||
| 599 | end | ||
| 600 | |||
| 601 | if persisted? && slug_was == CccConventions::TRASH_SLUG && Node.find(id).trash_node? | ||
| 602 | errors.add(:slug, "of the Trash node cannot change") if slug_changed? | ||
| 603 | errors.add(:parent_id, "of the Trash node cannot change") if parent_id_changed? | ||
| 604 | errors.add(:staged_slug, "must stay empty on the Trash node") if staged_slug.present? | ||
| 605 | errors.add(:staged_parent_id, "must stay empty on the Trash node") if staged_parent_id.present? | ||
| 606 | end | ||
| 607 | end | ||
| 608 | |||
| 609 | def trash_node_already_me? | ||
| 610 | existing = Node.root&.children&.find_by(:slug => CccConventions::TRASH_SLUG) | ||
| 611 | existing.nil? || existing.id == id | ||
| 612 | end | ||
| 613 | |||
| 614 | def no_head_inside_trash | ||
| 615 | return unless head_id.present? | ||
| 616 | errors.add(:head_id, "cannot exist inside the Trash") if in_trash? || trash_node? | ||
| 617 | end | ||
| 618 | |||
| 619 | def refuse_destroying_trash_node | ||
| 620 | return unless trash_node? | ||
| 621 | errors.add(:base, "The Trash node cannot be destroyed") | ||
| 622 | throw :abort | ||
| 623 | end | ||
| 446 | end | 624 | end |
