lib/nested_set/base.rb in nested_set-1.6.4 vs lib/nested_set/base.rb in nested_set-1.6.5
- old
+ new
@@ -23,10 +23,11 @@
# item.children.create(:name => "child1")
#
module SingletonMethods
# Configuration options are:
#
+ # * +:primary_key_column+ - specifies the column name to use for keeping the position integer (default: id)
# * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_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 level cache data, default "depth"
# * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
@@ -41,10 +42,11 @@
# See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
# CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
# to acts_as_nested_set models
def acts_as_nested_set(options = {})
options = {
+ :primary_key_column => self.primary_key,
:parent_column => 'parent_id',
:left_column => 'lft',
:right_column => 'rgt',
:depth_column => 'depth',
:dependent => :delete_all, # or :destroy
@@ -52,12 +54,12 @@
if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
options[:scope] = "#{options[:scope]}_id".intern
end
- write_inheritable_attribute :acts_as_nested_set_options, options
- class_inheritable_reader :acts_as_nested_set_options
+ class_attribute :acts_as_nested_set_options
+ self.acts_as_nested_set_options = options
unless self.is_a?(ClassMethods)
include Comparable
include Columns
include InstanceMethods
@@ -140,11 +142,12 @@
# This arranged hash can be rendered with recursive render_tree helper
def arrange
arranged = ActiveSupport::OrderedHash.new
insertion_points = [arranged]
depth = 0
- order(quoted_left_column_name).each_with_level do |node, level|
+ order("#{quoted_table_name}.#{quoted_left_column_name}").each_with_level do |node, level|
+ next if level > depth && insertion_points.last.keys.last && node.parent_id != insertion_points.last.keys.last.id
insertion_points.push insertion_points.last.values.last if level > depth
(depth - level).times { insertion_points.pop } if level < depth
insertion_points.last.merge! node => ActiveSupport::OrderedHash.new
depth = level
end
@@ -173,21 +176,22 @@
scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
connection.quote_column_name(c)
end.push(nil).join(", ")
[quoted_left_column_name, quoted_right_column_name].all? do |column|
# No duplicates
- first(
+ unscoped.first(
:select => "#{scope_string}#{column}, COUNT(#{column})",
- :group => "#{scope_string}#{column}
- HAVING COUNT(#{column}) > 1").nil?
+ :group => "#{scope_string}#{column}",
+ :having => "COUNT(#{column}) > 1"
+ ).nil?
end
end
# Wrapper for each_root_valid? that can deal with scope.
def all_roots_valid?
if acts_as_nested_set_options[:scope]
- roots.group(scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
+ roots.group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
each_root_valid?(grouped_roots)
end
else
each_root_valid?(roots)
end
@@ -250,10 +254,18 @@
end
yield(i, level)
end
end
+ def map_with_level(objects = nil)
+ result = []
+ each_with_level objects do |object, level|
+ result << yield(object, level)
+ end
+ result
+ end
+
def before_move(*args, &block)
set_callback :move, :before, *args, &block
end
def after_move(*args, &block)
@@ -281,11 +293,11 @@
order(order_for_rebuild).
all
end
def order_for_rebuild
- "#{quoted_left_column_name}, #{quoted_right_column_name}, id"
+ "#{quoted_left_column_name}, #{quoted_right_column_name}, #{primary_key_column_name}"
end
end
# Mixed into both classes and instances to provide easy access to the column names
@@ -308,10 +320,14 @@
def depth_column_name
acts_as_nested_set_options[:depth_column]
end
+ def primary_key_column_name
+ acts_as_nested_set_options[:primary_key_column]
+ end
+
def quoted_left_column_name
connection.quote_column_name(left_column_name)
end
def quoted_right_column_name
@@ -381,10 +397,15 @@
# Returns root
def root
self_and_ancestors.first
end
+ # Returns the array of all children and self
+ def self_and_children
+ nested_set_scope.scoped.where("#{q_parent} = ? or id = ?", id, id)
+ end
+
# Returns the array of all parents and self
def self_and_ancestors
nested_set_scope.scoped.where("#{q_left} <= ? AND #{q_right} >= ?", left, right)
end
@@ -494,12 +515,12 @@
# detect impossible move
!((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
end
def to_text
- self_and_descendants.map do |node|
- "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
+ self.class.map_with_level(self_and_descendants) do |node,level|
+ "#{'*'*(level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
end.join("\n")
end
protected
@@ -509,20 +530,24 @@
def q_right
"#{self.class.quoted_table_name}.#{quoted_right_column_name}"
end
+ def q_parent
+ "#{self.class.quoted_table_name}.#{quoted_parent_column_name}"
+ end
+
def without_self(scope)
scope.where("#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self)
end
# All nested set queries should use this nested_set_scope, which performs finds on
# the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
# declaration.
def nested_set_scope
- conditions = Array(acts_as_nested_set_options[:scope]).inject({}) do |conditions, attr|
- conditions.merge attr => self[attr]
+ conditions = Array(acts_as_nested_set_options[:scope]).inject({}) do |cnd, attr|
+ cnd.merge attr => self[attr]
end
self.class.base_class.order(q_left).where(conditions)
end
@@ -549,10 +574,11 @@
# Prunes a branch off of the tree, shifting all of the elements on the right
# back to the left so the counts still work.
def destroy_descendants
return if right.nil? || left.nil? || skip_before_destroy
+ reload_nested_set
self.class.base_class.transaction do
if acts_as_nested_set_options[:dependent] == :destroy
descendants.each do |model|
model.skip_before_destroy = true
@@ -620,9 +646,16 @@
return if bound == self[right_column_name] || bound == self[left_column_name]
# we have defined the boundaries of two non-overlapping intervals,
# so sorting puts both the intervals and their boundaries in order
a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
+
+ # select the rows in the model between a and d, and apply a lock
+ self.class.base_class.find(:all,
+ :select => primary_key_column_name,
+ :conditions => ["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}],
+ :lock => true
+ )
new_parent = case position
when :child; target.id
when :root; nil
else target[parent_column_name]