Search code examples
ruby-on-railsrubyacts-as-tree

acts_as_tree does not destroy the model's children


I have this Task model:

class Task < ActiveRecord::Base
  acts_as_tree :order => 'sort_order'
end

And I have this test

class TaskTest < Test::Unit::TestCase
  def setup
    @root = create_root
  end

  def test_destroying_a_task_should_destroy_all_of_its_descendants
    d1 = create_task(:parent_id => @root.id, :sort_order => 2)
    d2 = create_task(:parent_id => d1.id, :sort_order => 3)
    d3 = create_task(:parent_id => d2.id, :sort_order => 4)
    d4 = create_task(:parent_id => d1.id, :sort_order => 5)
    assert_equal 5, Task.count

    d1.destroy

    assert_equal @root, Task.find(:first)
    assert_equal 1, Task.count
  end
end

The test is successful: when I destroy d1, it destroys all the descendants of d1. Thus, after the destroy only the root is left.

However, this test is now failing after I have added a before_save callback to the Task. This is the code I added to Task:

before_save :update_descendants_if_necessary

def update_descendants_if_necessary
  handle_parent_id_change if self.parent_id_changed?
  return true
end

def handle_parent_id_change
  self.children.each do |sub_task|
    #the code within the loop is deliberately commented out
  end
end

When I added this code, assert_equal 1, Task.count fails, with Task.count == 4. I think self.children under handled_parent_id_change is the culprit, because when I comment out the self.children.each do |sub_task| block, the test passes again.

Any ideas?


Solution

  • I found the bug. The line

    d1 = create_task(:parent_id => @root.id, :sort_order => 2)
    

    creates d1. This calls the before_save callback, which in turn calls self.children. As Orion pointed out, this caches the children of d1.

    However, at this point, d1 doesn't have any children yet. So d1's cache of children is empty.

    Thus, when I try to destroy d1, the program tries to destroy d1's children. It encounters the cache, finds that it is empty, and a result doesn't destroy d2, d3, and d4.

    I solved this by changing the task creations like this:

    @root.children << (d1 = new_task(:sort_order => 2))
    @root.save!
    

    This worked so I'm ok with it :) I think it is also possible to fix this by either reloading d1 (d1.reload) or self.children (self.children(true)) although I didn't try any of these solutions.