[Ovirt-devel] [PATCH] rails 2.1 upgraded betternestedset

Scott Seago sseago at redhat.com
Wed Sep 24 17:48:15 UTC 2008


As of now, our custom hacks to betternestedset are no longer required, as the latest code incorporates the filtering internally.

Signed-off-by: Scott Seago <sseago at redhat.com>
---
 src/vendor/plugins/betternestedset/README          |    3 +-
 src/vendor/plugins/betternestedset/init.rb         |    5 +-
 .../betternestedset/lib/better_nested_set.rb       | 1055 +++++++++++++++-----
 .../plugins/betternestedset/test/abstract_unit.rb  |    6 -
 .../test/acts_as_nested_set_test.rb                |  833 +++++++++++++---
 .../plugins/betternestedset/test/fixtures/mixin.rb |   19 +-
 6 files changed, 1489 insertions(+), 432 deletions(-)

diff --git a/src/vendor/plugins/betternestedset/README b/src/vendor/plugins/betternestedset/README
index 3f1297e..d11d4b5 100644
--- a/src/vendor/plugins/betternestedset/README
+++ b/src/vendor/plugins/betternestedset/README
@@ -98,9 +98,10 @@ Other instance methods added by this plugin include:
 * <tt>self_and_ancestors</tt> - array of all parents and self
 * <tt>siblings</tt> - array of all siblings (items sharing the same parent)
 * <tt>self_and_siblings</tt> - array of itself and all siblings
-* <tt>children_count</tt> - count of all nested children
+* <tt>children_count</tt> - count of all direct children
 * <tt>children</tt> - array of all immediate children
 * <tt>all_children</tt> - array of all children and nested children
+* <tt>all_children_count</tt> - count of all nested children
 * <tt>full_set</tt> - array of itself and all children and nested children
 * <tt>leaves</tt> - array of the children of this node who do not have children
 * <tt>leaves_count</tt> - the number of leaves
diff --git a/src/vendor/plugins/betternestedset/init.rb b/src/vendor/plugins/betternestedset/init.rb
index 32401ad..6d880f3 100644
--- a/src/vendor/plugins/betternestedset/init.rb
+++ b/src/vendor/plugins/betternestedset/init.rb
@@ -8,4 +8,7 @@ require 'better_nested_set_helper'
 ActiveRecord::Base.class_eval do
   include SymetrieCom::Acts::NestedSet
 end
-ActionView::Base.send :include, SymetrieCom::Acts::BetterNestedSetHelper
\ No newline at end of file
+
+if Object.const_defined?('ActionView')
+  ActionView::Base.send :include, SymetrieCom::Acts::BetterNestedSetHelper
+end
\ No newline at end of file
diff --git a/src/vendor/plugins/betternestedset/lib/better_nested_set.rb b/src/vendor/plugins/betternestedset/lib/better_nested_set.rb
index e91ca93..9efbf25 100644
--- a/src/vendor/plugins/betternestedset/lib/better_nested_set.rb
+++ b/src/vendor/plugins/betternestedset/lib/better_nested_set.rb
@@ -1,59 +1,74 @@
 module SymetrieCom
   module Acts #:nodoc:
     module NestedSet #:nodoc:
+
       def self.included(base)
-        base.extend(ClassMethods)              
+        base.extend(ClassMethods)
       end
       # This module provides an enhanced acts_as_nested_set mixin for ActiveRecord.
       # Please see the README for background information, examples, and tips on usage.
       module ClassMethods
         # Configuration options are:
+        # * +dependent+ - behaviour for cascading destroy operations (default: :delete_all)
         # * +parent_column+ - Column name for the parent/child foreign key (default: +parent_id+).
-        # * +left_column+ - Column name for the left index (default: +lft+). 
-        # * +right_column+ - Column name for the right index (default: +rgt+). NOTE: 
+        # * +left_column+ - Column name for the left index (default: +lft+).
+        # * +right_column+ - Column name for the right index (default: +rgt+). NOTE:
         #   Don't use +left+ and +right+, since these are reserved database words.
-        # * +scope+ - Restricts what is to be considered a tree. Given a symbol, it'll attach "_id" 
-        #   (if it isn't there already) and use that as the foreign key restriction. It's also possible 
+        # * +scope+ - Restricts what is to be considered a tree. Given a symbol, it'll attach "_id"
+        #   (if it isn't there already) and use that as the foreign key restriction. It's also possible
         #   to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
         #   Example: <tt>acts_as_nested_set :scope => 'tree_id = #{tree_id} AND completed = 0'</tt>
-        # * +text_column+ - Column name for the title field (optional). Used as default in the 
-        #   {your-class}_options_for_select helper method. If empty, will use the first string field 
+        # * +text_column+ - Column name for the title field (optional). Used as default in the
+        #   {your-class}_options_for_select helper method. If empty, will use the first string field
         #   of your model class.
-        def acts_as_nested_set(options = {})          
-          
+        def acts_as_nested_set(options = {})
+
+          extend(SingletonMethods) unless respond_to?(:find_in_nestedset)
+
           options[:scope] = "#{options[:scope]}_id".intern if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
-          
+
           write_inheritable_attribute(:acts_as_nested_set_options,
              { :parent_column  => (options[:parent_column] || 'parent_id'),
                :left_column    => (options[:left_column]   || 'lft'),
                :right_column   => (options[:right_column]  || 'rgt'),
                :scope          => (options[:scope] || '1 = 1'),
                :text_column    => (options[:text_column] || columns.collect{|c| (c.type == :string) ? c.name : nil }.compact.first),
-               :class          => self # for single-table inheritance
+               :class          => self, # for single-table inheritance
+               :dependent      => (options[:dependent] || :delete_all) # accepts :delete_all and :destroy
               } )
-          
+
           class_inheritable_reader :acts_as_nested_set_options
-          
+
+          base_set_class.class_inheritable_accessor :acts_as_nested_set_scope_enabled
+          base_set_class.acts_as_nested_set_scope_enabled = true
+
           if acts_as_nested_set_options[:scope].is_a?(Symbol)
             scope_condition_method = %(
               def scope_condition
                 if #{acts_as_nested_set_options[:scope].to_s}.nil?
-                  "#{acts_as_nested_set_options[:scope].to_s} IS NULL"
+                  self.class.use_scope_condition? ? "#{table_name}.#{acts_as_nested_set_options[:scope].to_s} IS NULL" : "(1 = 1)"
                 else
-                  "#{acts_as_nested_set_options[:scope].to_s} = \#{#{acts_as_nested_set_options[:scope].to_s}}"
+                  self.class.use_scope_condition? ? "#{table_name}.#{acts_as_nested_set_options[:scope].to_s} = \#{#{acts_as_nested_set_options[:scope].to_s}}" : "(1 = 1)"
                 end
               end
             )
           else
-            scope_condition_method = "def scope_condition() \"#{acts_as_nested_set_options[:scope]}\" end"
+            scope_condition_method = "def scope_condition(); self.class.use_scope_condition? ? \"#{acts_as_nested_set_options[:scope]}\" : \"(1 = 1)\"; end"
           end
-          
+
+          # skip recursive destroy calls
+          attr_accessor  :skip_before_destroy
+
           # no bulk assignment
           attr_protected  acts_as_nested_set_options[:left_column].intern,
                           acts_as_nested_set_options[:right_column].intern,
                           acts_as_nested_set_options[:parent_column].intern
           # no assignment to structure fields
-          module_eval <<-"end_eval", __FILE__, __LINE__
+          class_eval <<-EOV
+            before_create :set_left_right
+            before_destroy :destroy_descendants
+            include SymetrieCom::Acts::NestedSet::InstanceMethods
+
             def #{acts_as_nested_set_options[:left_column]}=(x)
               raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:left_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
             end
@@ -64,120 +79,362 @@ module SymetrieCom
               raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{acts_as_nested_set_options[:parent_column]}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
             end
             #{scope_condition_method}
-          end_eval
-          
-          
-          include SymetrieCom::Acts::NestedSet::InstanceMethods
-          extend SymetrieCom::Acts::NestedSet::ClassMethods
-          
-          # adds the helper for the class
-#          ActionView::Base.send(:define_method, "#{Inflector.underscore(self.class)}_options_for_select") { special=nil
-#              "#{acts_as_nested_set_options[:text_column]} || "#{self.class} id #{id}"
-#            }
-          
-        end
-        
-        
-        # Returns the single root for the class (or just the first root, if there are several).
-        # Deprecation note: the original acts_as_nested_set allowed roots to have parent_id = 0,
-        # so we currently do the same. This silliness will not be tolerated in future versions, however.
-        def root(find_opts={})
-          roots_internal(:first, find_opts)
-        end
-        
-        # Returns the roots and/or virtual roots of all trees. See the explanation of virtual roots in the README.
-        def roots(find_opts={})
-          roots_internal(:all, find_opts)
-        end
-        def roots_internal(all_or_one, find_opts={})
-          conditions = "(#{acts_as_nested_set_options[:parent_column]} IS NULL"
-          conditions += " OR #{acts_as_nested_set_options[:parent_column]} = 0)"
-          order_col = "#{acts_as_nested_set_options[:left_column]}"
-          opts = merge_incoming_opts({:conditions => conditions,
-                                      :order => order_col},
-                                     find_opts)
-          acts_as_nested_set_options[:class].find(all_or_one, opts)
-        end
-        
-        # Checks the left/right indexes of all records, 
-        # returning the number of records checked. Throws ActiveRecord::ActiveRecordError if it finds a problem.
-        def check_all
-          total = 0
-          transaction do
-            # if there are virtual roots, only call check_full_tree on the first, because it will check other virtual roots in that tree.
-            total = roots.inject(0) {|sum, r| sum + (r[r.left_col_name] == 1 ? r.check_full_tree : 0 )}
-            raise ActiveRecord::ActiveRecordError, "Scope problems or nodes without a valid root" unless acts_as_nested_set_options[:class].count == total
-          end
-          return total
-        end
-        
-        # Re-calculate the left/right values of all nodes. Can be used to convert ordinary trees into nested sets.
-        def renumber_all
-          scopes = []
-          # only call it once for each scope_condition (if the scope conditions are messed up, this will obviously cause problems)
-          roots.each do |r|
-            r.renumber_full_tree unless scopes.include?(r.scope_condition)
-            scopes << r.scope_condition
-          end
-        end
-        
-        # Returns an SQL fragment that matches _items_ *and* all of their descendants, for use in a WHERE clause.
-        # You can pass it a single object, a single ID, or an array of objects and/or IDs.
-        #   # if a.lft = 2, a.rgt = 7, b.lft = 12 and b.rgt = 13
-        #   Set.sql_for([a,b]) # returns "((lft BETWEEN 2 AND 7) OR (lft BETWEEN 12 AND 13))"
-        # Returns "1 != 1" if passed no items. If you need to exclude items, just use "NOT (#{sql_for(items)})".
-        # Note that if you have multiple trees, it is up to you to apply your scope condition.
-        def sql_for(items)
-          items = [items] unless items.is_a?(Array)
-          # get objects for IDs
-          items.collect! {|s| s.is_a?(acts_as_nested_set_options[:class]) ? s : acts_as_nested_set_options[:class].find(s)}.uniq
-          items.reject! {|e| e.new_record?} # exclude unsaved items, since they don't have left/right values yet
-          
-          return "1 != 1" if items.empty? # PostgreSQL didn't like '0', and SQLite3 didn't like 'FALSE'
-          items.map! {|e| "(#{acts_as_nested_set_options[:left_column]} BETWEEN #{e[acts_as_nested_set_options[:left_column]]} AND #{e[acts_as_nested_set_options[:right_column]]})" }
-          "(#{items.join(' OR ')})"
-        end
-
-        # accept incoming opts to allow filtering of results.
-        # So far only tested in limited use cases encountered in oVirt devel.
-        def merge_incoming_opts(set_opts, incoming_opts)
-          new_conditions = incoming_opts.delete(:conditions)
-          set_opts[:conditions] = "(#{set_opts[:conditions]}) AND (#{new_conditions})" if new_conditions
-          set_opts.merge(incoming_opts)
-        end
-        
+          EOV
+        end
+
+        module SingletonMethods
+
+          # Most query methods are wrapped in with_scope to provide further filtering
+          # find_in_nested_set(what, outer_scope, inner_scope)
+          # inner scope is user supplied, while outer_scope is the normal query
+          # this way the user can override most scope attributes, except :conditions
+          # which is merged; use :reverse => true to sort result in reverse direction
+          def find_in_nested_set(*args)
+            what, outer_scope, inner_scope = case args.length
+              when 3 then [args[0], args[1], args[2]]
+              when 2 then [args[0], nil, args[1]]
+              when 1 then [args[0], nil, nil]
+              else [:all, nil, nil]
+            end
+            if inner_scope && outer_scope && inner_scope.delete(:reverse) && outer_scope[:order] == "#{prefixed_left_col_name}"
+              outer_scope[:order] = "#{prefixed_right_col_name} DESC"
+            end
+            acts_as_nested_set_options[:class].with_scope(:find => (outer_scope || {})) do
+              acts_as_nested_set_options[:class].find(what, inner_scope || {})
+            end
+          end
+
+          # Count wrapped in with_scope
+          def count_in_nested_set(*args)
+            outer_scope, inner_scope = case args.length
+              when 2 then [args[0], args[1]]
+              when 1 then [nil, args[0]]
+              else [nil, nil]
+            end
+            acts_as_nested_set_options[:class].with_scope(:find => (outer_scope || {})) do
+              acts_as_nested_set_options[:class].count(inner_scope || {})
+            end
+          end
+
+          # Loop through set using block
+          # pass :nested => false when result is not fully parent-child relational
+          # for example with filtered result sets
+          def recurse_result_set(result, options = {}, &block)
+            return result unless block_given?
+            inner_recursion = options.delete(:inner_recursion)
+            result_set = inner_recursion ? result : result.dup
+
+            parent_id = (options.delete(:parent_id) || result_set.first[result_set.first.parent_col_name]) rescue nil
+            options[:level] ||= 0
+            options[:nested] = true unless options.key?(:nested)
+
+            siblings = options[:nested] ? result_set.select { |s| s.parent_id == parent_id } : result_set
+            siblings.each do |sibling|
+              result_set.delete(sibling)
+              block.call(sibling, options[:level])
+              opts = { :parent_id => sibling.id, :level => options[:level] + 1, :inner_recursion => true }
+              recurse_result_set(result_set, opts, &block) if options[:nested]
+            end
+            result_set.each { |orphan| block.call(orphan, options[:level]) } unless inner_recursion
+          end
+
+          # Loop and create a nested array of hashes (with children property)
+          # pass :nested => false when result is not fully parent-child relational
+          # for example with filtered result sets
+          def result_to_array(result, options = {}, &block)
+            array = []
+            inner_recursion = options.delete(:inner_recursion)
+            result_set = inner_recursion ? result : result.dup
+
+            parent_id = (options.delete(:parent_id) || result_set.first[result_set.first.parent_col_name]) rescue nil
+            level = options[:level]   || 0
+            options[:children]        ||= 'children'
+            options[:methods]         ||= []
+            options[:nested] = true unless options.key?(:nested)
+            options[:symbolize_keys] = true unless options.key?(:symbolize_keys)
+
+            if options[:only].blank? && options[:except].blank?
+              options[:except] = [:left_column, :right_column, :parent_column].inject([]) do |ex, opt|
+                column = acts_as_nested_set_options[opt].to_sym
+                ex << column unless ex.include?(column)
+                ex
+              end
+            end
+
+            siblings = options[:nested] ? result_set.select { |s| s.parent_id == parent_id } : result_set
+            siblings.each do |sibling|
+              result_set.delete(sibling)
+              node = block_given? ? block.call(sibling, level) : sibling.attributes(:only => options[:only], :except => options[:except])
+              options[:methods].inject(node) { |enum, m| enum[m.to_s] = sibling.send(m) if sibling.respond_to?(m); enum }
+              if options[:nested]
+                opts = options.merge(:parent_id => sibling.id, :level => level + 1, :inner_recursion => true)
+                childnodes = result_to_array(result_set, opts, &block)
+                node[ options[:children] ] = childnodes if !childnodes.empty? && node.respond_to?(:[]=)
+              end
+              array << (options[:symbolize_keys] && node.respond_to?(:symbolize_keys) ? node.symbolize_keys : node)
+            end
+            unless inner_recursion
+              result_set.each do |orphan|
+                node = (block_given? ? block.call(orphan, level) : orphan.attributes(:only => options[:only], :except => options[:except]))
+                options[:methods].inject(node) { |enum, m| enum[m.to_s] = orphan.send(m) if orphan.respond_to?(m); enum }
+                array << (options[:symbolize_keys] && node.respond_to?(:symbolize_keys) ? node.symbolize_keys : node)
+              end
+            end
+            array
+          end
+
+          # Loop and create an xml structure. The following options are available
+          # :root sets the root tag, :children sets the siblings tag
+          # :record sets the node item tag, if given
+          # see also: result_to_array and ActiveRecord::XmlSerialization
+          def result_to_xml(result, options = {}, &block)
+            inner_recursion = options.delete(:inner_recursion)
+            result_set = inner_recursion ? result : result.dup
+
+            parent_id = (options.delete(:parent_id) || result_set.first[result_set.first.parent_col_name]) rescue nil
+            options[:nested] = true unless options.key?(:nested)
+
+            options[:except] ||= []
+            [:left_column, :right_column, :parent_column].each do |opt|
+              column = acts_as_nested_set_options[opt].intern
+              options[:except] << column unless options[:except].include?(column)
+            end
+
+            options[:indent]  ||= 2
+            options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
+            options[:builder].instruct! unless options.delete(:skip_instruct)
+
+            record = options.delete(:record)
+            root = options.delete(:root) || :nodes
+            children = options.delete(:children) || :children
+
+            attrs = {}
+            attrs[:xmlns] = options[:namespace] if options[:namespace]
+
+            siblings = options[:nested] ? result_set.select { |s| s.parent_id == parent_id } : result_set
+            options[:builder].tag!(root, attrs) do
+              siblings.each do |sibling|
+                result_set.delete(sibling) if options[:nested]
+                procs = options[:procs] ? options[:procs].dup : []
+                procs << Proc.new { |opts| block.call(opts, sibling) } if block_given?
+                if options[:nested]
+                  proc = Proc.new do |opts|
+                    proc_opts = opts.merge(:parent_id => sibling.id, :root => children, :record => record, :inner_recursion => true)
+                    proc_opts[:procs] ||= options[:procs] if options[:procs]
+                    proc_opts[:methods] ||= options[:methods] if options[:methods]
+                    sibling.class.result_to_xml(result_set, proc_opts, &block)
+                  end
+                  procs << proc
+                end
+                opts = options.merge(:procs => procs, :skip_instruct => true, :root => record)
+                sibling.to_xml(opts)
+              end
+            end
+            options[:builder].target!
+          end
+
+          # Loop and create a nested xml representation of nodes with attributes
+          # pass :nested => false when result is not fully parent-child relational
+          # for example with filtered result sets
+          def result_to_attributes_xml(result, options = {}, &block)
+            inner_recursion = options.delete(:inner_recursion)
+            result_set = inner_recursion ? result : result.dup
+
+            parent_id = (options.delete(:parent_id) || result_set.first[result_set.first.parent_col_name]) rescue nil
+            level = options[:level] || 0
+            options[:methods]       ||= []
+            options[:nested] = true unless options.key?(:nested)
+            options[:dasherize] = true unless options.key?(:dasherize)
+
+            if options[:only].blank? && options[:except].blank?
+              options[:except] = [:left_column, :right_column, :parent_column].inject([]) do |ex, opt|
+                column = acts_as_nested_set_options[opt].to_sym
+                ex << column unless ex.include?(column)
+                ex
+              end
+            end
+
+            options[:indent]  ||= 2
+            options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
+            options[:builder].instruct! unless options.delete(:skip_instruct)
+
+            parent_attrs = {}
+            parent_attrs[:xmlns] = options[:namespace] if options[:namespace]
+
+            siblings = options[:nested] ? result_set.select { |s| s.parent_id == parent_id } : result_set
+            siblings.each do |sibling|
+              result_set.delete(sibling)
+              node_tag = (options[:record] || sibling[sibling.class.inheritance_column] || 'node').underscore
+              node_tag = node_tag.dasherize unless options[:dasherize]
+              attrs = block_given? ? block.call(sibling, level) : sibling.attributes(:only => options[:only], :except => options[:except])
+              options[:methods].inject(attrs) { |enum, m| enum[m.to_s] = sibling.send(m) if sibling.respond_to?(m); enum }
+              if options[:nested] && sibling.children?
+                opts = options.merge(:parent_id => sibling.id, :level => level + 1, :inner_recursion => true, :skip_instruct => true)
+                options[:builder].tag!(node_tag, attrs) { result_to_attributes_xml(result_set, opts, &block) }
+              else
+                options[:builder].tag!(node_tag, attrs)
+              end
+            end
+            unless inner_recursion
+              result_set.each do |orphan|
+                node_tag = (options[:record] || orphan[orphan.class.inheritance_column] || 'node').underscore
+                node_tag = node_tag.dasherize unless options[:dasherize]
+                attrs = block_given? ? block.call(orphan, level) : orphan.attributes(:only => options[:only], :except => options[:except])
+                options[:methods].inject(attrs) { |enum, m| enum[m.to_s] = orphan.send(m) if orphan.respond_to?(m); enum }
+                options[:builder].tag!(node_tag, attrs)
+              end
+            end
+            options[:builder].target!
+          end
+
+          # Returns the single root for the class (or just the first root, if there are several).
+          # Deprecation note: the original acts_as_nested_set allowed roots to have parent_id = 0,
+          # so we currently do the same. This silliness will not be tolerated in future versions, however.
+          def root(scope = {})
+            find_in_nested_set(:first, { :conditions => "(#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)" }, scope)
+          end
+
+          # Returns the roots and/or virtual roots of all trees. See the explanation of virtual roots in the README.
+          def roots(scope = {})
+            find_in_nested_set(:all, { :conditions => "(#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)", :order => "#{prefixed_left_col_name}" }, scope)
+          end
+
+          # Checks the left/right indexes of all records,
+          # returning the number of records checked. Throws ActiveRecord::ActiveRecordError if it finds a problem.
+          def check_all
+            total = 0
+            transaction do
+              # if there are virtual roots, only call check_full_tree on the first, because it will check other virtual roots in that tree.
+              total = roots.inject(0) {|sum, r| sum + (r[r.left_col_name] == 1 ? r.check_full_tree : 0 )}
+              raise ActiveRecord::ActiveRecordError, "Scope problems or nodes without a valid root" unless acts_as_nested_set_options[:class].count == total
+            end
+            return total
+          end
+
+          # Re-calculate the left/right values of all nodes. Can be used to convert ordinary trees into nested sets.
+          def renumber_all
+            scopes = []
+            # only call it once for each scope_condition (if the scope conditions are messed up, this will obviously cause problems)
+            roots.each do |r|
+              r.renumber_full_tree unless scopes.include?(r.scope_condition)
+              scopes << r.scope_condition
+            end
+          end
+
+          # Returns an SQL fragment that matches _items_ *and* all of their descendants, for use in a WHERE clause.
+          # You can pass it a single object, a single ID, or an array of objects and/or IDs.
+          #   # if a.lft = 2, a.rgt = 7, b.lft = 12 and b.rgt = 13
+          #   Set.sql_for([a,b]) # returns "((lft BETWEEN 2 AND 7) OR (lft BETWEEN 12 AND 13))"
+          # Returns "1 != 1" if passed no items. If you need to exclude items, just use "NOT (#{sql_for(items)})".
+          # Note that if you have multiple trees, it is up to you to apply your scope condition.
+          def sql_for(items)
+            items = [items] unless items.is_a?(Array)
+            # get objects for IDs
+            items.collect! {|s| s.is_a?(acts_as_nested_set_options[:class]) ? s : acts_as_nested_set_options[:class].find(s)}.uniq
+            items.reject! {|e| e.new_record?} # exclude unsaved items, since they don't have left/right values yet
+
+            return "1 != 1" if items.empty? # PostgreSQL didn't like '0', and SQLite3 didn't like 'FALSE'
+            items.map! {|e| "(#{prefixed_left_col_name} BETWEEN #{e[left_col_name]} AND #{e[right_col_name]})" }
+            "(#{items.join(' OR ')})"
+          end
+
+          # Wrap a method with this block to disable the default scope_condition
+          def without_scope_condition(&block)
+            if block_given?
+              disable_scope_condition
+              yield
+              enable_scope_condition
+            end
+          end
+
+          def use_scope_condition?#:nodoc:
+            base_set_class.acts_as_nested_set_scope_enabled == true
+          end
+
+          def disable_scope_condition#:nodoc:
+            base_set_class.acts_as_nested_set_scope_enabled = false
+          end
+
+          def enable_scope_condition#:nodoc:
+            base_set_class.acts_as_nested_set_scope_enabled = true
+          end
+
+          def left_col_name#:nodoc:
+            acts_as_nested_set_options[:left_column]
+          end
+          def prefixed_left_col_name#:nodoc:
+            "#{table_name}.#{left_col_name}"
+          end
+          def right_col_name#:nodoc:
+            acts_as_nested_set_options[:right_column]
+          end
+          def prefixed_right_col_name#:nodoc:
+            "#{table_name}.#{right_col_name}"
+          end
+          def parent_col_name#:nodoc:
+            acts_as_nested_set_options[:parent_column]
+          end
+          def prefixed_parent_col_name#:nodoc:
+            "#{table_name}.#{parent_col_name}"
+          end
+          def base_set_class#:nodoc:
+            acts_as_nested_set_options[:class] # for single-table inheritance
+          end
+
+        end
+
       end
 
       # This module provides instance methods for an enhanced acts_as_nested_set mixin. Please see the README for background information, examples, and tips on usage.
       module InstanceMethods
         # convenience methods to make the code more readable
-        def left_col_name()#:nodoc:
-          acts_as_nested_set_options[:left_column]
+        def left_col_name#:nodoc:
+          self.class.left_col_name
+        end
+        def prefixed_left_col_name#:nodoc:
+          self.class.prefixed_left_col_name
         end
-        def right_col_name()#:nodoc:
-          acts_as_nested_set_options[:right_column]
+        def right_col_name#:nodoc:
+         self.class.right_col_name
         end
-        def parent_col_name()#:nodoc:
-          acts_as_nested_set_options[:parent_column]
+        def prefixed_right_col_name#:nodoc:
+          self.class.prefixed_right_col_name
+        end
+        def parent_col_name#:nodoc:
+          self.class.parent_col_name
+        end
+        def prefixed_parent_col_name#:nodoc:
+          self.class.prefixed_parent_col_name
         end
         alias parent_column parent_col_name#:nodoc: Deprecated
-        def base_set_class()#:nodoc:
+        def base_set_class#:nodoc:
           acts_as_nested_set_options[:class] # for single-table inheritance
         end
-        
+
+        # This takes care of valid queries when called on a root node
+        def sibling_condition
+          self[parent_col_name] ? "#{prefixed_parent_col_name} = #{self[parent_col_name]}" : "(#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)"
+        end
+
         # On creation, automatically add the new node to the right of all existing nodes in this tree.
-        def before_create # already protected by a transaction
+        def set_left_right # already protected by a transaction within #create
           maxright = base_set_class.maximum(right_col_name, :conditions => scope_condition) || 0
           self[left_col_name] = maxright+1
           self[right_col_name] = maxright+2
         end
-        
+
         # On destruction, delete all children and shift the lft/rgt values back to the left so the counts still work.
-        def before_destroy # already protected by a transaction
-          return if self[right_col_name].nil? || self[left_col_name].nil?
-          self.reload # in case a concurrent move has altered the indexes
+        def destroy_descendants # already protected by a transaction within #destroy
+          return if self[right_col_name].nil? || self[left_col_name].nil? || self.skip_before_destroy
+          reloaded = self.reload rescue nil # in case a concurrent move has altered the indexes - rescue if non-existent
+          return unless reloaded
           dif = self[right_col_name] - self[left_col_name] + 1
-          base_set_class.delete_all( "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})" )
+          if acts_as_nested_set_options[:dependent] == :delete_all
+            base_set_class.delete_all( "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})" )
+          else
+            set = base_set_class.find(:all, :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})", :order => "#{prefixed_right_col_name} DESC")
+            set.each { |child| child.skip_before_destroy = true; remove_descendant(child) }
+          end
           base_set_class.update_all("#{left_col_name} = CASE \
                                       WHEN #{left_col_name} > #{self[right_col_name]} THEN (#{left_col_name} - #{dif}) \
                                       ELSE #{left_col_name} END, \
@@ -186,127 +443,264 @@ module SymetrieCom
                                       ELSE #{right_col_name} END",
                                  scope_condition)
         end
-        
+
         # By default, records are compared and sorted using the left column.
         def <=>(x)
           self[left_col_name] <=> x[left_col_name]
         end
-        
+
         # Deprecated. Returns true if this is a root node.
         def root?
           parent_id = self[parent_col_name]
           (parent_id == 0 || parent_id.nil?) && self[right_col_name] && self[left_col_name] && (self[right_col_name] > self[left_col_name])
         end
-        
+
         # Deprecated. Returns true if this is a child node
-        def child?                          
+        def child?
           parent_id = self[parent_col_name]
           !(parent_id == 0 || parent_id.nil?) && (self[left_col_name] > 1) && (self[right_col_name] > self[left_col_name])
         end
-        
+
         # Deprecated. Returns true if we have no idea what this is
         def unknown?
           !root? && !child?
         end
-        
+
         # Returns this record's root ancestor.
-        def root
+        def root(scope = {})
           # the BETWEEN clause is needed to ensure we get the right virtual root, if using those
-          base_set_class.find(:first, :conditions => "#{scope_condition} \
-            AND (#{parent_col_name} IS NULL OR #{parent_col_name} = 0) AND (#{self[left_col_name]} BETWEEN #{left_col_name} AND #{right_col_name})")
+          self.class.find_in_nested_set(:first, { :conditions => "#{scope_condition} \
+            AND (#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0) AND (#{self[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name})" }, scope)
         end
-        
+
         # Returns the root or virtual roots of this record's tree (a tree cannot have more than one real root). See the explanation of virtual roots in the README.
-        def roots
-          base_set_class.find(:all, :conditions => "#{scope_condition} AND (#{parent_col_name} IS NULL OR #{parent_col_name} = 0)", :order => "#{left_col_name}")
+        def roots(scope = {})
+          self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{prefixed_parent_col_name} IS NULL OR #{prefixed_parent_col_name} = 0)", :order => "#{prefixed_left_col_name}" }, scope)
         end
-        
+
         # Returns this record's parent.
         def parent
-          base_set_class.find(self[parent_col_name]) if self[parent_col_name]
+          self.class.find_in_nested_set(self[parent_col_name]) if self[parent_col_name]
         end
-        
+
         # Returns an array of all parents, starting with the root.
-        def ancestors
-          self_and_ancestors - [self]
+        def ancestors(scope = {})
+          self_and_ancestors(scope) - [self]
         end
-        
+
         # Returns an array of all parents plus self, starting with the root.
-        def self_and_ancestors
-          base_set_class.find(:all, :conditions => "#{scope_condition} AND (#{self[left_col_name]} BETWEEN #{left_col_name} AND #{right_col_name})", :order => left_col_name )
+        def self_and_ancestors(scope = {})
+          self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{self[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name})", :order => "#{prefixed_left_col_name}" }, scope)
         end
-        
+
         # Returns all the children of this node's parent, except self.
-        def siblings
-          self_and_siblings - [self]
+        def siblings(scope = {})
+          self_and_siblings(scope) - [self]
+        end
+
+        # Returns all siblings to the left of self, in descending order, so the first sibling is the one closest to the left of self
+        def previous_siblings(scope = {})
+          self.class.find_in_nested_set(:all,
+            { :conditions => ["#{scope_condition} AND #{sibling_condition} AND #{self.class.table_name}.id != ? AND #{prefixed_right_col_name} < ?", self.id, self[left_col_name]], :order => "#{prefixed_left_col_name} DESC" }, scope)
         end
-        
+
+        # Returns all siblings to the right of self, in ascending order, so the first sibling is the one closest to the right of self
+        def next_siblings(scope = {})
+          self.class.find_in_nested_set(:all,
+            { :conditions => ["#{scope_condition} AND #{sibling_condition} AND #{self.class.table_name}.id != ? AND #{prefixed_left_col_name} > ?", self.id, self[right_col_name]], :order => "#{prefixed_left_col_name} ASC"}, scope)
+        end
+
+        # Returns first siblings amongst it's siblings.
+        def first_sibling(scope = {})
+          self_and_siblings(scope.merge(:limit => 1, :order => "#{prefixed_left_col_name} ASC")).first
+        end
+
+        def first_sibling?(scope = {})
+          self == first_sibling(scope)
+        end
+        alias :first? :first_sibling?
+
+        # Returns last siblings amongst it's siblings.
+        def last_sibling(scope = {})
+          self_and_siblings(scope.merge(:limit => 1, :order => "#{prefixed_left_col_name} DESC")).first
+        end
+
+        def last_sibling?(scope = {})
+          self == last_sibling(scope)
+        end
+        alias :last? :last_sibling?
+
+        # Returns previous sibling of node or nil if there is none.
+        def previous_sibling(num = 1, scope = {})
+          scope[:limit] = num
+          siblings = previous_siblings(scope)
+          num == 1 ? siblings.first : siblings
+        end
+        alias :higher_item :previous_sibling
+
+        # Returns next sibling of node or nil if there is none.
+        def next_sibling(num = 1, scope = {})
+          scope[:limit] = num
+          siblings = next_siblings(scope)
+          num == 1 ? siblings.first : siblings
+        end
+        alias :lower_item :next_sibling
+
         # Returns all the children of this node's parent, including self.
-        def self_and_siblings
+        def self_and_siblings(scope = {})
           if self[parent_col_name].nil? || self[parent_col_name].zero?
             [self]
           else
-            base_set_class.find(:all, :conditions => "#{scope_condition} AND #{parent_col_name} = #{self[parent_col_name]}", :order => left_col_name)
+            self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND #{sibling_condition}", :order => "#{prefixed_left_col_name}" }, scope)
           end
         end
-        
+
         # Returns the level of this object in the tree, root level being 0.
-        def level
+        def level(scope = {})
           return 0 if self[parent_col_name].nil?
-          base_set_class.count(:conditions => "#{scope_condition} AND (#{self[left_col_name]} BETWEEN #{left_col_name} AND #{right_col_name})") - 1
+          self.class.count_in_nested_set({ :conditions => "#{scope_condition} AND (#{self[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name})" }, scope) - 1
         end
-        
+
         # Returns the number of nested children of this object.
-        def all_children_count
+        def all_children_count(scope = nil)
+          return all_children(scope).length if scope.is_a?(Hash)
           return (self[right_col_name] - self[left_col_name] - 1)/2
         end
-        
+
         # Returns itself and all nested children.
         # Pass :exclude => item, or id, or [items or id] to exclude one or more items *and* all of their descendants.
-        # in addition to the standard find opts
-        def full_set(find_opts={})
-          find_opts ||= {}
-          exclude = find_opts.delete(:exclude)
-          if exclude
+        def full_set(scope = {})
+          if exclude = scope.delete(:exclude)
             exclude_str = " AND NOT (#{base_set_class.sql_for(exclude)}) "
           elsif new_record? || self[right_col_name] - self[left_col_name] == 1
             return [self]
           end
-          opts = base_set_class.merge_incoming_opts({:conditions => "#{scope_condition} #{exclude_str} AND (#{left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})",
-                                       :order => left_col_name},
-                                     find_opts)
-          base_set_class.find(:all, opts)
+          self.class.find_in_nested_set(:all, {
+            :order => "#{prefixed_left_col_name}",
+            :conditions => "#{scope_condition} #{exclude_str} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]})"
+          }, scope)
+        end
+
+        # Returns the child for the requested id within the scope of its children, otherwise nil
+        def child_by_id(id, scope = {})
+          children_by_id(id, scope).first
+        end
+
+        # Returns a child collection for the requested ids within the scope of its children, otherwise empty array
+        def children_by_id(*args)
+          scope = args.last.is_a?(Hash) ? args.pop : {}
+          ids = args.flatten.compact.uniq
+          self.class.find_in_nested_set(:all, {
+            :conditions => ["#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{self.class.table_name}.#{self.class.primary_key} IN (?)", ids]
+          }, scope)
         end
-        
+
+        # Returns the child for the requested id within the scope of its immediate children, otherwise nil
+        def direct_child_by_id(id, scope = {})
+          direct_children_by_id(id, scope).first
+        end
+
+        # Returns a child collection for the requested ids within the scope of its immediate children, otherwise empty array
+        def direct_children_by_id(*args)
+          scope = args.last.is_a?(Hash) ? args.pop : {}
+          ids = args.flatten.compact.uniq
+          self.class.find_in_nested_set(:all, {
+            :conditions => ["#{scope_condition} AND #{prefixed_parent_col_name} = #{self.id} AND #{self.class.table_name}.#{self.class.primary_key} IN (?)", ids]
+          }, scope)
+        end
+
+        # Tests wether self is within scope of parent
+        def child_of?(parent, scope = {})
+          if !scope.empty? && parent.respond_to?(:child_by_id)
+            parent.child_by_id(self.id, scope).is_a?(self.class)
+          else
+            parent.respond_to?(left_col_name) && self[left_col_name] > parent[left_col_name] && self[right_col_name] < parent[right_col_name]
+          end
+        end
+
+        # Tests wether self is within immediate scope of parent
+        def direct_child_of?(parent, scope = {})
+          if !scope.empty? && parent.respond_to?(:direct_child_by_id)
+            parent.direct_child_by_id(self.id, scope).is_a?(self.class)
+          else
+            parent.respond_to?(parent_col_name) && self[parent_col_name] == parent.id
+          end
+        end
+
         # Returns all children and nested children.
         # Pass :exclude => item, or id, or [items or id] to exclude one or more items *and* all of their descendants.
-        # in addition to the standard find opts
-        def all_children(find_opts={})
-          full_set(find_opts) - [self]
+        def all_children(scope = {})
+          full_set(scope) - [self]
         end
-        
+
+        def children_count(scope= {})
+          self.class.count_in_nested_set({ :conditions => "#{scope_condition} AND #{prefixed_parent_col_name} = #{self.id}" }, scope)
+        end
+
         # Returns this record's immediate children.
-        def children(find_opts={})
-          opts = base_set_class.merge_incoming_opts({:conditions => "#{scope_condition} AND #{parent_col_name} = #{self.id}",
-                                      :order => left_col_name},
-                                     find_opts)
-          base_set_class.find(:all, opts)
+        def children(scope = {})
+          self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND #{prefixed_parent_col_name} = #{self.id}", :order => "#{prefixed_left_col_name}" }, scope)
         end
-        
+
+        def children?(scope = {})
+          children_count(scope) > 0
+        end
+
         # Deprecated
         alias direct_children children
-        
+
         # Returns this record's terminal children (nodes without children).
-        def leaves
-          base_set_class.find(:all, :conditions => "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{left_col_name} + 1 = #{right_col_name}", :order => left_col_name)
+        def leaves(scope = {})
+          self.class.find_in_nested_set(:all,
+            { :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{prefixed_left_col_name} + 1 = #{prefixed_right_col_name}", :order => "#{prefixed_left_col_name}" }, scope)
         end
-        
+
         # Returns the count of this record's terminal children (nodes without children).
-        def leaves_count
-          base_set_class.count(:conditions => "#{scope_condition} AND (#{left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{left_col_name} + 1 = #{right_col_name}")
+        def leaves_count(scope = {})
+          self.class.count_in_nested_set({ :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{self[left_col_name]} AND #{self[right_col_name]}) AND #{prefixed_left_col_name} + 1 = #{prefixed_right_col_name}" }, scope)
+        end
+
+        # All nodes between two nodes, those nodes included
+        # in effect all ancestors until the other is reached
+        def ancestors_and_self_through(other, scope = {})
+          first, last = [self, other].sort
+          self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND (#{last[left_col_name]} BETWEEN #{prefixed_left_col_name} AND #{prefixed_right_col_name}) AND #{prefixed_left_col_name} >= #{first[left_col_name]}",
+            :order => "#{prefixed_left_col_name}" }, scope)
+        end
+
+        # Ancestors until the other is reached - excluding self
+        def ancestors_through(other, scope = {})
+          ancestors_and_self_through(other, scope) - [self]
+        end
+
+        # All children until the other is reached - excluding self
+        def all_children_through(other, scope = {})
+          full_set_through(other, scope) - [self]
+        end
+
+        # All children until the other is reached - including self
+        def full_set_through(other, scope = {})
+          first, last = [self, other].sort
+          self.class.find_in_nested_set(:all,
+            { :conditions => "#{scope_condition} AND (#{prefixed_left_col_name} BETWEEN #{first[left_col_name]} AND #{first[right_col_name]}) AND #{prefixed_left_col_name} <= #{last[left_col_name]}", :order => "#{prefixed_left_col_name}" }, scope)
         end
-        
-        # Checks the left/right indexes of one node and all descendants. 
+
+        # All siblings until the other is reached - including self
+        def self_and_siblings_through(other, scope = {})
+          if self[parent_col_name].nil? || self[parent_col_name].zero?
+            [self]
+          else
+            first, last = [self, other].sort
+            self.class.find_in_nested_set(:all, { :conditions => "#{scope_condition} AND #{sibling_condition} AND (#{prefixed_left_col_name} BETWEEN #{first[left_col_name]} AND #{last[right_col_name]})", :order => "#{prefixed_left_col_name}" }, scope)
+          end
+        end
+
+        # All siblings until the other is reached - excluding self
+        def siblings_through(other, scope = {})
+          self_and_siblings_through(other, scope) - [self]
+        end
+
+        # Checks the left/right indexes of one node and all descendants.
         # Throws ActiveRecord::ActiveRecordError if it finds a problem.
         def check_subtree
           transaction do
@@ -314,8 +708,8 @@ module SymetrieCom
             check # this method is implemented via #check, so that we don't generate lots of unnecessary nested transactions
           end
         end
-        
-        # Checks the left/right indexes of the entire tree that this node belongs to, 
+
+        # Checks the left/right indexes of the entire tree that this node belongs to,
         # returning the number of records checked. Throws ActiveRecord::ActiveRecordError if it finds a problem.
         # This method is needed because check_subtree alone cannot find gaps between virtual roots, orphaned nodes or endless loops.
         def check_full_tree
@@ -323,7 +717,7 @@ module SymetrieCom
           transaction do
             # virtual roots make this method more complex than it otherwise would be
             n = 1
-            roots.each do |r| 
+            roots.each do |r|
               raise ActiveRecord::ActiveRecordError, "Gaps between roots in the tree containing record ##{r.id}" if r[left_col_name] != n
               r.check_subtree
               n = r[right_col_name] + 1
@@ -335,14 +729,14 @@ module SymetrieCom
           end
           return total_nodes
         end
-        
+
         # Re-calculate the left/right values of all nodes in this record's tree. Can be used to convert an ordinary tree into a nested set.
         def renumber_full_tree
           indexes = []
           n = 1
           transaction do
             for r in roots # because we may have virtual roots
-              n = r.calc_numbers(n, indexes)
+              n = 1 + r.calc_numbers(n, indexes)
             end
             for i in indexes
               base_set_class.update_all("#{left_col_name} = #{i[:lft]}, #{right_col_name} = #{i[:rgt]}", "#{self.class.primary_key} = #{i[:id]}")
@@ -350,7 +744,7 @@ module SymetrieCom
           end
           ## reload?
         end
-        
+
         # Deprecated. Adds a child to this object in the tree.  If this object hasn't been initialized,
         # it gets set up as a root node.
         #
@@ -391,7 +785,7 @@ module SymetrieCom
                                       scope_condition)
               child.reload
             end
-            
+
             child.move_to_child_of(self)
             # self.reload ## even though move_to calls target.reload, at least one object in the tests was not reloading (near the end of test_common_usage)
           end
@@ -405,10 +799,10 @@ module SymetrieCom
         #     # Looks like we're now the root node!  Woo
         #     self[left_col_name] = 1
         #     self[right_col_name] = 4
-        #     
+        #
         #     # What do to do about validation?
         #     return nil unless self.save
-        #     
+        #
         #     child[parent_col_name] = self.id
         #     child[left_col_name] = 2
         #     child[right_col_name]= 3
@@ -429,99 +823,197 @@ module SymetrieCom
         #   end
         # end
         end
-        
+
+        # Insert a node at a specific position among the children of target.
+        def insert_at(target, index = :last, scope = {})
+          level_nodes = target.children(scope)
+          current_index = level_nodes.index(self)
+          last_index = level_nodes.length - 1
+          as_first = (index == :first)
+          as_last  = (index == :last || (index.is_a?(Fixnum) && index > last_index))
+          index = 0 if as_first
+          index = last_index if as_last
+          if last_index < 0
+            move_to_child_of(target)
+          elsif index >= 0 && index <= last_index && level_nodes[index]
+            if as_last && index != current_index
+              move_to_right_of(level_nodes[index])
+            elsif (as_first || index == 0) && index != current_index
+              move_to_left_of(level_nodes[index])
+            elsif !current_index.nil? && index > current_index
+              move_to_right_of(level_nodes[index])
+            elsif !current_index.nil? && index < current_index
+              move_to_left_of(level_nodes[index])
+            elsif current_index.nil?
+              move_to_left_of(level_nodes[index])
+            end
+          end
+        end
+
         # Move this node to the left of _target_ (you can pass an object or just an id).
         # Unsaved changes in either object will be lost. Raises ActiveRecord::ActiveRecordError if it encounters a problem.
         def move_to_left_of(target)
           self.move_to target, :left
         end
-        
+
         # Move this node to the right of _target_ (you can pass an object or just an id).
         # Unsaved changes in either object will be lost. Raises ActiveRecord::ActiveRecordError if it encounters a problem.
         def move_to_right_of(target)
           self.move_to target, :right
         end
-        
+
         # Make this node a child of _target_ (you can pass an object or just an id).
         # Unsaved changes in either object will be lost. Raises ActiveRecord::ActiveRecordError if it encounters a problem.
         def move_to_child_of(target)
           self.move_to target, :child
         end
-        
+
+        # Moves a node to a certain position amongst its siblings.
+        def move_to_position(index, scope = {})
+          insert_at(self.parent, index, scope)
+        end
+
+        # Moves a node one up amongst its siblings. Does nothing if it's already
+        # the first sibling.
+        def move_lower
+          next_sib = next_sibling
+          move_to_right_of(next_sib) if next_sib
+        end
+
+        # Moves a node one down amongst its siblings. Does nothing if it's already
+        # the last sibling.
+        def move_higher
+          prev_sib = previous_sibling
+          move_to_left_of(prev_sib) if prev_sib
+        end
+
+        # Moves a node one to be the first amongst its siblings. Does nothing if it's already
+        # the first sibling.
+        def move_to_top
+          first_sib = first_sibling
+          move_to_left_of(first_sib) if first_sib && self != first_sib
+        end
+
+        # Moves a node one to be the last amongst its siblings. Does nothing if it's already
+        # the last sibling.
+        def move_to_bottom
+          last_sib = last_sibling
+          move_to_right_of(last_sib) if last_sib && self != last_sib
+        end
+
+        # Swaps the position of two sibling nodes preserving a sibling's descendants.
+        # The current implementation only works amongst siblings.
+        def swap(target, transact = true)
+          move_to(target, :swap, transact)
+        end
+
+        # Reorder children according to an array of ids
+        def reorder_children(*ids)
+          transaction do
+            ordered_ids = ids.flatten.uniq
+            current_children = children({ :conditions => { :id => ordered_ids } })
+            current_children_ids = current_children.map(&:id)
+            ordered_ids = ordered_ids & current_children_ids
+            return [] unless ordered_ids.length > 1 && ordered_ids != current_children_ids
+            perform_reorder_of_children(ordered_ids, current_children)
+          end
+        end
+
         protected
-        def move_to(target, position) #:nodoc:
+        def move_to(target, position, transact = true) #:nodoc:
           raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if new_record?
           raise ActiveRecord::ActiveRecordError, "You cannot move a node if left or right is nil" unless self[left_col_name] && self[right_col_name]
-          
-          transaction do
-            self.reload # the lft/rgt values could be stale (target is reloaded below)
+
+          with_optional_transaction(transact) do
+            self.reload(:select => "#{left_col_name}, #{right_col_name}, #{parent_col_name}") # the lft/rgt values could be stale (target is reloaded below)
             if target.is_a?(base_set_class)
-              target.reload # could be stale
+              target.reload(:select => "#{left_col_name}, #{right_col_name}, #{parent_col_name}") # could be stale
             else
-              target = base_set_class.find(target) # load object if we were given an ID
+              target = self.class.find_in_nested_set(target) # load object if we were given an ID
             end
-            
+
             if (target[left_col_name] >= self[left_col_name]) && (target[right_col_name] <= self[right_col_name])
               raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
             end
-            
+
             # prevent moves between different trees
             if target.scope_condition != scope_condition
               raise ActiveRecord::ActiveRecordError, "Scope conditions do not match. Is the target in the same tree?"
             end
-            
-            # the move: we just need to define two adjoining segments of the left/right index and swap their positions
-            bound = case position
-              when :child then target[right_col_name]
-              when :left  then target[left_col_name]
-              when :right then target[right_col_name] + 1
-              else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left or :right ('#{position}' received)."
-            end
-            
-            if bound > self[right_col_name]
-              bound = bound - 1
-              other_bound = self[right_col_name] + 1
-            else
-              other_bound = self[left_col_name] - 1
-            end
-            
-            return if bound == self[right_col_name] || bound == self[left_col_name] # there would be no change, and other_bound is now wrong anyway
-            
-            # 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_col_name], self[right_col_name], bound, other_bound].sort
-            
-            # change nil to NULL for new parent
-            if position == :child
-              new_parent = target.id
+
+            if position == :swap
+              unless self.siblings.include?(target)
+                raise ActiveRecord::ActiveRecordError, "Impossible move, target node should be a sibling."
+              end
+
+              direction = (self[left_col_name] < target[left_col_name]) ? :down : :up
+
+              i0 = (direction == :up) ? target[left_col_name] : self[left_col_name]
+              i1 = (direction == :up) ? target[right_col_name] : self[right_col_name]
+              i2 = (direction == :up) ? self[left_col_name] : target[left_col_name]
+              i3 = (direction == :up) ? self[right_col_name] : target[right_col_name]
+
+              base_set_class.update_all(%[
+                #{left_col_name} = CASE WHEN #{left_col_name} BETWEEN #{i0} AND #{i1} THEN #{i3} + #{left_col_name} - #{i1}
+                  WHEN #{left_col_name} BETWEEN #{i2} AND #{i3} THEN #{i0} + #{left_col_name} - #{i2}
+                  ELSE #{i0} + #{i3} + #{left_col_name} - #{i1} - #{i2} END,
+                  #{right_col_name} = CASE WHEN #{right_col_name} BETWEEN #{i0} AND #{i1} THEN #{i3} + #{right_col_name} - #{i1}
+                  WHEN #{right_col_name} BETWEEN #{i2} AND #{i3} THEN #{i0} + #{right_col_name} - #{i2}
+                  ELSE #{i0} + #{i3} + #{right_col_name} - #{i1} - #{i2} END ], "#{left_col_name} BETWEEN #{i0} AND #{i3} AND #{i0} < #{i1} AND #{i1} < #{i2} AND #{i2} < #{i3} AND #{scope_condition}")
             else
-              new_parent = target[parent_col_name].nil? ? 'NULL' : target[parent_col_name]
-            end
-            
-            base_set_class.update_all("\
-              #{left_col_name} = CASE \
-                WHEN #{left_col_name} BETWEEN #{a} AND #{b} THEN #{left_col_name} + #{d - b} \
-                WHEN #{left_col_name} BETWEEN #{c} AND #{d} THEN #{left_col_name} + #{a - c} \
-                ELSE #{left_col_name} END, \
-              #{right_col_name} = CASE \
-                WHEN #{right_col_name} BETWEEN #{a} AND #{b} THEN #{right_col_name} + #{d - b} \
-                WHEN #{right_col_name} BETWEEN #{c} AND #{d} THEN #{right_col_name} + #{a - c} \
-                ELSE #{right_col_name} END, \
-              #{parent_col_name} = CASE \
-                WHEN #{self.class.primary_key} = #{self.id} THEN #{new_parent} \
-                ELSE #{parent_col_name} END",
-              scope_condition)
-            self.reload
-            target.reload
+              # the move: we just need to define two adjoining segments of the left/right index and swap their positions
+              bound = case position
+                when :child then target[right_col_name]
+                when :left  then target[left_col_name]
+                when :right then target[right_col_name] + 1
+                else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left or :right ('#{position}' received)."
+              end
+
+              if bound > self[right_col_name]
+                bound = bound - 1
+                other_bound = self[right_col_name] + 1
+              else
+                other_bound = self[left_col_name] - 1
+              end
+
+              return if bound == self[right_col_name] || bound == self[left_col_name] # there would be no change, and other_bound is now wrong anyway
+
+              # 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_col_name], self[right_col_name], bound, other_bound].sort
+
+              # change nil to NULL for new parent
+              if position == :child
+                new_parent = target.id
+              else
+                new_parent = target[parent_col_name].nil? ? 'NULL' : target[parent_col_name]
+              end
+
+              base_set_class.update_all("\
+                #{left_col_name} = CASE \
+                  WHEN #{left_col_name} BETWEEN #{a} AND #{b} THEN #{left_col_name} + #{d - b} \
+                  WHEN #{left_col_name} BETWEEN #{c} AND #{d} THEN #{left_col_name} + #{a - c} \
+                  ELSE #{left_col_name} END, \
+                #{right_col_name} = CASE \
+                  WHEN #{right_col_name} BETWEEN #{a} AND #{b} THEN #{right_col_name} + #{d - b} \
+                  WHEN #{right_col_name} BETWEEN #{c} AND #{d} THEN #{right_col_name} + #{a - c} \
+                  ELSE #{right_col_name} END, \
+                #{parent_col_name} = CASE \
+                  WHEN #{self.class.primary_key} = #{self.id} THEN #{new_parent} \
+                  ELSE #{parent_col_name} END",
+                scope_condition)
+            end
+            self.reload(:select => "#{left_col_name}, #{right_col_name}, #{parent_col_name}")
+            target.reload(:select => "#{left_col_name}, #{right_col_name}, #{parent_col_name}")
           end
         end
-        
+
         def check #:nodoc:
           # performance improvements (3X or more for tables with lots of columns) by using :select to load just id, lft and rgt
           ## i don't use the scope condition here, because it shouldn't be needed
-          my_children = base_set_class.find(:all, :conditions => "#{parent_col_name} = #{self.id}",
-            :order => left_col_name, :select => "#{self.class.primary_key}, #{left_col_name}, #{right_col_name}")
-          
+          my_children = self.class.find_in_nested_set(:all, :conditions => "#{prefixed_parent_col_name} = #{self.id}",
+            :order => "#{prefixed_left_col_name}", :select => "#{self.class.primary_key}, #{prefixed_left_col_name}, #{prefixed_right_col_name}")
+
           if my_children.empty?
             unless self[left_col_name] && self[right_col_name]
               raise ActiveRecord::ActiveRecordError, "#{self.class.name}##{self.id}.#{right_col_name} or #{left_col_name} is blank"
@@ -546,14 +1038,14 @@ module SymetrieCom
             end
           end
         end
-        
+
         # used by the renumbering methods
         def calc_numbers(n, indexes) #:nodoc:
           my_lft = n
           # performance improvements (3X or more for tables with lots of columns) by using :select to load just id, lft and rgt
           ## i don't use the scope condition here, because it shouldn't be needed
-          my_children = base_set_class.find(:all, :conditions => "#{parent_col_name} = #{self.id}",
-            :order => left_col_name, :select => "#{self.class.primary_key}, #{left_col_name}, #{right_col_name}")
+          my_children = self.class.find_in_nested_set(:all, :conditions => "#{prefixed_parent_col_name} = #{self.id}",
+            :order => "#{prefixed_left_col_name}", :select => "#{self.class.primary_key}, #{prefixed_left_col_name}, #{prefixed_right_col_name}")
           if my_children.empty?
             my_rgt = (n += 1)
           else
@@ -565,35 +1057,52 @@ module SymetrieCom
           indexes << {:id => self.id, :lft => my_lft, :rgt => my_rgt} unless self[left_col_name] == my_lft && self[right_col_name] == my_rgt
           return n
         end
-        
-        
-        
+
+        # Actually perform the ordering using calculated steps
+        def perform_reorder_of_children(ordered_ids, current)
+          steps = calculate_reorder_steps(ordered_ids, current)
+          steps.inject([]) do |result, (source, idx)|
+            target = current[idx]
+            if source.id != target.id
+              source.swap(target, false)
+              from = current.index(source)
+              current[from], current[idx] = current[idx], current[from]
+              result << source
+            end
+            result
+          end
+        end
+
+        # Calculate the least amount of swap steps to achieve the requested order
+        def calculate_reorder_steps(ordered_ids, current)
+          steps = []
+          current.each_with_index do |source, idx|
+            new_idx = ordered_ids.index(source.id)
+            steps << [source, new_idx] if idx != new_idx
+          end
+          steps
+        end
+
         # The following code is my crude method of making things concurrency-safe.
         # Basically, we need to ensure that whenever a record is saved, the lft/rgt
         # values are _not_ written to the database, because if any changes to the tree
-        # structure occurrred since the object was loaded, the lft/rgt values could 
-        # be out of date and corrupt the indexes. 
-        # I hope that someone with a little more ruby-foo can look at this and come
-        # up with a more elegant solution.
-        private
-          # override ActiveRecord to prevent lft/rgt values from being saved (can corrupt indexes under concurrent usage)
-          def update #:nodoc:
-            connection.update(
-              "UPDATE #{self.class.table_name} " +
-              "SET #{quoted_comma_pair_list(connection, special_attributes_with_quotes(false))} " +
-              "WHERE #{self.class.primary_key} = #{quote_value(id)}",
-              "#{self.class.name} Update"
-            )
-          end
+        # structure occurrred since the object was loaded, the lft/rgt values could
+        # be out of date and corrupt the indexes.
+        # There is an open ticket for this in the Rails Core: http://dev.rubyonrails.org/ticket/6896
 
-          # exclude the lft/rgt columns from update statements
-          def special_attributes_with_quotes(include_primary_key = true) #:nodoc:
-            attributes.inject({}) do |quoted, (name, value)|
+        private
+          # override the sql preparation method to exclude the lft/rgt columns
+          # under the same conditions that the primary key column is excluded
+          def attributes_with_quotes(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys) #:nodoc:
+            left_and_right_column = [acts_as_nested_set_options[:left_column], acts_as_nested_set_options[:right_column]]
+            quoted = {}
+            connection = self.class.connection
+            attribute_names.each do |name|
               if column = column_for_attribute(name)
-                quoted[name] = quote_value(value, column) unless (!include_primary_key && column.primary) || [acts_as_nested_set_options[:left_column], acts_as_nested_set_options[:right_column]].include?(column.name)
+                quoted[name] = connection.quote(read_attribute(name), column) unless !include_primary_key && (column.primary || left_and_right_column.include?(column.name))
               end
-              quoted
             end
+            include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)
           end
 
           # i couldn't figure out how to call attributes_with_quotes without cutting and pasting this private method in.  :(
@@ -602,9 +1111,17 @@ module SymetrieCom
             self.class.connection.quote(value, column)
           end
 
+          # optionally use a transaction
+          def with_optional_transaction(bool, &block)
+            bool ? transaction { yield } : yield
+          end
+
+          # as a seperate method to facilitate custom implementations based on :dependent option
+          def remove_descendant(descendant)
+            descendant.destroy
+          end
+
       end
     end
   end
 end
-
-
diff --git a/src/vendor/plugins/betternestedset/test/abstract_unit.rb b/src/vendor/plugins/betternestedset/test/abstract_unit.rb
index 3d7bd87..a8692da 100644
--- a/src/vendor/plugins/betternestedset/test/abstract_unit.rb
+++ b/src/vendor/plugins/betternestedset/test/abstract_unit.rb
@@ -1,12 +1,6 @@
 ENV['RAILS_ENV'] = 'test'
 require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
 require 'test_help'
-require File.dirname(__FILE__) + '/../init'
-
-
-ActiveRecord::Base.class_eval do
-  include SymetrieCom::Acts::NestedSet
-end
 
 config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
 ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/debug.log')
diff --git a/src/vendor/plugins/betternestedset/test/acts_as_nested_set_test.rb b/src/vendor/plugins/betternestedset/test/acts_as_nested_set_test.rb
index 144a83d..0cd1dfb 100644
--- a/src/vendor/plugins/betternestedset/test/acts_as_nested_set_test.rb
+++ b/src/vendor/plugins/betternestedset/test/acts_as_nested_set_test.rb
@@ -4,140 +4,154 @@ require 'pp'
 
 class MixinNestedSetTest < Test::Unit::TestCase
   fixtures :mixins
-  
+
+  def setup
+    # force so other tests besides test_destroy_dependent aren't affected
+    NestedSetWithStringScope.acts_as_nested_set_options[:dependent] = :delete_all
+  end
+
   ##########################################
   # HIGH LEVEL TESTS
   ##########################################
   def test_mixing_in_methods
     ns = NestedSet.new
     assert(ns.respond_to?(:all_children)) # test a random method
-    
+
     check_method_mixins(ns)
-    check_deprecated_method_mixins(ns) 
+    check_deprecated_method_mixins(ns)
     check_class_method_mixins(NestedSet)
   end
-  
+
   def check_method_mixins(obj)
-    [:<=>, :all_children, :all_children_count, :ancestors, :before_create, :before_destroy, :check_full_tree, 
-    :check_subtree, :children, :full_set, :leaves, :leaves_count, :left_col_name, :level, :move_to_child_of, 
-    :move_to_left_of, :move_to_right_of, :parent, :parent_col_name, :renumber_full_tree, :right_col_name, 
+    [:<=>, :all_children, :all_children_count, :ancestors, :before_create, :before_destroy, :check_full_tree,
+    :check_subtree, :children, :children_count, :full_set, :leaves, :leaves_count, :left_col_name, :level, :move_to_child_of,
+    :move_to_left_of, :move_to_right_of, :parent, :parent_col_name, :renumber_full_tree, :right_col_name,
     :root, :roots, :self_and_ancestors, :self_and_siblings, :siblings].each { |symbol| assert(obj.respond_to?(symbol)) }
   end
-  
+
   def check_deprecated_method_mixins(obj)
     [:add_child, :direct_children, :parent_column, :root?, :child?, :unknown?].each { |symbol| assert(obj.respond_to?(symbol)) }
   end
-  
+
   def check_class_method_mixins(klass)
     [:root, :roots, :check_all, :renumber_all].each { |symbol| assert(klass.respond_to?(symbol)) }
   end
-  
+
   def test_string_scope
     ns = NestedSet.new
-    assert_equal("root_id IS NULL", ns.scope_condition)
-    
+    assert_equal("mixins.root_id IS NULL", ns.scope_condition)
+
     ns = NestedSetWithStringScope.new
     ns.root_id = 1
-    assert_equal("root_id = 1", ns.scope_condition)
+    assert_equal("mixins.root_id = 1", ns.scope_condition)
     ns.root_id = 42
-    assert_equal("root_id = 42", ns.scope_condition)
+    assert_equal("mixins.root_id = 42", ns.scope_condition)
     check_method_mixins ns
   end
-  
+
+  def test_without_scope_condition
+    ns = NestedSet.new
+    assert_equal("mixins.root_id IS NULL", ns.scope_condition)
+    NestedSet.without_scope_condition do
+      assert_equal("(1 = 1)", ns.scope_condition)
+    end
+    assert_equal("mixins.root_id IS NULL", ns.scope_condition)
+  end
+
   def test_symbol_scope
     ns = NestedSetWithSymbolScope.new
     ns.root_id = 1
-    assert_equal("root_id = 1", ns.scope_condition)
+    assert_equal("mixins.root_id = 1", ns.scope_condition)
     ns.root_id = 42
-    assert_equal("root_id = 42", ns.scope_condition)
+    assert_equal("mixins.root_id = 42", ns.scope_condition)
     check_method_mixins ns
   end
-  
+
   def test_protected_attributes
     ns = NestedSet.new(:parent_id => 2, :lft => 3, :rgt => 2)
     [:parent_id, :lft, :rgt].each {|symbol| assert_equal(nil, ns.send(symbol))}
   end
-    
+
   def test_really_protected_attributes
     ns = NestedSet.new
     assert_raise(ActiveRecord::ActiveRecordError) {ns.parent_id = 1}
     assert_raise(ActiveRecord::ActiveRecordError) {ns.lft = 1}
     assert_raise(ActiveRecord::ActiveRecordError) {ns.rgt = 1}
   end
-  
+
   ##########################################
   # CLASS METHOD TESTS
   ##########################################
   def test_class_root
     NestedSetWithStringScope.roots.each {|r| r.destroy unless r.id == 4001}
-    assert_equal(NestedSetWithStringScope.find(4001), NestedSetWithStringScope.root)
+    assert_equal([NestedSetWithStringScope.find(4001)], NestedSetWithStringScope.roots)
     NestedSetWithStringScope.find(4001).destroy
     assert_equal(nil, NestedSetWithStringScope.root)
     ns = NestedSetWithStringScope.create(:root_id => 2)
     assert_equal(ns, NestedSetWithStringScope.root)
   end
-  
+
   def test_class_root_again
     NestedSetWithStringScope.roots.each {|r| r.destroy unless r.id == 101}
     assert_equal(NestedSetWithStringScope.find(101), NestedSetWithStringScope.root)
   end
-  
+
   def test_class_roots
     assert_equal(2, NestedSetWithStringScope.roots.size)
     assert_equal(10, NestedSet.roots.size) # May change if STI behavior changes
   end
-  
+
   def test_check_all_1
     assert_nothing_raised {NestedSetWithStringScope.check_all}
     NestedSetWithStringScope.update_all("lft = 3", "id = 103")
     assert_raise(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_check_all_2
     NestedSetWithStringScope.update_all("lft = lft + 1", "lft > 11 AND root_id = 101")
     NestedSetWithStringScope.update_all("rgt = rgt + 1", "lft > 11 AND root_id = 101")
-    assert_raise(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.check_all} 
+    assert_raise(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_check_all_3
     NestedSetWithStringScope.update_all("lft = lft + 2", "lft > 11 AND root_id = 101")
     NestedSetWithStringScope.update_all("rgt = rgt + 2", "lft > 11 AND root_id = 101")
-    assert_raise(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.check_all} 
+    assert_raise(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_check_all_4
     ns = NestedSetWithStringScope.create(:root_id => 101) # virtual root
     assert_nothing_raised {NestedSetWithStringScope.check_all}
     NestedSetWithStringScope.update_all("rgt = rgt + 2, lft = lft + 2", "id = #{ns.id}") # create a gap between virtual roots
     assert_nothing_raised {ns.check_subtree}
-    assert_raise(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.check_all} 
+    assert_raise(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_renumber_all
     NestedSetWithStringScope.update_all("lft = NULL, rgt = NULL")
     assert_raise(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.check_all}
-    NestedSetWithStringScope.renumber_all    
+    NestedSetWithStringScope.renumber_all
     assert_nothing_raised(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.check_all}
     NestedSetWithStringScope.update_all("lft = 1, rgt = 2")
     assert_raise(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.check_all}
-    NestedSetWithStringScope.renumber_all    
+    NestedSetWithStringScope.renumber_all
     assert_nothing_raised(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_sql_for
     assert_equal("1 != 1", Category.sql_for([]))
     c = Category.new
     assert_equal("1 != 1", Category.sql_for(c))
     assert_equal("1 != 1", Category.sql_for([c]))
     c.save
-    assert_equal("((lft BETWEEN 1 AND 2))", Category.sql_for(c))
-    assert_equal("((lft BETWEEN 1 AND 2))", Category.sql_for([c]))
-    assert_equal("((lft BETWEEN 1 AND 20))", NestedSetWithStringScope.sql_for(101))
-    assert_equal("((lft BETWEEN 1 AND 20) OR (lft BETWEEN 4 AND 11))", NestedSetWithStringScope.sql_for([101, set2(3)]))
-    assert_equal("((lft BETWEEN 5 AND 6) OR (lft BETWEEN 7 AND 8) OR (lft BETWEEN 9 AND 10))", NestedSetWithStringScope.sql_for(set2(3).children))
-  end
-  
-  
+    assert_equal("((mixins.lft BETWEEN 1 AND 2))", Category.sql_for(c))
+    assert_equal("((mixins.lft BETWEEN 1 AND 2))", Category.sql_for([c]))
+    assert_equal("((mixins.lft BETWEEN 1 AND 20))", NestedSetWithStringScope.sql_for(101))
+    assert_equal("((mixins.lft BETWEEN 1 AND 20) OR (mixins.lft BETWEEN 4 AND 11))", NestedSetWithStringScope.sql_for([101, set2(3)]))
+    assert_equal("((mixins.lft BETWEEN 5 AND 6) OR (mixins.lft BETWEEN 7 AND 8) OR (mixins.lft BETWEEN 9 AND 10))", NestedSetWithStringScope.sql_for(set2(3).children))
+  end
+
+
   ##########################################
   # CALLBACK TESTS
   ##########################################
@@ -150,22 +164,22 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_equal(3, ns.lft)
     assert_equal(4, ns.rgt)
   end
-  
+
   # test pruning a branch. only works if we allow the deletion of nodes with children
   def test_destroy
     big_tree = NestedSetWithStringScope.find(4001)
-    
+
     # Make sure we have the right one
     assert_equal(3, big_tree.direct_children.length)
     assert_equal(10, big_tree.full_set.length)
-    
+
     NestedSetWithStringScope.find(4005).destroy
 
     big_tree = NestedSetWithStringScope.find(4001)
-    
+
     assert_equal(7, big_tree.full_set.length)
     assert_equal(2, big_tree.direct_children.length)
-    
+
     assert_equal(1, NestedSetWithStringScope.find(4001).lft)
     assert_equal(2, NestedSetWithStringScope.find(4002).lft)
     assert_equal(3, NestedSetWithStringScope.find(4003).lft)
@@ -181,10 +195,10 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_equal(13, NestedSetWithStringScope.find(4008).rgt)
     assert_equal(14, NestedSetWithStringScope.find(4001).rgt)
   end
-  
+
   def test_destroy_2
     assert_nothing_raised {set2(1).check_subtree}
-    assert set2(10).destroy    
+    assert set2(10).destroy
     assert_nothing_raised {set2(1).reload.check_subtree}
     assert set2(9).children.empty?
     assert set2(9).destroy
@@ -192,7 +206,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_nothing_raised {set2(1).reload.check_subtree}
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_destroy_3
     assert set2(3).destroy
     assert_equal(2, set2(1).children.size)
@@ -203,35 +217,67 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_equal(12, set2(1).rgt)
     assert_nothing_raised {set2(1).check_subtree}
   end
-  
+
   def test_destroy_root
     NestedSetWithStringScope.find(4001).destroy
     assert_equal(0, NestedSetWithStringScope.count(:conditions => "root_id = 42"))
-  end            
-  
+  end
+
+  def test_destroy_dependent
+    NestedSetWithStringScope.acts_as_nested_set_options[:dependent] = :destroy
+
+    big_tree = NestedSetWithStringScope.find(4001)
+
+    # Make sure we have the right one
+    assert_equal(3, big_tree.direct_children.length)
+    assert_equal(10, big_tree.full_set.length)
+
+    NestedSetWithStringScope.find(4005).destroy
+
+    big_tree = NestedSetWithStringScope.find(4001)
+
+    assert_equal(7, big_tree.full_set.length)
+    assert_equal(2, big_tree.direct_children.length)
+
+    assert_equal(1, NestedSetWithStringScope.find(4001).lft)
+    assert_equal(2, NestedSetWithStringScope.find(4002).lft)
+    assert_equal(3, NestedSetWithStringScope.find(4003).lft)
+    assert_equal(4, NestedSetWithStringScope.find(4003).rgt)
+    assert_equal(5, NestedSetWithStringScope.find(4004).lft)
+    assert_equal(6, NestedSetWithStringScope.find(4004).rgt)
+    assert_equal(7, NestedSetWithStringScope.find(4002).rgt)
+    assert_equal(8, NestedSetWithStringScope.find(4008).lft)
+    assert_equal(9, NestedSetWithStringScope.find(4009).lft)
+    assert_equal(10, NestedSetWithStringScope.find(4009).rgt)
+    assert_equal(11, NestedSetWithStringScope.find(4010).lft)
+    assert_equal(12, NestedSetWithStringScope.find(4010).rgt)
+    assert_equal(13, NestedSetWithStringScope.find(4008).rgt)
+    assert_equal(14, NestedSetWithStringScope.find(4001).rgt)
+  end
+
   ##########################################
   # QUERY METHOD TESTS
   ##########################################
   def set(id) NestedSet.find(3000 + id) end # helper method
-  
+
   def set2(id) NestedSetWithStringScope.find(100 + id) end # helper method
-  
+
   def test_root?
     assert NestedSetWithStringScope.find(4001).root?
     assert !NestedSetWithStringScope.find(4002).root?
   end
-  
+
   def test_child?
     assert !NestedSetWithStringScope.find(4001).child?
-    assert NestedSetWithStringScope.find(4002).child?    
+    assert NestedSetWithStringScope.find(4002).child?
   end
-  
+
   # Deprecated, delete this test when we nuke the method
   def test_unknown?
     assert !NestedSetWithStringScope.find(4001).unknown?
-    assert !NestedSetWithStringScope.find(4002).unknown?        
+    assert !NestedSetWithStringScope.find(4002).unknown?
   end
-  
+
   # Test the <=> method implicitly
   def test_comparison
     ar = NestedSetWithStringScope.find(:all, :conditions => "root_id = 42", :order => "lft")
@@ -239,7 +285,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_not_equal(ar, ar2)
     assert_equal(ar, ar2.sort)
   end
-  
+
   def test_root
     assert_equal(NestedSetWithStringScope.find(4001), NestedSetWithStringScope.find(4007).root)
     assert_equal(set2(1), set2(8).root)
@@ -249,13 +295,13 @@ class MixinNestedSetTest < Test::Unit::TestCase
     c3.move_to_child_of(c2)
     assert_equal(c2, c3.root)
   end
-  
+
   def test_roots
     assert_equal([set2(1)], set2(8).roots)
     assert_equal([set2(1)], set2(1).roots)
     assert_equal(NestedSet.find(:all, :conditions => "id > 3000 AND id < 4000").size, set(1).roots.size)
   end
-  
+
   def test_parent
     ns = NestedSetWithStringScope.create(:root_id => 45)
     assert_equal(nil, ns.parent)
@@ -264,40 +310,80 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_equal(set2(1), set2(2).parent)
     assert_equal(set2(3), set2(7).parent)
   end
-  
+
   def test_ancestors
     assert_equal([], set2(1).ancestors)
     assert_equal([set2(1), set2(4), set2(9)], set2(10).ancestors)
   end
-  
+
   def test_self_and_ancestors
     assert_equal([set2(1)], set2(1).self_and_ancestors)
     assert_equal([set2(1), set2(4), set2(8)], set2(8).self_and_ancestors)
     assert_equal([set2(1), set2(4), set2(9), set2(10)], set2(10).self_and_ancestors)
   end
-  
+
   def test_siblings
     assert_equal([], set2(1).siblings)
     assert_equal([set2(2), set2(4)], set2(3).siblings)
   end
-  
+
+  def test_first_sibling
+    assert set2(2).first_sibling?
+    assert_equal(set2(2), set2(2).first_sibling)
+    assert_equal(set2(2), set2(3).first_sibling)
+    assert_equal(set2(2), set2(4).first_sibling)
+  end
+
+  def test_last_sibling
+    assert set2(4).last_sibling?
+    assert_equal(set2(4), set2(2).last_sibling)
+    assert_equal(set2(4), set2(3).last_sibling)
+    assert_equal(set2(4), set2(4).last_sibling)
+  end
+
+  def test_previous_siblings
+    assert_equal([], set2(2).previous_siblings)
+    assert_equal([set2(2)], set2(3).previous_siblings)
+    assert_equal([set2(3), set2(2)], set2(4).previous_siblings)
+  end
+
+  def test_previous_sibling
+    assert_equal(nil, set2(2).previous_sibling)
+    assert_equal(set2(2), set2(3).previous_sibling)
+    assert_equal(set2(3), set2(4).previous_sibling)
+    assert_equal([set2(3), set2(2)], set2(4).previous_sibling(2))
+  end
+
+  def test_next_siblings
+    assert_equal([], set2(4).next_siblings)
+    assert_equal([set2(4)], set2(3).next_siblings)
+    assert_equal([set2(3), set2(4)], set2(2).next_siblings)
+  end
+
+  def test_next_sibling
+    assert_equal(nil, set2(4).next_sibling)
+    assert_equal(set2(4), set2(3).next_sibling)
+    assert_equal(set2(3), set2(2).next_sibling)
+    assert_equal([set2(3), set2(4)], set2(2).next_sibling(2))
+  end
+
   def test_self_and_siblings
     assert_equal([set2(1)], set2(1).self_and_siblings)
-    assert_equal([set2(2), set2(3), set2(4)], set2(3).self_and_siblings)    
+    assert_equal([set2(2), set2(3), set2(4)], set2(3).self_and_siblings)
   end
-  
+
   def test_level
     assert_equal(0, set2(1).level)
     assert_equal(1, set2(3).level)
     assert_equal(3, set2(10).level)
   end
-  
+
   def test_all_children_count
     assert_equal(0, set2(10).all_children_count)
     assert_equal(1, set2(3).level)
-    assert_equal(3, set2(10).level)    
+    assert_equal(3, set2(10).level)
   end
-  
+
   def test_full_set
     assert_equal(NestedSetWithStringScope.find(:all, :conditions => "root_id = 101", :order => "lft"), set2(1).full_set)
     new_ns = NestedSetWithStringScope.new(:root_id => 101)
@@ -311,9 +397,9 @@ class MixinNestedSetTest < Test::Unit::TestCase
     ns = NestedSetWithStringScope.create(:root_id => 234)
     assert_equal([], ns.full_set(:exclude => ns))
     assert_equal([set2(4), set2(8), set2(9)], set2(4).full_set(:exclude => set2(10)))
-    assert_equal([set2(4), set2(8)], set2(4).full_set(:exclude => set2(9))) 
+    assert_equal([set2(4), set2(8)], set2(4).full_set(:exclude => set2(9)))
   end
-    
+
   def test_all_children
     assert_equal(NestedSetWithStringScope.find(:all, :conditions => "root_id = 101 AND id > 101", :order => "lft"), set2(1).all_children)
     assert_equal([], NestedSetWithStringScope.new(:root_id => 101).all_children)
@@ -323,27 +409,311 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_equal([set2(2), set2(4), set2(8)], set2(1).all_children(:exclude => [set2(9), 103]))
     assert_equal([set2(2), set2(4), set2(8)], set2(1).all_children(:exclude => [set2(9), 103, 106]))
   end
-  
+
   def test_children
-    assert_equal([], set2(10).children) 
-    assert_equal([], set(1).children) 
-    assert_equal([set2(2), set2(3), set2(4)], set2(1).children) 
-    assert_equal([set2(5), set2(6), set2(7)], set2(3).children) 
-    assert_equal([NestedSetWithStringScope.find(4006), NestedSetWithStringScope.find(4007)], NestedSetWithStringScope.find(4005).children) 
+    assert_equal([], set2(10).children)
+    assert_equal([], set(1).children)
+    assert_equal([set2(2), set2(3), set2(4)], set2(1).children)
+    assert_equal([set2(5), set2(6), set2(7)], set2(3).children)
+    assert_equal([NestedSetWithStringScope.find(4006), NestedSetWithStringScope.find(4007)], NestedSetWithStringScope.find(4005).children)
+  end
+
+  def test_children_count
+    assert_equal(0, set2(10).children_count)
+    assert_equal(3, set2(1).children_count)
   end
-  
+
   def test_leaves
     assert_equal([set2(10)], set2(9).leaves)
     assert_equal([set2(10)], set2(10).leaves)
     assert_equal([set2(2), set2(5), set2(6), set2(7), set2(8), set2(10)], set2(1).leaves)
   end
-  
+
   def test_leaves_count
     assert_equal(1, set2(10).leaves_count)
     assert_equal(1, set2(9).leaves_count)
     assert_equal(6, set2(1).leaves_count)
   end
-  
+
+  ##########################################
+  # CASTING RESULT TESTS
+  ##########################################
+
+  def test_recurse_result_set
+    result = []
+    NestedSetWithStringScope.recurse_result_set(set2(1).full_set) do |node, level|
+      result << [level, node.id]
+    end
+    expected = [[0, 101], [1, 102], [1, 103], [2, 105], [2, 106], [2, 107], [1, 104], [2, 108], [2, 109], [3, 110]]
+    assert_equal expected, result
+  end
+
+  def test_disjointed_result_set
+    result_set = set2(1).full_set(:conditions => { :type => 'NestedSetWithStringScope' })
+    result = []
+    NestedSetWithStringScope.recurse_result_set(result_set) do |node, level|
+      result << [level, node.id]
+    end
+    expected = [[0, 102], [0, 104], [0, 105], [0, 106], [0, 107], [0, 110]]
+    assert_equal expected, result
+  end
+
+  def test_result_to_array
+    result = NestedSetWithStringScope.result_to_array(set2(1).full_set) do |node, level|
+      { :id => node.id, :level => level }
+    end
+    expected = [{:level=>0, :children=>[{:level=>1, :id=>102}, {:level=>1,
+      :children=>[{:level=>2, :id=>105}, {:level=>2, :id=>106}, {:level=>2, :id=>107}], :id=>103}, {:level=>1,
+      :children=>[{:level=>2, :id=>108}, {:level=>2, :children=>[{:level=>3, :id=>110}], :id=>109}], :id=>104}], :id=>101}]
+    assert_equal expected, result
+  end
+
+  def test_result_to_array_with_method_calls
+    result = NestedSetWithStringScope.result_to_array(set2(1).full_set, :only => [:id], :methods => [:children_count])
+    expected = [{:children=>[{:children_count=>0, :id=>102}, {:children=>[{:children_count=>0, :id=>105}, {:children_count=>0, :id=>106},
+      {:children_count=>0, :id=>107}], :children_count=>3, :id=>103}, {:children=>[{:children_count=>0, :id=>108}, {:children=>[{:children_count=>0, :id=>110}],
+      :children_count=>1, :id=>109}], :children_count=>2, :id=>104}], :children_count=>3, :id=>101}]
+    assert_equal expected, result
+  end
+
+  def test_disjointed_result_to_array
+    result_set = set2(1).full_set(:conditions => { :type => 'NestedSetWithStringScope' })
+    result = NestedSetWithStringScope.result_to_array(result_set) do |node, level|
+      { :id => node.id, :level => level }
+    end
+    expected = [{:level=>0, :id=>102}, {:level=>0, :id=>104}, {:level=>0, :id=>105}, {:level=>0, :id=>106}, {:level=>0, :id=>107}, {:level=>0, :id=>110}]
+    assert_equal expected, result
+  end
+
+  def test_result_to_array_flat
+    result = NestedSetWithStringScope.result_to_array(set2(1).full_set, :nested => false) do |node, level|
+      { :id => node.id, :level => level }
+    end
+    expected = [{:level=>0, :id=>101}, {:level=>0, :id=>103}, {:level=>0, :id=>106}, {:level=>0, :id=>104}, {:level=>0, :id=>109},
+      {:level=>0, :id=>102}, {:level=>0, :id=>105}, {:level=>0, :id=>107}, {:level=>0, :id=>108}, {:level=>0, :id=>110}]
+    assert_equal expected, result
+  end
+
+  def test_result_to_xml
+    result = NestedSetWithStringScope.result_to_xml(set2(3).full_set, :record => 'node', :dasherize => false, :only => [:id]) do |options, subnode|
+       options[:builder].tag!('type', subnode[:type])
+    end
+    expected = '<?xml version="1.0" encoding="UTF-8"?>
+<nodes>
+  <node>
+    <id type="integer">103</id>
+    <type>NS2</type>
+    <children>
+      <node>
+        <id type="integer">105</id>
+        <type>NestedSetWithStringScope</type>
+        <children>
+        </children>
+      </node>
+      <node>
+        <id type="integer">106</id>
+        <type>NestedSetWithStringScope</type>
+        <children>
+        </children>
+      </node>
+      <node>
+        <id type="integer">107</id>
+        <type>NestedSetWithStringScope</type>
+        <children>
+        </children>
+      </node>
+    </children>
+  </node>
+</nodes>'
+    assert_equal expected, result.strip
+  end
+
+  def test_disjointed_result_to_xml
+    result_set = set2(1).full_set(:conditions => ['type IN(?)', ['NestedSetWithStringScope', 'NS2']])
+    result = NestedSetWithStringScope.result_to_xml(result_set, :only => [:id])
+    # note how nesting is preserved where possible; this is not always what you want though,
+    # so you can force a flattened set with :nested => false instead (see below)
+    expected = '<?xml version="1.0" encoding="UTF-8"?>
+<nodes>
+  <nested-set-with-string-scope>
+    <id type="integer">102</id>
+    <children>
+    </children>
+  </nested-set-with-string-scope>
+  <ns2>
+    <id type="integer">103</id>
+    <children>
+      <nested-set-with-string-scope>
+        <id type="integer">105</id>
+        <children>
+        </children>
+      </nested-set-with-string-scope>
+      <nested-set-with-string-scope>
+        <id type="integer">106</id>
+        <children>
+        </children>
+      </nested-set-with-string-scope>
+      <nested-set-with-string-scope>
+        <id type="integer">107</id>
+        <children>
+        </children>
+      </nested-set-with-string-scope>
+    </children>
+  </ns2>
+  <nested-set-with-string-scope>
+    <id type="integer">104</id>
+    <children>
+      <ns2>
+        <id type="integer">108</id>
+        <children>
+        </children>
+      </ns2>
+    </children>
+  </nested-set-with-string-scope>
+</nodes>'
+    assert_equal expected, result.strip
+  end
+
+  def test_result_to_xml_flat
+    result = NestedSetWithStringScope.result_to_xml(set2(3).full_set, :record => 'node', :dasherize => false, :only => [:id], :nested => false)
+    expected = '<?xml version="1.0" encoding="UTF-8"?>
+<nodes>
+  <node>
+    <id type="integer">103</id>
+  </node>
+  <node>
+    <id type="integer">105</id>
+  </node>
+  <node>
+    <id type="integer">106</id>
+  </node>
+  <node>
+    <id type="integer">107</id>
+  </node>
+</nodes>'
+    assert_equal expected, result.strip
+  end
+
+  def test_result_to_attribute_based_xml
+    result = NestedSetWithStringScope.result_to_attributes_xml(set2(1).full_set, :record => 'node', :only => [:id, :parent_id])
+    expected = '<?xml version="1.0" encoding="UTF-8"?>
+<node id="101" parent_id="0">
+  <node id="102" parent_id="101"/>
+  <node id="103" parent_id="101">
+    <node id="105" parent_id="103"/>
+    <node id="106" parent_id="103"/>
+    <node id="107" parent_id="103"/>
+  </node>
+  <node id="104" parent_id="101">
+    <node id="108" parent_id="104"/>
+    <node id="109" parent_id="104">
+      <node id="110" parent_id="109"/>
+    </node>
+  </node>
+</node>'
+    assert_equal expected, result.strip
+  end
+
+  def test_result_to_attribute_based_xml_flat
+    result = NestedSetWithStringScope.result_to_attributes_xml(set2(1).full_set, :only => [:id], :nested => false, :skip_instruct => true)
+    expected = '<ns1 id="101"/>
+<ns2 id="103"/>
+<nested_set_with_string_scope id="106"/>
+<nested_set_with_string_scope id="104"/>
+<ns1 id="109"/>
+<nested_set_with_string_scope id="102"/>
+<nested_set_with_string_scope id="105"/>
+<nested_set_with_string_scope id="107"/>
+<ns2 id="108"/>
+<nested_set_with_string_scope id="110"/>'
+    assert_equal expected, result.strip
+  end
+
+  ##########################################
+  # WITH_SCOPE QUERY TESTS
+  ##########################################
+
+  def test_filtered_full_set
+    result_set = set2(1).full_set(:conditions => { :type => 'NestedSetWithStringScope' })
+    assert_equal [102, 105, 106, 107, 104, 110], result_set.map(&:id)
+  end
+
+  def test_reverse_result_set
+    result_set = set2(1).full_set(:reverse => true)
+    assert_equal [101, 104, 109, 110, 108, 103, 107, 106, 105, 102], result_set.map(&:id)
+    # NestedSetWithStringScope.recurse_result_set(result_set) { |node, level| puts "#{'--' * level}#{node.id}" }
+  end
+
+  def test_reordered_full_set
+    result_set = set2(1).full_set(:order => 'id DESC')
+    assert_equal [110, 109, 108, 107, 106, 105, 104, 103, 102, 101], result_set.map(&:id)
+  end
+
+  def test_filtered_siblings
+    node = set2(2)
+    result_set = node.siblings(:conditions => { :type => node[:type] })
+    assert_equal [104], result_set.map(&:id)
+  end
+
+  def test_include_option_with_full_set
+    result_set = set2(3).full_set(:include => :parent_node)
+    assert_equal [[103, 101], [105, 103], [106, 103], [107, 103]], result_set.map { |n| [n.id, n.parent_node.id] }
+  end
+
+  ##########################################
+  # FIND UNTIL/THROUGH METHOD TESTS
+  ##########################################
+
+  def test_ancestors_and_self_through
+    result = set2(10).ancestors_and_self_through(set2(4))
+    assert_equal [104, 109, 110], result.map(&:id)
+    result = set2(10).ancestors_through(set2(4))
+    assert_equal [104, 109], result.map(&:id)
+  end
+
+  def test_full_set_through
+    result = set2(4).full_set_through(set2(10))
+    assert_equal [104, 108, 109, 110], result.map(&:id)
+  end
+
+  def test_all_children_through
+    result = set2(4).all_children_through(set2(10))
+    assert_equal [108, 109, 110], result.map(&:id)
+  end
+
+  def test_siblings_through
+    result = set2(5).self_and_siblings_through(set2(7))
+    assert_equal [105, 106, 107], result.map(&:id)
+    result = set2(7).siblings_through(set2(5))
+    assert_equal [105, 106], result.map(&:id)
+  end
+
+  ##########################################
+  # FIND CHILD BY ID METHOD TESTS
+  ##########################################
+
+  def test_child_by_id
+    assert_equal set2(6), set2(3).child_by_id(set2(6).id)
+    assert_nil set2(3).child_by_id(set2(8).id)
+  end
+
+  def test_child_of
+    assert  set2(6).child_of?(set2(3))
+    assert !set2(8).child_of?(set2(3))
+    assert set2(6).child_of?(set2(3), :conditions => '1 = 1')
+  end
+
+  def test_direct_child_by_id
+    assert_equal set2(9), set2(4).direct_child_by_id(set2(9).id)
+    assert_nil set2(4).direct_child_by_id(set2(10).id)
+  end
+
+  def test_direct_child_of
+    assert set2(9).direct_child_of?(set2(4))
+    assert !set2(10).direct_child_of?(set2(4))
+    assert set2(9).direct_child_of?(set2(4), :conditions => '1 = 1')
+  end
+
   ##########################################
   # INDEX-CHECKING METHOD TESTS
   ##########################################
@@ -362,7 +732,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_raise(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.find(4001).reload.check_subtree}
     # this method receives lots of additional testing through tests of check_full_tree and check_all
   end
-  
+
   def test_check_full_tree
     assert_nothing_raised {set2(1).check_full_tree}
     assert_nothing_raised {NestedSetWithStringScope.find(4006).check_full_tree}
@@ -374,31 +744,31 @@ class MixinNestedSetTest < Test::Unit::TestCase
     NestedSetWithStringScope.update_all("lft = lft + 1", "id > 101")
     assert_raise(ActiveRecord::ActiveRecordError) {set2(4).check_full_tree}
   end
-  
+
   def test_check_full_tree_orphan
     assert_raise(ActiveRecord::RecordNotFound) {NestedSetWithStringScope.find(99)} # make sure ID 99 doesn't exist
     ns = NestedSetWithStringScope.create(:root_id => 101)
     NestedSetWithStringScope.update_all("parent_id = 99", "id = #{ns.id}")
     assert_raise(ActiveRecord::ActiveRecordError) {set2(3).check_full_tree}
   end
-  
+
   def test_check_full_tree_endless_loop
     ns = NestedSetWithStringScope.create(:root_id => 101)
     NestedSetWithStringScope.update_all("parent_id = #{ns.id}", "id = #{ns.id}")
     assert_raise(ActiveRecord::ActiveRecordError) {set2(6).check_full_tree}
   end
-  
+
   def test_check_full_tree_virtual_roots
-    a = Category.create    
+    a = Category.create
     b = Category.create
-    
+
     assert_nothing_raised {a.check_full_tree}
     Category.update_all("rgt = rgt + 2, lft = lft + 2", "id = #{b.id}") # create a gap between virtual roots
     assert_raise(ActiveRecord::ActiveRecordError) {a.check_full_tree}
   end
-  
+
   # see also the tests of check_all under 'class method tests'
-    
+
   ##########################################
   # INDEX-ALTERING (UPDATE) METHOD TESTS
   ##########################################
@@ -416,7 +786,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_equal(2, set2(3).lft)
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_move_to_right_of # this method undergoes additional testing elsewhere
     set2(3).move_to_right_of(set2(2)) # should cause no change
     set2(4).move_to_right_of(set2(3)) # should cause no change
@@ -432,28 +802,28 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_equal(4, set2(4).lft)
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_adding_children
     assert(set(1).unknown?)
     assert(set(2).unknown?)
     set(1).add_child set(2)
-    
+
     # Did we maintain adding the parent_ids?
     assert(set(1).root?)
     assert(set(2).child?)
     assert(set(2).parent_id == set(1).id)
-    
+
     # Check boundaries
     assert_equal(set(1).lft, 1)
     assert_equal(set(2).lft, 2)
     assert_equal(set(2).rgt, 3)
     assert_equal(set(1).rgt, 4)
-    
+
     # Check children cound
     assert_equal(set(1).all_children_count, 1)
-    
+
     set(1).add_child set(3)
-    
+
     #check boundries
     assert_equal(set(1).lft, 1)
     assert_equal(set(2).lft, 2)
@@ -461,7 +831,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_equal(set(3).lft, 4)
     assert_equal(set(3).rgt, 5)
     assert_equal(set(1).rgt, 6)
-    
+
     # How is the count looking?
     assert_equal(set(1).all_children_count, 2)
 
@@ -476,26 +846,26 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_equal(set(3).lft, 6)
     assert_equal(set(3).rgt, 7)
     assert_equal(set(1).rgt, 8)
-    
+
     # Children count
     assert_equal(set(1).all_children_count, 3)
     assert_equal(set(2).all_children_count, 1)
     assert_equal(set(3).all_children_count, 0)
     assert_equal(set(4).all_children_count, 0)
-    
+
     set(2).add_child set(5)
     set(4).add_child set(6)
-    
+
     assert_equal(set(2).all_children_count, 3)
 
     # Children accessors
     assert_equal(set(1).full_set.length, 6)
     assert_equal(set(2).full_set.length, 4)
     assert_equal(set(4).full_set.length, 2)
-    
+
     assert_equal(set(1).all_children.length, 5)
     assert_equal(set(6).all_children.length, 0)
-    
+
     assert_equal(set(1).direct_children.length, 2)
 
     assert_nothing_raised {NestedSetWithStringScope.check_all}
@@ -505,39 +875,39 @@ class MixinNestedSetTest < Test::Unit::TestCase
     mixins(:set_1).add_child(mixins(:set_2))
     assert_equal(1, mixins(:set_1).direct_children.length)
 
-    mixins(:set_2).add_child(mixins(:set_3))                      
-    assert_equal(1, mixins(:set_1).direct_children.length)     
-    
+    mixins(:set_2).add_child(mixins(:set_3))
+    assert_equal(1, mixins(:set_1).direct_children.length)
+
     # Local cache is now out of date!
     # Problem: the update_alls update all objects up the tree
     mixins(:set_1).reload
-    assert_equal(2, mixins(:set_1).all_children.length)              
-    
+    assert_equal(2, mixins(:set_1).all_children.length)
+
     assert_equal(1, mixins(:set_1).lft)
     assert_equal(2, mixins(:set_2).lft)
     assert_equal(3, mixins(:set_3).lft)
     assert_equal(4, mixins(:set_3).rgt)
     assert_equal(5, mixins(:set_2).rgt)
-    assert_equal(6, mixins(:set_1).rgt)  
+    assert_equal(6, mixins(:set_1).rgt)
     assert(mixins(:set_1).root?)
-                  
+
     begin
       mixins(:set_4).add_child(mixins(:set_1))
       fail
     rescue
     end
-    
+
     assert_equal(2, mixins(:set_1).all_children.length)
     mixins(:set_1).add_child mixins(:set_4)
     assert_equal(3, mixins(:set_1).all_children.length)
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_move_to_child_of_1
     bill = NestedSetWithStringScope.new(:root_id => 101, :pos => 2)
-    assert_raise(ActiveRecord::ActiveRecordError) { bill.move_to_child_of(set2(1)) }    
-    assert_raise(ActiveRecord::ActiveRecordError) { set2(1).move_to_child_of(set2(1)) }    
-    assert_raise(ActiveRecord::ActiveRecordError) { set2(4).move_to_child_of(set2(9)) }    
+    assert_raise(ActiveRecord::ActiveRecordError) { bill.move_to_child_of(set2(1)) }
+    assert_raise(ActiveRecord::ActiveRecordError) { set2(1).move_to_child_of(set2(1)) }
+    assert_raise(ActiveRecord::ActiveRecordError) { set2(4).move_to_child_of(set2(9)) }
     assert bill.save
     assert_nothing_raised {set2(1).reload.check_subtree}
     assert bill.move_to_left_of(set2(3))
@@ -554,7 +924,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_equal(18, set2(9).lft) # to the right of existing children?
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_move_to_child_of_2
     bill = NestedSetWithStringScope.new(:root_id => 101)
     assert_nothing_raised {set2(1).check_subtree}
@@ -573,7 +943,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_nothing_raised {set2(1).reload.check_subtree}
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_move_to_child_of_3
     bill = NestedSetWithStringScope.new(:root_id => 101)
     assert bill.save
@@ -582,7 +952,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_nothing_raised {set2(1).reload.check_subtree}
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_move_1
     set2(4).move_to_child_of(set2(3))
     assert_equal(set2(3), set2(4).reload.parent)
@@ -593,7 +963,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_nothing_raised {set2(1).reload.check_subtree}
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_move_2
     initial = set2(1).full_set
     assert_raise(ActiveRecord::ActiveRecordError) { set2(3).move_to_child_of(set2(6)) } # can't set a current child as the parent-- creates a loop
@@ -601,7 +971,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     set2(2).move_to_child_of(set2(5))
     set2(4).move_to_child_of(set2(2))
     set2(10).move_to_right_of(set2(3))
-    
+
     assert_equal 105, set2(2).parent_id
     assert_equal 102, set2(4).parent_id
     assert_equal 101, set2(10).parent_id
@@ -615,7 +985,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     set2(4).move_to_right_of(set2(3))
     set2(10).move_to_child_of(set2(9))
     set2(2).move_to_left_of(set2(3))
-    
+
     # now everything should be back where it started-- check against initial
     final = set2(1).reload.full_set
     assert_equal(initial, final)
@@ -626,13 +996,138 @@ class MixinNestedSetTest < Test::Unit::TestCase
     end
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_scope_enforcement # prevent moves between trees
     assert_raise(ActiveRecord::ActiveRecordError) { set(3).move_to_child_of(set2(6)) }
     ns = NestedSetWithStringScope.create(:root_id => 214)
     assert_raise(ActiveRecord::ActiveRecordError) { ns.move_to_child_of(set2(1)) }
   end
-  
+
+  ##########################################
+  # ACTS_AS_LIST-LIKE BEHAVIOUR TESTS
+  ##########################################
+
+  def test_swap
+    set2(5).swap(set2(7))
+    assert_equal [107, 106, 105], set2(3).children.map(&:id)
+    assert_nothing_raised {set2(3).check_full_tree}
+    assert_raise(ActiveRecord::ActiveRecordError) { set2(3).swap(set2(10)) } # isn't a sibling...
+  end
+
+  def test_insert_at
+    child = NestedSetWithStringScope.create(:root_id => 101)
+    child.insert_at(set2(3), :last)
+    assert_equal child, set2(3).children.last
+
+    child = NestedSetWithStringScope.create(:root_id => 101)
+    child.insert_at(set2(3), :first)
+    assert_equal child, set2(3).children.first
+
+    child = NestedSetWithStringScope.create(:root_id => 101)
+    child.insert_at(set2(3), 2)
+    assert_equal child, set2(3).children[2]
+
+    child = NestedSetWithStringScope.create(:root_id => 101)
+    child.insert_at(set2(3), 1000)
+    assert_equal child, set2(3).children.last
+
+    child = NestedSetWithStringScope.create(:root_id => 101)
+    child.insert_at(set2(3), 1)
+    assert_equal child, set2(3).children[1]
+  end
+
+  def test_move_higher
+    set2(7).move_higher
+    assert_equal [105, 107, 106], set2(3).children.map(&:id)
+    set2(7).move_higher
+    assert_equal [107, 105, 106], set2(3).children.map(&:id)
+    set2(7).move_higher
+    assert_equal [107, 105, 106], set2(3).children.map(&:id)
+  end
+
+  def test_move_lower
+    set2(5).move_lower
+    assert_equal [106, 105, 107], set2(3).children.map(&:id)
+    set2(5).move_lower
+    assert_equal [106, 107, 105], set2(3).children.map(&:id)
+    set2(5).move_lower
+    assert_equal [106, 107, 105], set2(3).children.map(&:id)
+  end
+
+  def test_move_to_top
+    set2(7).move_to_top
+    assert_equal [107, 105, 106], set2(3).children.map(&:id)
+  end
+
+  def test_move_to_bottom
+    set2(5).move_to_bottom
+    assert_equal [106, 107, 105], set2(3).children.map(&:id)
+  end
+
+  def test_move_to_position
+    set2(7).move_to_position(:first)
+    assert_equal [107, 105, 106], set2(3).children.map(&:id)
+    set2(7).move_to_position(:last)
+    assert_equal [105, 106, 107], set2(3).children.map(&:id)
+  end
+
+  def test_move_to_position_limits
+    set2(7).move_to_position(0)
+    assert_equal [107, 105, 106], set2(3).children.map(&:id)
+    set2(7).move_to_position(100)
+    assert_equal [105, 106, 107], set2(3).children.map(&:id)
+  end
+
+  def test_move_to_position_index
+    set2(7).move_to_position(0)
+    assert_equal [107, 105, 106], set2(3).children.map(&:id)
+    set2(7).move_to_position(1)
+    assert_equal [105, 107, 106], set2(3).children.map(&:id)
+    set2(7).move_to_position(2)
+    assert_equal [105, 106, 107], set2(3).children.map(&:id)
+    set2(5).move_to_position(2)
+    assert_equal [106, 107, 105], set2(3).children.map(&:id)
+  end
+
+  def test_scoped_move_to_position
+    set2(7).move_to_position(0, :conditions => { :id => [105, 106, 107] })
+    assert_equal [107, 105, 106], set2(3).children.map(&:id)
+    set2(7).move_to_position(1, :conditions => { :id => [105, 107] })
+    assert_equal [105, 107, 106], set2(3).children.map(&:id)
+    set2(7).move_to_position(1, :conditions => { :id => [106, 107] })
+    assert_equal [105, 106, 107], set2(3).children.map(&:id)
+  end
+
+  def test_reorder_children
+    assert_equal [105], set2(3).reorder_children(107, 106, 105).map(&:id)
+    assert_equal [107, 106, 105], set2(3).children.map(&:id)
+    assert_equal [107, 106], set2(3).reorder_children(106, 105, 107).map(&:id)
+    assert_equal [106, 105, 107], set2(3).children.map(&:id)
+  end
+
+  def test_reorder_children_with_random_samples
+    10.times do
+      child = NestedSetWithStringScope.create(:root_id => 101)
+      child.move_to_child_of set2(3)
+    end
+    ordered_ids = set2(3).children.map(&:id).sort_by { rand }
+    set2(3).reorder_children(ordered_ids)
+    assert_equal ordered_ids, set2(3).children.map(&:id)
+  end
+
+  def test_reorder_children_with_partial_id_set
+    10.times do
+      child = NestedSetWithStringScope.create(:root_id => 101)
+      child.move_to_child_of set2(3)
+    end
+    child_ids = set2(3).children.map(&:id)
+    set2(3).reorder_children(child_ids.last, child_ids.first)
+    ordered_ids = set2(3).children.map(&:id)
+    assert_equal ordered_ids.first, child_ids.last
+    assert_equal ordered_ids.last, child_ids.first
+    assert_equal child_ids[1, -2], ordered_ids[1, -2]
+  end
+
   ##########################################
   # RENUMBERING TESTS
   ##########################################
@@ -648,7 +1143,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_equal 11, set2(3).rgt
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_renumber_full_tree_2
     NestedSetWithStringScope.update_all("lft = lft + 1, rgt = rgt + 1", "root_id = 101")
     assert_raise(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.check_all}
@@ -659,8 +1154,8 @@ class MixinNestedSetTest < Test::Unit::TestCase
     set2(8).renumber_full_tree
     assert_nothing_raised(ActiveRecord::ActiveRecordError) {NestedSetWithStringScope.check_all}
   end
-  
-  
+
+
   ##########################################
   # CONCURRENCY TESTS
   ##########################################
@@ -670,14 +1165,14 @@ class MixinNestedSetTest < Test::Unit::TestCase
     c1.move_to_right_of(c3)
     c2.save
     assert_nothing_raised {Category.check_all}
-    
+
     ns1 = set2(3)
     ns2 = set2(4)
     ns2.move_to_left_of(102) # ns1 is now out-of-date
     ns1.save
     assert_nothing_raised {set2(1).check_subtree}
   end
-  
+
   def test_concurrent_add_add
     c1 = Category.new
     c2 = Category.new
@@ -689,21 +1184,21 @@ class MixinNestedSetTest < Test::Unit::TestCase
     c3.save
     assert_nothing_raised {Category.check_all}
   end
-  
+
   def test_concurrent_add_delete
     ns = set2(3)
     new_ns = NestedSetWithStringScope.create(:root_id => 101)
     ns.destroy
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_concurrent_add_move
     ns = set2(3)
     new_ns = NestedSetWithStringScope.create(:root_id => 101)
     ns.move_to_left_of(102)
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_concurrent_delete_add
     ns = set2(3)
     new_ns = NestedSetWithStringScope.new(:root_id => 101)
@@ -711,7 +1206,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     new_ns.save
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_concurrent_delete_delete
     ns1 = set2(3)
     ns2 = set2(4)
@@ -719,7 +1214,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     ns2.destroy
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_concurrent_delete_move
     ns1 = set2(3)
     ns2 = set2(4)
@@ -727,7 +1222,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     ns2.move_to_left_of(102)
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_concurrent_move_add
     ns = set2(3)
     new_ns = NestedSetWithStringScope.new(:root_id => 101)
@@ -735,7 +1230,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     new_ns.save
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_concurrent_move_delete
     ns1 = set2(3)
     ns2 = set2(4)
@@ -743,15 +1238,50 @@ class MixinNestedSetTest < Test::Unit::TestCase
     ns1.destroy
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
   def test_concurrent_move_move
     ns1 = set2(3)
     ns2 = set2(4)
     ns1.move_to_left_of(102)
     ns2.move_to_child_of(102)
-    assert_nothing_raised {NestedSetWithStringScope.check_all}    
+    assert_nothing_raised {NestedSetWithStringScope.check_all}
+  end
+
+  ##########################################
+  # CALLBACK TESTS
+  ##########################################
+  # Because the nested set code relies heavily on callbacks, we
+  # want to ensure that we aren't causing problems for user-defined callbacks
+  def test_callbacks
+    # 1) Do all user-defined callbacks work?
+    $callbacks = []
+    ns = NS2.new(:root_id => 101) # NS2 adds symbols to $callbacks when the callbacks fire
+    assert_equal([], $callbacks)
+    ns.save!
+    assert_equal([:before_save, :before_create, :after_create, :after_save], $callbacks)
+    $callbacks = []
+    ns.pos = 2
+    ns.save!
+    assert_equal([:before_save, :before_update, :after_update, :after_save], $callbacks)
+    $callbacks = []
+    ns.destroy
+    assert_equal([:before_destroy, :after_destroy], $callbacks)
   end
-  
+
+  def test_callbacks2
+    # 2) Do our callbacks still work, even when a programmer defines
+    # their own callbacks in the overwriteable style?
+    # (the NS2 model defines callbacks in the overwritable style)
+    ns = NS2.create(:root_id => 101)
+    assert ns.lft != nil && ns.rgt != nil
+    child_ns = NS2.create(:root_id => 101)
+    child_ns.move_to_child_of(ns)
+    id = child_ns.id
+    ns.destroy
+    assert_equal(nil, NS2.find(:first, :conditions => "id = #{id}"))
+    # lots of implicit testing occurs in other test methods
+  end
+
   ##########################################
   # BUG-SPECIFIC TESTS
   ##########################################
@@ -763,17 +1293,17 @@ class MixinNestedSetTest < Test::Unit::TestCase
     sub.move_to_child_of main
     sub.save
     main.save
-    
+
     assert_equal(1, main.all_children_count)
     assert_equal([main, sub], main.full_set)
     assert_equal([sub], main.all_children)
-    
+
     assert_equal(1, main.lft)
     assert_equal(2, sub.lft)
     assert_equal(3, sub.rgt)
     assert_equal(4, main.rgt)
   end
-  
+
   def test_ticket_19
     # this test currently relies on the fact that objects are reloaded at the beginning of the move_to methods
     root = Category.create
@@ -781,7 +1311,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     second = Category.create
     first.move_to_child_of(root)
     second.move_to_child_of(root)
-    
+
     # now we should have the situation described in the ticket
     assert_nothing_raised {first.move_to_child_of(second)}
     assert_raise(ActiveRecord::ActiveRecordError) {second.move_to_child_of(first)} # try illegal move
@@ -789,7 +1319,7 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_nothing_raised {first.move_to_child_of(second)} # try it the other way-- first is now on the other side of second
     assert_nothing_raised {Category.check_all}
   end
-  
+
   # Note that single-table inheritance recieves extensive implicit testing,
   # because one of the fixture trees contains a hodge-podge of classes.
   def test_ticket_10
@@ -799,10 +1329,19 @@ class MixinNestedSetTest < Test::Unit::TestCase
     assert_equal(10, set2(9).rgt)
     NS2.find(103).destroy
     assert_equal(12, set2(1).rgt)
-    assert_equal(6, NestedSetWithStringScope.count("root_id = 101"))
+    assert_equal(6, NestedSetWithStringScope.count(:conditions => "root_id = 101"))
     assert_nothing_raised {NestedSetWithStringScope.check_all}
   end
-  
+
+  # the next virtual root was not starting with the correct lft value
+  def test_ticket_29
+    first = Category.create
+    second = Category.create
+    Category.renumber_all
+    second.reload
+    assert_equal(3, second.lft)
+  end
+
 end
 
 
@@ -810,7 +1349,7 @@ end
 ###################################################################
 ## Tests that don't pass yet or haven't been finished
 
-## make #destroy set left & rgt to nil? 
+## make #destroy set left & rgt to nil?
 
 #def test_find_insertion_point
 #  bill = NestedSetWithStringScope.create(:pos => 2, :root_id => 101)
diff --git a/src/vendor/plugins/betternestedset/test/fixtures/mixin.rb b/src/vendor/plugins/betternestedset/test/fixtures/mixin.rb
index 7238b92..7888d26 100644
--- a/src/vendor/plugins/betternestedset/test/fixtures/mixin.rb
+++ b/src/vendor/plugins/betternestedset/test/fixtures/mixin.rb
@@ -1,30 +1,33 @@
 class Mixin < ActiveRecord::Base
+  belongs_to :parent_node, :class_name => 'Mixin', :foreign_key => 'parent_id'
 end
 
 class NestedSet < Mixin
-  acts_as_nested_set :scope => "root_id IS NULL"
-  def self.table_name() "mixins" end
+  acts_as_nested_set :scope => "mixins.root_id IS NULL"
 end
 
 class NestedSetWithStringScope < Mixin
-  acts_as_nested_set :scope => 'root_id = #{root_id}'
-  def self.table_name() "mixins" end
+  acts_as_nested_set :scope => 'mixins.root_id = #{root_id}'
 end
 
 class NS1 < NestedSetWithStringScope
-  def self.table_name() "mixins" end
 end
 
 class NS2 < NS1
-  def self.table_name() "mixins" end
+  my_callbacks = [:before_create, :before_save, :before_update, :before_destroy,
+    :after_create, :after_save, :after_update, :after_destroy]
+  my_callbacks.each do |sym|
+    define_method(sym) do
+      $callbacks ||= []
+      $callbacks << sym
+    end
+  end
 end
 
 class NestedSetWithSymbolScope < Mixin
   acts_as_nested_set :scope => :root
-  def self.table_name() "mixins" end
 end
 
 class Category < Mixin
   acts_as_nested_set
-  def self.table_name() "mixins" end
 end
-- 
1.5.5.1




More information about the ovirt-devel mailing list