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