Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Add the :touch to update ancestors on save #135

Merged
merged 2 commits into from

6 participants

@adammck

This is analogous to the same option on Rails' belongs_to association. I needed it because I'm using nested key-based cache expiration to render some big trees, and since modifying a child node doesn't touch its parent, my child nodes' fragments were never being re-rendered.

I'm currently doing this with callbacks in my models, but this seems like it could be useful elsewhere (especially with Rails 4's preference for nested fragment caching), and the default behavior doesn't/shouldn't change at all.

@seanabrahams

:thumbsup: I too am doing this via callbacks with the same use case and intended to submit a PR. +1 for merging this.

@adammck

@seanabrahams, does my branch work for you? I tried not to codify any of my own assumptions, but I'm open to feedback.

@GarPit

Guys, it will be the most useful feature for me too! :+1:

@jmccartie

Yes please! :+1:

@fenec

add it please!

@adammck,
a suggestion:
I would rename 'ancestor_was_conditions' and 'ancestor_ids_was' to 'ancestor_previous_conditions' and 'ancestor_previous_ids' accordingly. I think it would be more clear.

@adammck

@fenec, the _was convention comes from ActiveModel::Dirty. I'm not sure what I was thinking when I named ancestor_was_conditions, though. Thanks for the heads up.

@stefankroes stefankroes merged commit e32b068 into stefankroes:master
@stefankroes
Owner

Thanks! Sorry this took me so long!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 21, 2013
  1. @adammck
  2. @adammck

    Add :touch option to README

    adammck authored
This page is out of date. Refresh to see the latest.
View
5 README.rdoc
@@ -73,7 +73,8 @@ The has_ancestry methods supports the following options:
:destroy All children are destroyed as well (default)
:rootify The children of the destroyed node become root nodes
: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)
If you turn depth_caching on for an existing model:
- Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
@@ -81,6 +82,8 @@ The has_ancestry methods supports the following options:
: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.
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
View
12 lib/ancestry/has_ancestry.rb
@@ -3,7 +3,7 @@ def has_ancestry options = {}
# Check options
raise Ancestry::AncestryException.new("Options for has_ancestry must be in a hash.") unless options.is_a? Hash
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}.")
end
end
@@ -25,7 +25,11 @@ def has_ancestry options = {}
# Save self as base class (for STI)
cattr_accessor :ancestry_base_class
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
validates_format_of ancestry_column, :with => Ancestry::ANCESTRY_PATTERN, :allow_nil => true
@@ -69,6 +73,10 @@ def has_ancestry options = {}
where("#{depth_cache_column} #{operator} ?", depth)
}
end
+
+ after_save :touch_ancestors_callback
+ after_touch :touch_ancestors_callback
+ after_destroy :touch_ancestors_callback
end
# Alias has_ancestry with acts_as_tree, if it's available.
View
35 lib/ancestry/instance_methods.rb
@@ -64,6 +64,25 @@ def apply_orphan_strategy
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
def child_ancestry
# New records cannot have children
@@ -77,8 +96,12 @@ def ancestry_changed?
changed.include?(self.ancestry_base_class.ancestry_column.to_s)
end
+ def parse_ancestry_column obj
+ obj.to_s.split('/').map { |id| cast_primary_key(id) }
+ end
+
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
def ancestor_conditions
@@ -86,7 +109,15 @@ def ancestor_conditions
end
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
def path_ids
View
44 test/has_ancestry_test.rb
@@ -810,4 +810,48 @@ def test_sort_by_ancestry_with_block
assert_equal [n1, n3, n5, n2, n4, n6].map(&:id), arranged.map(&:id)
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
Something went wrong with that request. Please try again.