import awesome_nested_set 2.1.5

60fe4f69e4

git-svn-id: http://svn.redmine.org/redmine/trunk@12677 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Toshi MARUYAMA 2014-01-20 05:59:16 +00:00
parent 77f29deb29
commit 6f78b3a408
24 changed files with 696 additions and 918 deletions

View File

@ -1,14 +1,17 @@
before_install: gem install bundler --pre
notifications: notifications:
email: email:
- parndt@gmail.com - parndt@gmail.com
env: env:
- DB=sqlite3 - DB=sqlite3
- DB=sqlite3mem - DB=sqlite3mem
- DB=postgresql
- DB=mysql
rvm: rvm:
- 1.8.7 - 1.8.7
- 1.9.2 - 1.9.2
- 1.9.3 - 1.9.3
- rbx-2.0 - rbx
- jruby - jruby
gemfile:
- gemfiles/Gemfile.rails-3.0.rb
- gemfiles/Gemfile.rails-3.1.rb
- gemfiles/Gemfile.rails-3.2.rb

View File

@ -1,3 +1,41 @@
2.1.5
* Worked around issues where AR#association wasn't present on Rails 3.0.x. [Philip Arndt]
* Adds option 'order_column' which defaults to 'left_column_name'. [gudata]
* Added moving with order functionality. [Sytse Sijbrandij]
* Use tablename in all select queries. [Mikhail Dieterle]
* Made sure all descendants' depths are updated when moving parent, not just immediate child. [Phil Thompson]
* Add documentation of the callbacks. [Tobias Maier]
2.1.4
* nested_set_options accept both Class & AR Relation. [Semyon Perepelitsa]
* Reduce the number of queries triggered by the canonical usage of `i.level` in the `nested_set` helpers. [thedarkone]
* Specifically require active_record [Bogdan Gusiev]
* compute_level now checks for a non nil association target. [Joel Nimety]
2.1.3
* Update child depth when parent node is moved. [Amanda Wagener]
* Added move_to_child_with_index. [Ben Zhang]
* Optimised self_and_descendants for when there's an index on lft. [Mark Torrance]
* Added support for an unsaved record to return the right 'root'. [Philip Arndt]
2.1.2
* Fixed regressions introduced. [Philip Arndt]
2.1.1
* Added 'depth' which indicates how many levels deep the node is.
This only works when you have a column called 'depth' in your table,
otherwise it doesn't set itself. [Philip Arndt]
* Rails 3.2 support added. [Gabriel Sobrinho]
* Oracle compatibility added. [Pikender Sharma]
* Adding row locking to deletion, locking source of pivot values, and adding retry on collisions. [Markus J. Q. Roberts]
* Added method and helper for sorting children by column. [bluegod]
* Fixed .all_roots_valid? to work with Postgres. [Joshua Clayton]
* Made compatible with polymorphic belongs_to. [Graham Randall]
* Added in the association callbacks to the children :has_many association. [Michael Deering]
* Modified helper to allow using array of objects as argument. [Rahmat Budiharso]
* Fixed cases where we were calling attr_protected. [Jacob Swanner]
* Fixed nil cases involving lft and rgt. [Stuart Coyle] and [Patrick Morgan]
2.0.2 2.0.2
* Fixed deprecation warning under Rails 3.1 [Philip Arndt] * Fixed deprecation warning under Rails 3.1 [Philip Arndt]
* Converted Test::Unit matchers to RSpec. [Uģis Ozols] * Converted Test::Unit matchers to RSpec. [Uģis Ozols]

View File

@ -16,7 +16,8 @@ This is a new implementation of nested set based off of BetterNestedSet that fix
== Usage == Usage
To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id: To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id.
You can also have an optional field: depth:
class CreateCategories < ActiveRecord::Migration class CreateCategories < ActiveRecord::Migration
def self.up def self.up
@ -25,6 +26,7 @@ To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt,
t.integer :parent_id t.integer :parent_id
t.integer :lft t.integer :lft
t.integer :rgt t.integer :rgt
t.integer :depth # this is optional.
end end
end end
@ -41,6 +43,57 @@ Enable the nested set functionality by declaring acts_as_nested_set on your mode
Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet for more info. Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet for more info.
== Callbacks
There are three callbacks called when moving a node. `before_move`, `after_move` and `around_move`.
class Category < ActiveRecord::Base
acts_as_nested_set
after_move :rebuild_slug
around_move :da_fancy_things_around
private
def rebuild_slug
# do whatever
end
def da_fancy_things_around
# do something...
yield # actually moves
# do something else...
end
end
Beside this there are also hooks to act on the newly added or removed children.
class Category < ActiveRecord::Base
acts_as_nested_set :before_add => :do_before_add_stuff,
:after_add => :do_after_add_stuff,
:before_remove => :do_before_remove_stuff,
:after_remove => :do_after_remove_stuff
private
def do_before_add_stuff(child_node)
# do whatever with the child
end
def do_after_add_stuff(child_node)
# do whatever with the child
end
def do_before_remove_stuff(child_node)
# do whatever with the child
end
def do_after_remove_stuff(child_node)
# do whatever with the child
end
end
== Protecting attributes from mass assignment == Protecting attributes from mass assignment
It's generally best to "white list" the attributes that can be used in mass assignment: It's generally best to "white list" the attributes that can be used in mass assignment:
@ -86,13 +139,13 @@ You can learn more about nested sets at: http://threebit.net/tutorials/nestedset
If you find what you might think is a bug: If you find what you might think is a bug:
1. Check the GitHub issue tracker to see if anyone else has had the same issue. 1. Check the GitHub issue tracker to see if anyone else has had the same issue.
http://github.com/collectiveidea/awesome_nested_set/issues/ https://github.com/collectiveidea/awesome_nested_set/issues/
2. If you don't see anything, create an issue with information on how to reproduce it. 2. If you don't see anything, create an issue with information on how to reproduce it.
If you want to contribute an enhancement or a fix: If you want to contribute an enhancement or a fix:
1. Fork the project on github. 1. Fork the project on GitHub.
http://github.com/collectiveidea/awesome_nested_set/ https://github.com/collectiveidea/awesome_nested_set/
2. Make your changes with tests. 2. Make your changes with tests.
3. Commit the changes without making changes to the Rakefile, VERSION, or any other files that aren't related to your enhancement or fix 3. Commit the changes without making changes to the Rakefile, VERSION, or any other files that aren't related to your enhancement or fix
4. Send a pull request. 4. Send a pull request.

View File

@ -19,4 +19,5 @@ Gem::Specification.new do |s|
s.rubygems_version = %q{1.3.6} s.rubygems_version = %q{1.3.6}
s.summary = %q{An awesome nested set implementation for Active Record} s.summary = %q{An awesome nested set implementation for Active Record}
s.add_runtime_dependency 'activerecord', '>= 3.0.0' s.add_runtime_dependency 'activerecord', '>= 3.0.0'
s.add_development_dependency 'rspec-rails', '~> 2.8'
end end

View File

@ -1 +0,0 @@
require File.dirname(__FILE__) + '/lib/awesome_nested_set'

View File

@ -1,4 +1,5 @@
require 'awesome_nested_set/awesome_nested_set' require 'awesome_nested_set/awesome_nested_set'
require 'active_record'
ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet
if defined?(ActionView) if defined?(ActionView)

View File

@ -23,6 +23,7 @@ module CollectiveIdea #:nodoc:
# * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id) # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
# * +:left_column+ - column name for left boundry data, default "lft" # * +:left_column+ - column name for left boundry data, default "lft"
# * +:right_column+ - column name for right boundry data, default "rgt" # * +:right_column+ - column name for right boundry data, default "rgt"
# * +:depth_column+ - column name for the depth data, default "depth"
# * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id" # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
# (if it hasn't been already) and use that as the foreign key restriction. You # (if it hasn't been already) and use that as the foreign key restriction. You
# can also pass an array to scope by multiple attributes. # can also pass an array to scope by multiple attributes.
@ -34,6 +35,8 @@ module CollectiveIdea #:nodoc:
# * +:counter_cache+ adds a counter cache for the number of children. # * +:counter_cache+ adds a counter cache for the number of children.
# defaults to false. # defaults to false.
# Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt> # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
# * +:order_column+ on which column to do sorting, by default it is the left_column_name
# Example: <tt>acts_as_nested_set :order_column => :position</tt>
# #
# See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
# CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
@ -43,9 +46,10 @@ module CollectiveIdea #:nodoc:
:parent_column => 'parent_id', :parent_column => 'parent_id',
:left_column => 'lft', :left_column => 'lft',
:right_column => 'rgt', :right_column => 'rgt',
:depth_column => 'depth',
:dependent => :delete_all, # or :destroy :dependent => :delete_all, # or :destroy
:counter_cache => false, :polymorphic => false,
:order => 'id' :counter_cache => false
}.merge(options) }.merge(options)
if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/ if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
@ -62,24 +66,32 @@ module CollectiveIdea #:nodoc:
belongs_to :parent, :class_name => self.base_class.to_s, belongs_to :parent, :class_name => self.base_class.to_s,
:foreign_key => parent_column_name, :foreign_key => parent_column_name,
:counter_cache => options[:counter_cache], :counter_cache => options[:counter_cache],
:inverse_of => :children :inverse_of => (:children unless options[:polymorphic]),
has_many :children, :class_name => self.base_class.to_s, :polymorphic => options[:polymorphic]
:foreign_key => parent_column_name, :order => left_column_name,
:inverse_of => :parent, has_many_children_options = {
:before_add => options[:before_add], :class_name => self.base_class.to_s,
:after_add => options[:after_add], :foreign_key => parent_column_name,
:before_remove => options[:before_remove], :order => order_column,
:after_remove => options[:after_remove] :inverse_of => (:parent unless options[:polymorphic]),
}
# Add callbacks, if they were supplied.. otherwise, we don't want them.
[:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
has_many_children_options.update(ar_callback => options[ar_callback]) if options[ar_callback]
end
has_many :children, has_many_children_options
attr_accessor :skip_before_destroy attr_accessor :skip_before_destroy
before_create :set_default_left_and_right before_create :set_default_left_and_right
before_save :store_new_parent before_save :store_new_parent
after_save :move_to_new_parent after_save :move_to_new_parent, :set_depth!
before_destroy :destroy_descendants before_destroy :destroy_descendants
# no assignment to structure fields # no assignment to structure fields
[left_column_name, right_column_name].each do |column| [left_column_name, right_column_name, depth_column_name].each do |column|
module_eval <<-"end_eval", __FILE__, __LINE__ module_eval <<-"end_eval", __FILE__, __LINE__
def #{column}=(x) def #{column}=(x)
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead." raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
@ -93,6 +105,10 @@ module CollectiveIdea #:nodoc:
module Model module Model
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do
delegate :quoted_table_name, :to => self
end
module ClassMethods module ClassMethods
# Returns the first root # Returns the first root
def root def root
@ -100,11 +116,11 @@ module CollectiveIdea #:nodoc:
end end
def roots def roots
where(parent_column_name => nil).order(quoted_left_column_name) where(parent_column_name => nil).order(quoted_left_column_full_name)
end end
def leaves def leaves
where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name) where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
end end
def valid? def valid?
@ -112,16 +128,19 @@ module CollectiveIdea #:nodoc:
end end
def left_and_rights_valid? def left_and_rights_valid?
joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " + ## AS clause not supported in Oracle in FROM clause for aliasing table name
"#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}"). joins("LEFT OUTER JOIN #{quoted_table_name}" +
(connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
"parent ON " +
"#{quoted_parent_column_full_name} = parent.#{primary_key}").
where( where(
"#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " + "#{quoted_left_column_full_name} IS NULL OR " +
"#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " + "#{quoted_right_column_full_name} IS NULL OR " +
"#{quoted_table_name}.#{quoted_left_column_name} >= " + "#{quoted_left_column_full_name} >= " +
"#{quoted_table_name}.#{quoted_right_column_name} OR " + "#{quoted_right_column_full_name} OR " +
"(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " + "(#{quoted_parent_column_full_name} IS NOT NULL AND " +
"(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " + "(#{quoted_left_column_full_name} <= parent.#{quoted_left_column_name} OR " +
"#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))" "#{quoted_right_column_full_name} >= parent.#{quoted_right_column_name}))"
).count == 0 ).count == 0
end end
@ -129,7 +148,7 @@ module CollectiveIdea #:nodoc:
scope_string = Array(acts_as_nested_set_options[:scope]).map do |c| scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
connection.quote_column_name(c) connection.quote_column_name(c)
end.push(nil).join(", ") end.push(nil).join(", ")
[quoted_left_column_name, quoted_right_column_name].all? do |column| [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
# No duplicates # No duplicates
select("#{scope_string}#{column}, COUNT(#{column})"). select("#{scope_string}#{column}, COUNT(#{column})").
group("#{scope_string}#{column}"). group("#{scope_string}#{column}").
@ -141,7 +160,7 @@ module CollectiveIdea #:nodoc:
# Wrapper for each_root_valid? that can deal with scope. # Wrapper for each_root_valid? that can deal with scope.
def all_roots_valid? def all_roots_valid?
if acts_as_nested_set_options[:scope] if acts_as_nested_set_options[:scope]
roots.group(scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots| roots.group_by {|record| scope_column_names.collect {|col| record.send(col.to_sym) } }.all? do |scope, grouped_roots|
each_root_valid?(grouped_roots) each_root_valid?(grouped_roots)
end end
else else
@ -179,14 +198,14 @@ module CollectiveIdea #:nodoc:
# set left # set left
node[left_column_name] = indices[scope.call(node)] += 1 node[left_column_name] = indices[scope.call(node)] += 1
# find # find
where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).order(acts_as_nested_set_options[:order]).each{|n| set_left_and_rights.call(n) } where(["#{quoted_parent_column_full_name} = ? #{scope.call(node)}", node]).order("#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, id").each{|n| set_left_and_rights.call(n) }
# set right # set right
node[right_column_name] = indices[scope.call(node)] += 1 node[right_column_name] = indices[scope.call(node)] += 1
node.save!(:validate => validate_nodes) node.save!(:validate => validate_nodes)
end end
# Find root node(s) # Find root node(s)
root_nodes = where("#{quoted_parent_column_name} IS NULL").order(acts_as_nested_set_options[:order]).each do |root_node| root_nodes = where("#{quoted_parent_column_full_name} IS NULL").order("#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, id").each do |root_node|
# setup index for this scope # setup index for this scope
indices[scope.call(root_node)] ||= 0 indices[scope.call(root_node)] ||= 0
set_left_and_rights.call(root_node) set_left_and_rights.call(root_node)
@ -205,7 +224,7 @@ module CollectiveIdea #:nodoc:
path = [nil] path = [nil]
objects.each do |o| objects.each do |o|
if o.parent_id != path.last if o.parent_id != path.last
# we are on a new level, did we decent or ascent? # we are on a new level, did we descend or ascend?
if path.include?(o.parent_id) if path.include?(o.parent_id)
# remove wrong wrong tailing paths elements # remove wrong wrong tailing paths elements
path.pop while path.last != o.parent_id path.pop while path.last != o.parent_id
@ -216,13 +235,56 @@ module CollectiveIdea #:nodoc:
yield(o, path.length - 1) yield(o, path.length - 1)
end end
end end
# Same as each_with_level - Accepts a string as a second argument to sort the list
# Example:
# Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
def sorted_each_with_level(objects, order)
path = [nil]
children = []
objects.each do |o|
children << o if o.leaf?
if o.parent_id != path.last
if !children.empty? && !o.leaf?
children.sort_by! &order
children.each { |c| yield(c, path.length-1) }
children = []
end
# we are on a new level, did we decent or ascent?
if path.include?(o.parent_id)
# remove wrong wrong tailing paths elements
path.pop while path.last != o.parent_id
else
path << o.parent_id
end
end
yield(o,path.length-1) if !o.leaf?
end
if !children.empty?
children.sort_by! &order
children.each { |c| yield(c, path.length-1) }
end
end
def associate_parents(objects)
if objects.all?{|o| o.respond_to?(:association)}
id_indexed = objects.index_by(&:id)
objects.each do |object|
if !(association = object.association(:parent)).loaded? && (parent = id_indexed[object.parent_id])
association.target = parent
association.set_inverse_instance(parent)
end
end
else
objects
end
end
end end
# Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder. # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
# #
# category.self_and_descendants.count # category.self_and_descendants.count
# category.ancestors.find(:all, :conditions => "name like '%foo%'") # category.ancestors.find(:all, :conditions => "name like '%foo%'")
# Value of the parent column # Value of the parent column
def parent_id def parent_id
self[parent_column_name] self[parent_column_name]
@ -243,24 +305,33 @@ module CollectiveIdea #:nodoc:
parent_id.nil? parent_id.nil?
end end
# Returns true if this is the end of a branch.
def leaf? def leaf?
new_record? || (right - left == 1) persisted? && right.to_i - left.to_i == 1
end end
# Returns true is this is a child node # Returns true is this is a child node
def child? def child?
!parent_id.nil? !root?
end end
# Returns root # Returns root
def root def root
if persisted?
self_and_ancestors.where(parent_column_name => nil).first self_and_ancestors.where(parent_column_name => nil).first
else
if parent_id && current_parent = nested_set_scope.find(parent_id)
current_parent.root
else
self
end
end
end end
# Returns the array of all parents and self # Returns the array of all parents and self
def self_and_ancestors def self_and_ancestors
nested_set_scope.where([ nested_set_scope.where([
"#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right "#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
]) ])
end end
@ -281,19 +352,20 @@ module CollectiveIdea #:nodoc:
# Returns a set of all of its nested children which do not have children # Returns a set of all of its nested children which do not have children
def leaves def leaves
descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1") descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
end end
# Returns the level of this object in the tree # Returns the level of this object in the tree
# root level is 0 # root level is 0
def level def level
parent_id.nil? ? 0 : ancestors.count parent_id.nil? ? 0 : compute_level
end end
# Returns a set of itself and all of its nested children # Returns a set of itself and all of its nested children
def self_and_descendants def self_and_descendants
nested_set_scope.where([ nested_set_scope.where([
"#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right "#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
# using _left_ for both sides here lets us benefit from an index on that column if one exists
]) ])
end end
@ -327,13 +399,13 @@ module CollectiveIdea #:nodoc:
# Find the first sibling to the left # Find the first sibling to the left
def left_sibling def left_sibling
siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]). siblings.where(["#{quoted_left_column_full_name} < ?", left]).
order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last order("#{quoted_left_column_full_name} DESC").last
end end
# Find the first sibling to the right # Find the first sibling to the right
def right_sibling def right_sibling
siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
end end
# Shorthand method for finding the left sibling and moving to the left of it. # Shorthand method for finding the left sibling and moving to the left of it.
@ -361,11 +433,44 @@ module CollectiveIdea #:nodoc:
move_to node, :child move_to node, :child
end end
# Move the node to the child of another node with specify index (you can pass id only)
def move_to_child_with_index(node, index)
if node.children.empty?
move_to_child_of(node)
elsif node.children.count == index
move_to_right_of(node.children.last)
else
move_to_left_of(node.children[index])
end
end
# Move the node to root nodes # Move the node to root nodes
def move_to_root def move_to_root
move_to nil, :root move_to nil, :root
end end
# Order children in a nested set by an attribute
# Can order by any attribute class that uses the Comparable mixin, for example a string or integer
# Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
def move_to_ordered_child_of(parent, order_attribute, ascending = true)
self.move_to_root and return unless parent
left = nil # This is needed, at least for the tests.
parent.children.each do |n| # Find the node immediately to the left of this node.
if ascending
left = n if n.send(order_attribute) < self.send(order_attribute)
else
left = n if n.send(order_attribute) > self.send(order_attribute)
end
end
self.move_to_child_of(parent)
return unless parent.children.count > 1 # Only need to order if there are multiple children.
if left # Self has a left neighbor.
self.move_to_right_of(left)
else # Self is the left most node.
self.move_to_left_of(parent.children[0])
end
end
def move_possible?(target) def move_possible?(target)
self != target && # Can't target self self != target && # Can't target self
same_scope?(target) && # can't be in different scopes same_scope?(target) && # can't be in different scopes
@ -381,6 +486,14 @@ module CollectiveIdea #:nodoc:
end end
protected protected
def compute_level
node, nesting = self, 0
while (association = node.association(:parent)).loaded? && association.target
nesting += 1
node = node.parent
end if node.respond_to? :association
node == self ? ancestors.count : node.level + nesting
end
def without_self(scope) def without_self(scope)
scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self]) scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
@ -390,12 +503,12 @@ module CollectiveIdea #:nodoc:
# the base ActiveRecord class, using the :scope declared in the acts_as_nested_set # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
# declaration. # declaration.
def nested_set_scope(options = {}) def nested_set_scope(options = {})
options = {:order => "#{self.class.quoted_table_name}.#{quoted_left_column_name}"}.merge(options) options = {:order => quoted_left_column_full_name}.merge(options)
scopes = Array(acts_as_nested_set_options[:scope]) scopes = Array(acts_as_nested_set_options[:scope])
options[:conditions] = scopes.inject({}) do |conditions,attr| options[:conditions] = scopes.inject({}) do |conditions,attr|
conditions.merge attr => self[attr] conditions.merge attr => self[attr]
end unless scopes.empty? end unless scopes.empty?
self.class.base_class.scoped options self.class.base_class.unscoped.scoped options
end end
def store_new_parent def store_new_parent
@ -411,9 +524,20 @@ module CollectiveIdea #:nodoc:
end end
end end
def set_depth!
if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
in_tenacious_transaction do
reload
nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
end
self[depth_column_name.to_sym] = self.level
end
end
# on creation, set automatically lft and rgt to the end of the tree # on creation, set automatically lft and rgt to the end of the tree
def set_default_left_and_right def set_default_left_and_right
highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").limit(1).lock(true).first highest_right_row = nested_set_scope(:order => "#{quoted_right_column_full_name} desc").limit(1).lock(true).first
maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
# adds the new node to the right of all existing nodes # adds the new node to the right of all existing nodes
self[left_column_name] = maxright + 1 self[left_column_name] = maxright + 1
@ -443,11 +567,8 @@ module CollectiveIdea #:nodoc:
in_tenacious_transaction do in_tenacious_transaction do
reload_nested_set reload_nested_set
# select the rows in the model that extend past the deletion point and apply a lock # select the rows in the model that extend past the deletion point and apply a lock
nested_set_scope. nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
select("id"). select(id).lock(true)
where("#{quoted_left_column_name} >= ?", left).
lock(true).
all
if acts_as_nested_set_options[:dependent] == :destroy if acts_as_nested_set_options[:dependent] == :destroy
descendants.each do |model| descendants.each do |model|
@ -455,24 +576,20 @@ module CollectiveIdea #:nodoc:
model.destroy model.destroy
end end
else else
nested_set_scope.delete_all( nested_set_scope.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", delete_all
left, right]
)
end end
# update lefts and rights for remaining nodes # update lefts and rights for remaining nodes
diff = right - left + 1 diff = right - left + 1
nested_set_scope.update_all( nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff], ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
["#{quoted_left_column_name} > ?", right] )
)
nested_set_scope.update_all( nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff], ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
["#{quoted_right_column_name} > ?", right]
) )
reload
# Don't allow multiple calls to destroy to corrupt the set # Don't allow multiple calls to destroy to corrupt the set
self.skip_before_destroy = true self.skip_before_destroy = true
end end
@ -481,7 +598,7 @@ reload
# reload left, right, and parent # reload left, right, and parent
def reload_nested_set def reload_nested_set
reload( reload(
:select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}", :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
:lock => true :lock => true
) )
end end
@ -526,7 +643,7 @@ reload
# select the rows in the model between a and d, and apply a lock # select the rows in the model between a and d, and apply a lock
self.class.base_class.select('id').lock(true).where( self.class.base_class.select('id').lock(true).where(
["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}] ["#{quoted_left_column_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
) )
new_parent = case position new_parent = case position
@ -555,6 +672,8 @@ reload
]) ])
end end
target.reload_nested_set if target target.reload_nested_set if target
self.set_depth!
self.descendants.each(&:save)
self.reload_nested_set self.reload_nested_set
end end
end end
@ -571,10 +690,18 @@ reload
acts_as_nested_set_options[:right_column] acts_as_nested_set_options[:right_column]
end end
def depth_column_name
acts_as_nested_set_options[:depth_column]
end
def parent_column_name def parent_column_name
acts_as_nested_set_options[:parent_column] acts_as_nested_set_options[:parent_column]
end end
def order_column
acts_as_nested_set_options[:order_column] || left_column_name
end
def scope_column_names def scope_column_names
Array(acts_as_nested_set_options[:scope]) Array(acts_as_nested_set_options[:scope])
end end
@ -587,6 +714,10 @@ reload
connection.quote_column_name(right_column_name) connection.quote_column_name(right_column_name)
end end
def quoted_depth_column_name
connection.quote_column_name(depth_column_name)
end
def quoted_parent_column_name def quoted_parent_column_name
connection.quote_column_name(parent_column_name) connection.quote_column_name(parent_column_name)
end end
@ -594,6 +725,18 @@ reload
def quoted_scope_column_names def quoted_scope_column_names
scope_column_names.collect {|column_name| connection.quote_column_name(column_name) } scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
end end
def quoted_left_column_full_name
"#{quoted_table_name}.#{quoted_left_column_name}"
end
def quoted_right_column_full_name
"#{quoted_table_name}.#{quoted_right_column_name}"
end
def quoted_parent_column_full_name
"#{quoted_table_name}.#{quoted_parent_column_name}"
end
end end
end end

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
module CollectiveIdea #:nodoc: module CollectiveIdea #:nodoc:
module Acts #:nodoc: module Acts #:nodoc:
module NestedSet #:nodoc: module NestedSet #:nodoc:
@ -24,12 +25,12 @@ module CollectiveIdea #:nodoc:
if class_or_item.is_a? Array if class_or_item.is_a? Array
items = class_or_item.reject { |e| !e.root? } items = class_or_item.reject { |e| !e.root? }
else else
class_or_item = class_or_item.roots if class_or_item.is_a?(Class) class_or_item = class_or_item.roots if class_or_item.respond_to?(:scoped)
items = Array(class_or_item) items = Array(class_or_item)
end end
result = [] result = []
items.each do |root| items.each do |root|
result += root.self_and_descendants.map do |i| result += root.class.associate_parents(root.self_and_descendants).map do |i|
if mover.nil? || mover.new_record? || mover.move_possible?(i) if mover.nil? || mover.new_record? || mover.move_possible?(i)
[yield(i), i.id] [yield(i), i.id]
end end
@ -38,6 +39,50 @@ module CollectiveIdea #:nodoc:
result result
end end
# Returns options for select as nested_set_options, sorted by an specific column
# It requires passing a string with the name of the column to sort the set with
# You can exclude some items from the tree.
# You can pass a block receiving an item and returning the string displayed in the select.
#
# == Params
# * +class_or_item+ - Class name or top level times
# * +:column+ - Column to sort the set (this will sort each children for all root elements)
# * +mover+ - The item that is being move, used to exlude impossible moves
# * +&block+ - a block that will be used to display: { |item| ... item.name }
#
# == Usage
#
# <%= f.select :parent_id, nested_set_options(Category, :sort_by_this_column, @category) {|i|
# "#{'' * i.level} #{i.name}"
# }) %>
#
def sorted_nested_set_options(class_or_item, order, mover = nil)
if class_or_item.is_a? Array
items = class_or_item.reject { |e| !e.root? }
else
class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
items = Array(class_or_item)
end
result = []
children = []
items.each do |root|
root.class.associate_parents(root.self_and_descendants).map do |i|
if mover.nil? || mover.new_record? || mover.move_possible?(i)
if !i.leaf?
children.sort_by! &order
children.each { |c| result << [yield(c), c.id] }
children = []
result << [yield(i), i.id]
else
children << i
end
end
end.compact
end
children.sort_by! &order
children.each { |c| result << [yield(c), c.id] }
result
end
end end
end end
end end

View File

@ -1,3 +1,3 @@
module AwesomeNestedSet module AwesomeNestedSet
VERSION = '2.1.0' unless defined?(::AwesomeNestedSet::VERSION) VERSION = '2.1.5' unless defined?(::AwesomeNestedSet::VERSION)
end end

View File

@ -1,13 +0,0 @@
require 'awesome_nested_set/compatability'
require 'awesome_nested_set'
ActiveRecord::Base.class_eval do
include CollectiveIdea::Acts::NestedSet
end
if defined?(ActionView)
require 'awesome_nested_set/helper'
ActionView::Base.class_eval do
include CollectiveIdea::Acts::NestedSet::Helper
end
end

View File

@ -17,13 +17,41 @@ describe "Helper" do
['- Child 3', 5], ['- Child 3', 5],
[" Top Level 2", 6] [" Top Level 2", 6]
] ]
actual = nested_set_options(Category) do |c| actual = nested_set_options(Category.scoped) do |c|
"#{'-' * c.level} #{c.name}" "#{'-' * c.level} #{c.name}"
end end
actual.should == expected actual.should == expected
end end
it "test_nested_set_options_with_mover" do it "test_nested_set_options_with_mover" do
expected = [
[" Top Level", 1],
["- Child 1", 2],
['- Child 3', 5],
[" Top Level 2", 6]
]
actual = nested_set_options(Category.scoped, categories(:child_2)) do |c|
"#{'-' * c.level} #{c.name}"
end
actual.should == expected
end
it "test_nested_set_options_with_class_as_argument" do
expected = [
[" Top Level", 1],
["- Child 1", 2],
['- Child 2', 3],
['-- Child 2.1', 4],
['- Child 3', 5],
[" Top Level 2", 6]
]
actual = nested_set_options(Category) do |c|
"#{'-' * c.level} #{c.name}"
end
actual.should == expected
end
it "test_nested_set_options_with_class_as_argument_with_mover" do
expected = [ expected = [
[" Top Level", 1], [" Top Level", 1],
["- Child 1", 2], ["- Child 1", 2],

View File

@ -36,6 +36,13 @@ describe "AwesomeNestedSet" do
RenamedColumns.new.right_column_name.should == 'black' RenamedColumns.new.right_column_name.should == 'black'
end end
it "has a depth_column_name" do
Default.depth_column_name.should == 'depth'
Default.new.depth_column_name.should == 'depth'
RenamedColumns.depth_column_name.should == 'pitch'
RenamedColumns.depth_column_name.should == 'pitch'
end
it "should have parent_column_name" do it "should have parent_column_name" do
Default.parent_column_name.should == 'parent_id' Default.parent_column_name.should == 'parent_id'
Default.new.parent_column_name.should == 'parent_id' Default.new.parent_column_name.should == 'parent_id'
@ -68,6 +75,12 @@ describe "AwesomeNestedSet" do
Default.new.quoted_right_column_name.should == quoted Default.new.quoted_right_column_name.should == quoted
end end
it "quoted_depth_column_name" do
quoted = Default.connection.quote_column_name('depth')
Default.quoted_depth_column_name.should == quoted
Default.new.quoted_depth_column_name.should == quoted
end
it "left_column_protected_from_assignment" do it "left_column_protected_from_assignment" do
lambda { lambda {
Category.new.lft = 1 Category.new.lft = 1
@ -80,6 +93,12 @@ describe "AwesomeNestedSet" do
}.should raise_exception(ActiveRecord::ActiveRecordError) }.should raise_exception(ActiveRecord::ActiveRecordError)
end end
it "depth_column_protected_from_assignment" do
lambda {
Category.new.depth = 1
}.should raise_exception(ActiveRecord::ActiveRecordError)
end
it "scoped_appends_id" do it "scoped_appends_id" do
ScopedCategory.acts_as_nested_set_options[:scope].should == :organization_id ScopedCategory.acts_as_nested_set_options[:scope].should == :organization_id
end end
@ -96,6 +115,16 @@ describe "AwesomeNestedSet" do
categories(:child_3).root.should == categories(:top_level) categories(:child_3).root.should == categories(:top_level)
end end
it "root when not persisted and parent_column_name value is self" do
new_category = Category.new
new_category.root.should == new_category
end
it "root when not persisted and parent_column_name value is set" do
last_category = Category.last
Category.new(Default.parent_column_name => last_category.id).root.should == last_category.root
end
it "root?" do it "root?" do
categories(:top_level).root?.should be_true categories(:top_level).root?.should be_true
categories(:top_level_2).root?.should be_true categories(:top_level_2).root?.should be_true
@ -159,27 +188,88 @@ describe "AwesomeNestedSet" do
categories(:top_level).leaves.should == leaves categories(:top_level).leaves.should == leaves
end end
it "level" do describe "level" do
it "returns the correct level" do
categories(:top_level).level.should == 0 categories(:top_level).level.should == 0
categories(:child_1).level.should == 1 categories(:child_1).level.should == 1
categories(:child_2_1).level.should == 2 categories(:child_2_1).level.should == 2
end end
context "given parent associations are loaded" do
it "returns the correct level" do
child = categories(:child_1)
if child.respond_to?(:association)
child.association(:parent).load_target
child.parent.association(:parent).load_target
child.level.should == 1
else
pending 'associations not used where child#association is not a method'
end
end
end
end
describe "depth" do
let(:lawyers) { Category.create!(:name => "lawyers") }
let(:us) { Category.create!(:name => "United States") }
let(:new_york) { Category.create!(:name => "New York") }
let(:patent) { Category.create!(:name => "Patent Law") }
before(:each) do
# lawyers > us > new_york > patent
us.move_to_child_of(lawyers)
new_york.move_to_child_of(us)
patent.move_to_child_of(new_york)
[lawyers, us, new_york, patent].each(&:reload)
end
it "updates depth when moved into child position" do
lawyers.depth.should == 0
us.depth.should == 1
new_york.depth.should == 2
patent.depth.should == 3
end
it "updates depth of all descendants when parent is moved" do
# lawyers
# us > new_york > patent
us.move_to_right_of(lawyers)
[lawyers, us, new_york, patent].each(&:reload)
us.depth.should == 0
new_york.depth.should == 1
patent.depth.should == 2
end
end
it "depth is magic and does not apply when column is missing" do
lambda { NoDepth.create!(:name => "shallow") }.should_not raise_error
lambda { NoDepth.first.save }.should_not raise_error
lambda { NoDepth.rebuild! }.should_not raise_error
NoDepth.method_defined?(:depth).should be_false
NoDepth.first.respond_to?(:depth).should be_false
end
it "has_children?" do it "has_children?" do
categories(:child_2_1).children.empty?.should be_true categories(:child_2_1).children.empty?.should be_true
categories(:child_2).children.empty?.should be_false categories(:child_2).children.empty?.should be_false
categories(:top_level).children.empty?.should be_false categories(:top_level).children.empty?.should be_false
end end
it "self_and_descendents" do it "self_and_descendants" do
parent = categories(:top_level) parent = categories(:top_level)
self_and_descendants = [parent, categories(:child_1), categories(:child_2), self_and_descendants = [
categories(:child_2_1), categories(:child_3)] parent,
categories(:child_1),
categories(:child_2),
categories(:child_2_1),
categories(:child_3)
]
self_and_descendants.should == parent.self_and_descendants self_and_descendants.should == parent.self_and_descendants
self_and_descendants.count.should == parent.self_and_descendants.count self_and_descendants.count.should == parent.self_and_descendants.count
end end
it "descendents" do it "descendants" do
lawyers = Category.create!(:name => "lawyers") lawyers = Category.create!(:name => "lawyers")
us = Category.create!(:name => "United States") us = Category.create!(:name => "United States")
us.move_to_child_of(lawyers) us.move_to_child_of(lawyers)
@ -192,10 +282,14 @@ describe "AwesomeNestedSet" do
lawyers.descendants.size.should == 2 lawyers.descendants.size.should == 2
end end
it "self_and_descendents" do it "self_and_descendants" do
parent = categories(:top_level) parent = categories(:top_level)
descendants = [categories(:child_1), categories(:child_2), descendants = [
categories(:child_2_1), categories(:child_3)] categories(:child_1),
categories(:child_2),
categories(:child_2_1),
categories(:child_3)
]
descendants.should == parent.descendants descendants.should == parent.descendants
end end
@ -350,6 +444,43 @@ describe "AwesomeNestedSet" do
Category.valid?.should be_true Category.valid?.should be_true
end end
describe "#move_to_child_with_index" do
it "move to a node without child" do
categories(:child_1).move_to_child_with_index(categories(:child_3), 0)
categories(:child_3).id.should == categories(:child_1).parent_id
categories(:child_1).left.should == 7
categories(:child_1).right.should == 8
categories(:child_3).left.should == 6
categories(:child_3).right.should == 9
Category.valid?.should be_true
end
it "move to a node to the left child" do
categories(:child_1).move_to_child_with_index(categories(:child_2), 0)
categories(:child_1).parent_id.should == categories(:child_2).id
categories(:child_2_1).left.should == 5
categories(:child_2_1).right.should == 6
categories(:child_1).left.should == 3
categories(:child_1).right.should == 4
categories(:child_2).reload
categories(:child_2).left.should == 2
categories(:child_2).right.should == 7
end
it "move to a node to the right child" do
categories(:child_1).move_to_child_with_index(categories(:child_2), 1)
categories(:child_1).parent_id.should == categories(:child_2).id
categories(:child_2_1).left.should == 3
categories(:child_2_1).right.should == 4
categories(:child_1).left.should == 5
categories(:child_1).right.should == 6
categories(:child_2).reload
categories(:child_2).left.should == 2
categories(:child_2).right.should == 7
end
end
it "move_to_child_of_appends_to_end" do it "move_to_child_of_appends_to_end" do
child = Category.create! :name => 'New Child' child = Category.create! :name => 'New Child'
child.move_to_child_of categories(:top_level) child.move_to_child_of categories(:top_level)
@ -444,6 +575,32 @@ describe "AwesomeNestedSet" do
Category.roots.last.to_text.should == output Category.roots.last.to_text.should == output
end end
it "should_move_to_ordered_child" do
node1 = Category.create(:name => 'Node-1')
node2 = Category.create(:name => 'Node-2')
node3 = Category.create(:name => 'Node-3')
node2.move_to_ordered_child_of(node1, "name")
assert_equal node1, node2.parent
assert_equal 1, node1.children.count
node3.move_to_ordered_child_of(node1, "name", true) # acending
assert_equal node1, node3.parent
assert_equal 2, node1.children.count
assert_equal node2.name, node1.children[0].name
assert_equal node3.name, node1.children[1].name
node3.move_to_ordered_child_of(node1, "name", false) # decending
node1.reload
assert_equal node1, node3.parent
assert_equal 2, node1.children.count
assert_equal node3.name, node1.children[0].name
assert_equal node2.name, node1.children[1].name
end
it "should be able to rebuild without validating each record" do it "should be able to rebuild without validating each record" do
root1 = Category.create(:name => 'Root1') root1 = Category.create(:name => 'Root1')
root2 = Category.create(:name => 'Root2') root2 = Category.create(:name => 'Root2')
@ -617,7 +774,15 @@ describe "AwesomeNestedSet" do
end end
it "quoting_of_multi_scope_column_names" do it "quoting_of_multi_scope_column_names" do
["\"notable_id\"", "\"notable_type\""].should == Note.quoted_scope_column_names ## Proper Array Assignment for different DBs as per their quoting column behavior
if Note.connection.adapter_name.match(/Oracle/)
expected_quoted_scope_column_names = ["\"NOTABLE_ID\"", "\"NOTABLE_TYPE\""]
elsif Note.connection.adapter_name.match(/Mysql/)
expected_quoted_scope_column_names = ["`notable_id`", "`notable_type`"]
else
expected_quoted_scope_column_names = ["\"notable_id\"", "\"notable_type\""]
end
expected_quoted_scope_column_names.should == Note.quoted_scope_column_names
end end
it "equal_in_same_scope" do it "equal_in_same_scope" do
@ -730,7 +895,8 @@ describe "AwesomeNestedSet" do
[1, "Child 1"], [1, "Child 1"],
[1, "Child 2"], [1, "Child 2"],
[2, "Child 2.1"], [2, "Child 2.1"],
[1, "Child 3" ]] [1, "Child 3" ]
]
check_structure(Category.root.self_and_descendants, levels) check_structure(Category.root.self_and_descendants, levels)
@ -756,7 +922,8 @@ describe "AwesomeNestedSet" do
[2, "Child 1.2"], [2, "Child 1.2"],
[1, "Child 2"], [1, "Child 2"],
[2, "Child 2.1"], [2, "Child 2.1"],
[1, "Child 3" ]] [1, "Child 3" ]
]
check_structure(Category.root.self_and_descendants, levels) check_structure(Category.root.self_and_descendants, levels)
end end
@ -838,4 +1005,78 @@ describe "AwesomeNestedSet" do
root.after_remove.should == child root.after_remove.should == child
end end
end end
describe 'creating roots with a default scope ordering' do
it "assigns rgt and lft correctly" do
alpha = Order.create(:name => 'Alpha')
gamma = Order.create(:name => 'Gamma')
omega = Order.create(:name => 'Omega')
alpha.lft.should == 1
alpha.rgt.should == 2
gamma.lft.should == 3
gamma.rgt.should == 4
omega.lft.should == 5
omega.rgt.should == 6
end
end
describe 'moving node from one scoped tree to another' do
xit "moves single node correctly" do
root1 = Note.create!(:body => "A-1", :notable_id => 4, :notable_type => 'Category')
child1_1 = Note.create!(:body => "B-1", :notable_id => 4, :notable_type => 'Category')
child1_2 = Note.create!(:body => "C-1", :notable_id => 4, :notable_type => 'Category')
child1_1.move_to_child_of root1
child1_2.move_to_child_of root1
root2 = Note.create!(:body => "A-2", :notable_id => 5, :notable_type => 'Category')
child2_1 = Note.create!(:body => "B-2", :notable_id => 5, :notable_type => 'Category')
child2_2 = Note.create!(:body => "C-2", :notable_id => 5, :notable_type => 'Category')
child2_1.move_to_child_of root2
child2_2.move_to_child_of root2
child1_1.update_attributes!(:notable_id => 5)
child1_1.move_to_child_of root2
root1.children.should == [child1_2]
root2.children.should == [child2_1, child2_2, child1_1]
Note.valid?.should == true
end
xit "moves node with children correctly" do
root1 = Note.create!(:body => "A-1", :notable_id => 4, :notable_type => 'Category')
child1_1 = Note.create!(:body => "B-1", :notable_id => 4, :notable_type => 'Category')
child1_2 = Note.create!(:body => "C-1", :notable_id => 4, :notable_type => 'Category')
child1_1.move_to_child_of root1
child1_2.move_to_child_of child1_1
root2 = Note.create!(:body => "A-2", :notable_id => 5, :notable_type => 'Category')
child2_1 = Note.create!(:body => "B-2", :notable_id => 5, :notable_type => 'Category')
child2_2 = Note.create!(:body => "C-2", :notable_id => 5, :notable_type => 'Category')
child2_1.move_to_child_of root2
child2_2.move_to_child_of root2
child1_1.update_attributes!(:notable_id => 5)
child1_1.move_to_child_of root2
root1.children.should == []
root2.children.should == [child2_1, child2_2, child1_1]
child1_1.children should == [child1_2]
root2.siblings.should == [child2_1, child2_2, child1_1, child1_2]
Note.valid?.should == true
end
end
describe 'specifying custom sort column' do
it "should sort by the default sort column" do
Category.order_column.should == 'lft'
end
it "should sort by custom sort column" do
OrderedCategory.acts_as_nested_set_options[:order_column].should == 'name'
OrderedCategory.order_column.should == 'name'
end
end
end end

View File

@ -16,3 +16,10 @@ mysql:
username: root username: root
password: password:
database: awesome_nested_set_plugin_test database: awesome_nested_set_plugin_test
## Add DB Configuration to run Oracle tests
oracle:
adapter: oracle_enhanced
host: localhost
username: awesome_nested_set_dev
password:
database: xe

View File

@ -5,6 +5,7 @@ ActiveRecord::Schema.define(:version => 0) do
t.column :parent_id, :integer t.column :parent_id, :integer
t.column :lft, :integer t.column :lft, :integer
t.column :rgt, :integer t.column :rgt, :integer
t.column :depth, :integer
t.column :organization_id, :integer t.column :organization_id, :integer
end end
@ -17,6 +18,7 @@ ActiveRecord::Schema.define(:version => 0) do
t.column :parent_id, :integer t.column :parent_id, :integer
t.column :lft, :integer t.column :lft, :integer
t.column :rgt, :integer t.column :rgt, :integer
t.column :depth, :integer
t.column :notable_id, :integer t.column :notable_id, :integer
t.column :notable_type, :string t.column :notable_type, :string
end end
@ -26,6 +28,7 @@ ActiveRecord::Schema.define(:version => 0) do
t.column :mother_id, :integer t.column :mother_id, :integer
t.column :red, :integer t.column :red, :integer
t.column :black, :integer t.column :black, :integer
t.column :pitch, :integer
end end
create_table :things, :force => true do |t| create_table :things, :force => true do |t|
@ -33,6 +36,7 @@ ActiveRecord::Schema.define(:version => 0) do
t.column :parent_id, :integer t.column :parent_id, :integer
t.column :lft, :integer t.column :lft, :integer
t.column :rgt, :integer t.column :rgt, :integer
t.column :depth, :integer
t.column :children_count, :integer t.column :children_count, :integer
end end
@ -41,5 +45,21 @@ ActiveRecord::Schema.define(:version => 0) do
t.column :parent_id, :integer t.column :parent_id, :integer
t.column :lft, :integer t.column :lft, :integer
t.column :rgt, :integer t.column :rgt, :integer
t.column :depth, :integer
end
create_table :orders, :force => true do |t|
t.column :name, :string
t.column :parent_id, :integer
t.column :lft, :integer
t.column :rgt, :integer
t.column :depth, :integer
end
create_table :no_depths, :force => true do |t|
t.column :name, :string
t.column :parent_id, :integer
t.column :lft, :integer
t.column :rgt, :integer
end end
end end

View File

@ -12,8 +12,16 @@ class ScopedCategory < ActiveRecord::Base
acts_as_nested_set :scope => :organization acts_as_nested_set :scope => :organization
end end
class OrderedCategory < ActiveRecord::Base
self.table_name = 'categories'
acts_as_nested_set :order_column => 'name'
end
class RenamedColumns < ActiveRecord::Base class RenamedColumns < ActiveRecord::Base
acts_as_nested_set :parent_column => 'mother_id', :left_column => 'red', :right_column => 'black' acts_as_nested_set :parent_column => 'mother_id',
:left_column => 'red',
:right_column => 'black',
:depth_column => 'pitch'
end end
class Category < ActiveRecord::Base class Category < ActiveRecord::Base
@ -70,3 +78,13 @@ end
class Broken < ActiveRecord::Base class Broken < ActiveRecord::Base
acts_as_nested_set acts_as_nested_set
end end
class Order < ActiveRecord::Base
acts_as_nested_set
default_scope order(:name)
end
class NoDepth < ActiveRecord::Base
acts_as_nested_set
end

View File

@ -1,41 +0,0 @@
require File.dirname(__FILE__) + '/../test_helper'
module CollectiveIdea
module Acts #:nodoc:
module NestedSet #:nodoc:
class AwesomeNestedSetTest < Test::Unit::TestCase
include Helper
fixtures :categories
def test_nested_set_options
expected = [
[" Top Level", 1],
["- Child 1", 2],
['- Child 2', 3],
['-- Child 2.1', 4],
['- Child 3', 5],
[" Top Level 2", 6]
]
actual = nested_set_options(Category) do |c|
"#{'-' * c.level} #{c.name}"
end
assert_equal expected, actual
end
def test_nested_set_options_with_mover
expected = [
[" Top Level", 1],
["- Child 1", 2],
['- Child 3', 5],
[" Top Level 2", 6]
]
actual = nested_set_options(Category, categories(:child_2)) do |c|
"#{'-' * c.level} #{c.name}"
end
assert_equal expected, actual
end
end
end
end
end

View File

@ -1,603 +0,0 @@
require File.dirname(__FILE__) + '/test_helper'
class Note < ActiveRecord::Base
acts_as_nested_set :scope => [:notable_id, :notable_type]
end
class AwesomeNestedSetTest < Test::Unit::TestCase
class Default < ActiveRecord::Base
acts_as_nested_set
self.table_name = 'categories'
end
class Scoped < ActiveRecord::Base
acts_as_nested_set :scope => :organization
self.table_name = 'categories'
end
def test_left_column_default
assert_equal 'lft', Default.acts_as_nested_set_options[:left_column]
end
def test_right_column_default
assert_equal 'rgt', Default.acts_as_nested_set_options[:right_column]
end
def test_parent_column_default
assert_equal 'parent_id', Default.acts_as_nested_set_options[:parent_column]
end
def test_scope_default
assert_nil Default.acts_as_nested_set_options[:scope]
end
def test_left_column_name
assert_equal 'lft', Default.left_column_name
assert_equal 'lft', Default.new.left_column_name
end
def test_right_column_name
assert_equal 'rgt', Default.right_column_name
assert_equal 'rgt', Default.new.right_column_name
end
def test_parent_column_name
assert_equal 'parent_id', Default.parent_column_name
assert_equal 'parent_id', Default.new.parent_column_name
end
def test_quoted_left_column_name
quoted = Default.connection.quote_column_name('lft')
assert_equal quoted, Default.quoted_left_column_name
assert_equal quoted, Default.new.quoted_left_column_name
end
def test_quoted_right_column_name
quoted = Default.connection.quote_column_name('rgt')
assert_equal quoted, Default.quoted_right_column_name
assert_equal quoted, Default.new.quoted_right_column_name
end
def test_left_column_protected_from_assignment
assert_raises(ActiveRecord::ActiveRecordError) { Category.new.lft = 1 }
end
def test_right_column_protected_from_assignment
assert_raises(ActiveRecord::ActiveRecordError) { Category.new.rgt = 1 }
end
def test_parent_column_protected_from_assignment
assert_raises(ActiveRecord::ActiveRecordError) { Category.new.parent_id = 1 }
end
def test_colums_protected_on_initialize
c = Category.new(:lft => 1, :rgt => 2, :parent_id => 3)
assert_nil c.lft
assert_nil c.rgt
assert_nil c.parent_id
end
def test_scoped_appends_id
assert_equal :organization_id, Scoped.acts_as_nested_set_options[:scope]
end
def test_roots_class_method
assert_equal Category.find_all_by_parent_id(nil), Category.roots
end
def test_root_class_method
assert_equal categories(:top_level), Category.root
end
def test_root
assert_equal categories(:top_level), categories(:child_3).root
end
def test_root?
assert categories(:top_level).root?
assert categories(:top_level_2).root?
end
def test_leaves_class_method
assert_equal Category.find(:all, :conditions => "#{Category.right_column_name} - #{Category.left_column_name} = 1"), Category.leaves
assert_equal Category.leaves.count, 4
assert (Category.leaves.include? categories(:child_1))
assert (Category.leaves.include? categories(:child_2_1))
assert (Category.leaves.include? categories(:child_3))
assert (Category.leaves.include? categories(:top_level_2))
end
def test_leaf
assert categories(:child_1).leaf?
assert categories(:child_2_1).leaf?
assert categories(:child_3).leaf?
assert categories(:top_level_2).leaf?
assert !categories(:top_level).leaf?
assert !categories(:child_2).leaf?
end
def test_parent
assert_equal categories(:child_2), categories(:child_2_1).parent
end
def test_self_and_ancestors
child = categories(:child_2_1)
self_and_ancestors = [categories(:top_level), categories(:child_2), child]
assert_equal self_and_ancestors, child.self_and_ancestors
end
def test_ancestors
child = categories(:child_2_1)
ancestors = [categories(:top_level), categories(:child_2)]
assert_equal ancestors, child.ancestors
end
def test_self_and_siblings
child = categories(:child_2)
self_and_siblings = [categories(:child_1), child, categories(:child_3)]
assert_equal self_and_siblings, child.self_and_siblings
assert_nothing_raised do
tops = [categories(:top_level), categories(:top_level_2)]
assert_equal tops, categories(:top_level).self_and_siblings
end
end
def test_siblings
child = categories(:child_2)
siblings = [categories(:child_1), categories(:child_3)]
assert_equal siblings, child.siblings
end
def test_leaves
leaves = [categories(:child_1), categories(:child_2_1), categories(:child_3), categories(:top_level_2)]
assert categories(:top_level).leaves, leaves
end
def test_level
assert_equal 0, categories(:top_level).level
assert_equal 1, categories(:child_1).level
assert_equal 2, categories(:child_2_1).level
end
def test_has_children?
assert categories(:child_2_1).children.empty?
assert !categories(:child_2).children.empty?
assert !categories(:top_level).children.empty?
end
def test_self_and_descendents
parent = categories(:top_level)
self_and_descendants = [parent, categories(:child_1), categories(:child_2),
categories(:child_2_1), categories(:child_3)]
assert_equal self_and_descendants, parent.self_and_descendants
assert_equal self_and_descendants, parent.self_and_descendants.count
end
def test_descendents
lawyers = Category.create!(:name => "lawyers")
us = Category.create!(:name => "United States")
us.move_to_child_of(lawyers)
patent = Category.create!(:name => "Patent Law")
patent.move_to_child_of(us)
lawyers.reload
assert_equal 1, lawyers.children.size
assert_equal 1, us.children.size
assert_equal 2, lawyers.descendants.size
end
def test_self_and_descendents
parent = categories(:top_level)
descendants = [categories(:child_1), categories(:child_2),
categories(:child_2_1), categories(:child_3)]
assert_equal descendants, parent.descendants
end
def test_children
category = categories(:top_level)
category.children.each {|c| assert_equal category.id, c.parent_id }
end
def test_is_or_is_ancestor_of?
assert categories(:top_level).is_or_is_ancestor_of?(categories(:child_1))
assert categories(:top_level).is_or_is_ancestor_of?(categories(:child_2_1))
assert categories(:child_2).is_or_is_ancestor_of?(categories(:child_2_1))
assert !categories(:child_2_1).is_or_is_ancestor_of?(categories(:child_2))
assert !categories(:child_1).is_or_is_ancestor_of?(categories(:child_2))
assert categories(:child_1).is_or_is_ancestor_of?(categories(:child_1))
end
def test_is_ancestor_of?
assert categories(:top_level).is_ancestor_of?(categories(:child_1))
assert categories(:top_level).is_ancestor_of?(categories(:child_2_1))
assert categories(:child_2).is_ancestor_of?(categories(:child_2_1))
assert !categories(:child_2_1).is_ancestor_of?(categories(:child_2))
assert !categories(:child_1).is_ancestor_of?(categories(:child_2))
assert !categories(:child_1).is_ancestor_of?(categories(:child_1))
end
def test_is_or_is_ancestor_of_with_scope
root = Scoped.root
child = root.children.first
assert root.is_or_is_ancestor_of?(child)
child.update_attribute :organization_id, 'different'
assert !root.is_or_is_ancestor_of?(child)
end
def test_is_or_is_descendant_of?
assert categories(:child_1).is_or_is_descendant_of?(categories(:top_level))
assert categories(:child_2_1).is_or_is_descendant_of?(categories(:top_level))
assert categories(:child_2_1).is_or_is_descendant_of?(categories(:child_2))
assert !categories(:child_2).is_or_is_descendant_of?(categories(:child_2_1))
assert !categories(:child_2).is_or_is_descendant_of?(categories(:child_1))
assert categories(:child_1).is_or_is_descendant_of?(categories(:child_1))
end
def test_is_descendant_of?
assert categories(:child_1).is_descendant_of?(categories(:top_level))
assert categories(:child_2_1).is_descendant_of?(categories(:top_level))
assert categories(:child_2_1).is_descendant_of?(categories(:child_2))
assert !categories(:child_2).is_descendant_of?(categories(:child_2_1))
assert !categories(:child_2).is_descendant_of?(categories(:child_1))
assert !categories(:child_1).is_descendant_of?(categories(:child_1))
end
def test_is_or_is_descendant_of_with_scope
root = Scoped.root
child = root.children.first
assert child.is_or_is_descendant_of?(root)
child.update_attribute :organization_id, 'different'
assert !child.is_or_is_descendant_of?(root)
end
def test_same_scope?
root = Scoped.root
child = root.children.first
assert child.same_scope?(root)
child.update_attribute :organization_id, 'different'
assert !child.same_scope?(root)
end
def test_left_sibling
assert_equal categories(:child_1), categories(:child_2).left_sibling
assert_equal categories(:child_2), categories(:child_3).left_sibling
end
def test_left_sibling_of_root
assert_nil categories(:top_level).left_sibling
end
def test_left_sibling_without_siblings
assert_nil categories(:child_2_1).left_sibling
end
def test_left_sibling_of_leftmost_node
assert_nil categories(:child_1).left_sibling
end
def test_right_sibling
assert_equal categories(:child_3), categories(:child_2).right_sibling
assert_equal categories(:child_2), categories(:child_1).right_sibling
end
def test_right_sibling_of_root
assert_equal categories(:top_level_2), categories(:top_level).right_sibling
assert_nil categories(:top_level_2).right_sibling
end
def test_right_sibling_without_siblings
assert_nil categories(:child_2_1).right_sibling
end
def test_right_sibling_of_rightmost_node
assert_nil categories(:child_3).right_sibling
end
def test_move_left
categories(:child_2).move_left
assert_nil categories(:child_2).left_sibling
assert_equal categories(:child_1), categories(:child_2).right_sibling
assert Category.valid?
end
def test_move_right
categories(:child_2).move_right
assert_nil categories(:child_2).right_sibling
assert_equal categories(:child_3), categories(:child_2).left_sibling
assert Category.valid?
end
def test_move_to_left_of
categories(:child_3).move_to_left_of(categories(:child_1))
assert_nil categories(:child_3).left_sibling
assert_equal categories(:child_1), categories(:child_3).right_sibling
assert Category.valid?
end
def test_move_to_right_of
categories(:child_1).move_to_right_of(categories(:child_3))
assert_nil categories(:child_1).right_sibling
assert_equal categories(:child_3), categories(:child_1).left_sibling
assert Category.valid?
end
def test_move_to_root
categories(:child_2).move_to_root
assert_nil categories(:child_2).parent
assert_equal 0, categories(:child_2).level
assert_equal 1, categories(:child_2_1).level
assert_equal 1, categories(:child_2).left
assert_equal 4, categories(:child_2).right
assert Category.valid?
end
def test_move_to_child_of
categories(:child_1).move_to_child_of(categories(:child_3))
assert_equal categories(:child_3).id, categories(:child_1).parent_id
assert Category.valid?
end
def test_move_to_child_of_appends_to_end
child = Category.create! :name => 'New Child'
child.move_to_child_of categories(:top_level)
assert_equal child, categories(:top_level).children.last
end
def test_subtree_move_to_child_of
assert_equal 4, categories(:child_2).left
assert_equal 7, categories(:child_2).right
assert_equal 2, categories(:child_1).left
assert_equal 3, categories(:child_1).right
categories(:child_2).move_to_child_of(categories(:child_1))
assert Category.valid?
assert_equal categories(:child_1).id, categories(:child_2).parent_id
assert_equal 3, categories(:child_2).left
assert_equal 6, categories(:child_2).right
assert_equal 2, categories(:child_1).left
assert_equal 7, categories(:child_1).right
end
def test_slightly_difficult_move_to_child_of
assert_equal 11, categories(:top_level_2).left
assert_equal 12, categories(:top_level_2).right
# create a new top-level node and move single-node top-level tree inside it.
new_top = Category.create(:name => 'New Top')
assert_equal 13, new_top.left
assert_equal 14, new_top.right
categories(:top_level_2).move_to_child_of(new_top)
assert Category.valid?
assert_equal new_top.id, categories(:top_level_2).parent_id
assert_equal 12, categories(:top_level_2).left
assert_equal 13, categories(:top_level_2).right
assert_equal 11, new_top.left
assert_equal 14, new_top.right
end
def test_difficult_move_to_child_of
assert_equal 1, categories(:top_level).left
assert_equal 10, categories(:top_level).right
assert_equal 5, categories(:child_2_1).left
assert_equal 6, categories(:child_2_1).right
# create a new top-level node and move an entire top-level tree inside it.
new_top = Category.create(:name => 'New Top')
categories(:top_level).move_to_child_of(new_top)
categories(:child_2_1).reload
assert Category.valid?
assert_equal new_top.id, categories(:top_level).parent_id
assert_equal 4, categories(:top_level).left
assert_equal 13, categories(:top_level).right
assert_equal 8, categories(:child_2_1).left
assert_equal 9, categories(:child_2_1).right
end
#rebuild swaps the position of the 2 children when added using move_to_child twice onto same parent
def test_move_to_child_more_than_once_per_parent_rebuild
root1 = Category.create(:name => 'Root1')
root2 = Category.create(:name => 'Root2')
root3 = Category.create(:name => 'Root3')
root2.move_to_child_of root1
root3.move_to_child_of root1
output = Category.roots.last.to_text
Category.update_all('lft = null, rgt = null')
Category.rebuild!
assert_equal Category.roots.last.to_text, output
end
# doing move_to_child twice onto same parent from the furthest right first
def test_move_to_child_more_than_once_per_parent_outside_in
node1 = Category.create(:name => 'Node-1')
node2 = Category.create(:name => 'Node-2')
node3 = Category.create(:name => 'Node-3')
node2.move_to_child_of node1
node3.move_to_child_of node1
output = Category.roots.last.to_text
Category.update_all('lft = null, rgt = null')
Category.rebuild!
assert_equal Category.roots.last.to_text, output
end
def test_valid_with_null_lefts
assert Category.valid?
Category.update_all('lft = null')
assert !Category.valid?
end
def test_valid_with_null_rights
assert Category.valid?
Category.update_all('rgt = null')
assert !Category.valid?
end
def test_valid_with_missing_intermediate_node
# Even though child_2_1 will still exist, it is a sign of a sloppy delete, not an invalid tree.
assert Category.valid?
Category.delete(categories(:child_2).id)
assert Category.valid?
end
def test_valid_with_overlapping_and_rights
assert Category.valid?
categories(:top_level_2)['lft'] = 0
categories(:top_level_2).save
assert !Category.valid?
end
def test_rebuild
assert Category.valid?
before_text = Category.root.to_text
Category.update_all('lft = null, rgt = null')
Category.rebuild!
assert Category.valid?
assert_equal before_text, Category.root.to_text
end
def test_move_possible_for_sibling
assert categories(:child_2).move_possible?(categories(:child_1))
end
def test_move_not_possible_to_self
assert !categories(:top_level).move_possible?(categories(:top_level))
end
def test_move_not_possible_to_parent
categories(:top_level).descendants.each do |descendant|
assert !categories(:top_level).move_possible?(descendant)
assert descendant.move_possible?(categories(:top_level))
end
end
def test_is_or_is_ancestor_of?
[:child_1, :child_2, :child_2_1, :child_3].each do |c|
assert categories(:top_level).is_or_is_ancestor_of?(categories(c))
end
assert !categories(:top_level).is_or_is_ancestor_of?(categories(:top_level_2))
end
def test_left_and_rights_valid_with_blank_left
assert Category.left_and_rights_valid?
categories(:child_2)[:lft] = nil
categories(:child_2).save(false)
assert !Category.left_and_rights_valid?
end
def test_left_and_rights_valid_with_blank_right
assert Category.left_and_rights_valid?
categories(:child_2)[:rgt] = nil
categories(:child_2).save(false)
assert !Category.left_and_rights_valid?
end
def test_left_and_rights_valid_with_equal
assert Category.left_and_rights_valid?
categories(:top_level_2)[:lft] = categories(:top_level_2)[:rgt]
categories(:top_level_2).save(false)
assert !Category.left_and_rights_valid?
end
def test_left_and_rights_valid_with_left_equal_to_parent
assert Category.left_and_rights_valid?
categories(:child_2)[:lft] = categories(:top_level)[:lft]
categories(:child_2).save(false)
assert !Category.left_and_rights_valid?
end
def test_left_and_rights_valid_with_right_equal_to_parent
assert Category.left_and_rights_valid?
categories(:child_2)[:rgt] = categories(:top_level)[:rgt]
categories(:child_2).save(false)
assert !Category.left_and_rights_valid?
end
def test_moving_dirty_objects_doesnt_invalidate_tree
r1 = Category.create
r2 = Category.create
r3 = Category.create
r4 = Category.create
nodes = [r1, r2, r3, r4]
r2.move_to_child_of(r1)
assert Category.valid?
r3.move_to_child_of(r1)
assert Category.valid?
r4.move_to_child_of(r2)
assert Category.valid?
end
def test_multi_scoped_no_duplicates_for_columns?
assert_nothing_raised do
Note.no_duplicates_for_columns?
end
end
def test_multi_scoped_all_roots_valid?
assert_nothing_raised do
Note.all_roots_valid?
end
end
def test_multi_scoped
note1 = Note.create!(:body => "A", :notable_id => 2, :notable_type => 'Category')
note2 = Note.create!(:body => "B", :notable_id => 2, :notable_type => 'Category')
note3 = Note.create!(:body => "C", :notable_id => 2, :notable_type => 'Default')
assert_equal [note1, note2], note1.self_and_siblings
assert_equal [note3], note3.self_and_siblings
end
def test_multi_scoped_rebuild
root = Note.create!(:body => "A", :notable_id => 3, :notable_type => 'Category')
child1 = Note.create!(:body => "B", :notable_id => 3, :notable_type => 'Category')
child2 = Note.create!(:body => "C", :notable_id => 3, :notable_type => 'Category')
child1.move_to_child_of root
child2.move_to_child_of root
Note.update_all('lft = null, rgt = null')
Note.rebuild!
assert_equal Note.roots.find_by_body('A'), root
assert_equal [child1, child2], Note.roots.find_by_body('A').children
end
def test_same_scope_with_multi_scopes
assert_nothing_raised do
notes(:scope1).same_scope?(notes(:child_1))
end
assert notes(:scope1).same_scope?(notes(:child_1))
assert notes(:child_1).same_scope?(notes(:scope1))
assert !notes(:scope1).same_scope?(notes(:scope2))
end
def test_quoting_of_multi_scope_column_names
assert_equal ["\"notable_id\"", "\"notable_type\""], Note.quoted_scope_column_names
end
def test_equal_in_same_scope
assert_equal notes(:scope1), notes(:scope1)
assert_not_equal notes(:scope1), notes(:child_1)
end
def test_equal_in_different_scopes
assert_not_equal notes(:scope1), notes(:scope2)
end
end

View File

@ -1,18 +0,0 @@
sqlite3:
adapter: sqlite3
dbfile: awesome_nested_set.sqlite3.db
sqlite3mem:
:adapter: sqlite3
:dbfile: ":memory:"
postgresql:
:adapter: postgresql
:username: postgres
:password: postgres
:database: awesome_nested_set_plugin_test
:min_messages: ERROR
mysql:
:adapter: mysql
:host: localhost
:username: root
:password:
:database: awesome_nested_set_plugin_test

View File

@ -1,23 +0,0 @@
ActiveRecord::Schema.define(:version => 0) do
create_table :categories, :force => true do |t|
t.column :name, :string
t.column :parent_id, :integer
t.column :lft, :integer
t.column :rgt, :integer
t.column :organization_id, :integer
end
create_table :departments, :force => true do |t|
t.column :name, :string
end
create_table :notes, :force => true do |t|
t.column :body, :text
t.column :parent_id, :integer
t.column :lft, :integer
t.column :rgt, :integer
t.column :notable_id, :integer
t.column :notable_type, :string
end
end

View File

@ -1,34 +0,0 @@
top_level:
id: 1
name: Top Level
lft: 1
rgt: 10
child_1:
id: 2
name: Child 1
parent_id: 1
lft: 2
rgt: 3
child_2:
id: 3
name: Child 2
parent_id: 1
lft: 4
rgt: 7
child_2_1:
id: 4
name: Child 2.1
parent_id: 3
lft: 5
rgt: 6
child_3:
id: 5
name: Child 3
parent_id: 1
lft: 8
rgt: 9
top_level_2:
id: 6
name: Top Level 2
lft: 11
rgt: 12

View File

@ -1,15 +0,0 @@
class Category < ActiveRecord::Base
acts_as_nested_set
def to_s
name
end
def recurse &block
block.call self, lambda{
self.children.each do |child|
child.recurse &block
end
}
end
end

View File

@ -1,3 +0,0 @@
top:
id: 1
name: Top

View File

@ -1,38 +0,0 @@
scope1:
id: 1
body: Top Level
lft: 1
rgt: 10
notable_id: 1
notable_type: Category
child_1:
id: 2
body: Child 1
parent_id: 1
lft: 2
rgt: 3
notable_id: 1
notable_type: Category
child_2:
id: 3
body: Child 2
parent_id: 1
lft: 4
rgt: 7
notable_id: 1
notable_type: Category
child_3:
id: 4
body: Child 3
parent_id: 1
lft: 8
rgt: 9
notable_id: 1
notable_type: Category
scope2:
id: 5
body: Top Level 2
lft: 1
rgt: 2
notable_id: 1
notable_type: Departments

View File

@ -1,31 +0,0 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
plugin_test_dir = File.dirname(__FILE__)
require 'rubygems'
require 'test/unit'
require 'multi_rails_init'
# gem 'activerecord', '>= 2.0'
require 'active_record'
require 'action_controller'
require 'action_view'
require 'active_record/fixtures'
require plugin_test_dir + '/../init.rb'
ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log")
ActiveRecord::Base.configurations = YAML::load(IO.read(plugin_test_dir + "/db/database.yml"))
ActiveRecord::Base.establish_connection(ENV["DB"] || "sqlite3mem")
ActiveRecord::Migration.verbose = false
load(File.join(plugin_test_dir, "db", "schema.rb"))
Dir["#{plugin_test_dir}/fixtures/*.rb"].each {|file| require file }
class Test::Unit::TestCase #:nodoc:
self.fixture_path = File.dirname(__FILE__) + "/fixtures/"
self.use_transactional_fixtures = true
self.use_instantiated_fixtures = false
fixtures :categories, :notes, :departments
end