Permalink
Browse files

ancestry should skip default scopes for some of the node update methods.

  • Loading branch information...
1 parent 91bc017 commit eeadeef8e04514dcebb95a23db2b58258279d4d9 @gstokkink gstokkink committed May 4, 2012
View
5 lib/ancestry.rb
@@ -1 +1,4 @@
-require 'ancestry/has_ancestry'
+require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/class_methods')
+require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/instance_methods')
+require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/exceptions')
+require File.join(File.expand_path(File.dirname(__FILE__)), 'ancestry/has_ancestry')
View
117 lib/ancestry/class_methods.rb
@@ -73,31 +73,34 @@ def sort_by_ancestry(nodes)
def check_ancestry_integrity! options = {}
parents = {}
exceptions = [] if options[:report] == :list
- # For each node ...
- self.base_class.find_each do |node|
- begin
- # ... check validity of ancestry column
- if !node.valid? and !node.errors[node.class.ancestry_column].blank?
- raise Ancestry::AncestryIntegrityException.new("Invalid format for ancestry column of node #{node.id}: #{node.read_attribute node.ancestry_column}.")
- end
- # ... check that all ancestors exist
- node.ancestor_ids.each do |ancestor_id|
- unless exists? ancestor_id
- raise Ancestry::AncestryIntegrityException.new("Reference to non-existent node in node #{node.id}: #{ancestor_id}.")
+
+ self.base_class.send(:with_exclusive_scope) do
+ # For each node ...
+ self.base_class.find_each do |node|
+ begin
+ # ... check validity of ancestry column
+ if !node.valid? and !node.errors[node.class.ancestry_column].blank?
+ raise Ancestry::AncestryIntegrityException.new("Invalid format for ancestry column of node #{node.id}: #{node.read_attribute node.ancestry_column}.")
end
- end
- # ... check that all node parents are consistent with values observed earlier
- node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
- parents[node_id] = parent_id unless parents.has_key? node_id
- unless parents[node_id] == parent_id
- raise Ancestry::AncestryIntegrityException.new("Conflicting parent id found in node #{node.id}: #{parent_id || 'nil'} for node #{node_id} while expecting #{parents[node_id] || 'nil'}")
+ # ... check that all ancestors exist
+ node.ancestor_ids.each do |ancestor_id|
+ unless exists? ancestor_id
+ raise Ancestry::AncestryIntegrityException.new("Reference to non-existent node in node #{node.id}: #{ancestor_id}.")
+ end
+ end
+ # ... check that all node parents are consistent with values observed earlier
+ node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
+ parents[node_id] = parent_id unless parents.has_key? node_id
+ unless parents[node_id] == parent_id
+ raise Ancestry::AncestryIntegrityException.new("Conflicting parent id found in node #{node.id}: #{parent_id || 'nil'} for node #{node_id} while expecting #{parents[node_id] || 'nil'}")
+ end
+ end
+ rescue Ancestry::AncestryIntegrityException => integrity_exception
+ case options[:report]
+ when :list then exceptions << integrity_exception
+ when :echo then puts integrity_exception
+ else raise integrity_exception
end
- end
- rescue Ancestry::AncestryIntegrityException => integrity_exception
- case options[:report]
- when :list then exceptions << integrity_exception
- when :echo then puts integrity_exception
- else raise integrity_exception
end
end
end
@@ -109,53 +112,61 @@ def restore_ancestry_integrity!
parents = {}
# Wrap the whole thing in a transaction ...
self.base_class.transaction do
- # For each node ...
- self.base_class.find_each do |node|
- # ... set its ancestry to nil if invalid
- if !node.valid? and !node.errors[node.class.ancestry_column].blank?
- node.without_ancestry_callbacks do
- node.update_attribute node.ancestry_column, nil
+ self.base_class.send(:with_exclusive_scope) do
+ # For each node ...
+ self.base_class.find_each do |node|
+ # ... set its ancestry to nil if invalid
+ if !node.valid? and !node.errors[node.class.ancestry_column].blank?
+ node.without_ancestry_callbacks do
+ node.update_attribute node.ancestry_column, nil
+ end
end
- end
- # ... save parent of this node in parents array if it exists
- parents[node.id] = node.parent_id if exists? node.parent_id
+ # ... save parent of this node in parents array if it exists
+ parents[node.id] = node.parent_id if exists? node.parent_id
- # Reset parent id in array to nil if it introduces a cycle
- parent = parents[node.id]
- until parent.nil? || parent == node.id
- parent = parents[parent]
- end
- parents[node.id] = nil if parent == node.id
- end
- # For each node ...
- self.base_class.find_each do |node|
- # ... rebuild ancestry from parents array
- ancestry, parent = nil, parents[node.id]
- until parent.nil?
- ancestry, parent = if ancestry.nil? then parent else "#{parent}/#{ancestry}" end, parents[parent]
+ # Reset parent id in array to nil if it introduces a cycle
+ parent = parents[node.id]
+ until parent.nil? || parent == node.id
+ parent = parents[parent]
+ end
+ parents[node.id] = nil if parent == node.id
end
- node.without_ancestry_callbacks do
- node.update_attribute node.ancestry_column, ancestry
+
+ # For each node ...
+ self.base_class.find_each do |node|
+ # ... rebuild ancestry from parents array
+ ancestry, parent = nil, parents[node.id]
+ until parent.nil?
+ ancestry, parent = if ancestry.nil? then parent else "#{parent}/#{ancestry}" end, parents[parent]
+ end
+ node.without_ancestry_callbacks do
+ node.update_attribute node.ancestry_column, ancestry
+ end
end
end
end
end
# Build ancestry from parent id's for migration purposes
def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
- self.base_class.find_each(:conditions => {:parent_id => parent_id}) do |node|
- node.without_ancestry_callbacks do
- node.update_attribute ancestry_column, ancestry
+ self.base_class.send(:with_exclusive_scope) do
+ self.base_class.find_each(:conditions => {:parent_id => parent_id}) do |node|
+ node.without_ancestry_callbacks do
+ node.update_attribute ancestry_column, ancestry
+ end
+ build_ancestry_from_parent_ids! node.id, if ancestry.nil? then "#{node.id}" else "#{ancestry}/#{node.id}" end
end
- build_ancestry_from_parent_ids! node.id, if ancestry.nil? then "#{node.id}" else "#{ancestry}/#{node.id}" end
end
end
# Rebuild depth cache if it got corrupted or if depth caching was just turned on
def rebuild_depth_cache!
raise Ancestry::AncestryException.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_column
- self.base_class.find_each do |node|
- node.update_attribute depth_cache_column, node.depth
+
+ self.base_class.send(:with_exclusive_scope) do
+ self.base_class.find_each do |node|
+ node.update_attribute depth_cache_column, node.depth
+ end
end
end
end
View
4 lib/ancestry/has_ancestry.rb
@@ -1,7 +1,3 @@
-require 'ancestry/class_methods'
-require 'ancestry/instance_methods'
-require 'ancestry/exceptions'
-
class << ActiveRecord::Base
def has_ancestry options = {}
# Check options
View
14 lib/ancestry/instance_methods.rb
@@ -12,7 +12,7 @@ def update_descendants_with_new_ancestry
# If node is valid, not a new record and ancestry was updated ...
if changed.include?(self.base_class.ancestry_column.to_s) && !new_record? && valid?
# ... for each descendant ...
- descendants.each do |descendant|
+ unscoped_descendants.each do |descendant|
# ... replace old ancestry with new ancestry
descendant.without_ancestry_callbacks do
descendant.update_attribute(
@@ -34,16 +34,16 @@ def apply_orphan_strategy
unless ancestry_callbacks_disabled?
# If this isn't a new record ...
unless new_record?
- # ... make al children root if orphan strategy is rootify
+ # ... make all children root if orphan strategy is rootify
if self.base_class.orphan_strategy == :rootify
- descendants.each do |descendant|
+ unscoped_descendants.each do |descendant|
descendant.without_ancestry_callbacks do
descendant.update_attribute descendant.class.ancestry_column, (if descendant.ancestry == child_ancestry then nil else descendant.ancestry.gsub(/^#{child_ancestry}\//, '') end)
end
end
# ... destroy all descendants if orphan strategy is destroy
elsif self.base_class.orphan_strategy == :destroy
- descendants.all.each do |descendant|
+ unscoped_descendants.each do |descendant|
descendant.without_ancestry_callbacks do
descendant.destroy
end
@@ -228,5 +228,11 @@ def cast_primary_key(key)
def primary_key_type
@primary_key_type ||= column_for_attribute(self.class.primary_key).type
end
+
+ def unscoped_descendants
+ self.base_class.send(:with_exclusive_scope) do
+ self.base_class.all(:conditions => descendant_conditions)
+ end
+ end
end
end
View
16 test/environment.rb
@@ -3,7 +3,7 @@
require 'active_record'
require 'active_support/test_case'
require 'test/unit'
-require 'ancestry'
+require 'lib/ancestry'
class AncestryTestDatabase
def self.setup
@@ -12,10 +12,11 @@ def self.setup
end
def self.with_model options = {}
- depth = options.delete(:depth) || 0
- width = options.delete(:width) || 0
- extra_columns = options.delete(:extra_columns)
- primary_key_type = options.delete(:primary_key_type) || :default
+ depth = options.delete(:depth) || 0
+ width = options.delete(:width) || 0
+ extra_columns = options.delete(:extra_columns)
+ primary_key_type = options.delete(:primary_key_type) || :default
+ default_scope_params = options.delete(:default_scope_params)
ActiveRecord::Base.connection.create_table 'test_nodes', :id => (primary_key_type == :default) do |table|
table.string :id, :null => false if primary_key_type == :string
@@ -29,12 +30,12 @@ def self.with_model options = {}
begin
model = Class.new(ActiveRecord::Base)
(class << model; self; end).send :define_method, :model_name do; Struct.new(:human, :underscore, :i18n_key).new 'TestNode', 'test_node', 'test_node'; end
- const_set 'TestNode', model
if primary_key_type == :string
- model.before_create { self.id = ActiveSupport::SecureRandom.hex(10) }
+ model.before_create { |instance| instance.id = ActiveSupport::SecureRandom.hex(10) }
end
model.send :set_table_name, 'test_nodes'
+ model.send :default_scope, default_scope_params if default_scope_params.present?
model.has_ancestry options unless options.delete(:skip_ancestry)
if depth > 0
@@ -44,7 +45,6 @@ def self.with_model options = {}
end
ensure
ActiveRecord::Base.connection.drop_table 'test_nodes'
- remove_const "TestNode"
end
end
View
52 test/has_ancestry_test.rb
@@ -704,4 +704,56 @@ def test_sort_by_ancestry
assert_equal [n1, n2, n4, n3, n5].map(&:id), arranged.map(&:id)
end
end
+
+ def test_node_excluded_by_default_scope_should_still_move_with_parent
+ AncestryTestDatabase.with_model(
+ :width => 3, :depth => 3, :extra_columns => {:deleted_at => :datetime},
+ :default_scope_params => {:conditions => {:deleted_at => nil}}
+ ) do |model, roots|
+ grandparent = model.roots.all[0]
+ new_grandparent = model.roots.all[1]
+ parent = grandparent.children.first
+ child = parent.children.first
+
+ child.update_attributes :deleted_at => Time.now
+ parent.update_attributes :parent => new_grandparent
+ child.update_attributes :deleted_at => nil
+
+ assert child.reload.ancestors.include? new_grandparent
+ end
+ end
+
+ def test_node_excluded_by_default_scope_should_be_destroyed_with_parent
+ AncestryTestDatabase.with_model(
+ :width => 1, :depth => 2, :extra_columns => {:deleted_at => :datetime},
+ :default_scope_params => {:conditions => {:deleted_at => nil}},
+ :orphan_strategy => :destroy
+ ) do |model, roots|
+ parent = model.roots.first
+ child = parent.children.first
+
+ child.update_attributes :deleted_at => Time.now
+ parent.destroy
+ child.update_attributes :deleted_at => nil
+
+ assert model.count.zero?
+ end
+ end
+
+ def test_node_excluded_by_default_scope_should_be_rootified
+ AncestryTestDatabase.with_model(
+ :width => 1, :depth => 2, :extra_columns => {:deleted_at => :datetime},
+ :default_scope_params => {:conditions => {:deleted_at => nil}},
+ :orphan_strategy => :rootify
+ ) do |model, roots|
+ parent = model.roots.first
+ child = parent.children.first
+
+ child.update_attributes :deleted_at => Time.now
+ parent.destroy
+ child.update_attributes :deleted_at => nil
+
+ assert child.reload.is_root?
+ end
+ end
end

0 comments on commit eeadeef

Please sign in to comment.