module Mongoid::Tree

Mongoid::Tree

This module extends any Mongoid document with tree functionality.

Usage

Simply include the module in any Mongoid document:

class Node
  include Mongoid::Document
  include Mongoid::Tree
end

Using the tree structure

Each document references many children. You can access them using the #children method.

node = Node.create
node.children.create
node.children.count # => 1

Every document references one parent (unless it's a root document).

node = Node.create
node.parent # => nil
node.children.create
node.children.first.parent # => node

Destroying

Mongoid::Tree does not handle destroying of nodes by default. However it provides several strategies that help you to deal with children of deleted documents. You can simply add them as before_destroy callbacks.

Available strategies are:

Example:

class Node
  include Mongoid::Document
  include Mongoid::Tree

  before_destroy :nullify_children
end

Callbacks

Mongoid::Tree offers callbacks for its rearranging process. This enables you to rebuild certain fields when the document was moved in the tree. Rearranging happens before the document is validated. This gives you a chance to validate your additional changes done in your callbacks. See ActiveModel::Callbacks and ActiveSupport::Callbacks for further details on callbacks.

Example:

class Page
  include Mongoid::Document
  include Mongoid::Tree

  after_rearrange :rebuild_path

  field :slug
  field :path

  private

  def rebuild_path
    self.path = self.ancestors_and_self.collect(&:slug).join('/')
  end
end

Public Instance Methods

ancestor_of?(other) click to toggle source

Is this document an ancestor of the other document?

@param [Mongoid::Tree] other document to check against

@return [Boolean] The document is an ancestor of the other document

# File lib/mongoid/tree.rb, line 307
def ancestor_of?(other)
  other.parent_ids.include?(self.id)
end
ancestors() click to toggle source

Returns a chainable criteria for this document's ancestors

@return [Mongoid::Criteria] Mongoid criteria to retrieve the documents ancestors

# File lib/mongoid/tree.rb, line 289
def ancestors
  base_class.where(:_id.in => parent_ids).order(:depth => :asc)
end
ancestors_and_self() click to toggle source

Returns an array of this document's ancestors and itself

@return [Array<Mongoid::Document>] Array of the document's ancestors and itself

# File lib/mongoid/tree.rb, line 297
def ancestors_and_self
  ancestors + [self]
end
delete_descendants() click to toggle source

Deletes all descendants using the database (doesn't invoke callbacks)

@return [undefined]

# File lib/mongoid/tree.rb, line 413
def delete_descendants
  base_class.delete_all(:conditions => { :parent_ids => self.id })
end
depth() click to toggle source

Returns the depth of this document (number of ancestors)

@example

Node.root.depth # => 0
Node.root.children.first.depth # => 1

@return [Fixnum] Depth of this document

Calls superclass method
# File lib/mongoid/tree.rb, line 248
def depth
  super || parent_ids.count
end
descendant_of?(other) click to toggle source

Is this document a descendant of the other document?

@param [Mongoid::Tree] other document to check against

@return [Boolean] The document is a descendant of the other document

# File lib/mongoid/tree.rb, line 333
def descendant_of?(other)
  self.parent_ids.include?(other.id)
end
descendants() click to toggle source

Returns a chainable criteria for this document's descendants

@return [Mongoid::Criteria] Mongoid criteria to retrieve the document's descendants

# File lib/mongoid/tree.rb, line 315
def descendants
  base_class.where(:parent_ids => self.id)
end
descendants_and_self() click to toggle source

Returns and array of this document and it's descendants

@return [Array<Mongoid::Document>] Array of the document itself and it's descendants

# File lib/mongoid/tree.rb, line 323
def descendants_and_self
  [self] + descendants
end
destroy_children() click to toggle source

Destroys all children by calling their destroy method (does invoke callbacks)

@return [undefined]

# File lib/mongoid/tree.rb, line 421
def destroy_children
  children.destroy_all
end
leaf?() click to toggle source

Is this document a leaf node (has no children)?

@return [Boolean] Whether the document is a leaf node

# File lib/mongoid/tree.rb, line 264
def leaf?
  children.empty?
end
leaves() click to toggle source

Returns all leaves of this document (be careful, currently involves two queries)

@return [Mongoid::Criteria] Mongoid criteria to retrieve the document's leaves

# File lib/mongoid/tree.rb, line 367
def leaves
  base_class.where(:_id.nin => base_class.only(:parent_id).collect(&:parent_id)).and(:parent_ids => self.id)
end
move_children_to_parent() click to toggle source

Moves all children to this document's parent

@return [undefined]

# File lib/mongoid/tree.rb, line 402
def move_children_to_parent
  children.each do |c|
    c.parent = self.parent
    c.save
  end
end
nullify_children() click to toggle source

Nullifies all children's parent_id

@return [undefined]

# File lib/mongoid/tree.rb, line 391
def nullify_children
  children.each do |c|
    c.parent = c.parent_id = nil
    c.save
  end
end
rearrange_children!() click to toggle source

Forces rearranging of all children after next save

@return [undefined]

# File lib/mongoid/tree.rb, line 375
def rearrange_children!
  @rearrange_children = true
end
rearrange_children?() click to toggle source

Will the children be rearranged after next save?

@return [Boolean] Whether the children will be rearranged

# File lib/mongoid/tree.rb, line 383
def rearrange_children?
  !!@rearrange_children
end
root() click to toggle source

Returns this document's root node. Returns `self` if the current document is a root node

@example

node = Node.find(...)
node.root

@return [Mongoid::Document] The documents root node

# File lib/mongoid/tree.rb, line 277
def root
  if parent_ids.present?
    base_class.find(parent_ids.first)
  else
    self.root? ? self : self.parent.root
  end
end
root?() click to toggle source

Is this document a root node (has no parent)?

@return [Boolean] Whether the document is a root node

# File lib/mongoid/tree.rb, line 256
def root?
  parent_id.nil?
end
sibling_of?(other) click to toggle source

Is this document a sibling of the other document?

@param [Mongoid::Tree] other document to check against

@return [Boolean] The document is a sibling of the other document

# File lib/mongoid/tree.rb, line 359
def sibling_of?(other)
  self.parent_id == other.parent_id
end
siblings() click to toggle source

Returns this document's siblings

@return [Mongoid::Criteria] Mongoid criteria to retrieve the document's siblings

# File lib/mongoid/tree.rb, line 341
def siblings
  siblings_and_self.excludes(:id => self.id)
end
siblings_and_self() click to toggle source

Returns this document's siblings and itself

@return [Mongoid::Criteria] Mongoid criteria to retrieve the document's siblings and itself

# File lib/mongoid/tree.rb, line 349
def siblings_and_self
  base_class.where(:parent_id => self.parent_id)
end

Private Instance Methods

position_in_tree() click to toggle source
# File lib/mongoid/tree.rb, line 450
def position_in_tree
  errors.add(:parent_id, :invalid) if self.parent_ids.include?(self.id)
end
rearrange() click to toggle source

Updates the parent_ids and marks the children for rearrangement when the parent_ids changed

@private @return [undefined]

# File lib/mongoid/tree.rb, line 433
def rearrange
  if self.parent_id
    self.parent_ids = parent.parent_ids + [self.parent_id]
  else
    self.parent_ids = []
  end

  self.depth = parent_ids.size

  rearrange_children! if self.parent_ids_changed?
end
rearrange_children() click to toggle source
# File lib/mongoid/tree.rb, line 445
def rearrange_children
  @rearrange_children = false
  self.children.each { |c| c.save }
end