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:
-
:nullify_children – Sets the children’s parent_id to null
-
:move_children_to_parent – Moves the children to the current document’s parent
-
:destroy_children – Destroys all children by calling their destroy method (invokes callbacks)
-
:delete_descendants – Deletes all descendants using a database query (doesn’t invoke callbacks)
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
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
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
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
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
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
# File lib/nobrainer/tree.rb, line 237 def depth super || self.parent_ids.count end
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
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
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
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
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
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
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
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
Forces rearranging of all children after next save
@return [undefined]
# File lib/nobrainer/tree.rb, line 364 def rearrange_children! @rearrange_children = true end
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
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
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
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
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
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
# File lib/nobrainer/tree.rb, line 436 def position_in_tree errors.add(:parent_id, :invalid) if self.parent_ids.include?(self.id) end
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
# File lib/nobrainer/tree.rb, line 431 def rearrange_children @rearrange_children = false self.children.each { |c| c.save } end