module SymetrieCom::Acts::NestedSet::InstanceMethods
This module provides instance methods for an enhanced acts_as_nested_set mixin. Please see the README for background information, examples, and tips on usage.
Public Instance Methods
By default, records are compared and sorted using the left column.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 450 def <=>(x) self[left_col_name] <=> x[left_col_name] end
Deprecated. Adds a child to this object in the tree. If this object hasn’t been initialized, it gets set up as a root node.
This method exists only for compatibility and will be removed in future versions.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 754 def add_child(child) transaction do self.reload; child.reload # for compatibility with old version # the old version allows records with nil values for lft and rgt unless self[left_col_name] && self[right_col_name] if child[left_col_name] || child[right_col_name] raise ActiveRecord::ActiveRecordError, "If parent lft or rgt are nil, you can't add a child with non-nil lft or rgt" end base_set_class.update_all("#{left_col_name} = CASE \ WHEN id = #{self.id} \ THEN 1 \ WHEN id = #{child.id} \ THEN 3 \ ELSE #{left_col_name} END, \ #{right_col_name} = CASE \ WHEN id = #{self.id} \ THEN 2 \ WHEN id = #{child.id} \ THEN 4 \ ELSE #{right_col_name} END", scope_condition) self.reload; child.reload end unless child[left_col_name] && child[right_col_name] maxright = base_set_class.maximum(right_col_name, :conditions => scope_condition) || 0 base_set_class.update_all("#{left_col_name} = CASE \ WHEN id = #{child.id} \ THEN #{maxright + 1} \ ELSE #{left_col_name} END, \ #{right_col_name} = CASE \ WHEN id = #{child.id} \ THEN #{maxright + 2} \ ELSE #{right_col_name} END", scope_condition) child.reload end child.move_to_child_of(self) # self.reload ## even though move_to calls target.reload, at least one object in the tests was not reloading (near the end of test_common_usage) end # self.reload # child.reload # # if child.root? # raise ActiveRecord::ActiveRecordError, "Adding sub-tree isn\'t currently supported" # else # if ( (self[left_col_name] == nil) || (self[right_col_name] == nil) ) # # Looks like we're now the root node! Woo # self[left_col_name] = 1 # self[right_col_name] = 4 # # # What do to do about validation? # return nil unless self.save # # child[parent_col_name] = self.id # child[left_col_name] = 2 # child[right_col_name]= 3 # return child.save # else # # OK, we need to add and shift everything else to the right # child[parent_col_name] = self.id # right_bound = self[right_col_name] # child[left_col_name] = right_bound # child[right_col_name] = right_bound + 1 # self[right_col_name] += 2 # self.class.transaction { # self.class.update_all( "#{left_col_name} = (#{left_col_name} + 2)", "#{scope_condition} AND #{left_col_name} >= #{right_bound}" ) # self.class.update_all( "#{right_col_name} = (#{right_col_name} + 2)", "#{scope_condition} AND #{right_col_name} >= #{right_bound}" ) # self.save # child.save # } # end # end end
Returns all children and nested children. Pass :exclude => item, or id, or [items or id] to exclude one or more items and all of their descendants.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 634 def all_children(scope = {}) full_set(scope) - [self] end
Returns the number of nested children of this object.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 567 def all_children_count(scope = nil) return all_children(scope).length if scope.is_a?(Hash) return (self[right_col_name] - self[left_col_name] - 1)/2 end
All children until the other is reached - excluding self
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 679 def all_children_through(other, scope = {}) full_set_through(other, scope) - [self] end
Returns an array of all parents, starting with the root.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 489 def ancestors(scope = {}) self_and_ancestors(scope) - [self] end
All nodes between two nodes, those nodes included in effect all ancestors until the other is reached
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 667 def ancestors_and_self_through(other, scope = {}) first, last = [self, other].sort self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{last[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name}) AND #{prefixed_left_col_name} >= #{first[left_col_name]}", :order => "#{prefixed_left_col_name}" }, scope) end
Ancestors until the other is reached - excluding self
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 674 def ancestors_through(other, scope = {}) ancestors_and_self_through(other, scope) - [self] end
Checks the left/right indexes of the entire tree that this node belongs to, returning the number of records checked. Throws ActiveRecord::ActiveRecordError if it finds a problem. This method is needed because check_subtree
alone cannot find gaps between virtual roots, orphaned nodes or endless loops.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 717 def check_full_tree total_nodes = 0 transaction do # virtual roots make this method more complex than it otherwise would be n = 1 roots.each do |r| raise ActiveRecord::ActiveRecordError, "Gaps between roots in the tree containing record ##{r.id}" if r[left_col_name] != n r.check_subtree n = r[right_col_name] + 1 end total_nodes = roots.inject(0) {|sum, r| sum + r.all_children_count + 1 } unless base_set_class.count(:conditions => "#{scope_condition}") == total_nodes raise ActiveRecord::ActiveRecordError, "Orphaned nodes or endless loops in the tree containing record ##{self.id}" end end return total_nodes end
Checks the left/right indexes of one node and all descendants. Throws ActiveRecord::ActiveRecordError if it finds a problem.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 707 def check_subtree transaction do self.reload check # this method is implemented via #check, so that we don't generate lots of unnecessary nested transactions end end
Deprecated. Returns true if this is a child node
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 461 def child? parent_id = self[parent_col_name] !(parent_id == 0 || parent_id.nil?) && (self[left_col_name] > 1) && (self[right_col_name] > self[left_col_name]) end
Returns the child for the requested id within the scope of its children, otherwise nil
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 587 def child_by_id(id, scope = {}) children_by_id(id, scope).first end
Tests wether self is within scope of parent
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 615 def child_of?(parent, scope = {}) if !scope.empty? && parent.respond_to?(:child_by_id) parent.child_by_id(self.id, scope).is_a?(self.class) else parent.respond_to?(left_col_name) && self[left_col_name] > parent[left_col_name] && self[right_col_name] < parent[right_col_name] end end
Returns this record’s immediate children.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 643 def children(scope = {}) self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND #{prefixed_parent_col_name} = #{self.id}", :order => "#{prefixed_left_col_name}" }, scope) end
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 647 def children?(scope = {}) children_count(scope) > 0 end
Returns a child collection for the requested ids within the scope of its children, otherwise empty array
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 592 def children_by_id(*args) scope = args.last.is_a?(Hash) ? args.pop : {} ids = args.flatten.compact.uniq self.class.find_in_nested_set(:all, { :conditions => ["#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{self.class.table_name}.#{self.class.primary_key} IN (?)", ids] }, scope) end
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 638 def children_count(scope= {}) self.class.count_in_nested_set({ :conditions => "#{scope_condition} AND #{prefixed_parent_col_name} = #{self.id}" }, scope) end
On destruction, delete all children and shift the lft/rgt values back to the left so the counts still work.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 429 def destroy_descendants # already protected by a transaction within #destroy return if self[right_col_name].nil? || self[left_col_name].nil? || self.skip_before_destroy reloaded = self.reload rescue nil # in case a concurrent move has altered the indexes - rescue if non-existent return unless reloaded dif = self[right_col_name] - self[left_col_name] + 1 if acts_as_nested_set_options[:dependent] == :delete_all base_set_class.delete_all( "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})" ) else set = base_set_class.find(:all, :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})", :order => "#{prefixed_right_col_name} DESC") set.each { |child| child.skip_before_destroy = true; remove_descendant(child) } end base_set_class.update_all("#{left_col_name} = CASE \ WHEN #{left_col_name} > #{self[right_col_name]} THEN (#{left_col_name} - #{dif}) \ ELSE #{left_col_name} END, \ #{right_col_name} = CASE \ WHEN #{right_col_name} > #{self[right_col_name]} THEN (#{right_col_name} - #{dif} ) \ ELSE #{right_col_name} END", scope_condition) end
Returns the child for the requested id within the scope of its immediate children, otherwise nil
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 601 def direct_child_by_id(id, scope = {}) direct_children_by_id(id, scope).first end
Tests wether self is within immediate scope of parent
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 624 def direct_child_of?(parent, scope = {}) if !scope.empty? && parent.respond_to?(:direct_child_by_id) parent.direct_child_by_id(self.id, scope).is_a?(self.class) else parent.respond_to?(parent_col_name) && self[parent_col_name] == parent.id end end
Returns a child collection for the requested ids within the scope of its immediate children, otherwise empty array
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 606 def direct_children_by_id(*args) scope = args.last.is_a?(Hash) ? args.pop : {} ids = args.flatten.compact.uniq self.class.find_in_nested_set(:all, { :conditions => ["#{scope_condition} AND #{prefixed_parent_col_name} = #{self.id} AND #{self.class.table_name}.#{self.class.primary_key} IN (?)", ids] }, scope) end
Returns first siblings amongst it’s siblings.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 516 def first_sibling(scope = {}) self_and_siblings(scope.merge(:limit => 1, :order => "#{prefixed_left_col_name} ASC")).first end
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 520 def first_sibling?(scope = {}) self == first_sibling(scope) end
Returns itself and all nested children. Pass :exclude => item, or id, or [items or id] to exclude one or more items and all of their descendants.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 574 def full_set(scope = {}) if exclude = scope.delete(:exclude) exclude_str = " AND NOT (#{base_set_class.sql_for(exclude)}) " elsif new_record? || self[right_col_name] - self[left_col_name] == 1 return [self] end self.class.find_in_nested_set(:all, { :order => "#{prefixed_left_col_name}", :conditions => "#{scope_condition} #{exclude_str} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})" }, scope) end
All children until the other is reached - including self
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 684 def full_set_through(other, scope = {}) first, last = [self, other].sort self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{first[left_col_name]} AND #{first[right_col_name]}) AND #{prefixed_left_col_name} <= #{last[left_col_name]}", :order => "#{prefixed_left_col_name}" }, scope) end
Insert a node at a specific position among the children of target.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 830 def insert_at(target, index = :last, scope = {}) level_nodes = target.children(scope) current_index = level_nodes.index(self) last_index = level_nodes.length - 1 as_first = (index == :first) as_last = (index == :last || (index.is_a?(Fixnum) && index > last_index)) index = 0 if as_first index = last_index if as_last if last_index < 0 move_to_child_of(target) elsif index >= 0 && index <= last_index && level_nodes[index] if as_last && index != current_index move_to_right_of(level_nodes[index]) elsif (as_first || index == 0) && index != current_index move_to_left_of(level_nodes[index]) elsif !current_index.nil? && index > current_index move_to_right_of(level_nodes[index]) elsif !current_index.nil? && index < current_index move_to_left_of(level_nodes[index]) elsif current_index.nil? move_to_left_of(level_nodes[index]) end end end
Returns last siblings amongst it’s siblings.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 526 def last_sibling(scope = {}) self_and_siblings(scope.merge(:limit => 1, :order => "#{prefixed_left_col_name} DESC")).first end
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 530 def last_sibling?(scope = {}) self == last_sibling(scope) end
Returns this record’s terminal children (nodes without children).
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 655 def leaves(scope = {}) self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{prefixed_left_col_name} + 1 = #{prefixed_right_col_name}", :order => "#{prefixed_left_col_name}" }, scope) end
Returns the count of this record’s terminal children (nodes without children).
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 661 def leaves_count(scope = {}) self.class.count_in_nested_set({ :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{prefixed_left_col_name} + 1 = #{prefixed_right_col_name}" }, scope) end
Returns the level of this object in the tree, root level being 0.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 561 def level(scope = {}) return 0 if self[parent_col_name].nil? self.class.count_in_nested_set({ :conditions => "#{scope_condition} AND (#{self[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name})" }, scope) - 1 end
Moves a node one down amongst its siblings. Does nothing if it’s already the last sibling.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 887 def move_higher prev_sib = previous_sibling move_to_left_of(prev_sib) if prev_sib end
Moves a node one up amongst its siblings. Does nothing if it’s already the first sibling.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 880 def move_lower next_sib = next_sibling move_to_right_of(next_sib) if next_sib end
Moves a node one to be the last amongst its siblings. Does nothing if it’s already the last sibling.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 901 def move_to_bottom last_sib = last_sibling move_to_right_of(last_sib) if last_sib && self != last_sib end
Make this node a child of target (you can pass an object or just an id). Unsaved changes in either object will be lost. Raises ActiveRecord::ActiveRecordError if it encounters a problem.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 869 def move_to_child_of(target) self.move_to target, :child end
Move this node to the left of target (you can pass an object or just an id). Unsaved changes in either object will be lost. Raises ActiveRecord::ActiveRecordError if it encounters a problem.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 857 def move_to_left_of(target) self.move_to target, :left end
Moves a node to a certain position amongst its siblings.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 874 def move_to_position(index, scope = {}) insert_at(self.parent, index, scope) end
Move this node to the right of target (you can pass an object or just an id). Unsaved changes in either object will be lost. Raises ActiveRecord::ActiveRecordError if it encounters a problem.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 863 def move_to_right_of(target) self.move_to target, :right end
Moves a node one to be the first amongst its siblings. Does nothing if it’s already the first sibling.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 894 def move_to_top first_sib = first_sibling move_to_left_of(first_sib) if first_sib && self != first_sib end
Returns next sibling of node or nil if there is none.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 544 def next_sibling(num = 1, scope = {}) scope[:limit] = num siblings = next_siblings(scope) num == 1 ? siblings.first : siblings end
Returns all siblings to the right of self, in ascending order, so the first sibling is the one closest to the right of self
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 510 def next_siblings(scope = {}) self.class.find_in_nested_set(:all, { :conditions => ["#{scope_condition} AND #{sibling_condition} AND #{self.class.table_name}.id != ? AND #{prefixed_left_col_name} > ?", self.id, self[right_col_name]], :order => "#{prefixed_left_col_name} ASC"}, scope) end
Returns this record’s parent.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 484 def parent self.class.find_in_nested_set(self[parent_col_name]) if self[parent_col_name] end
Returns previous sibling of node or nil if there is none.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 536 def previous_sibling(num = 1, scope = {}) scope[:limit] = num siblings = previous_siblings(scope) num == 1 ? siblings.first : siblings end
Returns all siblings to the left of self, in descending order, so the first sibling is the one closest to the left of self
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 504 def previous_siblings(scope = {}) self.class.find_in_nested_set(:all, { :conditions => ["#{scope_condition} AND #{sibling_condition} AND #{self.class.table_name}.id != ? AND #{prefixed_right_col_name} < ?", self.id, self[left_col_name]], :order => "#{prefixed_left_col_name} DESC" }, scope) end
Re-calculate the left/right values of all nodes in this record’s tree. Can be used to convert an ordinary tree into a nested set.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 736 def renumber_full_tree indexes = [] n = 1 transaction do for r in roots # because we may have virtual roots n = 1 + r.calc_numbers(n, indexes) end for i in indexes base_set_class.update_all("#{left_col_name} = #{i[:lft]}, #{right_col_name} = #{i[:rgt]}", "#{self.class.primary_key} = #{i[:id]}") end end ## reload? end
Reorder children according to an array of ids
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 913 def reorder_children(*ids) transaction do ordered_ids = ids.flatten.uniq current_children = children({ :conditions => { :id => ordered_ids } }) current_children_ids = current_children.map(&:id) ordered_ids = ordered_ids & current_children_ids return [] unless ordered_ids.length > 1 && ordered_ids != current_children_ids perform_reorder_of_children(ordered_ids, current_children) end end
Returns this record’s root ancestor.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 472 def root(scope = {}) # the BETWEEN clause is needed to ensure we get the right virtual root, if using those self.class.find_in_nested_set(:first, { :conditions => "#{scope_condition} \ AND (#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0) AND (#{self[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name})" }, scope) end
Deprecated. Returns true if this is a root node.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 455 def root? parent_id = self[parent_col_name] (parent_id == 0 || parent_id.nil?) && self[right_col_name] && self[left_col_name] && (self[right_col_name] > self[left_col_name]) end
Returns the root or virtual roots of this record’s tree (a tree cannot have more than one real root). See the explanation of virtual roots in the README.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 479 def roots(scope = {}) self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)", :order => "#{prefixed_left_col_name}" }, scope) end
Returns an array of all parents plus self, starting with the root.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 494 def self_and_ancestors(scope = {}) self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{self[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name})", :order => "#{prefixed_left_col_name}" }, scope) end
Returns all the children of this node’s parent, including self.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 552 def self_and_siblings(scope = {}) if self[parent_col_name].nil? || self[parent_col_name].zero? [self] else self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND #{sibling_condition}", :order => "#{prefixed_left_col_name}" }, scope) end end
All siblings until the other is reached - including self
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 691 def self_and_siblings_through(other, scope = {}) if self[parent_col_name].nil? || self[parent_col_name].zero? [self] else first, last = [self, other].sort self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND #{sibling_condition} AND (#{prefixed_left_col_name} BETWEEN #{first[left_col_name]} AND #{last[right_col_name]})", :order => "#{prefixed_left_col_name}" }, scope) end end
On creation, automatically add the new node to the right of all existing nodes in this tree.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 422 def set_left_right # already protected by a transaction within #create maxright = base_set_class.maximum(right_col_name, :conditions => scope_condition) || 0 self[left_col_name] = maxright+1 self[right_col_name] = maxright+2 end
This takes care of valid queries when called on a root node
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 417 def sibling_condition self[parent_col_name] ? "#{prefixed_parent_col_name} = #{self[parent_col_name]}" : "(#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)" end
Returns all the children of this node’s parent, except self.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 499 def siblings(scope = {}) self_and_siblings(scope) - [self] end
All siblings until the other is reached - excluding self
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 701 def siblings_through(other, scope = {}) self_and_siblings_through(other, scope) - [self] end
Swaps the position of two sibling nodes preserving a sibling’s descendants. The current implementation only works amongst siblings.
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 908 def swap(target, transact = true) move_to(target, :swap, transact) end
Deprecated. Returns true if we have no idea what this is
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 467 def unknown? !root? && !child? end
Protected Instance Methods
Calculate the least amount of swap steps to achieve the requested order
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 1079 def calculate_reorder_steps(ordered_ids, current) steps = [] current.each_with_index do |source, idx| new_idx = ordered_ids.index(source.id) steps << [source, new_idx] if idx != new_idx end steps end
Actually perform the ordering using calculated steps
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 1064 def perform_reorder_of_children(ordered_ids, current) steps = calculate_reorder_steps(ordered_ids, current) steps.inject([]) do |result, (source, idx)| target = current[idx] if source.id != target.id source.swap(target, false) from = current.index(source) current[from], current[idx] = current[idx], current[from] result << source end result end end
Private Instance Methods
as a seperate method to facilitate custom implementations based on :dependent option
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 1122 def remove_descendant(descendant) descendant.destroy end
optionally use a transaction
# File lib/symetrie_com/acts_as_better_nested_set.rb, line 1117 def with_optional_transaction(bool, &block) bool ? transaction { yield } : yield end