awesome_nested_set: import git 2-1-stable branch revision 606847769 (#6579)

https://github.com/collectiveidea/awesome_nested_set/commit/606847769

git-svn-id: http://svn.redmine.org/redmine/trunk@13143 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Toshi MARUYAMA
2014-05-24 10:16:38 +00:00
parent 5368af4847
commit 43e84c6c10
26 changed files with 1282 additions and 922 deletions

View File

@@ -1,7 +1,4 @@
language: ruby language: ruby
notifications:
email:
- parndt@gmail.com
script: bundle exec rspec spec script: bundle exec rspec spec
env: env:
- DB=sqlite3 - DB=sqlite3
@@ -11,9 +8,9 @@ env:
rvm: rvm:
- 2.0.0 - 2.0.0
- 1.9.3 - 1.9.3
- 1.8.7
- rbx-19mode - rbx-19mode
- jruby-19mode - jruby-19mode
- 1.8.7
- rbx-18mode - rbx-18mode
- jruby-18mode - jruby-18mode
gemfile: gemfile:

View File

@@ -0,0 +1,14 @@
# Contributing to AwesomeNestedSet
If you find what you might think is a bug:
1. Check the [GitHub issue tracker](https://github.com/collectiveidea/awesome_nested_set/issues/) to see if anyone else has had the same issue.
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](https://github.com/collectiveidea/awesome_nested_set)
2. Make your changes with tests.
3. Commit the changes without making changes to the [Rakefile](Rakefile) or any other files that aren't related to your enhancement or fix.
4. Write an entry in the [CHANGELOG](CHANGELOG)
5. Send a pull request.

View File

@@ -1,4 +1,4 @@
gem 'combustion', :github => 'pat/combustion' gem 'combustion', :github => 'pat/combustion', :branch => 'master'
source 'https://rubygems.org' source 'https://rubygems.org'
@@ -7,6 +7,7 @@ gemspec :path => File.expand_path('../', __FILE__)
platforms :jruby do platforms :jruby do
gem 'activerecord-jdbcsqlite3-adapter' gem 'activerecord-jdbcsqlite3-adapter'
gem 'activerecord-jdbcmysql-adapter' gem 'activerecord-jdbcmysql-adapter'
gem 'jdbc-mysql'
gem 'activerecord-jdbcpostgresql-adapter' gem 'activerecord-jdbcpostgresql-adapter'
gem 'jruby-openssl' gem 'jruby-openssl'
end end
@@ -27,5 +28,5 @@ gem 'actionpack', RAILS_VERSION
# gem 'activerecord-oracle_enhanced-adapter' # gem 'activerecord-oracle_enhanced-adapter'
# Debuggers # Debuggers
# gem 'pry' gem 'pry'
# gem 'pry-nav' gem 'pry-nav'

View File

@@ -0,0 +1,163 @@
# AwesomeNestedSet
Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models.
It is a 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.
[![Code Climate](https://codeclimate.com/github/collectiveidea/awesome_nested_set.png)](https://codeclimate.com/github/collectiveidea/awesome_nested_set)
## Installation
Add to your Gemfile:
```ruby
gem 'awesome_nested_set'
```
## Usage
To make use of `awesome_nested_set`, your model needs to have 3 fields:
`lft`, `rgt`, and `parent_id`. The names of these fields are configurable.
You can also have an optional field, `depth`:
```ruby
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
t.integer :depth # this is optional.
end
end
def self.down
drop_table :categories
end
end
```
Enable the nested set functionality by declaring `acts_as_nested_set` on your model
```ruby
class Category < ActiveRecord::Base
acts_as_nested_set
end
```
Run `rake rdoc` to generate the API docs and see [CollectiveIdea::Acts::NestedSet](lib/awesome_nested_set/awesome_nested_set.rb) for more information.
## Callbacks
There are three callbacks called when moving a node:
`before_move`, `after_move` and `around_move`.
```ruby
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.
```ruby
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
It's generally best to "whitelist" the attributes that can be used in mass assignment:
```ruby
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:
```ruby
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:
```ruby
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:
```erb
<%= 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](lib/awesome_nested_set/helper.rb) 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
Please see the ['Contributing' document](CONTRIBUTING.md).
Copyright © 2008 - 2013 Collective Idea, released under the MIT license

View File

@@ -1,153 +0,0 @@
= 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.
You can also have an optional field: depth:
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
t.integer :depth # this is optional.
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.
== 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
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.
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.
If you want to contribute an enhancement or a fix:
1. Fork the project on GitHub.
https://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

@@ -7,10 +7,9 @@ Gem::Specification.new do |s|
s.authors = ["Brandon Keepers", "Daniel Morrison", "Philip Arndt"] s.authors = ["Brandon Keepers", "Daniel Morrison", "Philip Arndt"]
s.description = %q{An awesome nested set implementation for Active Record} s.description = %q{An awesome nested set implementation for Active Record}
s.email = %q{info@collectiveidea.com} s.email = %q{info@collectiveidea.com}
s.extra_rdoc_files = %w[README.rdoc] s.files = Dir.glob("lib/**/*") + %w(MIT-LICENSE README.md CHANGELOG)
s.files = Dir.glob("lib/**/*") + %w(MIT-LICENSE README.rdoc CHANGELOG)
s.homepage = %q{http://github.com/collectiveidea/awesome_nested_set} s.homepage = %q{http://github.com/collectiveidea/awesome_nested_set}
s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"] s.rdoc_options = ["--inline-source", "--line-numbers"]
s.require_paths = ["lib"] s.require_paths = ["lib"]
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}
@@ -21,4 +20,5 @@ Gem::Specification.new do |s|
s.add_development_dependency 'rspec-rails', '~> 2.12' s.add_development_dependency 'rspec-rails', '~> 2.12'
s.add_development_dependency 'rake', '~> 10' s.add_development_dependency 'rake', '~> 10'
s.add_development_dependency 'combustion', '>= 0.3.3' s.add_development_dependency 'combustion', '>= 0.3.3'
s.add_development_dependency 'database_cleaner'
end end

View File

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

View File

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

View File

@@ -1,3 +1,6 @@
require 'awesome_nested_set/columns'
require 'awesome_nested_set/model'
module CollectiveIdea #:nodoc: module CollectiveIdea #:nodoc:
module Acts #:nodoc: module Acts #:nodoc:
module NestedSet #:nodoc: module NestedSet #:nodoc:
@@ -42,7 +45,58 @@ module CollectiveIdea #:nodoc:
# CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
# to acts_as_nested_set models # to acts_as_nested_set models
def acts_as_nested_set(options = {}) def acts_as_nested_set(options = {})
options = { acts_as_nested_set_parse_options! options
include Model
include Columns
extend Columns
acts_as_nested_set_relate_parent!
acts_as_nested_set_relate_children!
attr_accessor :skip_before_destroy
acts_as_nested_set_prevent_assignment_to_reserved_columns!
acts_as_nested_set_define_callbacks!
end
private
def acts_as_nested_set_define_callbacks!
# on creation, set automatically lft and rgt to the end of the tree
before_create :set_default_left_and_right
before_save :store_new_parent
after_save :move_to_new_parent, :set_depth!
before_destroy :destroy_descendants
define_model_callbacks :move
end
def acts_as_nested_set_relate_children!
has_many_children_options = {
:class_name => self.base_class.to_s,
:foreign_key => parent_column_name,
:order => quoted_order_column_name,
:inverse_of => (:parent unless acts_as_nested_set_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 => acts_as_nested_set_options[ar_callback]) if acts_as_nested_set_options[ar_callback]
end
has_many :children, has_many_children_options
end
def acts_as_nested_set_relate_parent!
belongs_to :parent, :class_name => self.base_class.to_s,
:foreign_key => parent_column_name,
:counter_cache => acts_as_nested_set_options[:counter_cache],
:inverse_of => (:children unless acts_as_nested_set_options[:polymorphic]),
:polymorphic => acts_as_nested_set_options[:polymorphic]
end
def acts_as_nested_set_default_options
{
:parent_column => 'parent_id', :parent_column => 'parent_id',
:left_column => 'lft', :left_column => 'lft',
:right_column => 'rgt', :right_column => 'rgt',
@@ -50,7 +104,11 @@ module CollectiveIdea #:nodoc:
:dependent => :delete_all, # or :destroy :dependent => :delete_all, # or :destroy
:polymorphic => false, :polymorphic => false,
:counter_cache => false :counter_cache => false
}.merge(options) }.freeze
end
def acts_as_nested_set_parse_options!(options)
options = acts_as_nested_set_default_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$/
options[:scope] = "#{options[:scope]}_id".intern options[:scope] = "#{options[:scope]}_id".intern
@@ -58,38 +116,9 @@ module CollectiveIdea #:nodoc:
class_attribute :acts_as_nested_set_options class_attribute :acts_as_nested_set_options
self.acts_as_nested_set_options = options self.acts_as_nested_set_options = options
end
include CollectiveIdea::Acts::NestedSet::Model def acts_as_nested_set_prevent_assignment_to_reserved_columns!
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 unless options[:polymorphic]),
:polymorphic => options[:polymorphic]
has_many_children_options = {
:class_name => self.base_class.to_s,
:foreign_key => parent_column_name,
:order => order_column,
: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
before_create :set_default_left_and_right
before_save :store_new_parent
after_save :move_to_new_parent, :set_depth!
before_destroy :destroy_descendants
# no assignment to structure fields # no assignment to structure fields
[left_column_name, right_column_name, depth_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__
@@ -98,675 +127,7 @@ module CollectiveIdea #:nodoc:
end end
end_eval end_eval
end end
define_model_callbacks :move
end end
module Model
extend ActiveSupport::Concern
included do
delegate :quoted_table_name, :to => self
end
module ClassMethods
# Returns the first root
def root
roots.first
end
def roots
where(parent_column_name => nil).order(quoted_left_column_full_name)
end
def leaves
where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
end
def valid?
left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
end
def left_and_rights_valid?
## AS clause not supported in Oracle in FROM clause for aliasing table name
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(
"#{quoted_left_column_full_name} IS NULL OR " +
"#{quoted_right_column_full_name} IS NULL OR " +
"#{quoted_left_column_full_name} >= " +
"#{quoted_right_column_full_name} OR " +
"(#{quoted_parent_column_full_name} IS NOT NULL AND " +
"(#{quoted_left_column_full_name} <= parent.#{quoted_left_column_name} OR " +
"#{quoted_right_column_full_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_full_name, quoted_right_column_full_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_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)
# default_scope with order may break database queries so we do all operation without scope
unscoped do
# 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_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
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_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
indices[scope.call(root_node)] ||= 0
set_left_and_rights.call(root_node)
end
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 descend or ascend?
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
# 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
# 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%'")
# 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
# Returns true if this is the end of a branch.
def leaf?
persisted? && right.to_i - left.to_i == 1
end
# Returns true is this is a child node
def child?
!root?
end
# Returns root
def root
if persisted?
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
# Returns the array of all parents and self
def self_and_ancestors
nested_set_scope.where([
"#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_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("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
end
# Returns the level of this object in the tree
# root level is 0
def level
parent_id.nil? ? 0 : compute_level
end
# Returns a set of itself and all of its nested children
def self_and_descendants
nested_set_scope.where([
"#{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
# 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(["#{quoted_left_column_full_name} < ?", left]).
order("#{quoted_left_column_full_name} DESC").last
end
# Find the first sibling to the right
def right_sibling
siblings.where(["#{quoted_left_column_full_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 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
def move_to_root
move_to nil, :root
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)
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 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)
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 => quoted_left_column_full_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.unscoped.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
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
def right_most_bound
right_most_node =
self.class.base_class.unscoped.
order("#{quoted_right_column_full_name} desc").limit(1).lock(true).first
right_most_node ? (right_most_node[right_column_name] || 0) : 0
end
# on creation, set automatically lft and rgt to the end of the tree
def set_default_left_and_right
# adds the new node to the right of all existing nodes
self[left_column_name] = right_most_bound + 1
self[right_column_name] = right_most_bound + 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
nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
select(id).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.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
delete_all
end
# update lefts and rights for remaining nodes
diff = right - left + 1
nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
)
nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
)
# 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_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_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_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
)
new_parent = case position
when :child; target.id
when :root; nil
else target[parent_column_name]
end
where_statement = ["not (#{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 AND " +
"#{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 AND " +
"#{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} ]
self.nested_set_scope.where(*where_statement).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.set_depth!
self.descendants.each(&:save)
self.reload_nested_set
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 depth_column_name
acts_as_nested_set_options[:depth_column]
end
def parent_column_name
acts_as_nested_set_options[:parent_column]
end
def order_column
acts_as_nested_set_options[:order_column] || left_column_name
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_depth_column_name
connection.quote_column_name(depth_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
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 end
end end

View File

@@ -0,0 +1,68 @@
# Mixed into both classes and instances to provide easy access to the column names
module CollectiveIdea #:nodoc:
module Acts #:nodoc:
module NestedSet #:nodoc:
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 depth_column_name
acts_as_nested_set_options[:depth_column]
end
def parent_column_name
acts_as_nested_set_options[:parent_column]
end
def order_column
acts_as_nested_set_options[:order_column] || left_column_name
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_depth_column_name
connection.quote_column_name(depth_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
def quoted_order_column_name
connection.quote_column_name(order_column)
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

@@ -38,51 +38,6 @@ module CollectiveIdea #:nodoc:
end end
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

@@ -0,0 +1,29 @@
module CollectiveIdea #:nodoc:
module Acts #:nodoc:
module NestedSet #:nodoc:
class Iterator
attr_reader :objects
def initialize(objects)
@objects = objects
end
def each_with_level
path = [nil]
objects.each do |o|
if o.parent_id != path.last
# we are on a new level, did we descend or ascend?
if path.include?(o.parent_id)
# remove 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
end
end
end

View File

@@ -0,0 +1,212 @@
require 'awesome_nested_set/model/prunable'
require 'awesome_nested_set/model/movable'
require 'awesome_nested_set/model/transactable'
require 'awesome_nested_set/model/relatable'
require 'awesome_nested_set/model/rebuildable'
require 'awesome_nested_set/model/validatable'
require 'awesome_nested_set/iterator'
module CollectiveIdea #:nodoc:
module Acts #:nodoc:
module NestedSet #:nodoc:
module Model
extend ActiveSupport::Concern
included do
delegate :quoted_table_name, :arel_table, :to => self
extend Validatable
extend Rebuildable
include Movable
include Prunable
include Relatable
include Transactable
end
module ClassMethods
def associate_parents(objects)
return objects unless objects.all? {|o| o.respond_to?(:association)}
id_indexed = objects.index_by(&:id)
objects.each do |object|
association = object.association(:parent)
parent = id_indexed[object.parent_id]
if !association.loaded? && parent
association.target = parent
association.set_inverse_instance(parent)
end
end
end
def children_of(parent_id)
where arel_table[parent_column_name].eq(parent_id)
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, &block)
Iterator.new(objects).each_with_level(&block)
end
def leaves
nested_set_scope.where "#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1"
end
def left_of(node)
where arel_table[left_column_name].lt(node)
end
def left_of_right_side(node)
where arel_table[right_column_name].lteq(node)
end
def right_of(node)
where arel_table[left_column_name].gteq(node)
end
def nested_set_scope(options = {})
options = {:order => quoted_order_column_name}.merge(options)
order(options.delete(:order)).scoped options
end
def primary_key_scope(id)
where arel_table[primary_key].eq(id)
end
def root
roots.first
end
def roots
nested_set_scope.children_of nil
end
end # end class methods
# 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%'")
# Value of the parent column
def parent_id(target = self)
target[parent_column_name]
end
# Value of the left column
def left(target = self)
target[left_column_name]
end
# Value of the right column
def right(target = self)
target[right_column_name]
end
# Returns true if this is a root node.
def root?
parent_id.nil?
end
# Returns true is this is a child node
def child?
!root?
end
# Returns true if this is the end of a branch.
def leaf?
persisted? && right.to_i - left.to_i == 1
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 = {})
if (scopes = Array(acts_as_nested_set_options[:scope])).any?
options[:conditions] = scopes.inject({}) do |conditions,attr|
conditions.merge attr => self[attr]
end
end
self.class.nested_set_scope options
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)
return scope if new_record?
scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
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 has_depth_column?
nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
end
def right_most_node
@right_most_node ||= self.class.base_class.unscoped.nested_set_scope(
:order => "#{quoted_right_column_full_name} desc"
).first
end
def right_most_bound
@right_most_bound ||= begin
return 0 if right_most_node.nil?
right_most_node.lock!
right_most_node[right_column_name] || 0
end
end
def set_depth!
return unless has_depth_column?
in_tenacious_transaction do
reload
nested_set_scope.primary_key_scope(id).
update_all(["#{quoted_depth_column_name} = ?", level])
end
self[depth_column_name] = self.level
end
def set_default_left_and_right
# adds the new node to the right of all existing nodes
self[left_column_name] = right_most_bound + 1
self[right_column_name] = right_most_bound + 2
end
# reload left, right, and parent
def reload_nested_set
reload(
:select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
:lock => true
)
end
def reload_target(target)
if target.is_a? self.class.base_class
target.reload
else
nested_set_scope.find(target)
end
end
end
end
end
end

View File

@@ -0,0 +1,137 @@
require 'awesome_nested_set/move'
module CollectiveIdea #:nodoc:
module Acts #:nodoc:
module NestedSet #:nodoc:
module Model
module Movable
def move_possible?(target)
self != target && # Can't target self
same_scope?(target) && # can't be in different scopes
# detect impossible move
within_bounds?(target.left, target.left) &&
within_bounds?(target.right, target.right)
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
def move_to_left_of(node)
move_to node, :left
end
# Move the node to the left of another node
def move_to_right_of(node)
move_to node, :right
end
# Move the node to the child of another node
def move_to_child_of(node)
move_to node, :child
end
# Move the node to the child of another node with specify index
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
def move_to_root
move_to_right_of(root)
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_neighbor = find_left_neighbor(parent, order_attribute, ascending)
self.move_to_child_of(parent)
return unless parent.children.many?
if left_neighbor
self.move_to_right_of(left_neighbor)
else # Self is the left most node.
self.move_to_left_of(parent.children[0])
end
end
# Find the node immediately to the left of this node.
def find_left_neighbor(parent, order_attribute, ascending)
left = nil
parent.children.each do |n|
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
left
end
def move_to(target, position)
prevent_unpersisted_move
run_callbacks :move do
in_tenacious_transaction do
target = reload_target(target)
self.reload_nested_set
Move.new(target, position, self).move
end
after_move_to(target, position)
end
end
protected
def after_move_to(target, position)
target.reload_nested_set if target
self.set_depth!
self.descendants.each(&:save)
self.reload_nested_set
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
def out_of_bounds?(left_bound, right_bound)
left <= left_bound && right >= right_bound
end
def prevent_unpersisted_move
if self.new_record?
raise ActiveRecord::ActiveRecordError, "You cannot move a new node"
end
end
def within_bounds?(left_bound, right_bound)
!out_of_bounds?(left_bound, right_bound)
end
end
end
end
end
end

View File

@@ -0,0 +1,58 @@
module CollectiveIdea #:nodoc:
module Acts #:nodoc:
module NestedSet #:nodoc:
module Model
module Prunable
# 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
nested_set_scope.right_of(left).select(id).lock(true)
destroy_or_delete_descendants
# update lefts and rights for remaining nodes
update_siblings_for_remaining_nodes
# Don't allow multiple calls to destroy to corrupt the set
self.skip_before_destroy = true
end
end
def destroy_or_delete_descendants
if acts_as_nested_set_options[:dependent] == :destroy
descendants.each do |model|
model.skip_before_destroy = true
model.destroy
end
else
descendants.delete_all
end
end
def update_siblings_for_remaining_nodes
update_siblings(:left)
update_siblings(:right)
end
def update_siblings(direction)
full_column_name = send("quoted_#{direction}_column_full_name")
column_name = send("quoted_#{direction}_column_name")
nested_set_scope.where(["#{full_column_name} > ?", right]).
update_all(["#{column_name} = (#{column_name} - ?)", diff])
end
def diff
right - left + 1
end
end
end
end
end
end

View File

@@ -0,0 +1,41 @@
require 'awesome_nested_set/tree'
module CollectiveIdea
module Acts
module NestedSet
module Model
module Rebuildable
# Rebuilds the left & rights if unset or invalid.
# Also very useful for converting from acts_as_tree.
def rebuild!(validate_nodes = true)
# default_scope with order may break database queries so we do all operation without scope
unscoped do
Tree.new(self, validate_nodes).rebuild!
end
end
private
def scope_for_rebuild
scope = proc {}
if acts_as_nested_set_options[:scope]
scope = proc {|node|
scope_column_names.inject("") {|str, column_name|
str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name))} "
}
}
end
scope
end
def order_for_rebuild
"#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{primary_key}"
end
end
end
end
end
end

View File

@@ -0,0 +1,121 @@
module CollectiveIdea
module Acts
module NestedSet
module Model
module Relatable
# Returns an collection of all parents
def ancestors
without_self self_and_ancestors
end
# Returns the collection of all parents and self
def self_and_ancestors
nested_set_scope.
where(arel_table[left_column_name].lteq(left)).
where(arel_table[right_column_name].gteq(right))
end
# Returns the collection of all children of the parent, except self
def siblings
without_self self_and_siblings
end
# Returns the collection of all children of the parent, including self
def self_and_siblings
nested_set_scope.children_of parent_id
end
# Returns a set of all of its nested children which do not have children
def leaves
descendants.where(
"#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1"
)
end
# Returns the level of this object in the tree
# root level is 0
def level
parent_id.nil? ? 0 : compute_level
end
# Returns a collection including all of its children and nested children
def descendants
without_self self_and_descendants
end
# Returns a collection including itself and all of its nested children
def self_and_descendants
# using _left_ for both sides here lets us benefit from an index on that column if one exists
nested_set_scope.right_of(left).left_of(right)
end
def is_descendant_of?(other)
within_node?(other, self) && same_scope?(other)
end
def is_or_is_descendant_of?(other)
(other == self || within_node?(other, self)) && same_scope?(other)
end
def is_ancestor_of?(other)
within_node?(self, other) && same_scope?(other)
end
def is_or_is_ancestor_of?(other)
(self == other || within_node?(self, other)) && 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.left_of(left).last
end
# Find the first sibling to the right
def right_sibling
siblings.right_of(left).first
end
def root
return self_and_ancestors.children_of(nil).first if persisted?
if parent_id && current_parent = nested_set_scope.find(parent_id)
current_parent.root
else
self
end
end
protected
def compute_level
node, nesting = determine_depth
node == self ? ancestors.count : node.level + nesting
end
def determine_depth(node = self, nesting = 0)
while (association = node.association(:parent)).loaded? && association.target
nesting += 1
node = node.parent
end if node.respond_to?(:association)
[node, nesting]
end
def within_node?(node, within)
node.left < within.left && within.left < node.right
end
end
end
end
end
end

View File

@@ -0,0 +1,27 @@
module CollectiveIdea #:nodoc:
module Acts #:nodoc:
module NestedSet #:nodoc:
module Model
module Transactable
protected
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
end
end
end
end
end

View File

@@ -0,0 +1,69 @@
require 'awesome_nested_set/set_validator'
module CollectiveIdea
module Acts
module NestedSet
module Model
module Validatable
def valid?
left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
end
def left_and_rights_valid?
SetValidator.new(self).valid?
end
def no_duplicates_for_columns?
[quoted_left_column_full_name, quoted_right_column_full_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]
all_roots_valid_by_scope?(roots)
else
each_root_valid?(roots)
end
end
def all_roots_valid_by_scope?(roots_to_validate)
roots_grouped_by_scope(roots_to_validate).all? do |scope, grouped_roots|
each_root_valid?(grouped_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
private
def roots_grouped_by_scope(roots_to_group)
roots_to_group.group_by {|record|
scope_column_names.collect {|col| record.send(col) }
}
end
def scope_string
Array(acts_as_nested_set_options[:scope]).map do |c|
connection.quote_column_name(c)
end.push(nil).join(", ")
end
end
end
end
end
end

View File

@@ -0,0 +1,117 @@
module CollectiveIdea #:nodoc:
module Acts #:nodoc:
module NestedSet #:nodoc:
class Move
attr_reader :target, :position, :instance
def initialize(target, position, instance)
@target = target
@position = position
@instance = instance
end
def move
prevent_impossible_move
bound, other_bound = get_boundaries
# there would be no change
return if bound == right || bound == left
# 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 = [left, right, bound, other_bound].sort
lock_nodes_between! a, d
nested_set_scope.where(where_statement(a, d)).
update_all(conditions(a, b, c, d))
end
private
delegate :left, :right, :left_column_name, :right_column_name,
:quoted_left_column_name, :quoted_right_column_name,
:quoted_parent_column_name, :parent_column_name, :nested_set_scope,
:to => :instance
delegate :arel_table, :class, :to => :instance, :prefix => true
delegate :base_class, :to => :instance_class, :prefix => :instance
def where_statement(left_bound, right_bound)
instance_arel_table[left_column_name].in(left_bound..right_bound).
or(instance_arel_table[right_column_name].in(left_bound..right_bound))
end
def conditions(a, b, c, d)
[
case_condition_for_direction(:quoted_left_column_name) +
case_condition_for_direction(:quoted_right_column_name) +
case_condition_for_parent,
{:a => a, :b => b, :c => c, :d => d, :id => instance.id, :new_parent => new_parent}
]
end
def case_condition_for_direction(column_name)
column = send(column_name)
"#{column} = CASE " +
"WHEN #{column} BETWEEN :a AND :b " +
"THEN #{column} + :d - :b " +
"WHEN #{column} BETWEEN :c AND :d " +
"THEN #{column} + :a - :c " +
"ELSE #{column} END, "
end
def case_condition_for_parent
"#{quoted_parent_column_name} = CASE " +
"WHEN #{instance_base_class.primary_key} = :id THEN :new_parent " +
"ELSE #{quoted_parent_column_name} END"
end
def lock_nodes_between!(left_bound, right_bound)
# select the rows in the model between a and d, and apply a lock
instance_base_class.right_of(left_bound).left_of_right_side(right_bound).
select(:id).lock(true)
end
def root
position == :root
end
def new_parent
case position
when :child
target.id
else
target[parent_column_name]
end
end
def get_boundaries
if (bound = target_bound) > right
bound -= 1
other_bound = right + 1
else
other_bound = left - 1
end
[bound, other_bound]
end
def prevent_impossible_move
if !root && !instance.move_possible?(target)
raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
end
end
def target_bound
case position
when :child; right(target)
when :left; left(target)
when :right; right(target) + 1
else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
end
end
end
end
end
end

View File

@@ -0,0 +1,63 @@
module CollectiveIdea #:nodoc:
module Acts #:nodoc:
module NestedSet #:nodoc:
class SetValidator
def initialize(model)
@model = model
@scope = model.scoped
@parent = arel_table.alias('parent')
end
def valid?
query.count == 0
end
private
attr_reader :model, :parent
attr_accessor :scope
delegate :parent_column_name, :primary_key, :left_column_name, :right_column_name, :arel_table,
:quoted_table_name, :quoted_parent_column_full_name, :quoted_left_column_full_name, :quoted_right_column_full_name, :quoted_left_column_name, :quoted_right_column_name,
:to => :model
def query
join_scope
filter_scope
end
def join_scope
join_arel = arel_table.join(parent, Arel::Nodes::OuterJoin).on(parent[primary_key].eq(arel_table[parent_column_name]))
self.scope = scope.joins(join_arel.join_sql)
end
def filter_scope
self.scope = scope.where(
bound_is_null(left_column_name).
or(bound_is_null(right_column_name)).
or(left_bound_greater_than_right).
or(parent_not_null.and(bounds_outside_parent))
)
end
def bound_is_null(column_name)
arel_table[column_name].eq(nil)
end
def left_bound_greater_than_right
arel_table[left_column_name].gteq(arel_table[right_column_name])
end
def parent_not_null
arel_table[parent_column_name].not_eq(nil)
end
def bounds_outside_parent
arel_table[left_column_name].lteq(parent[left_column_name]).or(arel_table[right_column_name].gteq(parent[right_column_name]))
end
end
end
end
end

View File

@@ -0,0 +1,63 @@
module CollectiveIdea #:nodoc:
module Acts #:nodoc:
module NestedSet #:nodoc:
class Tree
attr_reader :model, :validate_nodes
attr_accessor :indices
delegate :left_column_name, :right_column_name, :quoted_parent_column_full_name,
:order_for_rebuild, :scope_for_rebuild,
:to => :model
def initialize(model, validate_nodes)
@model = model
@validate_nodes = validate_nodes
@indices = {}
end
def rebuild!
# Don't rebuild a valid tree.
return true if model.valid?
root_nodes.each do |root_node|
# setup index for this scope
indices[scope_for_rebuild.call(root_node)] ||= 0
set_left_and_rights(root_node)
end
end
private
def increment_indice!(node)
indices[scope_for_rebuild.call(node)] += 1
end
def set_left_and_rights(node)
set_left!(node)
# find
node_children(node).each { |n| set_left_and_rights(n) }
set_right!(node)
node.save!(:validate => validate_nodes)
end
def node_children(node)
model.where(["#{quoted_parent_column_full_name} = ? #{scope_for_rebuild.call(node)}", node]).
order(order_for_rebuild)
end
def root_nodes
model.where("#{quoted_parent_column_full_name} IS NULL").order(order_for_rebuild)
end
def set_left!(node)
node[left_column_name] = increment_indice!(node)
end
def set_right!(node)
node[right_column_name] = increment_indice!(node)
end
end
end
end
end

View File

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

View File

@@ -77,7 +77,10 @@ describe "Helper" do
actual = nested_set_options(Category.all) do |c| actual = nested_set_options(Category.all) do |c|
"#{'-' * c.level} #{c.name}" "#{'-' * c.level} #{c.name}"
end end
actual.should == expected actual.length.should == expected.length
expected.flatten.each do |node|
actual.flatten.should include(node)
end
end end
it "test_nested_set_options_with_array_as_argument_with_mover" do it "test_nested_set_options_with_array_as_argument_with_mover" do
@@ -90,7 +93,10 @@ describe "Helper" do
actual = nested_set_options(Category.all, categories(:child_2)) do |c| actual = nested_set_options(Category.all, categories(:child_2)) do |c|
"#{'-' * c.level} #{c.name}" "#{'-' * c.level} #{c.name}"
end end
actual.should == expected actual.length.should == expected.length
expected.flatten.each do |node|
actual.flatten.should include(node)
end
end end
end end
end end

View File

@@ -81,6 +81,12 @@ describe "AwesomeNestedSet" do
Default.new.quoted_depth_column_name.should == quoted Default.new.quoted_depth_column_name.should == quoted
end end
it "quoted_order_column_name" do
quoted = Default.connection.quote_column_name('lft')
Default.quoted_order_column_name.should == quoted
Default.new.quoted_order_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
@@ -104,7 +110,12 @@ describe "AwesomeNestedSet" do
end end
it "roots_class_method" do it "roots_class_method" do
Category.roots.should == Category.find_all_by_parent_id(nil) found_by_us = Category.where(:parent_id => nil).to_a
found_by_roots = Category.roots.to_a
found_by_us.length.should == found_by_roots.length
found_by_us.each do |root|
found_by_roots.should include(root)
end
end end
it "root_class_method" do it "root_class_method" do
@@ -131,7 +142,6 @@ describe "AwesomeNestedSet" do
end end
it "leaves_class_method" do 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.count.should == 4
Category.leaves.should include(categories(:child_1)) Category.leaves.should include(categories(:child_1))
Category.leaves.should include(categories(:child_2_1)) Category.leaves.should include(categories(:child_2_1))
@@ -158,7 +168,7 @@ describe "AwesomeNestedSet" do
it "self_and_ancestors" do it "self_and_ancestors" do
child = categories(:child_2_1) child = categories(:child_2_1)
self_and_ancestors = [categories(:top_level), categories(:child_2), child] self_and_ancestors = [categories(:top_level), categories(:child_2), child]
self_and_ancestors.should == child.self_and_ancestors child.self_and_ancestors.should == self_and_ancestors
end end
it "ancestors" do it "ancestors" do
@@ -433,8 +443,8 @@ describe "AwesomeNestedSet" do
categories(:child_2).parent.should be_nil categories(:child_2).parent.should be_nil
categories(:child_2).level.should == 0 categories(:child_2).level.should == 0
categories(:child_2_1).level.should == 1 categories(:child_2_1).level.should == 1
categories(:child_2).left.should == 1 categories(:child_2).left.should == 7
categories(:child_2).right.should == 4 categories(:child_2).right.should == 10
Category.valid?.should be_true Category.valid?.should be_true
end end
@@ -775,14 +785,14 @@ describe "AwesomeNestedSet" do
it "quoting_of_multi_scope_column_names" do it "quoting_of_multi_scope_column_names" do
## Proper Array Assignment for different DBs as per their quoting column behavior ## Proper Array Assignment for different DBs as per their quoting column behavior
if Note.connection.adapter_name.match(/Oracle/) if Note.connection.adapter_name.match(/oracle/i)
expected_quoted_scope_column_names = ["\"NOTABLE_ID\"", "\"NOTABLE_TYPE\""] expected_quoted_scope_column_names = ["\"NOTABLE_ID\"", "\"NOTABLE_TYPE\""]
elsif Note.connection.adapter_name.match(/Mysql/) elsif Note.connection.adapter_name.match(/mysql/i)
expected_quoted_scope_column_names = ["`notable_id`", "`notable_type`"] expected_quoted_scope_column_names = ["`notable_id`", "`notable_type`"]
else else
expected_quoted_scope_column_names = ["\"notable_id\"", "\"notable_type\""] expected_quoted_scope_column_names = ["\"notable_id\"", "\"notable_type\""]
end end
expected_quoted_scope_column_names.should == Note.quoted_scope_column_names Note.quoted_scope_column_names.should == expected_quoted_scope_column_names
end end
it "equal_in_same_scope" do it "equal_in_same_scope" do

View File

@@ -2,6 +2,7 @@ plugin_test_dir = File.dirname(__FILE__)
require 'rubygems' require 'rubygems'
require 'bundler/setup' require 'bundler/setup'
require 'pry'
require 'logger' require 'logger'
require 'active_record' require 'active_record'
@@ -22,6 +23,7 @@ require 'support/models'
require 'action_controller' require 'action_controller'
require 'rspec/rails' require 'rspec/rails'
require 'database_cleaner'
RSpec.configure do |config| RSpec.configure do |config|
config.fixture_path = "#{plugin_test_dir}/fixtures" config.fixture_path = "#{plugin_test_dir}/fixtures"
config.use_transactional_fixtures = true config.use_transactional_fixtures = true