Add predicates ancestor_of?, parent_of?, root_of?, children_of?, sibling_of? and descendant_of?. #45

Closed
wants to merge 6 commits into
from
View
@@ -10,17 +10,17 @@ Gem::Specification.new do |s|
s.homepage = 'http://github.com/stefankroes/ancestry'
s.files = [
- 'ancestry.gemspec',
- 'init.rb',
- 'install.rb',
- 'lib/ancestry.rb',
- 'lib/ancestry/has_ancestry.rb',
- 'lib/ancestry/exceptions.rb',
- 'lib/ancestry/class_methods.rb',
- 'lib/ancestry/instance_methods.rb',
- 'MIT-LICENSE',
+ 'ancestry.gemspec',
+ 'init.rb',
+ 'install.rb',
+ 'lib/ancestry.rb',
+ 'lib/ancestry/has_ancestry.rb',
+ 'lib/ancestry/exceptions.rb',
+ 'lib/ancestry/class_methods.rb',
+ 'lib/ancestry/instance_methods.rb',
+ 'MIT-LICENSE',
'README.rdoc'
]
-
+
s.add_dependency 'activerecord', '>= 2.2.2'
end
@@ -3,8 +3,8 @@ module ClassMethods
# Fetch tree node if necessary
def to_node object
if object.is_a?(self.base_class) then object else find(object) end
- end
-
+ end
+
# Scope on relative depth options
def scope_depth depth_options, depth
depth_options.inject(self.base_class) do |scope, option|
@@ -26,7 +26,7 @@ def orphan_strategy= orphan_strategy
raise Ancestry::AncestryException.new("Invalid orphan strategy, valid ones are :rootify, :restrict and :destroy.")
end
end
-
+
# Arrangement
def arrange options = {}
scope =
@@ -125,7 +125,7 @@ def restore_ancestry_integrity!
until parent.nil? || parent == node.id
parent = parents[parent]
end
- parents[node.id] = nil if parent == node.id
+ parents[node.id] = nil if parent == node.id
end
# For each node ...
self.base_class.find_each do |node|
@@ -140,7 +140,7 @@ def restore_ancestry_integrity!
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|
@@ -150,7 +150,7 @@ def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
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
@@ -11,7 +11,7 @@ def has_ancestry options = {}
raise Ancestry::AncestryException.new("Unknown option for has_ancestry: #{key.inspect} => #{value.inspect}.")
end
end
-
+
# Include instance methods
include Ancestry::InstanceMethods
@@ -29,18 +29,18 @@ def has_ancestry options = {}
# Save self as base class (for STI)
cattr_accessor :base_class
self.base_class = self
-
+
# Validate format of ancestry column value
primary_key_format = options[:primary_key_format] || /[0-9]+/
validates_format_of ancestry_column, :with => /\A#{primary_key_format.source}(\/#{primary_key_format.source})*\Z/, :allow_nil => true
# Validate that the ancestor ids don't include own id
validate :ancestry_exclude_self
-
+
# Save ActiveRecord version
self.cattr_accessor :rails_3
self.rails_3 = defined?(ActiveRecord::VERSION) && ActiveRecord::VERSION::MAJOR >= 3
-
+
# Workaround to support Rails 2
scope_method = if rails_3 then :scope else :named_scope end
@@ -53,7 +53,7 @@ def has_ancestry options = {}
send scope_method, :siblings_of, lambda { |object| {:conditions => to_node(object).sibling_conditions} }
send scope_method, :ordered_by_ancestry, :order => "(case when #{table_name}.#{ancestry_column} is null then 0 else 1 end), #{table_name}.#{ancestry_column}"
send scope_method, :ordered_by_ancestry_and, lambda { |order| {:order => "(case when #{table_name}.#{ancestry_column} is null then 0 else 1 end), #{table_name}.#{ancestry_column}, #{order}"} }
-
+
# Update descendants with new ancestry before save
before_save :update_descendants_with_new_ancestry
@@ -72,7 +72,7 @@ def has_ancestry options = {}
# Validate depth column
validates_numericality_of depth_cache_column, :greater_than_or_equal_to => 0, :only_integer => true, :allow_nil => false
end
-
+
# Create named scopes for depth
{:before_depth => '<', :to_depth => '<=', :at_depth => '=', :from_depth => '>=', :after_depth => '>'}.each do |scope_name, operator|
send scope_method, scope_name, lambda { |depth|
@@ -81,9 +81,9 @@ def has_ancestry options = {}
}
end
end
-
+
# Alias has_ancestry with acts_as_tree, if it's available.
- if !defined?(ActsAsTree)
+ if !defined?(ActsAsTree)
alias_method :acts_as_tree, :has_ancestry
end
end
@@ -97,6 +97,10 @@ def cache_depth
write_attribute self.base_class.depth_cache_column, depth
end
+ def ancestor_of?(node)
+ node.ancestor_ids.include?(self.id)
+ end
+
# Parent
def parent= parent
write_attribute(self.base_class.ancestry_column, if parent.blank? then nil else parent.child_ancestry end)
@@ -114,6 +118,10 @@ def parent
if parent_id.blank? then nil else self.base_class.find(parent_id) end
end
+ def parent_of?(node)
+ self.id == node.parent_id
+ end
+
# Root
def root_id
if ancestor_ids.empty? then id else ancestor_ids.first end
@@ -127,6 +135,10 @@ def is_root?
read_attribute(self.base_class.ancestry_column).blank?
end
+ def root_of?(node)
+ self.id == node.root_id
+ end
+
# Children
def child_conditions
{self.base_class.ancestry_column => child_ancestry}
@@ -148,6 +160,10 @@ def is_childless?
!has_children?
end
+ def child_of?(node)
+ self.parent_id == node.id
+ end
+
# Siblings
def sibling_conditions
{self.base_class.ancestry_column => read_attribute(self.base_class.ancestry_column)}
@@ -169,6 +185,10 @@ def is_only_child?
!has_siblings?
end
+ def sibling_of?(node)
+ self.ancestry == node.ancestry
+ end
+
# Descendants
def descendant_conditions
["#{self.base_class.table_name}.#{self.base_class.ancestry_column} like ? or #{self.base_class.table_name}.#{self.base_class.ancestry_column} = ?", "#{child_ancestry}/%", child_ancestry]
@@ -182,6 +202,10 @@ def descendant_ids depth_options = {}
descendants(depth_options).all(:select => self.base_class.primary_key).collect(&self.base_class.primary_key.to_sym)
end
+ def descendant_of?(node)
+ ancestor_ids.include?(node.id)
+ end
+
# Subtree
def subtree_conditions
["#{self.base_class.table_name}.#{self.base_class.primary_key} = ? or #{self.base_class.table_name}.#{self.base_class.ancestry_column} like ? or #{self.base_class.table_name}.#{self.base_class.ancestry_column} = ?", self.id, "#{child_ancestry}/%", child_ancestry]
View
@@ -28,14 +28,25 @@ 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).new 'TestNode', 'test_node'; end
const_set 'TestNode', model
- if primary_key_type == :string
- model.before_create { self.id = ActiveSupport::SecureRandom.hex(10) }
+ model.class_eval do
+ before_create :set_id if primary_key_type == :string
+ has_ancestry options unless options.delete(:skip_ancestry)
+
+ def self.model_name
+ if rails_3
+ ActiveModel::Name.new(TestNode)
+ else
+ Struct.new(:human, :underscore).new('TestNode', 'test_node')
+ end
+ end
+
+ private
+ def set_id
+ self.id = ActiveSupport::SecureRandom.hex(10)
+ end
end
- model.send :set_table_name, 'test_nodes'
- model.has_ancestry options unless options.delete(:skip_ancestry)
if depth > 0
yield model, create_test_nodes(model, depth, width)
@@ -44,7 +55,7 @@ def self.with_model options = {}
end
ensure
ActiveRecord::Base.connection.drop_table 'test_nodes'
- remove_const "TestNode"
+ remove_const 'TestNode'
end
end
View
@@ -88,17 +88,12 @@ def test_tree_navigation
# Root assertions
assert_equal lvl0_node.id, lvl0_node.root_id
assert_equal lvl0_node, lvl0_node.root
- assert lvl0_node.is_root?
# Children assertions
assert_equal lvl0_children.map(&:first).map(&:id), lvl0_node.child_ids
assert_equal lvl0_children.map(&:first), lvl0_node.children
- assert lvl0_node.has_children?
- assert !lvl0_node.is_childless?
# Siblings assertions
assert_equal roots.map(&:first).map(&:id), lvl0_node.sibling_ids
assert_equal roots.map(&:first), lvl0_node.siblings
- assert lvl0_node.has_siblings?
- assert !lvl0_node.is_only_child?
# Descendants assertions
descendants = model.all.find_all do |node|
node.ancestor_ids.include? lvl0_node.id
@@ -120,17 +115,12 @@ def test_tree_navigation
# Root assertions
assert_equal lvl0_node.id, lvl1_node.root_id
assert_equal lvl0_node, lvl1_node.root
- assert !lvl1_node.is_root?
# Children assertions
assert_equal lvl1_children.map(&:first).map(&:id), lvl1_node.child_ids
assert_equal lvl1_children.map(&:first), lvl1_node.children
- assert lvl1_node.has_children?
- assert !lvl1_node.is_childless?
# Siblings assertions
assert_equal lvl0_children.map(&:first).map(&:id), lvl1_node.sibling_ids
assert_equal lvl0_children.map(&:first), lvl1_node.siblings
- assert lvl1_node.has_siblings?
- assert !lvl1_node.is_only_child?
# Descendants assertions
descendants = model.all.find_all do |node|
node.ancestor_ids.include? lvl1_node.id
@@ -152,17 +142,12 @@ def test_tree_navigation
# Root assertions
assert_equal lvl0_node.id, lvl2_node.root_id
assert_equal lvl0_node, lvl2_node.root
- assert !lvl2_node.is_root?
# Children assertions
assert_equal [], lvl2_node.child_ids
assert_equal [], lvl2_node.children
- assert !lvl2_node.has_children?
- assert lvl2_node.is_childless?
# Siblings assertions
assert_equal lvl1_children.map(&:first).map(&:id), lvl2_node.sibling_ids
assert_equal lvl1_children.map(&:first), lvl2_node.siblings
- assert lvl2_node.has_siblings?
- assert !lvl2_node.is_only_child?
# Descendants assertions
descendants = model.all.find_all do |node|
node.ancestor_ids.include? lvl2_node.id
@@ -176,6 +161,40 @@ def test_tree_navigation
end
end
+ def test_tree_predicates
+ AncestryTestDatabase.with_model :depth => 2, :width => 3 do |model, roots|
+ roots.each do |lvl0_node, lvl0_children|
+ root, children = lvl0_node, lvl0_children.map(&:first)
+ # Ancestors assertions
+ assert children.map { |n| root.ancestor_of?(n) }.all?
+ assert children.map { |n| !n.ancestor_of?(root) }.all?
+ # Parent assertions
+ assert children.map { |n| root.parent_of?(n) }.all?
+ assert children.map { |n| !n.parent_of?(root) }.all?
+ # Root assertions
+ assert root.is_root?
+ assert children.map { |n| !n.is_root? }.all?
+ assert children.map { |n| root.root_of?(n) }.all?
+ assert children.map { |n| !n.root_of?(root) }.all?
+ # Children assertions
+ assert root.has_children?
+ assert !root.is_childless?
+ assert children.map { |n| n.is_childless? }.all?
+ assert children.map { |n| !root.child_of?(n) }.all?
+ assert children.map { |n| n.child_of?(root) }.all?
+ # Siblings assertions
+ assert root.has_siblings?
+ assert !root.is_only_child?
+ assert children.map { |n| !n.is_only_child? }.all?
+ assert children.map { |n| !root.sibling_of?(n) }.all?
+ assert children.permutation(2).map { |l, r| l.sibling_of?(r) }.all?
+ # Descendants assertions
+ assert children.map { |n| !root.descendant_of?(n) }.all?
+ assert children.map { |n| n.descendant_of?(root) }.all?
+ end
+ end
+ end
+
def test_ancestors_with_string_primary_keys
AncestryTestDatabase.with_model :depth => 3, :width => 3, :primary_key_type => :string, :primary_key_format => /[a-z0-9]+/ do |model, roots|
roots.each do |lvl0_node, lvl0_children|
@@ -691,15 +710,15 @@ def test_arrange_order_option
end
end
end
-
+
def test_sort_by_ancestry
AncestryTestDatabase.with_model do |model|
n1 = model.create!
n2 = model.create!(:parent => n1)
n3 = model.create!(:parent => n2)
n4 = model.create!(:parent => n2)
n5 = model.create!(:parent => n1)
-
+
arranged = model.sort_by_ancestry(model.all.sort_by(&:id).reverse)
assert_equal [n1, n2, n4, n3, n5].map(&:id), arranged.map(&:id)
end