diff options
| -rw-r--r-- | app/models/concerns/nested_tree.rb | 8 | ||||
| -rw-r--r-- | app/models/node.rb | 16 | ||||
| -rw-r--r-- | test/models/node_test.rb | 37 |
3 files changed, 50 insertions, 11 deletions
diff --git a/app/models/concerns/nested_tree.rb b/app/models/concerns/nested_tree.rb index 9a2eb0e..befcdb3 100644 --- a/app/models/concerns/nested_tree.rb +++ b/app/models/concerns/nested_tree.rb | |||
| @@ -9,8 +9,6 @@ module NestedTree | |||
| 9 | included do | 9 | included do |
| 10 | belongs_to :parent, class_name: name, foreign_key: :parent_id, optional: true, inverse_of: :children | 10 | belongs_to :parent, class_name: name, foreign_key: :parent_id, optional: true, inverse_of: :children |
| 11 | has_many :children, -> { order(:id) }, class_name: name, foreign_key: :parent_id, inverse_of: :parent | 11 | has_many :children, -> { order(:id) }, class_name: name, foreign_key: :parent_id, inverse_of: :parent |
| 12 | |||
| 13 | before_destroy :delete_descendants | ||
| 14 | end | 12 | end |
| 15 | 13 | ||
| 16 | class_methods do | 14 | class_methods do |
| @@ -77,10 +75,4 @@ module NestedTree | |||
| 77 | def move_to_child_of(new_parent) | 75 | def move_to_child_of(new_parent) |
| 78 | update!(parent_id: new_parent.id) | 76 | update!(parent_id: new_parent.id) |
| 79 | end | 77 | end |
| 80 | |||
| 81 | private | ||
| 82 | |||
| 83 | def delete_descendants | ||
| 84 | self.class.where(id: descendants.pluck(:id)).delete_all | ||
| 85 | end | ||
| 86 | end | 78 | end |
diff --git a/app/models/node.rb b/app/models/node.rb index 70ed4da..6ce6591 100644 --- a/app/models/node.rb +++ b/app/models/node.rb | |||
| @@ -12,9 +12,10 @@ class Node < ApplicationRecord | |||
| 12 | belongs_to :lock_owner, :class_name => "User", :foreign_key => :locking_user_id, optional: true | 12 | belongs_to :lock_owner, :class_name => "User", :foreign_key => :locking_user_id, optional: true |
| 13 | 13 | ||
| 14 | # Callbacks | 14 | # Callbacks |
| 15 | after_create :initialize_empty_page | 15 | after_create :initialize_empty_page |
| 16 | before_save :check_for_changed_slug | 16 | before_save :check_for_changed_slug |
| 17 | after_save :update_unique_names_of_children | 17 | after_save :update_unique_names_of_children |
| 18 | before_destroy :refuse_destroy_with_children | ||
| 18 | 19 | ||
| 19 | # Validations | 20 | # Validations |
| 20 | validates_length_of :slug, :within => 1..255, :unless => -> { parent_id.nil? || slug.blank? } | 21 | validates_length_of :slug, :within => 1..255, :unless => -> { parent_id.nil? || slug.blank? } |
| @@ -366,6 +367,15 @@ class Node < ApplicationRecord | |||
| 366 | ) | 367 | ) |
| 367 | end | 368 | end |
| 368 | 369 | ||
| 370 | # Nodes are never destroyed recursively | ||
| 371 | # Descendants must be removed or reparented individually first. | ||
| 372 | # The Trash feature will be the ordinary path to deletion. | ||
| 373 | def refuse_destroy_with_children | ||
| 374 | return unless children.exists? | ||
| 375 | errors.add(:base, "Cannot destroy a node that still has children") | ||
| 376 | throw :abort | ||
| 377 | end | ||
| 378 | |||
| 369 | def self.recently_changed | 379 | def self.recently_changed |
| 370 | includes(:head).where( | 380 | includes(:head).where( |
| 371 | "pages.updated_at < ? AND pages.updated_at > ? AND nodes.parent_id IS NOT NULL", | 381 | "pages.updated_at < ? AND pages.updated_at > ? AND nodes.parent_id IS NOT NULL", |
diff --git a/test/models/node_test.rb b/test/models/node_test.rb index 4401651..9c4834e 100644 --- a/test/models/node_test.rb +++ b/test/models/node_test.rb | |||
| @@ -770,4 +770,41 @@ class NodeTest < ActiveSupport::TestCase | |||
| 770 | node.default_template_name = "standard_template" | 770 | node.default_template_name = "standard_template" |
| 771 | assert node.valid? | 771 | assert node.valid? |
| 772 | end | 772 | end |
| 773 | |||
| 774 | test "destroying a node with children is refused" do | ||
| 775 | parent = Node.root.children.create!(:slug => "destroy_guard_parent") | ||
| 776 | parent.children.create!(:slug => "destroy_guard_child") | ||
| 777 | |||
| 778 | assert_no_difference "Node.count" do | ||
| 779 | assert_not parent.destroy | ||
| 780 | end | ||
| 781 | assert parent.errors[:base].any? | ||
| 782 | end | ||
| 783 | |||
| 784 | test "destroying a childless node leaves no orphaned pages or asset links" do | ||
| 785 | node = create_node_with_published_page | ||
| 786 | page_ids = node.pages.pluck(:id) | ||
| 787 | asset = Asset.create!(:name => "destroy cascade probe", | ||
| 788 | :upload_file_name => "test_image.png", | ||
| 789 | :upload_content_type => "image/png", | ||
| 790 | :upload_file_size => 49854, | ||
| 791 | :upload_updated_at => Time.current) | ||
| 792 | node.head.related_assets.create!(:asset_id => asset.id, :position => 1) | ||
| 793 | |||
| 794 | node.destroy | ||
| 795 | |||
| 796 | assert_equal 0, Page.where(:id => page_ids).count | ||
| 797 | assert_equal 0, RelatedAsset.where(:page_id => page_ids).count | ||
| 798 | end | ||
| 799 | |||
| 800 | test "destroying a node also removes its autosave page" do | ||
| 801 | node = create_node_with_published_page | ||
| 802 | node.lock_for_editing!(@user1) | ||
| 803 | node.autosave!({:title => "in flight"}, @user1) | ||
| 804 | autosave_id = node.autosave_id | ||
| 805 | |||
| 806 | node.destroy | ||
| 807 | |||
| 808 | assert_equal 0, Page.where(:id => autosave_id).count | ||
| 809 | end | ||
| 773 | end | 810 | end |
