Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

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

Open
wants to merge 6 commits into from

2 participants

Denis Stefan Kroes
Denis

Hi!
I added few helper methods, maybe it will be interesting for you.

Stefan Kroes
Owner

Hi,

The pull requests looks good, I would like to merge it.

Would you mind renaming 'children_of?' to 'child_of?'?

Maybe also pull the tests out of test_tree_navigation and write a separate test method test_tree_checks and test some false cases?

Kind regards,

Stefan

Denis

Ok, i will fix it.

Denis

Hi!
Sorry for the delay.

I pushed my commits. I squashed a commit with renaming children_of?.
I tested it with rails 2.3.12, 3.0.0 and 3.0.8.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
20 ancestry.gemspec
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
12 lib/ancestry/class_methods.rb
View
@@ -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
16 lib/ancestry/has_ancestry.rb
View
@@ -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
24 lib/ancestry/instance_methods.rb
View
@@ -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]
23 test/environment.rb
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
53 test/has_ancestry_test.rb
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,7 +710,7 @@ def test_arrange_order_option
end
end
end
-
+
def test_sort_by_ancestry
AncestryTestDatabase.with_model do |model|
n1 = model.create!
@@ -699,7 +718,7 @@ def test_sort_by_ancestry
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
Something went wrong with that request. Please try again.