mceachen/closure_tree

View on GitHub
lib/closure_tree/model.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'active_support/concern'

module ClosureTree
  module Model
    extend ActiveSupport::Concern

    included do

      belongs_to :parent, nil,
        class_name: _ct.model_class.to_s,
        foreign_key: _ct.parent_column_name,
        inverse_of: :children,
        touch: _ct.options[:touch],
        optional: true

      order_by_generations = -> { Arel.sql("#{_ct.quoted_hierarchy_table_name}.generations ASC") }

      has_many :children, *_ct.has_many_order_with_option, **{
        class_name: _ct.model_class.to_s,
        foreign_key: _ct.parent_column_name,
        dependent: _ct.options[:dependent],
        inverse_of: :parent } do
          # We have to redefine hash_tree because the activerecord relation is already scoped to parent_id.
          def hash_tree(options = {})
            # we want limit_depth + 1 because we don't do self_and_descendants.
            limit_depth = options[:limit_depth]
            _ct.hash_tree(@association.owner.descendants, limit_depth ? limit_depth + 1 : nil)
          end
        end

      has_many :ancestor_hierarchies, *_ct.has_many_order_without_option(order_by_generations),
        class_name: _ct.hierarchy_class_name,
        foreign_key: 'descendant_id'

      has_many :self_and_ancestors, *_ct.has_many_order_without_option(order_by_generations),
        through: :ancestor_hierarchies,
        source: :ancestor

      has_many :descendant_hierarchies, *_ct.has_many_order_without_option(order_by_generations),
        class_name: _ct.hierarchy_class_name,
        foreign_key: 'ancestor_id'

      has_many :self_and_descendants, *_ct.has_many_order_with_option(order_by_generations),
        through: :descendant_hierarchies,
        source: :descendant
    end

    # Delegate to the Support instance on the class:
    def _ct
      self.class._ct
    end

    # Returns true if this node has no parents.
    def root?
      # Accessing the parent will fetch that row from the database,
      # so if we are persisted, just check that the parent_id column is nil.
      persisted? ? _ct_parent_id.nil? : parent.nil?
    end

    # Returns true if this node has a parent, and is not a root.
    def child?
      !root?
    end

    # Returns true if this node has no children.
    def leaf?
      children.empty?
    end

    # Returns the farthest ancestor, or self if +root?+
    def root
      self_and_ancestors.where(_ct.parent_column_name.to_sym => nil).first
    end

    def leaves
      self_and_descendants.leaves
    end

    def depth
      ancestor_hierarchies.size - 1
    end

    alias_method :level, :depth

    # enumerable of ancestors, immediate parent is first, root is last.
    def ancestors
      without_self(self_and_ancestors)
    end

    def ancestor_ids
      _ct.ids_from(ancestors)
    end

    def self_and_ancestors_ids
      _ct.ids_from(self_and_ancestors)
    end

    # Returns an array, root first, of self_and_ancestors' values of the +to_s_column+, which defaults
    # to the +name_column+.
    # (so child.ancestry_path == +%w{grandparent parent child}+
    def ancestry_path(to_s_column = _ct.name_column)
      self_and_ancestors.map { |n| n.send to_s_column.to_sym }.reverse
    end

    def child_ids
      _ct.ids_from(children)
    end

    def descendants
      without_self(self_and_descendants)
    end

    def self_and_descendant_ids
      _ct.ids_from(self_and_descendants)
    end

    def descendant_ids
      _ct.ids_from(descendants)
    end

    def self_and_siblings
      _ct.scope_with_order(_ct.base_class.where(_ct.parent_column_sym => _ct_parent_id))
    end

    def siblings
      without_self(self_and_siblings)
    end

    def sibling_ids
      _ct.ids_from(siblings)
    end

    # node's parent is this record
    def parent_of?(node)
      self == node.parent
    end

    # node's root is this record
    def root_of?(node)
      self == node.root
    end

    # node's ancestors include this record
    def ancestor_of?(node)
      node.ancestors.include? self
    end

    # node is record's ancestor
    def descendant_of?(node)
      self.ancestors.include? node
    end

    # node is record's parent
    def child_of?(node)
      self.parent == node
    end

    # node and record have a same root
    def family_of?(node)
      self.root == node.root
    end

    # Alias for appending to the children collection.
    # You can also add directly to the children collection, if you'd prefer.
    def add_child(child_node)
      children << child_node
      child_node
    end

    def _ct_parent_id
      read_attribute(_ct.parent_column_sym)
    end

    def _ct_quoted_parent_id
      _ct.quoted_value(_ct_parent_id)
    end

    def _ct_id
      read_attribute(_ct.model_class.primary_key)
    end

    def _ct_quoted_id
      _ct.quoted_value(_ct_id)
    end
  end
end