Files

CollectiveIdea::Acts::NestedSet::Model

Public Instance Methods

ancestors() click to toggle source

Returns an array of all parents

# File lib/awesome_nested_set/awesome_nested_set.rb, line 342
def ancestors
  without_self self_and_ancestors
end
child?() click to toggle source

Returns true is this is a child node

# File lib/awesome_nested_set/awesome_nested_set.rb, line 317
def child?
  !root?
end
descendants() click to toggle source

Returns a set of all of its children and nested children

# File lib/awesome_nested_set/awesome_nested_set.rb, line 376
def descendants
  without_self self_and_descendants
end
is_ancestor_of?(other) click to toggle source
# File lib/awesome_nested_set/awesome_nested_set.rb, line 388
def is_ancestor_of?(other)
  self.left < other.left && other.left < self.right && same_scope?(other)
end
is_descendant_of?(other) click to toggle source
# File lib/awesome_nested_set/awesome_nested_set.rb, line 380
def is_descendant_of?(other)
  other.left < self.left && self.left < other.right && same_scope?(other)
end
is_or_is_ancestor_of?(other) click to toggle source
# File lib/awesome_nested_set/awesome_nested_set.rb, line 392
def is_or_is_ancestor_of?(other)
  self.left <= other.left && other.left < self.right && same_scope?(other)
end
is_or_is_descendant_of?(other) click to toggle source
# File lib/awesome_nested_set/awesome_nested_set.rb, line 384
def is_or_is_descendant_of?(other)
  other.left <= self.left && self.left < other.right && same_scope?(other)
end
leaf?() click to toggle source

Returns true if this is the end of a branch.

# File lib/awesome_nested_set/awesome_nested_set.rb, line 312
def leaf?
  persisted? && right.to_i - left.to_i == 1
end
leaves() click to toggle source

Returns a set of all of its nested children which do not have children

# File lib/awesome_nested_set/awesome_nested_set.rb, line 357
def leaves
  descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
end
left() click to toggle source

Value of the left column

# File lib/awesome_nested_set/awesome_nested_set.rb, line 297
def left
  self[left_column_name]
end
left_sibling() click to toggle source

Find the first sibling to the left

# File lib/awesome_nested_set/awesome_nested_set.rb, line 404
def left_sibling
  siblings.where(["#{quoted_left_column_full_name} < ?", left]).
          order("#{quoted_left_column_full_name} DESC").last
end
level() click to toggle source

Returns the level of this object in the tree root level is 0

# File lib/awesome_nested_set/awesome_nested_set.rb, line 363
def level
  parent_id.nil? ? 0 : compute_level
end
move_left() click to toggle source

Shorthand method for finding the left sibling and moving to the left of it.

# File lib/awesome_nested_set/awesome_nested_set.rb, line 415
def move_left
  move_to_left_of left_sibling
end
move_possible?(target) click to toggle source
# File lib/awesome_nested_set/awesome_nested_set.rb, line 477
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
move_right() click to toggle source

Shorthand method for finding the right sibling and moving to the right of it.

# File lib/awesome_nested_set/awesome_nested_set.rb, line 420
def move_right
  move_to_right_of right_sibling
end
move_to_child_of(node) click to toggle source

Move the node to the child of another node (you can pass id only)

# File lib/awesome_nested_set/awesome_nested_set.rb, line 435
def move_to_child_of(node)
  move_to node, :child
end
move_to_child_with_index(node, index) click to toggle source

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 440
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_to_left_of(node) click to toggle source

Move the node to the left of another node (you can pass id only)

# File lib/awesome_nested_set/awesome_nested_set.rb, line 425
def move_to_left_of(node)
  move_to node, :left
end
move_to_ordered_child_of(parent, order_attribute, ascending = true) click to toggle source

Order children in a nested set by an attribute Can order by any attribute class that uses the Comparable mixin, for example a string or integer Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, “name”)

# File lib/awesome_nested_set/awesome_nested_set.rb, line 458
def move_to_ordered_child_of(parent, order_attribute, ascending = true)
  self.move_to_root and return unless parent
  left = nil # This is needed, at least for the tests.
  parent.children.each do |n| # Find the node immediately to the left of this node.
    if ascending
      left = n if n.send(order_attribute) < self.send(order_attribute)
    else
      left = n if n.send(order_attribute) > self.send(order_attribute)
    end
  end
  self.move_to_child_of(parent)
  return unless parent.children.count > 1 # Only need to order if there are multiple children.
  if left # Self has a left neighbor.
    self.move_to_right_of(left)
  else # Self is the left most node.
    self.move_to_left_of(parent.children[0])
  end
end
move_to_right_of(node) click to toggle source

Move the node to the left of another node (you can pass id only)

# File lib/awesome_nested_set/awesome_nested_set.rb, line 430
def move_to_right_of(node)
  move_to node, :right
end
move_to_root() click to toggle source

Move the node to root nodes

# File lib/awesome_nested_set/awesome_nested_set.rb, line 451
def move_to_root
  move_to nil, :root
end
parent_id() click to toggle source

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 292
def parent_id
  self[parent_column_name]
end
right() click to toggle source

Value of the right column

# File lib/awesome_nested_set/awesome_nested_set.rb, line 302
def right
  self[right_column_name]
end
right_sibling() click to toggle source

Find the first sibling to the right

# File lib/awesome_nested_set/awesome_nested_set.rb, line 410
def right_sibling
  siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
end
root() click to toggle source

Returns root

# File lib/awesome_nested_set/awesome_nested_set.rb, line 322
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
root?() click to toggle source

Returns true if this is a root node.

# File lib/awesome_nested_set/awesome_nested_set.rb, line 307
def root?
  parent_id.nil?
end
same_scope?(other) click to toggle source

Check if other model is in the same scope

# File lib/awesome_nested_set/awesome_nested_set.rb, line 397
def same_scope?(other)
  Array(acts_as_nested_set_options[:scope]).all? do |attr|
    self.send(attr) == other.send(attr)
  end
end
self_and_ancestors() click to toggle source

Returns the array of all parents and self

# File lib/awesome_nested_set/awesome_nested_set.rb, line 335
def self_and_ancestors
  nested_set_scope.where([
    "#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
  ])
end
self_and_descendants() click to toggle source

Returns a set of itself and all of its nested children

# File lib/awesome_nested_set/awesome_nested_set.rb, line 368
def self_and_descendants
  nested_set_scope.where([
    "#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
    # using _left_ for both sides here lets us benefit from an index on that column if one exists
  ])
end
self_and_siblings() click to toggle source

Returns the array of all children of the parent, including self

# File lib/awesome_nested_set/awesome_nested_set.rb, line 347
def self_and_siblings
  nested_set_scope.where(parent_column_name => parent_id)
end
siblings() click to toggle source

Returns the array of all children of the parent, except self

# File lib/awesome_nested_set/awesome_nested_set.rb, line 352
def siblings
  without_self self_and_siblings
end
to_text() click to toggle source
# File lib/awesome_nested_set/awesome_nested_set.rb, line 485
def to_text
  self_and_descendants.map do |node|
    "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
  end.join("\n")
end

Protected Instance Methods

compute_level() click to toggle source
# File lib/awesome_nested_set/awesome_nested_set.rb, line 492
def compute_level
  node, nesting = self, 0
  while (association = node.association(:parent)).loaded? && association.target
    nesting += 1
    node = node.parent
  end if node.respond_to? :association
  node == self ? ancestors.count : node.level + nesting
end
destroy_descendants() click to toggle source

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 567
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
    nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
                          select(id).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.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
                       delete_all
    end

    # update lefts and rights for remaining nodes
    diff = right - left + 1
    nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
      ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
    )

    nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
      ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
    )

    # Don't allow multiple calls to destroy to corrupt the set
    self.skip_before_destroy = true
  end
end
in_tenacious_transaction(&block) click to toggle source
# File lib/awesome_nested_set/awesome_nested_set.rb, line 550
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
move_to(target, position) click to toggle source
# File lib/awesome_nested_set/awesome_nested_set.rb, line 609
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_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
      )

      new_parent = case position
        when :child;  target.id
        when :root;   nil
        else          target[parent_column_name]
      end

      where_statement = ["not (#{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 AND " +
                             "#{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 AND " +
                             "#{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}    ]




      self.nested_set_scope.where(*where_statement).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.descendants.each(&:save)
    self.reload_nested_set
  end
end
move_to_new_parent() click to toggle source
# File lib/awesome_nested_set/awesome_nested_set.rb, line 522
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
nested_set_scope(options = {}) click to toggle source

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 508
def nested_set_scope(options = {})
  options = {:order => quoted_left_column_full_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_nested_set() click to toggle source

reload left, right, and parent

# File lib/awesome_nested_set/awesome_nested_set.rb, line 602
def reload_nested_set
  reload(
    :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
    :lock => true
  )
end
set_default_left_and_right() click to toggle source

on creation, set automatically lft and rgt to the end of the tree

# File lib/awesome_nested_set/awesome_nested_set.rb, line 542
def set_default_left_and_right
  highest_right_row = nested_set_scope(:order => "#{quoted_right_column_full_name} desc").limit(1).lock(true).first
  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
set_depth!() click to toggle source
# File lib/awesome_nested_set/awesome_nested_set.rb, line 530
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
store_new_parent() click to toggle source
# File lib/awesome_nested_set/awesome_nested_set.rb, line 517
def store_new_parent
  @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
  true # force callback to return true
end
without_self(scope) click to toggle source
# File lib/awesome_nested_set/awesome_nested_set.rb, line 501
def without_self(scope)
  scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
end

[Validate]

Generated with the Darkfish Rdoc Generator 2.