Skip to content

Commit

Permalink
Add scope for leaves
Browse files Browse the repository at this point in the history
  • Loading branch information
fursich committed May 5, 2018
1 parent c07775b commit ca13aa1
Show file tree
Hide file tree
Showing 7 changed files with 73 additions and 1 deletion.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ To navigate an Ancestry model, use the following instance methods:
|`is_only_child?` <br/> `only_child?` |true if the record is the only child of its parent|
|`descendants` |direct and indirect children of the record|
|`descendant_ids` |direct and indirect children's ids of the record|
|`leaves` |leaves of the record (i.e. childless descendants of the node)|
|`leaf_ids` |a list of all the leaves' ids of the record|
|`leaf?` </br> `is_leaf?` |Returns true if the record is a leaf node (ie. childless), false otherwise|
|`subtree` |the model on descendants and itself|
|`subtree_ids` |a list of all ids in the record's subtree|
|`depth` |the depth of the node, root nodes are at depth 0|
Expand All @@ -123,6 +126,7 @@ There are also instance methods to determine the relationship between 2 nodes:
|`root_of?(node)` | node's root is this record|
|`ancestor_of?(node)`| node's ancestors include this record|
|`child_of?(node)` | node is record's parent|
|`leaf_of?(node)` | node's leaves include this record|

# Options for `has_ancestry`

Expand Down Expand Up @@ -166,6 +170,7 @@ For convenience, a couple of named scopes are included at the class level:
descendants_of(node) Descendants of node, node can be either a record or an id
subtree_of(node) Subtree of node, node can be either a record or an id
siblings_of(node) Siblings of node, node can be either a record or an id
leaves_of(node) Leaves of node, node can be either a record or an id

Thanks to some convenient rails magic, it is even possible to create nodes
through the children and siblings scopes:
Expand Down
1 change: 1 addition & 0 deletions lib/ancestry/has_ancestry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def has_ancestry options = {}
scope :descendants_of, lambda { |object| where(descendant_conditions(object)) }
scope :subtree_of, lambda { |object| where(subtree_conditions(object)) }
scope :siblings_of, lambda { |object| where(sibling_conditions(object)) }
scope :leaves_of, lambda { |object| where(leaf_conditions(object)) }
scope :ordered_by_ancestry, Proc.new { |order|
if %w(mysql mysql2 sqlite sqlite3 postgresql).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::MAJOR >= 5
reorder(Arel.sql("coalesce(#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)}, '')"), order)
Expand Down
26 changes: 26 additions & 0 deletions lib/ancestry/instance_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,32 @@ def descendant_of?(node)
ancestor_ids.include?(node.id)
end

# Leaves

# def leaf_conditions
# self.ancestry_base_class.leaf_conditions(self)
# end
def leaf_conditions
self.ancestry_base_class.leaf_conditions(self)
end

def leaves
self.ancestry_base_class.where leaf_conditions
end

def leaf_ids
self.leaves.pluck(self.ancestry_base_class.primary_key)
end

def is_leaf?
self.leaves.to_a == [self]
end
alias_method :leaf?, :is_leaf?

def leaf_of?(node)
node.leaf_ids.include?(self.id)
end

# Subtree

def subtree_conditions
Expand Down
8 changes: 8 additions & 0 deletions lib/ancestry/materialized_path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ def sibling_conditions(object)
t[ancestry_column].eq(node[ancestry_column])
end

# idintifies leaves that belongs to the object (excluding itself)
def leaf_conditions(object)
t = arel_table
node = to_node(object)
all_ancestor_ids = self.ancestry_base_class.all.map(&:ancestor_ids).flatten.uniq
t[primary_key].not_in_all(all_ancestor_ids).and(descendant_conditions(object).or(t[primary_key].eq(node.id)))
end

module InstanceMethods
# Validates the ancestry, but can also be applied if validation is bypassed to determine if children should be affected
def sane_ancestry?
Expand Down
3 changes: 3 additions & 0 deletions test/concerns/scopes_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ def test_scopes
# Assertions for siblings_of named scope
assert_equal test_node.siblings.to_a, model.siblings_of(test_node).to_a
assert_equal test_node.siblings.to_a, model.siblings_of(test_node.id).to_a
# Assertions for leaves_of named scope
assert_equal test_node.leaves.to_a, model.leaves_of(test_node).to_a
assert_equal test_node.leaves.to_a, model.leaves_of(test_node.id).to_a
# Assertions for path_of named scope
assert_equal test_node.path.to_a, model.path_of(test_node).to_a
assert_equal test_node.path.to_a, model.path_of(test_node.id).to_a
Expand Down
26 changes: 25 additions & 1 deletion test/concerns/tree_navigration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ def test_tree_navigation
assert_equal descendants.map(&:id), lvl0_node.descendant_ids
assert_equal descendants, lvl0_node.descendants
assert_equal [lvl0_node] + descendants, lvl0_node.subtree
# Leaves assertions
leaf_ids = model.all.map(&:id) - model.all.map(&:ancestor_ids).flatten.uniq
leaves = model.all.find_all do |node|
leaf_ids.include?(node.id) && node.path_ids.include?(lvl0_node.id)
end
assert_equal leaves.map(&:id), lvl0_node.leaf_ids
assert_equal leaves, lvl0_node.leaves
assert !lvl0_node.is_leaf?

lvl0_children.each do |lvl1_node, lvl1_children|
# Ancestors assertions
Expand Down Expand Up @@ -68,6 +76,14 @@ def test_tree_navigation
assert_equal descendants.map(&:id), lvl1_node.descendant_ids
assert_equal descendants, lvl1_node.descendants
assert_equal [lvl1_node] + descendants, lvl1_node.subtree
# Leaves assertions
leaf_ids = model.all.map(&:id) - model.all.map(&:ancestor_ids).flatten.uniq
leaves = model.all.find_all do |node|
leaf_ids.include?(node.id) && node.path_ids.include?(lvl1_node.id)
end
assert_equal leaves.map(&:id), lvl1_node.leaf_ids
assert_equal leaves, lvl1_node.leaves
assert !lvl1_node.is_leaf?

lvl1_children.each do |lvl2_node, lvl2_children|
# Ancestors assertions
Expand Down Expand Up @@ -101,9 +117,17 @@ def test_tree_navigation
assert_equal descendants.map(&:id), lvl2_node.descendant_ids
assert_equal descendants, lvl2_node.descendants
assert_equal [lvl2_node] + descendants, lvl2_node.subtree
# Leaves assertions
leaf_ids = model.all.map(&:id) - model.all.map(&:ancestor_ids).flatten.uniq
leaves = model.all.find_all do |node|
leaf_ids.include?(node.id) && node.path_ids.include?(lvl2_node.id)
end
assert_equal leaves.map(&:id), lvl2_node.leaf_ids
assert_equal leaves, lvl2_node.leaves
assert lvl2_node.is_leaf?
end
end
end
end
end
end
end
5 changes: 5 additions & 0 deletions test/concerns/tree_predicate_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ def test_tree_predicates
# Descendants assertions
assert children.map { |n| !root.descendant_of?(n) }.all?
assert children.map { |n| n.descendant_of?(root) }.all?
# Leaves assertions
assert !root.is_leaf?
assert children.map { |n| n.is_leaf? }.all?
assert children.map { |n| !root.leaf_of?(n) }.all?
assert children.map { |n| n.leaf_of?(root) }.all?
end
end
end
Expand Down

0 comments on commit ca13aa1

Please sign in to comment.