require 'awesome_nested_set/columns'
require 'awesome_nested_set/model'
module CollectiveIdea #:nodoc:
module Acts #:nodoc:
module NestedSet #:nodoc:
# This acts provides Nested Set functionality. Nested Set is a smart way to implement
# an _ordered_ tree, with the added feature that you can select the children and all of their
# descendants with a single query. The drawback is that insertion or move need some complex
# sql queries. But everything is done here by this module!
#
# Nested sets are appropriate each time you want either an orderd tree (menus,
# commercial categories) or an efficient way of querying big trees (threaded posts).
#
# == API
#
# Methods names are aligned with acts_as_tree as much as possible to make replacment from one
# by another easier.
#
# item.children.create(:name => "child1")
#
# Configuration options are:
#
# * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
# * +:primary_column+ - specifies the column name to use as the inverse of the parent column (default: id)
# * +:left_column+ - column name for left boundry data, default "lft"
# * +:right_column+ - column name for right boundry data, default "rgt"
# * +:depth_column+ - column name for the depth data, default "depth"
# * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
# (if it hasn't been already) and use that as the foreign key restriction. You
# can also pass an array to scope by multiple attributes.
# Example: acts_as_nested_set :scope => [:notable_id, :notable_type]
# * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
# child objects are destroyed alongside this object by calling their destroy
# method. If set to :delete_all (default), all the child objects are deleted
# without calling their destroy method.
# * +:counter_cache+ adds a counter cache for the number of children.
# defaults to false.
# Example: acts_as_nested_set :counter_cache => :children_count
# * +:order_column+ on which column to do sorting, by default it is the left_column_name
# Example: acts_as_nested_set :order_column => :position
#
# See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
# CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
# to acts_as_nested_set models
def acts_as_nested_set(options = {})
acts_as_nested_set_parse_options! options
include Model
include Columns
extend Columns
acts_as_nested_set_relate_parent!
acts_as_nested_set_relate_children!
attr_accessor :skip_before_destroy
acts_as_nested_set_prevent_assignment_to_reserved_columns!
acts_as_nested_set_define_callbacks!
end
private
def acts_as_nested_set_define_callbacks!
# on creation, set automatically lft and rgt to the end of the tree
before_create :set_default_left_and_right
before_save :store_new_parent
after_save :move_to_new_parent, :set_depth!
before_destroy :destroy_descendants
define_model_callbacks :move
end
def acts_as_nested_set_relate_children!
has_many_children_options = {
:class_name => self.base_class.to_s,
:foreign_key => parent_column_name,
:primary_key => primary_column_name,
:inverse_of => (:parent unless acts_as_nested_set_options[:polymorphic]),
}
# Add callbacks, if they were supplied.. otherwise, we don't want them.
[:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
has_many_children_options.update(
ar_callback => acts_as_nested_set_options[ar_callback]
) if acts_as_nested_set_options[ar_callback]
end
has_many :children, -> { order(quoted_order_column_full_name) },
has_many_children_options
end
def acts_as_nested_set_relate_parent!
options = {
:class_name => self.base_class.to_s,
:foreign_key => parent_column_name,
:primary_key => primary_column_name,
:counter_cache => acts_as_nested_set_options[:counter_cache],
:inverse_of => (:children unless acts_as_nested_set_options[:polymorphic]),
:polymorphic => acts_as_nested_set_options[:polymorphic],
:touch => acts_as_nested_set_options[:touch]
}
options[:optional] = true if ActiveRecord::VERSION::MAJOR >= 5
belongs_to :parent, options
end
def acts_as_nested_set_default_options
{
:parent_column => 'parent_id',
:primary_column => 'id',
:left_column => 'lft',
:right_column => 'rgt',
:depth_column => 'depth',
:dependent => :delete_all, # or :destroy
:polymorphic => false,
:counter_cache => false,
:touch => false
}.freeze
end
def acts_as_nested_set_parse_options!(options)
options = acts_as_nested_set_default_options.merge(options)
if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
options[:scope] = "#{options[:scope]}_id".intern
end
class_attribute :acts_as_nested_set_options
self.acts_as_nested_set_options = options
end
def acts_as_nested_set_prevent_assignment_to_reserved_columns!
# no assignment to structure fields
[left_column_name, right_column_name, depth_column_name].each do |column|
module_eval <<-"end_eval", __FILE__, __LINE__
def #{column}=(x)
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
end
end_eval
end
end
end
end
end