Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add siblings-only finders (excluding the targeted node) #285

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 25 additions & 22 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -76,28 +76,30 @@ You can also create children through the children relation on a node:

To navigate an Ancestry model, use the following methods on any instance / record:

parent Returns the parent of the record, nil for a root node
parent_id Returns the id of the parent of the record, nil for a root node
root Returns the root of the tree the record is in, self for a root node
root_id Returns the id of the root of the tree the record is in
root?, is_root? Returns true if the record is a root node, false otherwise
ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
ancestors Scopes the model on ancestors of the record
path_ids Returns a list the path ids, starting with the root id and ending with the node's own id
path Scopes model on path records of the record
children Scopes the model on children of the record
child_ids Returns a list of child ids
has_children? Returns true if the record has any children, false otherwise
is_childless? Returns true is the record has no children, false otherwise
siblings Scopes the model on siblings of the record, the record itself is included*
sibling_ids Returns a list of sibling ids
has_siblings? Returns true if the record's parent has more than one child
is_only_child? Returns true if the record is the only child of its parent
descendants Scopes the model on direct and indirect children of the record
descendant_ids Returns a list of a descendant ids
subtree Scopes the model on descendants and itself
subtree_ids Returns a list of all ids in the record's subtree
depth Return the depth of the node, root nodes are at depth 0
parent Returns the parent of the record, nil for a root node
parent_id Returns the id of the parent of the record, nil for a root node
root Returns the root of the tree the record is in, self for a root node
root_id Returns the id of the root of the tree the record is in
root?, is_root? Returns true if the record is a root node, false otherwise
ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
ancestors Scopes the model on ancestors of the record
path_ids Returns a list the path ids, starting with the root id and ending with the node's own id
path Scopes model on path records of the record
children Scopes the model on children of the record
child_ids Returns a list of child ids
has_children? Returns true if the record has any children, false otherwise
is_childless? Returns true is the record has no children, false otherwise
siblings Scopes the model on siblings of the record, the record itself is included*
sibling_ids Returns a list of sibling ids
siblings_only Scopes the model on siblings of the record, the record is excluded*
siblings_only_ids Returns a list of sibling ids, excluding the record
has_siblings? Returns true if the record's parent has more than one child
is_only_child? Returns true if the record is the only child of its parent
descendants Scopes the model on direct and indirect children of the record
descendant_ids Returns a list of a descendant ids
subtree Scopes the model on descendants and itself
subtree_ids Returns a list of all ids in the record's subtree
depth Return the depth of the node, root nodes are at depth 0

* If the record is a root, other root records are considered siblings

Expand Down Expand Up @@ -138,6 +140,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
siblings_only_of(node) Siblings of node, excluding the 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 @@ -43,6 +43,7 @@ def has_ancestry options = {}
scope :descendants_of, lambda { |object| where(to_node(object).descendant_conditions) }
scope :subtree_of, lambda { |object| where(to_node(object).subtree_conditions) }
scope :siblings_of, lambda { |object| where(to_node(object).sibling_conditions) }
scope :siblings_only_of, lambda { |object| where(to_node(object).sibling_conditions(exclude_self: true)) }
scope :ordered_by_ancestry, lambda { reorder("(CASE WHEN #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)} IS NULL THEN 0 ELSE 1 END), #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)}") }
scope :ordered_by_ancestry_and, lambda { |order| reorder("(CASE WHEN #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)} IS NULL THEN 0 ELSE 1 END), #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)}, #{order}") }

Expand Down
19 changes: 16 additions & 3 deletions lib/ancestry/instance_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -225,21 +225,34 @@ def child_of?(node)

# Siblings

def sibling_conditions
def sibling_conditions arel_conditions = {}
t = get_arel_table
t[get_ancestry_column].eq(read_attribute(self.ancestry_base_class.ancestry_column))
conditions = t[get_ancestry_column].eq(read_attribute(self.ancestry_base_class.ancestry_column))
if arel_conditions[:exclude_self] == true
uuid = self.send(self.ancestry_base_class.primary_key.to_sym)
conditions = conditions.and(t[self.ancestry_base_class.primary_key].not_eq(uuid))
end
conditions
end

def siblings
self.ancestry_base_class.where sibling_conditions
end

def siblings_only
self.ancestry_base_class.where sibling_conditions(exclude_self: true)
end

def sibling_ids
siblings.select(self.ancestry_base_class.primary_key).collect(&self.ancestry_base_class.primary_key.to_sym)
end

def siblings_only_ids
siblings_only.select(self.ancestry_base_class.primary_key).collect(&self.ancestry_base_class.primary_key.to_sym)
end

def has_siblings?
self.siblings.count > 1
self.siblings_only.count > 0
end
alias_method :siblings?, :has_siblings?

Expand Down
5 changes: 4 additions & 1 deletion 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 siblings_only_of named scope
assert_equal test_node.siblings_only.to_a, model.siblings_only_of(test_node).to_a
assert_equal test_node.siblings_only.to_a, model.siblings_only_of(test_node.id).to_a
end
end
end
Expand Down Expand Up @@ -66,4 +69,4 @@ def test_scoping_in_callbacks
assert child = parent.children.create
end
end
end
end
8 changes: 7 additions & 1 deletion test/concerns/tree_navigration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ def test_tree_navigation
# Siblings assertions
assert_equal roots.map(&:first).map(&:id), lvl0_node.sibling_ids
assert_equal roots.map(&:first), lvl0_node.siblings
assert_equal roots.map(&:first).map(&:id) - [lvl0_node.id], lvl0_node.siblings_only_ids
assert_equal roots.map(&:first).reject {|n| n == lvl0_node}, lvl0_node.siblings_only
assert lvl0_node.has_siblings?
assert !lvl0_node.is_only_child?
# Descendants assertions
Expand Down Expand Up @@ -59,6 +61,8 @@ def test_tree_navigation
# Siblings assertions
assert_equal lvl0_children.map(&:first).map(&:id), lvl1_node.sibling_ids
assert_equal lvl0_children.map(&:first), lvl1_node.siblings
assert_equal lvl0_children.map(&:first).map(&:id) - [lvl1_node.id], lvl1_node.siblings_only_ids
assert_equal lvl0_children.map(&:first).reject {|n| n == lvl1_node}, lvl1_node.siblings_only
assert lvl1_node.has_siblings?
assert !lvl1_node.is_only_child?
# Descendants assertions
Expand Down Expand Up @@ -92,6 +96,8 @@ def test_tree_navigation
# Siblings assertions
assert_equal lvl1_children.map(&:first).map(&:id), lvl2_node.sibling_ids
assert_equal lvl1_children.map(&:first), lvl2_node.siblings
assert_equal lvl1_children.map(&:first).map(&:id) - [lvl2_node.id], lvl2_node.siblings_only_ids
assert_equal lvl1_children.map(&:first).reject {|n| n == lvl2_node}, lvl2_node.siblings_only
assert lvl2_node.has_siblings?
assert !lvl2_node.is_only_child?
# Descendants assertions
Expand All @@ -106,4 +112,4 @@ def test_tree_navigation
end
end
end
end
end