From 03bde9fb42235af7147d6312e2cf6d928111dee1 Mon Sep 17 00:00:00 2001 From: hukl Date: Tue, 8 Sep 2009 11:30:11 +0200 Subject: updated awesome_nested_set plugin --- .../awesome_nested_set/lib/awesome_nested_set.rb | 173 ++++++++++++--------- .../lib/awesome_nested_set/compatability.rb | 29 ---- .../lib/awesome_nested_set/named_scope.rb | 140 ----------------- 3 files changed, 102 insertions(+), 240 deletions(-) delete mode 100644 vendor/plugins/awesome_nested_set/lib/awesome_nested_set/compatability.rb delete mode 100644 vendor/plugins/awesome_nested_set/lib/awesome_nested_set/named_scope.rb (limited to 'vendor/plugins/awesome_nested_set/lib') diff --git a/vendor/plugins/awesome_nested_set/lib/awesome_nested_set.rb b/vendor/plugins/awesome_nested_set/lib/awesome_nested_set.rb index 3e10891..bafdf4c 100644 --- a/vendor/plugins/awesome_nested_set/lib/awesome_nested_set.rb +++ b/vendor/plugins/awesome_nested_set/lib/awesome_nested_set.rb @@ -15,24 +15,11 @@ module CollectiveIdea #:nodoc: # # == API # - # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one - # by another easier, except for the creation: + # Methods names are aligned with acts_as_tree as much as possible to make replacment from one + # by another easier. # - # in acts_as_tree: # item.children.create(:name => "child1") # - # in acts_as_nested_set: - # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1 - # child = MyClass.new(:name => "child1") - # child.save - # # now move the item to its right place - # child.move_to_child_of my_item - # - # You can pass an id or an object to: - # * #move_to_child_of - # * #move_to_right_of - # * #move_to_left_of - # module SingletonMethods # Configuration options are: # @@ -66,35 +53,43 @@ module CollectiveIdea #:nodoc: write_inheritable_attribute :acts_as_nested_set_options, options class_inheritable_reader :acts_as_nested_set_options - include Comparable - include Columns - include InstanceMethods - extend Columns - extend ClassMethods + unless self.is_a?(ClassMethods) + include Comparable + include Columns + include InstanceMethods + extend Columns + extend ClassMethods + + belongs_to :parent, :class_name => self.base_class.class_name, + :foreign_key => parent_column_name + has_many :children, :class_name => self.base_class.class_name, + :foreign_key => parent_column_name - # no bulk assignment - attr_protected left_column_name.intern, - right_column_name.intern, - parent_column_name.intern + attr_accessor :skip_before_destroy + + # no bulk assignment + attr_protected left_column_name.intern, + right_column_name.intern - before_create :set_default_left_and_right - before_destroy :prune_from_tree + before_create :set_default_left_and_right + before_save :store_new_parent + after_save :move_to_new_parent + before_destroy :destroy_descendants - # no assignment to structure fields - [left_column_name, right_column_name, parent_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 + # no assignment to structure fields + [left_column_name, right_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 - named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name - named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name - if self.respond_to?(:define_callbacks) - define_callbacks("before_move", "after_move") - end + named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name + named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name + define_callbacks("before_move", "after_move") if self.respond_to?(:define_callbacks) + end end @@ -192,6 +187,30 @@ module CollectiveIdea #:nodoc: set_left_and_rights.call(root_node) end end + + # Iterates over tree elements and determines the current level in the tree. + # Only accepts default ordering, odering by an other column than lft + # does not work. This method is much more efficent than calling level + # because it doesn't require any additional database queries. + # + # Example: + # Category.each_with_level(Category.root.self_and_descendants) do |o, level| + # + def each_with_level(objects) + path = [nil] + objects.each do |o| + if o.parent_id != path.last + # we are on a new level, did we decent or ascent? + if path.include?(o.parent_id) + # remove wrong wrong tailing paths elements + path.pop while path.last != o.parent_id + else + path << o.parent_id + end + end + yield(o, path.length - 1) + end + end end # Mixed into both classes and instances to provide easy access to the column names @@ -255,7 +274,7 @@ module CollectiveIdea #:nodoc: end def leaf? - right - left == 1 + !new_record? && right - left == 1 end # Returns true is this is a child node @@ -281,15 +300,10 @@ module CollectiveIdea #:nodoc: self_and_ancestors.find(:first) end - # Returns the immediate parent - def parent - nested_set_scope.find_by_id(parent_id) if parent_id - end - # Returns the array of all parents and self def self_and_ancestors nested_set_scope.scoped :conditions => [ - "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right + "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right ] end @@ -310,7 +324,7 @@ module CollectiveIdea #:nodoc: # Returns a set of all of its nested children which do not have children def leaves - descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1" + descendants.scoped :conditions => "#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1" end # Returns the level of this object in the tree @@ -322,7 +336,7 @@ module CollectiveIdea #:nodoc: # Returns a set of itself and all of its nested children def self_and_descendants nested_set_scope.scoped :conditions => [ - "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right + "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right ] end @@ -331,11 +345,6 @@ module CollectiveIdea #:nodoc: without_self self_and_descendants end - # Returns a set of only this entry's immediate children - def children - nested_set_scope.scoped :conditions => {parent_column_name => self} - end - def is_descendant_of?(other) other.left < self.left && self.left < other.right && same_scope?(other) end @@ -361,13 +370,13 @@ module CollectiveIdea #:nodoc: # Find the first sibling to the left def left_sibling - siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left], - :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC") + siblings.find(:first, :conditions => ["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left], + :order => "#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC") end # Find the first sibling to the right def right_sibling - siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left]) + siblings.find(:first, :conditions => ["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]) end # Shorthand method for finding the left sibling and moving to the left of it. @@ -417,7 +426,7 @@ module CollectiveIdea #:nodoc: protected def without_self(scope) - scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self] + scope.scoped :conditions => ["#{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 @@ -432,6 +441,19 @@ module CollectiveIdea #:nodoc: self.class.base_class.scoped options end + def store_new_parent + @move_to_new_parent_id = parent_id_changed? ? parent_id : false + true # force callback to return true + end + + def move_to_new_parent + if @move_to_new_parent_id.nil? + move_to_root + elsif @move_to_new_parent_id + move_to_child_of(@move_to_new_parent_id) + end + end + # on creation, set automatically lft and rgt to the end of the tree def set_default_left_and_right maxright = nested_set_scope.maximum(right_column_name) || 0 @@ -442,29 +464,38 @@ module CollectiveIdea #:nodoc: # 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 prune_from_tree - return if right.nil? || left.nil? - diff = right - left + 1 - - delete_method = acts_as_nested_set_options[:dependent] == :destroy ? - :destroy_all : :delete_all - + def destroy_descendants + return if right.nil? || left.nil? || skip_before_destroy + self.class.base_class.transaction do - nested_set_scope.send(delete_method, - ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", - left, right] - ) + if acts_as_nested_set_options[:dependent] == :destroy + descendants.each do |model| + model.skip_before_destroy = true + model.destroy + end + else + nested_set_scope.delete_all( + ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", + left, right] + ) + end + + # update lefts and rights for remaining nodes + diff = right - left + 1 nested_set_scope.update_all( ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff], - ["#{quoted_left_column_name} >= ?", right] + ["#{quoted_left_column_name} > ?", right] ) nested_set_scope.update_all( ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff], - ["#{quoted_right_column_name} >= ?", right] + ["#{quoted_right_column_name} > ?", right] ) + + # Don't allow multiple calls to destroy to corrupt the set + self.skip_before_destroy = true end end - + # reload left, right, and parent def reload_nested_set reload(:select => "#{quoted_left_column_name}, " + diff --git a/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/compatability.rb b/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/compatability.rb deleted file mode 100644 index 2d11da3..0000000 --- a/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/compatability.rb +++ /dev/null @@ -1,29 +0,0 @@ -# Rails <2.x doesn't define #except -class Hash #:nodoc: - # Returns a new hash without the given keys. - def except(*keys) - clone.except!(*keys) - end unless method_defined?(:except) - - # Replaces the hash without the given keys. - def except!(*keys) - keys.map! { |key| convert_key(key) } if respond_to?(:convert_key) - keys.each { |key| delete(key) } - self - end unless method_defined?(:except!) -end - -# NamedScope is new to Rails 2.1 -unless defined? ActiveRecord::NamedScope - require 'awesome_nested_set/named_scope' - ActiveRecord::Base.class_eval do - include CollectiveIdea::NamedScope - end -end - -# Rails 1.2.x doesn't define #quoted_table_name -class ActiveRecord::Base #:nodoc: - def self.quoted_table_name - self.connection.quote_column_name(self.table_name) - end unless methods.include?('quoted_table_name') -end \ No newline at end of file diff --git a/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/named_scope.rb b/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/named_scope.rb deleted file mode 100644 index 1836498..0000000 --- a/vendor/plugins/awesome_nested_set/lib/awesome_nested_set/named_scope.rb +++ /dev/null @@ -1,140 +0,0 @@ -# Taken from Rails 2.1 -module CollectiveIdea #:nodoc: - module NamedScope #:nodoc: - # All subclasses of ActiveRecord::Base have two named_scopes: - # * all, which is similar to a find(:all) query, and - # * scoped, which allows for the creation of anonymous scopes, on the fly: - # - # Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions) - # - # These anonymous scopes tend to be useful when procedurally generating complex queries, where passing - # intermediate values (scopes) around as first-class objects is convenient. - def self.included(base) - base.class_eval do - extend ClassMethods - named_scope :scoped, lambda { |scope| scope } - end - end - - module ClassMethods #:nodoc: - def scopes - read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {}) - end - - # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query, - # such as :conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions. - # - # class Shirt < ActiveRecord::Base - # named_scope :red, :conditions => {:color => 'red'} - # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true] - # end - # - # The above calls to named_scope define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, - # in effect, represents the query Shirt.find(:all, :conditions => {:color => 'red'}). - # - # Unlike Shirt.find(...), however, the object returned by Shirt.red is not an Array; it resembles the association object - # constructed by a has_many declaration. For instance, you can invoke Shirt.red.find(:first), Shirt.red.count, - # Shirt.red.find(:all, :conditions => {:size => 'small'}). Also, just - # as with the association objects, name scopes acts like an Array, implementing Enumerable; Shirt.red.each(&block), - # Shirt.red.first, and Shirt.red.inject(memo, &block) all behave as if Shirt.red really were an Array. - # - # These named scopes are composable. For instance, Shirt.red.dry_clean_only will produce all shirts that are both red and dry clean only. - # Nested finds and calculations also work with these compositions: Shirt.red.dry_clean_only.count returns the number of garments - # for which these criteria obtain. Similarly with Shirt.red.dry_clean_only.average(:thread_count). - # - # All scopes are available as class methods on the ActiveRecord descendent upon which the scopes were defined. But they are also available to - # has_many associations. If, - # - # class Person < ActiveRecord::Base - # has_many :shirts - # end - # - # then elton.shirts.red.dry_clean_only will return all of Elton's red, dry clean - # only shirts. - # - # Named scopes can also be procedural. - # - # class Shirt < ActiveRecord::Base - # named_scope :colored, lambda { |color| - # { :conditions => { :color => color } } - # } - # end - # - # In this example, Shirt.colored('puce') finds all puce shirts. - # - # Named scopes can also have extensions, just as with has_many declarations: - # - # class Shirt < ActiveRecord::Base - # named_scope :red, :conditions => {:color => 'red'} do - # def dom_id - # 'red_shirts' - # end - # end - # end - # - # - # For testing complex named scopes, you can examine the scoping options using the - # proxy_options method on the proxy itself. - # - # class Shirt < ActiveRecord::Base - # named_scope :colored, lambda { |color| - # { :conditions => { :color => color } } - # } - # end - # - # expected_options = { :conditions => { :colored => 'red' } } - # assert_equal expected_options, Shirt.colored('red').proxy_options - def named_scope(name, options = {}, &block) - scopes[name] = lambda do |parent_scope, *args| - Scope.new(parent_scope, case options - when Hash - options - when Proc - options.call(*args) - end, &block) - end - (class << self; self end).instance_eval do - define_method name do |*args| - scopes[name].call(self, *args) - end - end - end - end - - class Scope #:nodoc: - attr_reader :proxy_scope, :proxy_options - [].methods.each { |m| delegate m, :to => :proxy_found unless m =~ /(^__|^nil\?|^send|class|extend|find|count|sum|average|maximum|minimum|paginate)/ } - delegate :scopes, :with_scope, :to => :proxy_scope - - def initialize(proxy_scope, options, &block) - [options[:extend]].flatten.each { |extension| extend extension } if options[:extend] - extend Module.new(&block) if block_given? - @proxy_scope, @proxy_options = proxy_scope, options.except(:extend) - end - - def reload - load_found; self - end - - protected - def proxy_found - @found || load_found - end - - private - def method_missing(method, *args, &block) - if scopes.include?(method) - scopes[method].call(self, *args) - else - with_scope :find => proxy_options do - proxy_scope.send(method, *args, &block) - end - end - end - - def load_found - @found = find(:all) - end - end - end -end \ No newline at end of file -- cgit v1.3