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]