Moved Rails plugins required by the core to lib/plugins.

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@9533 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Jean-Philippe Lang
2012-04-26 17:23:24 +00:00
parent 26868d8b14
commit 8d73ddf73f
237 changed files with 15 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
Autotest.add_hook :initialize do |at|
at.clear_mappings
at.add_mapping %r%^lib/(.*)\.rb$% do |_, m|
at.files_matching %r%^test/#{m[1]}_test.rb$%
end
at.add_mapping(%r%^test/.*\.rb$%) {|filename, _| filename }
at.add_mapping %r%^test/fixtures/(.*)s.yml% do |_, _|
at.files_matching %r%^test/.*\.rb$%
end
end

View File

@@ -0,0 +1,14 @@
notifications:
email:
- parndt@gmail.com
env:
- DB=sqlite3
- DB=sqlite3mem
- DB=postgresql
- DB=mysql
rvm:
- 1.8.7
- 1.9.2
- 1.9.3
- rbx-2.0
- jruby

View File

@@ -0,0 +1,14 @@
2.0.2
* Fixed deprecation warning under Rails 3.1 [Philip Arndt]
* Converted Test::Unit matchers to RSpec. [Uģis Ozols]
* Added inverse_of to associations to improve performance rendering trees. [Sergio Cambra]
* Added row locking and fixed some race conditions. [Markus J. Q. Roberts]
2.0.1
* Fixed a bug with move_to not using nested_set_scope [Andreas Sekine]
2.0.0.pre
* Expect Rails 3
* Changed how callbacks work. Returning false in a before_move action does not block save operations. Use a validation or exception in the callback if you need that.
* Switched to RSpec
* Remove use of Comparable

View File

@@ -0,0 +1,20 @@
Copyright (c) 2007-2011 Collective Idea
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,100 @@
= AwesomeNestedSet
Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models. It is replacement for acts_as_nested_set and BetterNestedSet, but more awesome.
Version 2 supports Rails 3. Gem versions prior to 2.0 support Rails 2.
== What makes this so awesome?
This is a new implementation of nested set based off of BetterNestedSet that fixes some bugs, removes tons of duplication, adds a few useful methods, and adds STI support.
== Installation
Add to your Gemfile:
gem 'awesome_nested_set'
== Usage
To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id:
class CreateCategories < ActiveRecord::Migration
def self.up
create_table :categories do |t|
t.string :name
t.integer :parent_id
t.integer :lft
t.integer :rgt
end
end
def self.down
drop_table :categories
end
end
Enable the nested set functionality by declaring acts_as_nested_set on your model
class Category < ActiveRecord::Base
acts_as_nested_set
end
Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet for more info.
== Protecting attributes from mass assignment
It's generally best to "white list" the attributes that can be used in mass assignment:
class Category < ActiveRecord::Base
acts_as_nested_set
attr_accessible :name, :parent_id
end
If for some reason that is not possible, you will probably want to protect the lft and rgt attributes:
class Category < ActiveRecord::Base
acts_as_nested_set
attr_protected :lft, :rgt
end
== Conversion from other trees
Coming from acts_as_tree or another system where you only have a parent_id? No problem. Simply add the lft & rgt fields as above, and then run
Category.rebuild!
Your tree will be converted to a valid nested set. Awesome!
== View Helper
The view helper is called #nested_set_options.
Example usage:
<%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %>
<%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %>
See CollectiveIdea::Acts::NestedSet::Helper for more information about the helpers.
== References
You can learn more about nested sets at: http://threebit.net/tutorials/nestedset/tutorial1.html
== How to contribute
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.
http://github.com/collectiveidea/awesome_nested_set/issues/
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:
1. Fork the project on github.
http://github.com/collectiveidea/awesome_nested_set/
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
4. Send a pull request.
Copyright ©2008 Collective Idea, released under the MIT license

View File

@@ -0,0 +1,28 @@
# -*- encoding: utf-8 -*-
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
require 'rubygems'
require 'bundler/setup'
require 'awesome_nested_set/version'
require "rspec/core/rake_task"
RSpec::Core::RakeTask.new(:spec)
task :default => :spec
task :build do
system "gem build awesome_nested_set.gemspec"
end
task :release => :build do
system "gem push awesome_nested_set-#{ActsAsGeocodable::VERSION}.gem"
end
require 'rdoc/task'
desc 'Generate documentation for the awesome_nested_set plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'AwesomeNestedSet'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View File

@@ -0,0 +1,22 @@
# -*- encoding: utf-8 -*-
lib = File.expand_path('../lib/', __FILE__)
$:.unshift lib unless $:.include?(lib)
require 'awesome_nested_set/version'
Gem::Specification.new do |s|
s.name = %q{awesome_nested_set}
s.version = ::AwesomeNestedSet::VERSION
s.authors = ["Brandon Keepers", "Daniel Morrison", "Philip Arndt"]
s.description = %q{An awesome nested set implementation for Active Record}
s.email = %q{info@collectiveidea.com}
s.extra_rdoc_files = [
"README.rdoc"
]
s.files = Dir.glob("lib/**/*") + %w(MIT-LICENSE README.rdoc CHANGELOG)
s.homepage = %q{http://github.com/collectiveidea/awesome_nested_set}
s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.6}
s.summary = %q{An awesome nested set implementation for Active Record}
s.add_runtime_dependency 'activerecord', '>= 3.0.0'
end

View File

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

View File

@@ -0,0 +1,7 @@
require 'awesome_nested_set/awesome_nested_set'
ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet
if defined?(ActionView)
require 'awesome_nested_set/helper'
ActionView::Base.send :include, CollectiveIdea::Acts::NestedSet::Helper
end

View File

@@ -0,0 +1,603 @@
module CollectiveIdea #:nodoc:
module Acts #:nodoc:
module NestedSet #:nodoc:
# This acts provides Nested Set functionality. Nested Set is a smart way to implement
# an _ordered_ tree, with the added feature that you can select the children and all of their
# descendants with a single query. The drawback is that insertion or move need some complex
# sql queries. But everything is done here by this module!
#
# Nested sets are appropriate each time you want either an orderd tree (menus,
# commercial categories) or an efficient way of querying big trees (threaded posts).
#
# == API
#
# Methods names are aligned with acts_as_tree as much as possible to make replacment from one
# by another easier.
#
# item.children.create(:name => "child1")
#
# Configuration options are:
#
# * +: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"
# * +:right_column+ - column name for right boundry data, default "rgt"
# * +: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
# can also pass an array to scope by multiple attributes.
# Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
# * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
# child objects are destroyed alongside this object by calling their destroy
# method. If set to :delete_all (default), all the child objects are deleted
# without calling their destroy method.
# * +:counter_cache+ adds a counter cache for the number of children.
# defaults to false.
# Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
#
# See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
# CollectiveIdea::Acts::NestedSet::Model::InstanceMethods for a list of instance methods added
# to acts_as_nested_set models
def acts_as_nested_set(options = {})
options = {
:parent_column => 'parent_id',
:left_column => 'lft',
:right_column => 'rgt',
:dependent => :delete_all, # or :destroy
:counter_cache => false,
:order => 'id'
}.merge(options)
if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
options[:scope] = "#{options[:scope]}_id".intern
end
class_attribute :acts_as_nested_set_options
self.acts_as_nested_set_options = options
include CollectiveIdea::Acts::NestedSet::Model
include Columns
extend Columns
belongs_to :parent, :class_name => self.base_class.to_s,
:foreign_key => parent_column_name,
:counter_cache => options[:counter_cache],
:inverse_of => :children
has_many :children, :class_name => self.base_class.to_s,
:foreign_key => parent_column_name, :order => left_column_name,
:inverse_of => :parent,
:before_add => options[:before_add],
:after_add => options[:after_add],
:before_remove => options[:before_remove],
:after_remove => options[:after_remove]
attr_accessor :skip_before_destroy
before_create :set_default_left_and_right
before_save :store_new_parent
after_save :move_to_new_parent
before_destroy :destroy_descendants
# no assignment to structure fields
[left_column_name, right_column_name].each do |column|
module_eval <<-"end_eval", __FILE__, __LINE__
def #{column}=(x)
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
end
end_eval
end
define_model_callbacks :move
end
module Model
extend ActiveSupport::Concern
module ClassMethods
# Returns the first root
def root
roots.first
end
def roots
where(parent_column_name => nil).order(quoted_left_column_name)
end
def leaves
where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
end
def valid?
left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
end
def left_and_rights_valid?
joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
"#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}").
where(
"#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
"#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
"#{quoted_table_name}.#{quoted_left_column_name} >= " +
"#{quoted_table_name}.#{quoted_right_column_name} OR " +
"(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
"(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
"#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
).count == 0
end
def no_duplicates_for_columns?
scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
connection.quote_column_name(c)
end.push(nil).join(", ")
[quoted_left_column_name, quoted_right_column_name].all? do |column|
# No duplicates
select("#{scope_string}#{column}, COUNT(#{column})").
group("#{scope_string}#{column}").
having("COUNT(#{column}) > 1").
first.nil?
end
end
# Wrapper for each_root_valid? that can deal with scope.
def all_roots_valid?
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|
each_root_valid?(grouped_roots)
end
else
each_root_valid?(roots)
end
end
def each_root_valid?(roots_to_validate)
left = right = 0
roots_to_validate.all? do |root|
(root.left > left && root.right > right).tap do
left = root.left
right = root.right
end
end
end
# Rebuilds the left & rights if unset or invalid.
# Also very useful for converting from acts_as_tree.
def rebuild!(validate_nodes = true)
# Don't rebuild a valid tree.
return true if valid?
scope = lambda{|node|}
if acts_as_nested_set_options[:scope]
scope = lambda{|node|
scope_column_names.inject(""){|str, column_name|
str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
}
}
end
indices = {}
set_left_and_rights = lambda do |node|
# set left
node[left_column_name] = indices[scope.call(node)] += 1
# 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) }
# set right
node[right_column_name] = indices[scope.call(node)] += 1
node.save!(:validate => validate_nodes)
end
# Find root node(s)
root_nodes = where("#{quoted_parent_column_name} IS NULL").order("#{quoted_left_column_name}, #{quoted_right_column_name}, id").each do |root_node|
# setup index for this scope
indices[scope.call(root_node)] ||= 0
set_left_and_rights.call(root_node)
end
end
# Iterates over tree elements and determines the current level in the tree.
# Only accepts default ordering, odering by an other column than lft
# does not work. This method is much more efficent than calling level
# because it doesn't require any additional database queries.
#
# Example:
# Category.each_with_level(Category.root.self_and_descendants) do |o, level|
#
def each_with_level(objects)
path = [nil]
objects.each do |o|
if o.parent_id != path.last
# we are on a new level, did we decent or ascent?
if path.include?(o.parent_id)
# remove wrong wrong tailing paths elements
path.pop while path.last != o.parent_id
else
path << o.parent_id
end
end
yield(o, path.length - 1)
end
end
end
# 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.ancestors.find(:all, :conditions => "name like '%foo%'")
module InstanceMethods
# Value of the parent column
def parent_id
self[parent_column_name]
end
# Value of the left column
def left
self[left_column_name]
end
# Value of the right column
def right
self[right_column_name]
end
# Returns true if this is a root node.
def root?
parent_id.nil?
end
def leaf?
new_record? || (right - left == 1)
end
# Returns true is this is a child node
def child?
!parent_id.nil?
end
# Returns root
def root
self_and_ancestors.where(parent_column_name => nil).first
end
# Returns the array of all parents and self
def self_and_ancestors
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
])
end
# Returns an array of all parents
def ancestors
without_self self_and_ancestors
end
# Returns the array of all children of the parent, including self
def self_and_siblings
nested_set_scope.where(parent_column_name => parent_id)
end
# Returns the array of all children of the parent, except self
def siblings
without_self self_and_siblings
end
# Returns a set of all of its nested children which do not have children
def leaves
descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
end
# Returns the level of this object in the tree
# root level is 0
def level
parent_id.nil? ? 0 : ancestors.count
end
# Returns a set of itself and all of its nested children
def self_and_descendants
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
])
end
# Returns a set of all of its children and nested children
def descendants
without_self self_and_descendants
end
def is_descendant_of?(other)
other.left < self.left && self.left < other.right && same_scope?(other)
end
def is_or_is_descendant_of?(other)
other.left <= self.left && self.left < other.right && same_scope?(other)
end
def is_ancestor_of?(other)
self.left < other.left && other.left < self.right && same_scope?(other)
end
def is_or_is_ancestor_of?(other)
self.left <= other.left && other.left < self.right && same_scope?(other)
end
# Check if other model is in the same scope
def same_scope?(other)
Array(acts_as_nested_set_options[:scope]).all? do |attr|
self.send(attr) == other.send(attr)
end
end
# Find the first sibling to the left
def left_sibling
siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]).
order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last
end
# Find the first sibling to the right
def right_sibling
siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first
end
# Shorthand method for finding the left sibling and moving to the left of it.
def move_left
move_to_left_of left_sibling
end
# Shorthand method for finding the right sibling and moving to the right of it.
def move_right
move_to_right_of right_sibling
end
# Move the node to the left of another node (you can pass id only)
def move_to_left_of(node)
move_to node, :left
end
# Move the node to the left of another node (you can pass id only)
def move_to_right_of(node)
move_to node, :right
end
# Move the node to the child of another node (you can pass id only)
def move_to_child_of(node)
move_to node, :child
end
# Move the node to root nodes
def move_to_root
move_to nil, :root
end
def move_possible?(target)
self != target && # Can't target self
same_scope?(target) && # can't be in different scopes
# !(left..right).include?(target.left..target.right) # this needs tested more
# detect impossible move
!((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
end
def to_text
self_and_descendants.map do |node|
"#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
end.join("\n")
end
protected
def without_self(scope)
scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
end
# All nested set queries should use this nested_set_scope, which performs finds on
# the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
# declaration.
def nested_set_scope(options = {})
options = {:order => "#{self.class.quoted_table_name}.#{quoted_left_column_name}"}.merge(options)
scopes = Array(acts_as_nested_set_options[:scope])
options[:conditions] = scopes.inject({}) do |conditions,attr|
conditions.merge attr => self[attr]
end unless scopes.empty?
self.class.base_class.scoped options
end
def store_new_parent
@move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
true # force callback to return true
end
def move_to_new_parent
if @move_to_new_parent_id.nil?
move_to_root
elsif @move_to_new_parent_id
move_to_child_of(@move_to_new_parent_id)
end
end
# on creation, set automatically lft and rgt to the end of the tree
def set_default_left_and_right
highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").find(:first, :limit => 1,:lock => true )
maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
# adds the new node to the right of all existing nodes
self[left_column_name] = maxright + 1
self[right_column_name] = maxright + 2
end
def in_tenacious_transaction(&block)
retry_count = 0
begin
transaction(&block)
rescue ActiveRecord::StatementInvalid => error
raise unless connection.open_transactions.zero?
raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
raise unless retry_count < 10
retry_count += 1
logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
sleep(rand(retry_count)*0.1) # Aloha protocol
retry
end
end
# Prunes a branch off of the tree, shifting all of the elements on the right
# back to the left so the counts still work.
def destroy_descendants
return if right.nil? || left.nil? || skip_before_destroy
in_tenacious_transaction do
reload_nested_set
# select the rows in the model that extend past the deletion point and apply a lock
self.class.base_class.find(:all,
:select => "id",
:conditions => ["#{quoted_left_column_name} >= ?", left],
:lock => true
)
if acts_as_nested_set_options[:dependent] == :destroy
descendants.each do |model|
model.skip_before_destroy = true
model.destroy
end
else
nested_set_scope.delete_all(
["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
left, right]
)
end
# update lefts and rights for remaining nodes
diff = right - left + 1
nested_set_scope.update_all(
["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
["#{quoted_left_column_name} > ?", right]
)
nested_set_scope.update_all(
["#{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
self.skip_before_destroy = true
end
end
# reload left, right, and parent
def reload_nested_set
reload(
:select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}",
:lock => true
)
end
def move_to(target, position)
raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
run_callbacks :move do
in_tenacious_transaction do
if target.is_a? self.class.base_class
target.reload_nested_set
elsif position != :root
# load object if node is not an object
target = nested_set_scope.find(target)
end
self.reload_nested_set
unless position == :root || move_possible?(target)
raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
end
bound = case position
when :child; target[right_column_name]
when :left; target[left_column_name]
when :right; target[right_column_name] + 1
when :root; 1
else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
end
if bound > self[right_column_name]
bound = bound - 1
other_bound = self[right_column_name] + 1
else
other_bound = self[left_column_name] - 1
end
# there would be no change
return if bound == self[right_column_name] || bound == self[left_column_name]
# 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_column_name], self[right_column_name], bound, other_bound].sort
# select the rows in the model between a and d, and apply a lock
self.class.base_class.select('id').lock(true).where(
["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}]
)
new_parent = case position
when :child; target.id
when :root; nil
else target[parent_column_name]
end
self.nested_set_scope.update_all([
"#{quoted_left_column_name} = CASE " +
"WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
"THEN #{quoted_left_column_name} + :d - :b " +
"WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
"THEN #{quoted_left_column_name} + :a - :c " +
"ELSE #{quoted_left_column_name} END, " +
"#{quoted_right_column_name} = CASE " +
"WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
"THEN #{quoted_right_column_name} + :d - :b " +
"WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
"THEN #{quoted_right_column_name} + :a - :c " +
"ELSE #{quoted_right_column_name} END, " +
"#{quoted_parent_column_name} = CASE " +
"WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
"ELSE #{quoted_parent_column_name} END",
{:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
])
end
target.reload_nested_set if target
self.reload_nested_set
end
end
end
end
# Mixed into both classes and instances to provide easy access to the column names
module Columns
def left_column_name
acts_as_nested_set_options[:left_column]
end
def right_column_name
acts_as_nested_set_options[:right_column]
end
def parent_column_name
acts_as_nested_set_options[:parent_column]
end
def scope_column_names
Array(acts_as_nested_set_options[:scope])
end
def quoted_left_column_name
connection.quote_column_name(left_column_name)
end
def quoted_right_column_name
connection.quote_column_name(right_column_name)
end
def quoted_parent_column_name
connection.quote_column_name(parent_column_name)
end
def quoted_scope_column_names
scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
end
end
end
end
end

View File

@@ -0,0 +1,44 @@
module CollectiveIdea #:nodoc:
module Acts #:nodoc:
module NestedSet #:nodoc:
# This module provides some helpers for the model classes using acts_as_nested_set.
# It is included by default in all views.
#
module Helper
# Returns options for select.
# 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
# * +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, @category) {|i|
# "#{'' * i.level} #{i.name}"
# }) %>
#
def nested_set_options(class_or_item, 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 = []
items.each do |root|
result += root.self_and_descendants.map do |i|
if mover.nil? || mover.new_record? || mover.move_possible?(i)
[yield(i), i.id]
end
end.compact
end
result
end
end
end
end
end

View File

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

View File

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,67 @@
require 'spec_helper'
describe "Helper" do
include CollectiveIdea::Acts::NestedSet::Helper
before(:all) do
self.class.fixtures :categories
end
describe "nested_set_options" do
it "test_nested_set_options" 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_mover" do
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
actual.should == expected
end
it "test_nested_set_options_with_array_as_argument_without_mover" 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.all) do |c|
"#{'-' * c.level} #{c.name}"
end
actual.should == expected
end
it "test_nested_set_options_with_array_as_argument_with_mover" do
expected = [
[" Top Level", 1],
["- Child 1", 2],
['- Child 3', 5],
[" Top Level 2", 6]
]
actual = nested_set_options(Category.all, categories(:child_2)) do |c|
"#{'-' * c.level} #{c.name}"
end
actual.should == expected
end
end
end

View File

@@ -0,0 +1,841 @@
require 'spec_helper'
describe "AwesomeNestedSet" do
before(:all) do
self.class.fixtures :categories, :departments, :notes, :things, :brokens
end
describe "defaults" do
it "should have left_column_default" do
Default.acts_as_nested_set_options[:left_column].should == 'lft'
end
it "should have right_column_default" do
Default.acts_as_nested_set_options[:right_column].should == 'rgt'
end
it "should have parent_column_default" do
Default.acts_as_nested_set_options[:parent_column].should == 'parent_id'
end
it "should have scope_default" do
Default.acts_as_nested_set_options[:scope].should be_nil
end
it "should have left_column_name" do
Default.left_column_name.should == 'lft'
Default.new.left_column_name.should == 'lft'
RenamedColumns.left_column_name.should == 'red'
RenamedColumns.new.left_column_name.should == 'red'
end
it "should have right_column_name" do
Default.right_column_name.should == 'rgt'
Default.new.right_column_name.should == 'rgt'
RenamedColumns.right_column_name.should == 'black'
RenamedColumns.new.right_column_name.should == 'black'
end
it "should have parent_column_name" do
Default.parent_column_name.should == 'parent_id'
Default.new.parent_column_name.should == 'parent_id'
RenamedColumns.parent_column_name.should == 'mother_id'
RenamedColumns.new.parent_column_name.should == 'mother_id'
end
end
it "creation_with_altered_column_names" do
lambda {
RenamedColumns.create!()
}.should_not raise_exception
end
it "creation when existing record has nil left column" do
assert_nothing_raised do
Broken.create!
end
end
it "quoted_left_column_name" do
quoted = Default.connection.quote_column_name('lft')
Default.quoted_left_column_name.should == quoted
Default.new.quoted_left_column_name.should == quoted
end
it "quoted_right_column_name" do
quoted = Default.connection.quote_column_name('rgt')
Default.quoted_right_column_name.should == quoted
Default.new.quoted_right_column_name.should == quoted
end
it "left_column_protected_from_assignment" do
lambda {
Category.new.lft = 1
}.should raise_exception(ActiveRecord::ActiveRecordError)
end
it "right_column_protected_from_assignment" do
lambda {
Category.new.rgt = 1
}.should raise_exception(ActiveRecord::ActiveRecordError)
end
it "scoped_appends_id" do
ScopedCategory.acts_as_nested_set_options[:scope].should == :organization_id
end
it "roots_class_method" do
Category.roots.should == Category.find_all_by_parent_id(nil)
end
it "root_class_method" do
Category.root.should == categories(:top_level)
end
it "root" do
categories(:child_3).root.should == categories(:top_level)
end
it "root?" do
categories(:top_level).root?.should be_true
categories(:top_level_2).root?.should be_true
end
it "leaves_class_method" do
Category.find(:all, :conditions => "#{Category.right_column_name} - #{Category.left_column_name} = 1").should == Category.leaves
Category.leaves.count.should == 4
Category.leaves.should include(categories(:child_1))
Category.leaves.should include(categories(:child_2_1))
Category.leaves.should include(categories(:child_3))
Category.leaves.should include(categories(:top_level_2))
end
it "leaf" do
categories(:child_1).leaf?.should be_true
categories(:child_2_1).leaf?.should be_true
categories(:child_3).leaf?.should be_true
categories(:top_level_2).leaf?.should be_true
categories(:top_level).leaf?.should be_false
categories(:child_2).leaf?.should be_false
Category.new.leaf?.should be_false
end
it "parent" do
categories(:child_2_1).parent.should == categories(:child_2)
end
it "self_and_ancestors" do
child = categories(:child_2_1)
self_and_ancestors = [categories(:top_level), categories(:child_2), child]
self_and_ancestors.should == child.self_and_ancestors
end
it "ancestors" do
child = categories(:child_2_1)
ancestors = [categories(:top_level), categories(:child_2)]
ancestors.should == child.ancestors
end
it "self_and_siblings" do
child = categories(:child_2)
self_and_siblings = [categories(:child_1), child, categories(:child_3)]
self_and_siblings.should == child.self_and_siblings
lambda do
tops = [categories(:top_level), categories(:top_level_2)]
assert_equal tops, categories(:top_level).self_and_siblings
end.should_not raise_exception
end
it "siblings" do
child = categories(:child_2)
siblings = [categories(:child_1), categories(:child_3)]
siblings.should == child.siblings
end
it "leaves" do
leaves = [categories(:child_1), categories(:child_2_1), categories(:child_3)]
categories(:top_level).leaves.should == leaves
end
it "level" do
categories(:top_level).level.should == 0
categories(:child_1).level.should == 1
categories(:child_2_1).level.should == 2
end
it "has_children?" do
categories(:child_2_1).children.empty?.should be_true
categories(:child_2).children.empty?.should be_false
categories(:top_level).children.empty?.should be_false
end
it "self_and_descendents" do
parent = categories(:top_level)
self_and_descendants = [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.count.should == parent.self_and_descendants.count
end
it "descendents" do
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
lawyers.children.size.should == 1
us.children.size.should == 1
lawyers.descendants.size.should == 2
end
it "self_and_descendents" do
parent = categories(:top_level)
descendants = [categories(:child_1), categories(:child_2),
categories(:child_2_1), categories(:child_3)]
descendants.should == parent.descendants
end
it "children" do
category = categories(:top_level)
category.children.each {|c| category.id.should == c.parent_id }
end
it "order_of_children" do
categories(:child_2).move_left
categories(:child_2).should == categories(:top_level).children[0]
categories(:child_1).should == categories(:top_level).children[1]
categories(:child_3).should == categories(:top_level).children[2]
end
it "is_or_is_ancestor_of?" do
categories(:top_level).is_or_is_ancestor_of?(categories(:child_1)).should be_true
categories(:top_level).is_or_is_ancestor_of?(categories(:child_2_1)).should be_true
categories(:child_2).is_or_is_ancestor_of?(categories(:child_2_1)).should be_true
categories(:child_2_1).is_or_is_ancestor_of?(categories(:child_2)).should be_false
categories(:child_1).is_or_is_ancestor_of?(categories(:child_2)).should be_false
categories(:child_1).is_or_is_ancestor_of?(categories(:child_1)).should be_true
end
it "is_ancestor_of?" do
categories(:top_level).is_ancestor_of?(categories(:child_1)).should be_true
categories(:top_level).is_ancestor_of?(categories(:child_2_1)).should be_true
categories(:child_2).is_ancestor_of?(categories(:child_2_1)).should be_true
categories(:child_2_1).is_ancestor_of?(categories(:child_2)).should be_false
categories(:child_1).is_ancestor_of?(categories(:child_2)).should be_false
categories(:child_1).is_ancestor_of?(categories(:child_1)).should be_false
end
it "is_or_is_ancestor_of_with_scope" do
root = ScopedCategory.root
child = root.children.first
root.is_or_is_ancestor_of?(child).should be_true
child.update_attribute :organization_id, 'different'
root.is_or_is_ancestor_of?(child).should be_false
end
it "is_or_is_descendant_of?" do
categories(:child_1).is_or_is_descendant_of?(categories(:top_level)).should be_true
categories(:child_2_1).is_or_is_descendant_of?(categories(:top_level)).should be_true
categories(:child_2_1).is_or_is_descendant_of?(categories(:child_2)).should be_true
categories(:child_2).is_or_is_descendant_of?(categories(:child_2_1)).should be_false
categories(:child_2).is_or_is_descendant_of?(categories(:child_1)).should be_false
categories(:child_1).is_or_is_descendant_of?(categories(:child_1)).should be_true
end
it "is_descendant_of?" do
categories(:child_1).is_descendant_of?(categories(:top_level)).should be_true
categories(:child_2_1).is_descendant_of?(categories(:top_level)).should be_true
categories(:child_2_1).is_descendant_of?(categories(:child_2)).should be_true
categories(:child_2).is_descendant_of?(categories(:child_2_1)).should be_false
categories(:child_2).is_descendant_of?(categories(:child_1)).should be_false
categories(:child_1).is_descendant_of?(categories(:child_1)).should be_false
end
it "is_or_is_descendant_of_with_scope" do
root = ScopedCategory.root
child = root.children.first
child.is_or_is_descendant_of?(root).should be_true
child.update_attribute :organization_id, 'different'
child.is_or_is_descendant_of?(root).should be_false
end
it "same_scope?" do
root = ScopedCategory.root
child = root.children.first
child.same_scope?(root).should be_true
child.update_attribute :organization_id, 'different'
child.same_scope?(root).should be_false
end
it "left_sibling" do
categories(:child_1).should == categories(:child_2).left_sibling
categories(:child_2).should == categories(:child_3).left_sibling
end
it "left_sibling_of_root" do
categories(:top_level).left_sibling.should be_nil
end
it "left_sibling_without_siblings" do
categories(:child_2_1).left_sibling.should be_nil
end
it "left_sibling_of_leftmost_node" do
categories(:child_1).left_sibling.should be_nil
end
it "right_sibling" do
categories(:child_3).should == categories(:child_2).right_sibling
categories(:child_2).should == categories(:child_1).right_sibling
end
it "right_sibling_of_root" do
categories(:top_level_2).should == categories(:top_level).right_sibling
categories(:top_level_2).right_sibling.should be_nil
end
it "right_sibling_without_siblings" do
categories(:child_2_1).right_sibling.should be_nil
end
it "right_sibling_of_rightmost_node" do
categories(:child_3).right_sibling.should be_nil
end
it "move_left" do
categories(:child_2).move_left
categories(:child_2).left_sibling.should be_nil
categories(:child_1).should == categories(:child_2).right_sibling
Category.valid?.should be_true
end
it "move_right" do
categories(:child_2).move_right
categories(:child_2).right_sibling.should be_nil
categories(:child_3).should == categories(:child_2).left_sibling
Category.valid?.should be_true
end
it "move_to_left_of" do
categories(:child_3).move_to_left_of(categories(:child_1))
categories(:child_3).left_sibling.should be_nil
categories(:child_1).should == categories(:child_3).right_sibling
Category.valid?.should be_true
end
it "move_to_right_of" do
categories(:child_1).move_to_right_of(categories(:child_3))
categories(:child_1).right_sibling.should be_nil
categories(:child_3).should == categories(:child_1).left_sibling
Category.valid?.should be_true
end
it "move_to_root" do
categories(:child_2).move_to_root
categories(:child_2).parent.should be_nil
categories(:child_2).level.should == 0
categories(:child_2_1).level.should == 1
categories(:child_2).left.should == 1
categories(:child_2).right.should == 4
Category.valid?.should be_true
end
it "move_to_child_of" do
categories(:child_1).move_to_child_of(categories(:child_3))
categories(:child_3).id.should == categories(:child_1).parent_id
Category.valid?.should be_true
end
it "move_to_child_of_appends_to_end" do
child = Category.create! :name => 'New Child'
child.move_to_child_of categories(:top_level)
child.should == categories(:top_level).children.last
end
it "subtree_move_to_child_of" do
categories(:child_2).left.should == 4
categories(:child_2).right.should == 7
categories(:child_1).left.should == 2
categories(:child_1).right.should == 3
categories(:child_2).move_to_child_of(categories(:child_1))
Category.valid?.should be_true
categories(:child_1).id.should == categories(:child_2).parent_id
categories(:child_2).left.should == 3
categories(:child_2).right.should == 6
categories(:child_1).left.should == 2
categories(:child_1).right.should == 7
end
it "slightly_difficult_move_to_child_of" do
categories(:top_level_2).left.should == 11
categories(:top_level_2).right.should == 12
# create a new top-level node and move single-node top-level tree inside it.
new_top = Category.create(:name => 'New Top')
new_top.left.should == 13
new_top.right.should == 14
categories(:top_level_2).move_to_child_of(new_top)
Category.valid?.should be_true
new_top.id.should == categories(:top_level_2).parent_id
categories(:top_level_2).left.should == 12
categories(:top_level_2).right.should == 13
new_top.left.should == 11
new_top.right.should == 14
end
it "difficult_move_to_child_of" do
categories(:top_level).left.should == 1
categories(:top_level).right.should == 10
categories(:child_2_1).left.should == 5
categories(:child_2_1).right.should == 6
# 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
Category.valid?.should be_true
new_top.id.should == categories(:top_level).parent_id
categories(:top_level).left.should == 4
categories(:top_level).right.should == 13
categories(:child_2_1).left.should == 8
categories(:child_2_1).right.should == 9
end
#rebuild swaps the position of the 2 children when added using move_to_child twice onto same parent
it "move_to_child_more_than_once_per_parent_rebuild" do
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!
Category.roots.last.to_text.should == output
end
# doing move_to_child twice onto same parent from the furthest right first
it "move_to_child_more_than_once_per_parent_outside_in" do
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!
Category.roots.last.to_text.should == output
end
it "should be able to rebuild without validating each record" do
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
root2.name = nil
root2.save!(:validate => false)
output = Category.roots.last.to_text
Category.update_all('lft = null, rgt = null')
Category.rebuild!(false)
Category.roots.last.to_text.should == output
end
it "valid_with_null_lefts" do
Category.valid?.should be_true
Category.update_all('lft = null')
Category.valid?.should be_false
end
it "valid_with_null_rights" do
Category.valid?.should be_true
Category.update_all('rgt = null')
Category.valid?.should be_false
end
it "valid_with_missing_intermediate_node" do
# Even though child_2_1 will still exist, it is a sign of a sloppy delete, not an invalid tree.
Category.valid?.should be_true
Category.delete(categories(:child_2).id)
Category.valid?.should be_true
end
it "valid_with_overlapping_and_rights" do
Category.valid?.should be_true
categories(:top_level_2)['lft'] = 0
categories(:top_level_2).save
Category.valid?.should be_false
end
it "rebuild" do
Category.valid?.should be_true
before_text = Category.root.to_text
Category.update_all('lft = null, rgt = null')
Category.rebuild!
Category.valid?.should be_true
before_text.should == Category.root.to_text
end
it "move_possible_for_sibling" do
categories(:child_2).move_possible?(categories(:child_1)).should be_true
end
it "move_not_possible_to_self" do
categories(:top_level).move_possible?(categories(:top_level)).should be_false
end
it "move_not_possible_to_parent" do
categories(:top_level).descendants.each do |descendant|
categories(:top_level).move_possible?(descendant).should be_false
descendant.move_possible?(categories(:top_level)).should be_true
end
end
it "is_or_is_ancestor_of?" do
[:child_1, :child_2, :child_2_1, :child_3].each do |c|
categories(:top_level).is_or_is_ancestor_of?(categories(c)).should be_true
end
categories(:top_level).is_or_is_ancestor_of?(categories(:top_level_2)).should be_false
end
it "left_and_rights_valid_with_blank_left" do
Category.left_and_rights_valid?.should be_true
categories(:child_2)[:lft] = nil
categories(:child_2).save(:validate => false)
Category.left_and_rights_valid?.should be_false
end
it "left_and_rights_valid_with_blank_right" do
Category.left_and_rights_valid?.should be_true
categories(:child_2)[:rgt] = nil
categories(:child_2).save(:validate => false)
Category.left_and_rights_valid?.should be_false
end
it "left_and_rights_valid_with_equal" do
Category.left_and_rights_valid?.should be_true
categories(:top_level_2)[:lft] = categories(:top_level_2)[:rgt]
categories(:top_level_2).save(:validate => false)
Category.left_and_rights_valid?.should be_false
end
it "left_and_rights_valid_with_left_equal_to_parent" do
Category.left_and_rights_valid?.should be_true
categories(:child_2)[:lft] = categories(:top_level)[:lft]
categories(:child_2).save(:validate => false)
Category.left_and_rights_valid?.should be_false
end
it "left_and_rights_valid_with_right_equal_to_parent" do
Category.left_and_rights_valid?.should be_true
categories(:child_2)[:rgt] = categories(:top_level)[:rgt]
categories(:child_2).save(:validate => false)
Category.left_and_rights_valid?.should be_false
end
it "moving_dirty_objects_doesnt_invalidate_tree" do
r1 = Category.create :name => "Test 1"
r2 = Category.create :name => "Test 2"
r3 = Category.create :name => "Test 3"
r4 = Category.create :name => "Test 4"
nodes = [r1, r2, r3, r4]
r2.move_to_child_of(r1)
Category.valid?.should be_true
r3.move_to_child_of(r1)
Category.valid?.should be_true
r4.move_to_child_of(r2)
Category.valid?.should be_true
end
it "multi_scoped_no_duplicates_for_columns?" do
lambda {
Note.no_duplicates_for_columns?
}.should_not raise_exception
end
it "multi_scoped_all_roots_valid?" do
lambda {
Note.all_roots_valid?
}.should_not raise_exception
end
it "multi_scoped" do
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')
[note1, note2].should == note1.self_and_siblings
[note3].should == note3.self_and_siblings
end
it "multi_scoped_rebuild" do
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!
Note.roots.find_by_body('A').should == root
[child1, child2].should == Note.roots.find_by_body('A').children
end
it "same_scope_with_multi_scopes" do
lambda {
notes(:scope1).same_scope?(notes(:child_1))
}.should_not raise_exception
notes(:scope1).same_scope?(notes(:child_1)).should be_true
notes(:child_1).same_scope?(notes(:scope1)).should be_true
notes(:scope1).same_scope?(notes(:scope2)).should be_false
end
it "quoting_of_multi_scope_column_names" do
["\"notable_id\"", "\"notable_type\""].should == Note.quoted_scope_column_names
end
it "equal_in_same_scope" do
notes(:scope1).should == notes(:scope1)
notes(:scope1).should_not == notes(:child_1)
end
it "equal_in_different_scopes" do
notes(:scope1).should_not == notes(:scope2)
end
it "delete_does_not_invalidate" do
Category.acts_as_nested_set_options[:dependent] = :delete
categories(:child_2).destroy
Category.valid?.should be_true
end
it "destroy_does_not_invalidate" do
Category.acts_as_nested_set_options[:dependent] = :destroy
categories(:child_2).destroy
Category.valid?.should be_true
end
it "destroy_multiple_times_does_not_invalidate" do
Category.acts_as_nested_set_options[:dependent] = :destroy
categories(:child_2).destroy
categories(:child_2).destroy
Category.valid?.should be_true
end
it "assigning_parent_id_on_create" do
category = Category.create!(:name => "Child", :parent_id => categories(:child_2).id)
categories(:child_2).should == category.parent
categories(:child_2).id.should == category.parent_id
category.left.should_not be_nil
category.right.should_not be_nil
Category.valid?.should be_true
end
it "assigning_parent_on_create" do
category = Category.create!(:name => "Child", :parent => categories(:child_2))
categories(:child_2).should == category.parent
categories(:child_2).id.should == category.parent_id
category.left.should_not be_nil
category.right.should_not be_nil
Category.valid?.should be_true
end
it "assigning_parent_id_to_nil_on_create" do
category = Category.create!(:name => "New Root", :parent_id => nil)
category.parent.should be_nil
category.parent_id.should be_nil
category.left.should_not be_nil
category.right.should_not be_nil
Category.valid?.should be_true
end
it "assigning_parent_id_on_update" do
category = categories(:child_2_1)
category.parent_id = categories(:child_3).id
category.save
category.reload
categories(:child_3).reload
categories(:child_3).should == category.parent
categories(:child_3).id.should == category.parent_id
Category.valid?.should be_true
end
it "assigning_parent_on_update" do
category = categories(:child_2_1)
category.parent = categories(:child_3)
category.save
category.reload
categories(:child_3).reload
categories(:child_3).should == category.parent
categories(:child_3).id.should == category.parent_id
Category.valid?.should be_true
end
it "assigning_parent_id_to_nil_on_update" do
category = categories(:child_2_1)
category.parent_id = nil
category.save
category.parent.should be_nil
category.parent_id.should be_nil
Category.valid?.should be_true
end
it "creating_child_from_parent" do
category = categories(:child_2).children.create!(:name => "Child")
categories(:child_2).should == category.parent
categories(:child_2).id.should == category.parent_id
category.left.should_not be_nil
category.right.should_not be_nil
Category.valid?.should be_true
end
def check_structure(entries, structure)
structure = structure.dup
Category.each_with_level(entries) do |category, level|
expected_level, expected_name = structure.shift
expected_name.should == category.name
expected_level.should == level
end
end
it "each_with_level" do
levels = [
[0, "Top Level"],
[1, "Child 1"],
[1, "Child 2"],
[2, "Child 2.1"],
[1, "Child 3" ]]
check_structure(Category.root.self_and_descendants, levels)
# test some deeper structures
category = Category.find_by_name("Child 1")
c1 = Category.new(:name => "Child 1.1")
c2 = Category.new(:name => "Child 1.1.1")
c3 = Category.new(:name => "Child 1.1.1.1")
c4 = Category.new(:name => "Child 1.2")
[c1, c2, c3, c4].each(&:save!)
c1.move_to_child_of(category)
c2.move_to_child_of(c1)
c3.move_to_child_of(c2)
c4.move_to_child_of(category)
levels = [
[0, "Top Level"],
[1, "Child 1"],
[2, "Child 1.1"],
[3, "Child 1.1.1"],
[4, "Child 1.1.1.1"],
[2, "Child 1.2"],
[1, "Child 2"],
[2, "Child 2.1"],
[1, "Child 3" ]]
check_structure(Category.root.self_and_descendants, levels)
end
it "should not error on a model with attr_accessible" do
model = Class.new(ActiveRecord::Base)
model.set_table_name 'categories'
model.attr_accessible :name
lambda {
model.acts_as_nested_set
model.new(:name => 'foo')
}.should_not raise_exception
end
describe "before_move_callback" do
it "should fire the callback" do
categories(:child_2).should_receive(:custom_before_move)
categories(:child_2).move_to_root
end
it "should stop move when callback returns false" do
Category.test_allows_move = false
categories(:child_3).move_to_root.should be_false
categories(:child_3).root?.should be_false
end
it "should not halt save actions" do
Category.test_allows_move = false
categories(:child_3).parent_id = nil
categories(:child_3).save.should be_true
end
end
describe "counter_cache" do
it "should allow use of a counter cache for children" do
note1 = things(:parent1)
note1.children.count.should == 2
end
it "should increment the counter cache on create" do
note1 = things(:parent1)
note1.children.count.should == 2
note1[:children_count].should == 2
note1.children.create :body => 'Child 3'
note1.children.count.should == 3
note1.reload
note1[:children_count].should == 3
end
it "should decrement the counter cache on destroy" do
note1 = things(:parent1)
note1.children.count.should == 2
note1[:children_count].should == 2
note1.children.last.destroy
note1.children.count.should == 1
note1.reload
note1[:children_count].should == 1
end
end
describe "association callbacks on children" do
it "should call the appropriate callbacks on the children :has_many association " do
root = DefaultWithCallbacks.create
root.should_not be_new_record
child = root.children.build
root.before_add.should == child
root.after_add.should == child
root.before_remove.should_not == child
root.after_remove.should_not == child
child.save.should be_true
root.children.delete(child).should be_true
root.before_remove.should == child
root.after_remove.should == child
end
end
end

View File

@@ -0,0 +1,18 @@
sqlite3:
adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
database: awesome_nested_set.sqlite3.db
sqlite3mem:
adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
database: ":memory:"
postgresql:
adapter: postgresql
username: postgres
password: postgres
database: awesome_nested_set_plugin_test
min_messages: ERROR
mysql:
adapter: mysql2
host: localhost
username: root
password:
database: awesome_nested_set_plugin_test

View File

@@ -0,0 +1,45 @@
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
create_table :renamed_columns, :force => true do |t|
t.column :name, :string
t.column :mother_id, :integer
t.column :red, :integer
t.column :black, :integer
end
create_table :things, :force => true do |t|
t.column :body, :text
t.column :parent_id, :integer
t.column :lft, :integer
t.column :rgt, :integer
t.column :children_count, :integer
end
create_table :brokens, :force => true do |t|
t.column :name, :string
t.column :parent_id, :integer
t.column :lft, :integer
t.column :rgt, :integer
end
end

View File

@@ -0,0 +1,3 @@
one:
id: 1
name: One

View File

@@ -0,0 +1,34 @@
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

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

View File

@@ -0,0 +1,38 @@
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

@@ -0,0 +1,27 @@
parent1:
id: 1
body: Top Level
lft: 1
rgt: 10
children_count: 2
child_1:
id: 2
body: Child 1
parent_id: 1
lft: 2
rgt: 3
children_count: 0
child_2:
id: 3
body: Child 2
parent_id: 1
lft: 4
rgt: 7
children_count: 0
child_2_1:
id: 4
body: Child 2.1
parent_id: 3
lft: 8
rgt: 9
children_count: 0

View File

@@ -0,0 +1,32 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
plugin_test_dir = File.dirname(__FILE__)
require 'rubygems'
require 'bundler/setup'
require 'rspec'
require 'logger'
require 'active_support'
require 'active_model'
require 'active_record'
require 'action_controller'
require 'awesome_nested_set'
ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log")
require 'yaml'
require 'erb'
ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(plugin_test_dir + "/db/database.yml")).result)
ActiveRecord::Base.establish_connection(ENV["DB"] || "sqlite3mem")
ActiveRecord::Migration.verbose = false
load(File.join(plugin_test_dir, "db", "schema.rb"))
require 'support/models'
require 'rspec/rails'
RSpec.configure do |config|
config.fixture_path = "#{plugin_test_dir}/fixtures"
config.use_transactional_fixtures = true
end

View File

@@ -0,0 +1,72 @@
class Note < ActiveRecord::Base
acts_as_nested_set :scope => [:notable_id, :notable_type]
end
class Default < ActiveRecord::Base
set_table_name 'categories'
acts_as_nested_set
end
class ScopedCategory < ActiveRecord::Base
set_table_name 'categories'
acts_as_nested_set :scope => :organization
end
class RenamedColumns < ActiveRecord::Base
acts_as_nested_set :parent_column => 'mother_id', :left_column => 'red', :right_column => 'black'
end
class Category < ActiveRecord::Base
acts_as_nested_set
validates_presence_of :name
# Setup a callback that we can switch to true or false per-test
set_callback :move, :before, :custom_before_move
cattr_accessor :test_allows_move
@@test_allows_move = true
def custom_before_move
@@test_allows_move
end
def to_s
name
end
def recurse &block
block.call self, lambda{
self.children.each do |child|
child.recurse &block
end
}
end
end
class Thing < ActiveRecord::Base
acts_as_nested_set :counter_cache => 'children_count'
end
class DefaultWithCallbacks < ActiveRecord::Base
set_table_name 'categories'
attr_accessor :before_add, :after_add, :before_remove, :after_remove
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
[ :before_add, :after_add, :before_remove, :after_remove ].each do |hook_name|
define_method "do_#{hook_name}_stuff" do |child_node|
self.send("#{hook_name}=", child_node)
end
end
end
class Broken < ActiveRecord::Base
acts_as_nested_set
end

View File

@@ -0,0 +1,41 @@
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

@@ -0,0 +1,603 @@
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
set_table_name 'categories'
end
class Scoped < ActiveRecord::Base
acts_as_nested_set :scope => :organization
set_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

@@ -0,0 +1,18 @@
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

@@ -0,0 +1,23 @@
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

@@ -0,0 +1,34 @@
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

@@ -0,0 +1,15 @@
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

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

View File

@@ -0,0 +1,38 @@
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

@@ -0,0 +1,31 @@
$:.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