[Ovirt-devel] [PATCH] rails 2.1 upgraded betternestedset
Jason Guiditta
jguiditt at redhat.com
Mon Sep 29 13:48:49 UTC 2008
On Wed, 2008-09-24 at 13:48 -0400, Scott Seago wrote:
> 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
ACK, works for me.
More information about the ovirt-devel
mailing list