Returns an array of all parents
# File lib/awesome_nested_set/awesome_nested_set.rb, line 319 def ancestors without_self self_and_ancestors end
Returns true is this is a child node
# File lib/awesome_nested_set/awesome_nested_set.rb, line 294 def child? !root? end
Returns a set of all of its children and nested children
# File lib/awesome_nested_set/awesome_nested_set.rb, line 353 def descendants without_self self_and_descendants end
# File lib/awesome_nested_set/awesome_nested_set.rb, line 365 def is_ancestor_of?(other) self.left < other.left && other.left < self.right && same_scope?(other) end
# File lib/awesome_nested_set/awesome_nested_set.rb, line 357 def is_descendant_of?(other) other.left < self.left && self.left < other.right && same_scope?(other) end
# File lib/awesome_nested_set/awesome_nested_set.rb, line 369 def is_or_is_ancestor_of?(other) self.left <= other.left && other.left < self.right && same_scope?(other) end
# File lib/awesome_nested_set/awesome_nested_set.rb, line 361 def is_or_is_descendant_of?(other) other.left <= self.left && self.left < other.right && same_scope?(other) end
Returns true if this is the end of a branch.
# File lib/awesome_nested_set/awesome_nested_set.rb, line 289 def leaf? persisted? && right.to_i - left.to_i == 1 end
Returns a set of all of its nested children which do not have children
# File lib/awesome_nested_set/awesome_nested_set.rb, line 334 def leaves descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1") end
Value of the left column
# File lib/awesome_nested_set/awesome_nested_set.rb, line 274 def left self[left_column_name] end
Find the first sibling to the left
# File lib/awesome_nested_set/awesome_nested_set.rb, line 381 def left_sibling siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]). order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last end
Returns the level of this object in the tree root level is 0
# File lib/awesome_nested_set/awesome_nested_set.rb, line 340 def level parent_id.nil? ? 0 : ancestors.count end
Shorthand method for finding the left sibling and moving to the left of it.
# File lib/awesome_nested_set/awesome_nested_set.rb, line 392 def move_left move_to_left_of left_sibling end
# File lib/awesome_nested_set/awesome_nested_set.rb, line 432 def move_possible?(target) self != target && # Can't target self same_scope?(target) && # can't be in different scopes # !(left..right).include?(target.left..target.right) # this needs tested more # detect impossible move !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right)) end
Shorthand method for finding the right sibling and moving to the right of it.
# File lib/awesome_nested_set/awesome_nested_set.rb, line 397 def move_right move_to_right_of right_sibling end
Move the node to the child of another node (you can pass id only)
# File lib/awesome_nested_set/awesome_nested_set.rb, line 412 def move_to_child_of(node) move_to node, :child end
Move the node to the child of another node with specify index (you can pass id only)
# File lib/awesome_nested_set/awesome_nested_set.rb, line 417 def move_to_child_with_index(node, index) if node.children.empty? move_to_child_of(node) elsif node.children.count == index move_to_right_of(node.children.last) else move_to_left_of(node.children[index]) end end
Move the node to the left of another node (you can pass id only)
# File lib/awesome_nested_set/awesome_nested_set.rb, line 402 def move_to_left_of(node) move_to node, :left end
Move the node to the left of another node (you can pass id only)
# File lib/awesome_nested_set/awesome_nested_set.rb, line 407 def move_to_right_of(node) move_to node, :right end
Move the node to root nodes
# File lib/awesome_nested_set/awesome_nested_set.rb, line 428 def move_to_root move_to nil, :root end
Any instance method that returns a collection makes use of Rails 2.1’s named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
category.self_and_descendants.count category.ancestors.find(:all, :conditions => "name like '%foo%'")
Value of the parent column
# File lib/awesome_nested_set/awesome_nested_set.rb, line 269 def parent_id self[parent_column_name] end
Value of the right column
# File lib/awesome_nested_set/awesome_nested_set.rb, line 279 def right self[right_column_name] end
Find the first sibling to the right
# File lib/awesome_nested_set/awesome_nested_set.rb, line 387 def right_sibling siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first end
Returns root
# File lib/awesome_nested_set/awesome_nested_set.rb, line 299 def root if persisted? self_and_ancestors.where(parent_column_name => nil).first else if parent_id && current_parent = nested_set_scope.find(parent_id) current_parent.root else self end end end
Returns true if this is a root node.
# File lib/awesome_nested_set/awesome_nested_set.rb, line 284 def root? parent_id.nil? end
Check if other model is in the same scope
# File lib/awesome_nested_set/awesome_nested_set.rb, line 374 def same_scope?(other) Array(acts_as_nested_set_options[:scope]).all? do |attr| self.send(attr) == other.send(attr) end end
Returns the array of all parents and self
# File lib/awesome_nested_set/awesome_nested_set.rb, line 312 def self_and_ancestors nested_set_scope.where([ "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right ]) end
Returns a set of itself and all of its nested children
# File lib/awesome_nested_set/awesome_nested_set.rb, line 345 def self_and_descendants nested_set_scope.where([ "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left, right # using _left_ for both sides here lets us benefit from an index on that column if one exists ]) end
Returns the array of all children of the parent, including self
# File lib/awesome_nested_set/awesome_nested_set.rb, line 324 def self_and_siblings nested_set_scope.where(parent_column_name => parent_id) end
Prunes a branch off of the tree, shifting all of the elements on the right back to the left so the counts still work.
# File lib/awesome_nested_set/awesome_nested_set.rb, line 514 def destroy_descendants return if right.nil? || left.nil? || skip_before_destroy in_tenacious_transaction do reload_nested_set # select the rows in the model that extend past the deletion point and apply a lock self.class.base_class.find(:all, :select => "id", :conditions => ["#{quoted_left_column_name} >= ?", left], :lock => true ) if acts_as_nested_set_options[:dependent] == :destroy descendants.each do |model| model.skip_before_destroy = true model.destroy end else nested_set_scope.delete_all( ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right] ) end # update lefts and rights for remaining nodes diff = right - left + 1 nested_set_scope.update_all( ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff], ["#{quoted_left_column_name} > ?", right] ) nested_set_scope.update_all( ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff], ["#{quoted_right_column_name} > ?", right] ) # Don't allow multiple calls to destroy to corrupt the set self.skip_before_destroy = true end end
# File lib/awesome_nested_set/awesome_nested_set.rb, line 497 def in_tenacious_transaction(&block) retry_count = 0 begin transaction(&block) rescue ActiveRecord::StatementInvalid => error raise unless connection.open_transactions.zero? raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/ raise unless retry_count < 10 retry_count += 1 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction" sleep(rand(retry_count)*0.1) # Aloha protocol retry end end
# File lib/awesome_nested_set/awesome_nested_set.rb, line 562 def move_to(target, position) raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record? run_callbacks :move do in_tenacious_transaction do if target.is_a? self.class.base_class target.reload_nested_set elsif position != :root # load object if node is not an object target = nested_set_scope.find(target) end self.reload_nested_set unless position == :root || move_possible?(target) raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree." end bound = case position when :child; target[right_column_name] when :left; target[left_column_name] when :right; target[right_column_name] + 1 when :root; 1 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)." end if bound > self[right_column_name] bound = bound - 1 other_bound = self[right_column_name] + 1 else other_bound = self[left_column_name] - 1 end # there would be no change return if bound == self[right_column_name] || bound == self[left_column_name] # we have defined the boundaries of two non-overlapping intervals, # so sorting puts both the intervals and their boundaries in order a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort # select the rows in the model between a and d, and apply a lock self.class.base_class.select('id').lock(true).where( ["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}] ) new_parent = case position when :child; target.id when :root; nil else target[parent_column_name] end self.nested_set_scope.update_all([ "#{quoted_left_column_name} = CASE " + "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " + "THEN #{quoted_left_column_name} + :d - :b " + "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " + "THEN #{quoted_left_column_name} + :a - :c " + "ELSE #{quoted_left_column_name} END, " + "#{quoted_right_column_name} = CASE " + "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " + "THEN #{quoted_right_column_name} + :d - :b " + "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " + "THEN #{quoted_right_column_name} + :a - :c " + "ELSE #{quoted_right_column_name} END, " + "#{quoted_parent_column_name} = CASE " + "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " + "ELSE #{quoted_parent_column_name} END", {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent} ]) end target.reload_nested_set if target self.set_depth! self.children.each(&:save) self.reload_nested_set end end
# File lib/awesome_nested_set/awesome_nested_set.rb, line 469 def move_to_new_parent if @move_to_new_parent_id.nil? move_to_root elsif @move_to_new_parent_id move_to_child_of(@move_to_new_parent_id) end end
All nested set queries should use this nested_set_scope, which performs finds on the base ActiveRecord class, using the :scope declared in the acts_as_nested_set declaration.
# File lib/awesome_nested_set/awesome_nested_set.rb, line 455 def nested_set_scope(options = {}) options = {:order => quoted_left_column_name}.merge(options) scopes = Array(acts_as_nested_set_options[:scope]) options[:conditions] = scopes.inject({}) do |conditions,attr| conditions.merge attr => self[attr] end unless scopes.empty? self.class.base_class.unscoped.scoped options end
reload left, right, and parent
# File lib/awesome_nested_set/awesome_nested_set.rb, line 555 def reload_nested_set reload( :select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}", :lock => true ) end
on creation, set automatically lft and rgt to the end of the tree
# File lib/awesome_nested_set/awesome_nested_set.rb, line 489 def set_default_left_and_right highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").find(:first, :limit => 1,:lock => true ) maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0 # adds the new node to the right of all existing nodes self[left_column_name] = maxright + 1 self[right_column_name] = maxright + 2 end
# File lib/awesome_nested_set/awesome_nested_set.rb, line 477 def set_depth! if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s) in_tenacious_transaction do reload nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level]) end self[depth_column_name.to_sym] = self.level end end
Generated with the Darkfish Rdoc Generator 2.