module NoBrainer::Tree

NoBrainer::Tree

This module extends any NoBrainer document with tree functionality.

Usage

Simply include the module in any NoBrainer document:

class Node
  include NoBrainer::Document
  include NoBrainer::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

NoBrainer::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 NoBrainer::Document
  include NoBrainer::Tree

  before_destroy :nullify_children
end

Callbacks

NoBrainer::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 NoBrainer::Document
  include NoBrainer::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 [NoBrainer::Tree] other document to check against

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

# File lib/nobrainer/tree.rb, line 296
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 [NoBrainer::Criteria] NoBrainer criteria to retrieve the documents ancestors

# File lib/nobrainer/tree.rb, line 278
def ancestors
  base_class.without_index.where(:id.in => self.parent_ids).order_by(:depth => :asc)
end
ancestors_and_self() click to toggle source

Returns an array of this document’s ancestors and itself

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

# File lib/nobrainer/tree.rb, line 286
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/nobrainer/tree.rb, line 403
def delete_descendants
  self.descendants.delete_all
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/nobrainer/tree.rb, line 237
def depth
  super || self.parent_ids.count
end
descendant_of?(other) click to toggle source

Is this document a descendant of the other document?

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

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

# File lib/nobrainer/tree.rb, line 322
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 [NoBrainer::Criteria] NoBrainer criteria to retrieve the document’s descendants

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

Returns and array of this document and it’s descendants

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

# File lib/nobrainer/tree.rb, line 312
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/nobrainer/tree.rb, line 411
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/nobrainer/tree.rb, line 253
def leaf?
  self.children.empty?
end
leaves() click to toggle source

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

@return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document’s leaves

# File lib/nobrainer/tree.rb, line 356
def leaves
  base_class.where(:id.nin => base_class.pluck(:id, :_type, :parent_id).collect(&:parent_id).compact, :parent_ids.any => self.id)
end
move_children_to_parent() click to toggle source

Moves all children to this document’s parent

@return [undefined]

# File lib/nobrainer/tree.rb, line 391
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/nobrainer/tree.rb, line 380
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/nobrainer/tree.rb, line 364
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/nobrainer/tree.rb, line 372
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 [NoBrainer::Document] The documents root node

# File lib/nobrainer/tree.rb, line 266
def root
  if self.parent_ids.present?
    base_class.find(self.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/nobrainer/tree.rb, line 245
def root?
  self.parent_id.nil?
end
sibling_of?(other) click to toggle source

Is this document a sibling of the other document?

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

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

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

Returns this document’s siblings

@return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document’s siblings

# File lib/nobrainer/tree.rb, line 330
def siblings
  siblings_and_self.where(:id.ne => self.id)
end
siblings_and_self() click to toggle source

Returns this document’s siblings and itself

@return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document’s siblings and itself

# File lib/nobrainer/tree.rb, line 338
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/nobrainer/tree.rb, line 436
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/nobrainer/tree.rb, line 423
def rearrange
  self.parent_ids = (parent.parent_ids + [self.parent_id]) rescue []

  self.depth = parent_ids.size

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