Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the :touch to update ancestors on save #135

Merged
merged 2 commits into from Dec 5, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.rdoc
Expand Up @@ -73,14 +73,17 @@ The has_ancestry methods supports the following options:
:destroy All children are destroyed as well (default) :destroy All children are destroyed as well (default)
:rootify The children of the destroyed node become root nodes :rootify The children of the destroyed node become root nodes
:restrict An AncestryException is raised if any children exist :restrict An AncestryException is raised if any children exist
:adopt The orphan subtree is added to the parent of the deleted node.If the deleted node is Root, then rootify the orphan subtree. :adopt The orphan subtree is added to the parent of the deleted node.
If the deleted node is Root, then rootify the orphan subtree.
:cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false) :cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
If you turn depth_caching on for an existing model: If you turn depth_caching on for an existing model:
- Migrate: add_column [table], :ancestry_depth, :integer, :default => 0 - Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
- Build cache: TreeNode.rebuild_depth_cache! - Build cache: TreeNode.rebuild_depth_cache!
:depth_cache_column Pass in a symbol to store depth cache in a different column :depth_cache_column Pass in a symbol to store depth cache in a different column
:primary_key_format Supply a regular expression that matches the format of your primary key. :primary_key_format Supply a regular expression that matches the format of your primary key.
By default, primary keys only match integers ([0-9]+). By default, primary keys only match integers ([0-9]+).
:touch Instruct Ancestry to touch the ancestors of a node when it changes, to
invalidate nested key-based caches. (default: false)


= (Named) Scopes = (Named) Scopes


Expand Down
12 changes: 10 additions & 2 deletions lib/ancestry/has_ancestry.rb
Expand Up @@ -3,7 +3,7 @@ def has_ancestry options = {}
# Check options # Check options
raise Ancestry::AncestryException.new("Options for has_ancestry must be in a hash.") unless options.is_a? Hash raise Ancestry::AncestryException.new("Options for has_ancestry must be in a hash.") unless options.is_a? Hash
options.each do |key, value| options.each do |key, value|
unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column].include? key unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch].include? key
raise Ancestry::AncestryException.new("Unknown option for has_ancestry: #{key.inspect} => #{value.inspect}.") raise Ancestry::AncestryException.new("Unknown option for has_ancestry: #{key.inspect} => #{value.inspect}.")
end end
end end
Expand All @@ -25,7 +25,11 @@ def has_ancestry options = {}
# Save self as base class (for STI) # Save self as base class (for STI)
cattr_accessor :ancestry_base_class cattr_accessor :ancestry_base_class
self.ancestry_base_class = self self.ancestry_base_class = self


# Touch ancestors after updating
cattr_accessor :touch_ancestors
self.touch_ancestors = options[:touch] || false

# Validate format of ancestry column value # Validate format of ancestry column value
validates_format_of ancestry_column, :with => Ancestry::ANCESTRY_PATTERN, :allow_nil => true validates_format_of ancestry_column, :with => Ancestry::ANCESTRY_PATTERN, :allow_nil => true


Expand Down Expand Up @@ -69,6 +73,10 @@ def has_ancestry options = {}
where("#{depth_cache_column} #{operator} ?", depth) where("#{depth_cache_column} #{operator} ?", depth)
} }
end end

after_save :touch_ancestors_callback
after_touch :touch_ancestors_callback
after_destroy :touch_ancestors_callback
end end


# Alias has_ancestry with acts_as_tree, if it's available. # Alias has_ancestry with acts_as_tree, if it's available.
Expand Down
35 changes: 33 additions & 2 deletions lib/ancestry/instance_methods.rb
Expand Up @@ -64,6 +64,25 @@ def apply_orphan_strategy
end end
end end


# Touch each of this record's ancestors
def touch_ancestors_callback

# Skip this if callbacks are disabled
unless ancestry_callbacks_disabled?

# Only touch if the option is enabled
if self.ancestry_base_class.touch_ancestors

# Touch each of the old *and* new ancestors
self.class.where(id: (ancestor_ids + ancestor_ids_was).uniq).each do |ancestor|
ancestor.without_ancestry_callbacks do
ancestor.touch
end
end
end
end
end

# The ancestry value for this record's children # The ancestry value for this record's children
def child_ancestry def child_ancestry
# New records cannot have children # New records cannot have children
Expand All @@ -77,16 +96,28 @@ def ancestry_changed?
changed.include?(self.ancestry_base_class.ancestry_column.to_s) changed.include?(self.ancestry_base_class.ancestry_column.to_s)
end end


def parse_ancestry_column obj
obj.to_s.split('/').map { |id| cast_primary_key(id) }
end

def ancestor_ids def ancestor_ids
read_attribute(self.ancestry_base_class.ancestry_column).to_s.split('/').map { |id| cast_primary_key(id) } parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
end end


def ancestor_conditions def ancestor_conditions
{primary_key_with_table => ancestor_ids} {primary_key_with_table => ancestor_ids}
end end


def ancestors depth_options = {} def ancestors depth_options = {}
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where ancestor_conditions self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.where ancestor_conditions
end

def ancestor_was_conditions
{primary_key_with_table => ancestor_ids_was}
end

def ancestor_ids_was
parse_ancestry_column(changed_attributes[self.ancestry_base_class.ancestry_column.to_s])
end end


def path_ids def path_ids
Expand Down
44 changes: 44 additions & 0 deletions test/has_ancestry_test.rb
Expand Up @@ -810,4 +810,48 @@ def test_sort_by_ancestry_with_block
assert_equal [n1, n3, n5, n2, n4, n6].map(&:id), arranged.map(&:id) assert_equal [n1, n3, n5, n2, n4, n6].map(&:id), arranged.map(&:id)
end end
end end

def test_touch_option_disabled
AncestryTestDatabase.with_model(
:extra_columns => {:name => :string, :updated_at => :datetime},
:touch => false
) do |model|

yesterday = Time.now - 1.day
parent = model.create!(:updated_at => yesterday)
child = model.create!(:updated_at => yesterday, :parent => parent)

child.update_attributes(:name => "Changed")
assert_equal yesterday, parent.updated_at
end
end

def test_touch_option_enabled
AncestryTestDatabase.with_model(
:extra_columns => {:updated_at => :datetime},
:touch => true
) do |model|

way_back = Time.new(1984)
recently = Time.now - 1.minute

parent_1 = model.create!(:updated_at => way_back)
parent_2 = model.create!(:updated_at => way_back)
child_1_1 = model.create!(:updated_at => way_back, :parent => parent_1)
child_1_2 = model.create!(:updated_at => way_back, :parent => parent_1)
grandchild_1_1_1 = model.create!(:updated_at => way_back, :parent => child_1_1)
grandchild_1_1_2 = model.create!(:updated_at => way_back, :parent => child_1_1)

grandchild_1_1_1.parent = parent_2
grandchild_1_1_1.save!

assert grandchild_1_1_1.reload.updated_at > recently, "record was not touched"
assert child_1_1.reload.updated_at > recently, "old parent was not touched"
assert parent_1.reload.updated_at > recently, "old grandparent was not touched"
assert parent_2.reload.updated_at > recently, "new parent was not touched"

assert_equal way_back, grandchild_1_1_2.reload.updated_at, "old sibling was touched"
assert_equal way_back, child_1_2.reload.updated_at, "unrelated record was touched"
end
end
end end