##// END OF EJS Templates
import awesome_nested_set 2.1.5...
Toshi MARUYAMA -
r12402:6f78b3a4080f
parent child
Show More
@@ -1,14 +1,17
1 before_install: gem install bundler --pre
1 notifications:
2 notifications:
2 email:
3 email:
3 - parndt@gmail.com
4 - parndt@gmail.com
4 env:
5 env:
5 - DB=sqlite3
6 - DB=sqlite3
6 - DB=sqlite3mem
7 - DB=sqlite3mem
7 - DB=postgresql
8 - DB=mysql
9 rvm:
8 rvm:
10 - 1.8.7
9 - 1.8.7
11 - 1.9.2
10 - 1.9.2
12 - 1.9.3
11 - 1.9.3
13 - rbx-2.0
12 - rbx
14 - jruby No newline at end of file
13 - jruby
14 gemfile:
15 - gemfiles/Gemfile.rails-3.0.rb
16 - gemfiles/Gemfile.rails-3.1.rb
17 - gemfiles/Gemfile.rails-3.2.rb No newline at end of file
@@ -1,14 +1,52
1 2.1.5
2 * Worked around issues where AR#association wasn't present on Rails 3.0.x. [Philip Arndt]
3 * Adds option 'order_column' which defaults to 'left_column_name'. [gudata]
4 * Added moving with order functionality. [Sytse Sijbrandij]
5 * Use tablename in all select queries. [Mikhail Dieterle]
6 * Made sure all descendants' depths are updated when moving parent, not just immediate child. [Phil Thompson]
7 * Add documentation of the callbacks. [Tobias Maier]
8
9 2.1.4
10 * nested_set_options accept both Class & AR Relation. [Semyon Perepelitsa]
11 * Reduce the number of queries triggered by the canonical usage of `i.level` in the `nested_set` helpers. [thedarkone]
12 * Specifically require active_record [Bogdan Gusiev]
13 * compute_level now checks for a non nil association target. [Joel Nimety]
14
15 2.1.3
16 * Update child depth when parent node is moved. [Amanda Wagener]
17 * Added move_to_child_with_index. [Ben Zhang]
18 * Optimised self_and_descendants for when there's an index on lft. [Mark Torrance]
19 * Added support for an unsaved record to return the right 'root'. [Philip Arndt]
20
21 2.1.2
22 * Fixed regressions introduced. [Philip Arndt]
23
24 2.1.1
25 * Added 'depth' which indicates how many levels deep the node is.
26 This only works when you have a column called 'depth' in your table,
27 otherwise it doesn't set itself. [Philip Arndt]
28 * Rails 3.2 support added. [Gabriel Sobrinho]
29 * Oracle compatibility added. [Pikender Sharma]
30 * Adding row locking to deletion, locking source of pivot values, and adding retry on collisions. [Markus J. Q. Roberts]
31 * Added method and helper for sorting children by column. [bluegod]
32 * Fixed .all_roots_valid? to work with Postgres. [Joshua Clayton]
33 * Made compatible with polymorphic belongs_to. [Graham Randall]
34 * Added in the association callbacks to the children :has_many association. [Michael Deering]
35 * Modified helper to allow using array of objects as argument. [Rahmat Budiharso]
36 * Fixed cases where we were calling attr_protected. [Jacob Swanner]
37 * Fixed nil cases involving lft and rgt. [Stuart Coyle] and [Patrick Morgan]
38
1 2.0.2
39 2.0.2
2 * Fixed deprecation warning under Rails 3.1 [Philip Arndt]
40 * Fixed deprecation warning under Rails 3.1 [Philip Arndt]
3 * Converted Test::Unit matchers to RSpec. [Uģis Ozols]
41 * Converted Test::Unit matchers to RSpec. [Uģis Ozols]
4 * Added inverse_of to associations to improve performance rendering trees. [Sergio Cambra]
42 * Added inverse_of to associations to improve performance rendering trees. [Sergio Cambra]
5 * Added row locking and fixed some race conditions. [Markus J. Q. Roberts]
43 * Added row locking and fixed some race conditions. [Markus J. Q. Roberts]
6
44
7 2.0.1
45 2.0.1
8 * Fixed a bug with move_to not using nested_set_scope [Andreas Sekine]
46 * Fixed a bug with move_to not using nested_set_scope [Andreas Sekine]
9
47
10 2.0.0.pre
48 2.0.0.pre
11 * Expect Rails 3
49 * Expect Rails 3
12 * 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.
50 * 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.
13 * Switched to RSpec
51 * Switched to RSpec
14 * Remove use of Comparable
52 * Remove use of Comparable
@@ -1,100 +1,153
1 = AwesomeNestedSet
1 = AwesomeNestedSet
2
2
3 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.
3 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.
4
4
5 Version 2 supports Rails 3. Gem versions prior to 2.0 support Rails 2.
5 Version 2 supports Rails 3. Gem versions prior to 2.0 support Rails 2.
6
6
7 == What makes this so awesome?
7 == What makes this so awesome?
8
8
9 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.
9 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.
10
10
11 == Installation
11 == Installation
12
12
13 Add to your Gemfile:
13 Add to your Gemfile:
14
14
15 gem 'awesome_nested_set'
15 gem 'awesome_nested_set'
16
16
17 == Usage
17 == Usage
18
18
19 To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id:
19 To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id.
20 You can also have an optional field: depth:
20
21
21 class CreateCategories < ActiveRecord::Migration
22 class CreateCategories < ActiveRecord::Migration
22 def self.up
23 def self.up
23 create_table :categories do |t|
24 create_table :categories do |t|
24 t.string :name
25 t.string :name
25 t.integer :parent_id
26 t.integer :parent_id
26 t.integer :lft
27 t.integer :lft
27 t.integer :rgt
28 t.integer :rgt
29 t.integer :depth # this is optional.
28 end
30 end
29 end
31 end
30
32
31 def self.down
33 def self.down
32 drop_table :categories
34 drop_table :categories
33 end
35 end
34 end
36 end
35
37
36 Enable the nested set functionality by declaring acts_as_nested_set on your model
38 Enable the nested set functionality by declaring acts_as_nested_set on your model
37
39
38 class Category < ActiveRecord::Base
40 class Category < ActiveRecord::Base
39 acts_as_nested_set
41 acts_as_nested_set
40 end
42 end
41
43
42 Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet for more info.
44 Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet for more info.
43
45
46 == Callbacks
47
48 There are three callbacks called when moving a node. `before_move`, `after_move` and `around_move`.
49
50 class Category < ActiveRecord::Base
51 acts_as_nested_set
52
53 after_move :rebuild_slug
54 around_move :da_fancy_things_around
55
56 private
57
58 def rebuild_slug
59 # do whatever
60 end
61
62 def da_fancy_things_around
63 # do something...
64 yield # actually moves
65 # do something else...
66 end
67 end
68
69 Beside this there are also hooks to act on the newly added or removed children.
70
71 class Category < ActiveRecord::Base
72 acts_as_nested_set :before_add => :do_before_add_stuff,
73 :after_add => :do_after_add_stuff,
74 :before_remove => :do_before_remove_stuff,
75 :after_remove => :do_after_remove_stuff
76
77 private
78
79 def do_before_add_stuff(child_node)
80 # do whatever with the child
81 end
82
83 def do_after_add_stuff(child_node)
84 # do whatever with the child
85 end
86
87 def do_before_remove_stuff(child_node)
88 # do whatever with the child
89 end
90
91 def do_after_remove_stuff(child_node)
92 # do whatever with the child
93 end
94 end
95
96
44 == Protecting attributes from mass assignment
97 == Protecting attributes from mass assignment
45
98
46 It's generally best to "white list" the attributes that can be used in mass assignment:
99 It's generally best to "white list" the attributes that can be used in mass assignment:
47
100
48 class Category < ActiveRecord::Base
101 class Category < ActiveRecord::Base
49 acts_as_nested_set
102 acts_as_nested_set
50 attr_accessible :name, :parent_id
103 attr_accessible :name, :parent_id
51 end
104 end
52
105
53 If for some reason that is not possible, you will probably want to protect the lft and rgt attributes:
106 If for some reason that is not possible, you will probably want to protect the lft and rgt attributes:
54
107
55 class Category < ActiveRecord::Base
108 class Category < ActiveRecord::Base
56 acts_as_nested_set
109 acts_as_nested_set
57 attr_protected :lft, :rgt
110 attr_protected :lft, :rgt
58 end
111 end
59
112
60 == Conversion from other trees
113 == Conversion from other trees
61
114
62 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
115 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
63
116
64 Category.rebuild!
117 Category.rebuild!
65
118
66 Your tree will be converted to a valid nested set. Awesome!
119 Your tree will be converted to a valid nested set. Awesome!
67
120
68 == View Helper
121 == View Helper
69
122
70 The view helper is called #nested_set_options.
123 The view helper is called #nested_set_options.
71
124
72 Example usage:
125 Example usage:
73
126
74 <%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %>
127 <%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %>
75
128
76 <%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %>
129 <%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %>
77
130
78 See CollectiveIdea::Acts::NestedSet::Helper for more information about the helpers.
131 See CollectiveIdea::Acts::NestedSet::Helper for more information about the helpers.
79
132
80 == References
133 == References
81
134
82 You can learn more about nested sets at: http://threebit.net/tutorials/nestedset/tutorial1.html
135 You can learn more about nested sets at: http://threebit.net/tutorials/nestedset/tutorial1.html
83
136
84 == How to contribute
137 == How to contribute
85
138
86 If you find what you might think is a bug:
139 If you find what you might think is a bug:
87
140
88 1. Check the GitHub issue tracker to see if anyone else has had the same issue.
141 1. Check the GitHub issue tracker to see if anyone else has had the same issue.
89 http://github.com/collectiveidea/awesome_nested_set/issues/
142 https://github.com/collectiveidea/awesome_nested_set/issues/
90 2. If you don't see anything, create an issue with information on how to reproduce it.
143 2. If you don't see anything, create an issue with information on how to reproduce it.
91
144
92 If you want to contribute an enhancement or a fix:
145 If you want to contribute an enhancement or a fix:
93
146
94 1. Fork the project on github.
147 1. Fork the project on GitHub.
95 http://github.com/collectiveidea/awesome_nested_set/
148 https://github.com/collectiveidea/awesome_nested_set/
96 2. Make your changes with tests.
149 2. Make your changes with tests.
97 3. Commit the changes without making changes to the Rakefile, VERSION, or any other files that aren't related to your enhancement or fix
150 3. Commit the changes without making changes to the Rakefile, VERSION, or any other files that aren't related to your enhancement or fix
98 4. Send a pull request.
151 4. Send a pull request.
99
152
100 Copyright ©2008 Collective Idea, released under the MIT license
153 Copyright ©2008 Collective Idea, released under the MIT license
@@ -1,22 +1,23
1 # -*- encoding: utf-8 -*-
1 # -*- encoding: utf-8 -*-
2 lib = File.expand_path('../lib/', __FILE__)
2 lib = File.expand_path('../lib/', __FILE__)
3 $:.unshift lib unless $:.include?(lib)
3 $:.unshift lib unless $:.include?(lib)
4 require 'awesome_nested_set/version'
4 require 'awesome_nested_set/version'
5
5
6 Gem::Specification.new do |s|
6 Gem::Specification.new do |s|
7 s.name = %q{awesome_nested_set}
7 s.name = %q{awesome_nested_set}
8 s.version = ::AwesomeNestedSet::VERSION
8 s.version = ::AwesomeNestedSet::VERSION
9 s.authors = ["Brandon Keepers", "Daniel Morrison", "Philip Arndt"]
9 s.authors = ["Brandon Keepers", "Daniel Morrison", "Philip Arndt"]
10 s.description = %q{An awesome nested set implementation for Active Record}
10 s.description = %q{An awesome nested set implementation for Active Record}
11 s.email = %q{info@collectiveidea.com}
11 s.email = %q{info@collectiveidea.com}
12 s.extra_rdoc_files = [
12 s.extra_rdoc_files = [
13 "README.rdoc"
13 "README.rdoc"
14 ]
14 ]
15 s.files = Dir.glob("lib/**/*") + %w(MIT-LICENSE README.rdoc CHANGELOG)
15 s.files = Dir.glob("lib/**/*") + %w(MIT-LICENSE README.rdoc CHANGELOG)
16 s.homepage = %q{http://github.com/collectiveidea/awesome_nested_set}
16 s.homepage = %q{http://github.com/collectiveidea/awesome_nested_set}
17 s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
17 s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
18 s.require_paths = ["lib"]
18 s.require_paths = ["lib"]
19 s.rubygems_version = %q{1.3.6}
19 s.rubygems_version = %q{1.3.6}
20 s.summary = %q{An awesome nested set implementation for Active Record}
20 s.summary = %q{An awesome nested set implementation for Active Record}
21 s.add_runtime_dependency 'activerecord', '>= 3.0.0'
21 s.add_runtime_dependency 'activerecord', '>= 3.0.0'
22 s.add_development_dependency 'rspec-rails', '~> 2.8'
22 end
23 end
@@ -1,7 +1,8
1 require 'awesome_nested_set/awesome_nested_set'
1 require 'awesome_nested_set/awesome_nested_set'
2 require 'active_record'
2 ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet
3 ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet
3
4
4 if defined?(ActionView)
5 if defined?(ActionView)
5 require 'awesome_nested_set/helper'
6 require 'awesome_nested_set/helper'
6 ActionView::Base.send :include, CollectiveIdea::Acts::NestedSet::Helper
7 ActionView::Base.send :include, CollectiveIdea::Acts::NestedSet::Helper
7 end No newline at end of file
8 end
@@ -1,601 +1,744
1 module CollectiveIdea #:nodoc:
1 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
2 module Acts #:nodoc:
3 module NestedSet #:nodoc:
3 module NestedSet #:nodoc:
4
4
5 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
5 # This acts provides Nested Set functionality. Nested Set is a smart way to implement
6 # an _ordered_ tree, with the added feature that you can select the children and all of their
6 # an _ordered_ tree, with the added feature that you can select the children and all of their
7 # descendants with a single query. The drawback is that insertion or move need some complex
7 # descendants with a single query. The drawback is that insertion or move need some complex
8 # sql queries. But everything is done here by this module!
8 # sql queries. But everything is done here by this module!
9 #
9 #
10 # Nested sets are appropriate each time you want either an orderd tree (menus,
10 # Nested sets are appropriate each time you want either an orderd tree (menus,
11 # commercial categories) or an efficient way of querying big trees (threaded posts).
11 # commercial categories) or an efficient way of querying big trees (threaded posts).
12 #
12 #
13 # == API
13 # == API
14 #
14 #
15 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
15 # Methods names are aligned with acts_as_tree as much as possible to make replacment from one
16 # by another easier.
16 # by another easier.
17 #
17 #
18 # item.children.create(:name => "child1")
18 # item.children.create(:name => "child1")
19 #
19 #
20
20
21 # Configuration options are:
21 # Configuration options are:
22 #
22 #
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
24 # * +:left_column+ - column name for left boundry data, default "lft"
24 # * +:left_column+ - column name for left boundry data, default "lft"
25 # * +:right_column+ - column name for right boundry data, default "rgt"
25 # * +:right_column+ - column name for right boundry data, default "rgt"
26 # * +:depth_column+ - column name for the depth data, default "depth"
26 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
27 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
27 # (if it hasn't been already) and use that as the foreign key restriction. You
28 # (if it hasn't been already) and use that as the foreign key restriction. You
28 # can also pass an array to scope by multiple attributes.
29 # can also pass an array to scope by multiple attributes.
29 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
30 # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
30 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
31 # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
31 # child objects are destroyed alongside this object by calling their destroy
32 # child objects are destroyed alongside this object by calling their destroy
32 # method. If set to :delete_all (default), all the child objects are deleted
33 # method. If set to :delete_all (default), all the child objects are deleted
33 # without calling their destroy method.
34 # without calling their destroy method.
34 # * +:counter_cache+ adds a counter cache for the number of children.
35 # * +:counter_cache+ adds a counter cache for the number of children.
35 # defaults to false.
36 # defaults to false.
36 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
37 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
38 # * +:order_column+ on which column to do sorting, by default it is the left_column_name
39 # Example: <tt>acts_as_nested_set :order_column => :position</tt>
37 #
40 #
38 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
41 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
39 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
42 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
40 # to acts_as_nested_set models
43 # to acts_as_nested_set models
41 def acts_as_nested_set(options = {})
44 def acts_as_nested_set(options = {})
42 options = {
45 options = {
43 :parent_column => 'parent_id',
46 :parent_column => 'parent_id',
44 :left_column => 'lft',
47 :left_column => 'lft',
45 :right_column => 'rgt',
48 :right_column => 'rgt',
49 :depth_column => 'depth',
46 :dependent => :delete_all, # or :destroy
50 :dependent => :delete_all, # or :destroy
47 :counter_cache => false,
51 :polymorphic => false,
48 :order => 'id'
52 :counter_cache => false
49 }.merge(options)
53 }.merge(options)
50
54
51 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
55 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
52 options[:scope] = "#{options[:scope]}_id".intern
56 options[:scope] = "#{options[:scope]}_id".intern
53 end
57 end
54
58
55 class_attribute :acts_as_nested_set_options
59 class_attribute :acts_as_nested_set_options
56 self.acts_as_nested_set_options = options
60 self.acts_as_nested_set_options = options
57
61
58 include CollectiveIdea::Acts::NestedSet::Model
62 include CollectiveIdea::Acts::NestedSet::Model
59 include Columns
63 include Columns
60 extend Columns
64 extend Columns
61
65
62 belongs_to :parent, :class_name => self.base_class.to_s,
66 belongs_to :parent, :class_name => self.base_class.to_s,
67 :foreign_key => parent_column_name,
68 :counter_cache => options[:counter_cache],
69 :inverse_of => (:children unless options[:polymorphic]),
70 :polymorphic => options[:polymorphic]
71
72 has_many_children_options = {
73 :class_name => self.base_class.to_s,
63 :foreign_key => parent_column_name,
74 :foreign_key => parent_column_name,
64 :counter_cache => options[:counter_cache],
75 :order => order_column,
65 :inverse_of => :children
76 :inverse_of => (:parent unless options[:polymorphic]),
66 has_many :children, :class_name => self.base_class.to_s,
77 }
67 :foreign_key => parent_column_name, :order => left_column_name,
78
68 :inverse_of => :parent,
79 # Add callbacks, if they were supplied.. otherwise, we don't want them.
69 :before_add => options[:before_add],
80 [:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
70 :after_add => options[:after_add],
81 has_many_children_options.update(ar_callback => options[ar_callback]) if options[ar_callback]
71 :before_remove => options[:before_remove],
82 end
72 :after_remove => options[:after_remove]
83
84 has_many :children, has_many_children_options
73
85
74 attr_accessor :skip_before_destroy
86 attr_accessor :skip_before_destroy
75
87
76 before_create :set_default_left_and_right
88 before_create :set_default_left_and_right
77 before_save :store_new_parent
89 before_save :store_new_parent
78 after_save :move_to_new_parent
90 after_save :move_to_new_parent, :set_depth!
79 before_destroy :destroy_descendants
91 before_destroy :destroy_descendants
80
92
81 # no assignment to structure fields
93 # no assignment to structure fields
82 [left_column_name, right_column_name].each do |column|
94 [left_column_name, right_column_name, depth_column_name].each do |column|
83 module_eval <<-"end_eval", __FILE__, __LINE__
95 module_eval <<-"end_eval", __FILE__, __LINE__
84 def #{column}=(x)
96 def #{column}=(x)
85 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
97 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
86 end
98 end
87 end_eval
99 end_eval
88 end
100 end
89
101
90 define_model_callbacks :move
102 define_model_callbacks :move
91 end
103 end
92
104
93 module Model
105 module Model
94 extend ActiveSupport::Concern
106 extend ActiveSupport::Concern
95
107
108 included do
109 delegate :quoted_table_name, :to => self
110 end
111
96 module ClassMethods
112 module ClassMethods
97 # Returns the first root
113 # Returns the first root
98 def root
114 def root
99 roots.first
115 roots.first
100 end
116 end
101
117
102 def roots
118 def roots
103 where(parent_column_name => nil).order(quoted_left_column_name)
119 where(parent_column_name => nil).order(quoted_left_column_full_name)
104 end
120 end
105
121
106 def leaves
122 def leaves
107 where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
123 where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
108 end
124 end
109
125
110 def valid?
126 def valid?
111 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
127 left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
112 end
128 end
113
129
114 def left_and_rights_valid?
130 def left_and_rights_valid?
115 joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
131 ## AS clause not supported in Oracle in FROM clause for aliasing table name
116 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}").
132 joins("LEFT OUTER JOIN #{quoted_table_name}" +
133 (connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
134 "parent ON " +
135 "#{quoted_parent_column_full_name} = parent.#{primary_key}").
117 where(
136 where(
118 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
137 "#{quoted_left_column_full_name} IS NULL OR " +
119 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
138 "#{quoted_right_column_full_name} IS NULL OR " +
120 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
139 "#{quoted_left_column_full_name} >= " +
121 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
140 "#{quoted_right_column_full_name} OR " +
122 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
141 "(#{quoted_parent_column_full_name} IS NOT NULL AND " +
123 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
142 "(#{quoted_left_column_full_name} <= parent.#{quoted_left_column_name} OR " +
124 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
143 "#{quoted_right_column_full_name} >= parent.#{quoted_right_column_name}))"
125 ).count == 0
144 ).count == 0
126 end
145 end
127
146
128 def no_duplicates_for_columns?
147 def no_duplicates_for_columns?
129 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
148 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
130 connection.quote_column_name(c)
149 connection.quote_column_name(c)
131 end.push(nil).join(", ")
150 end.push(nil).join(", ")
132 [quoted_left_column_name, quoted_right_column_name].all? do |column|
151 [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
133 # No duplicates
152 # No duplicates
134 select("#{scope_string}#{column}, COUNT(#{column})").
153 select("#{scope_string}#{column}, COUNT(#{column})").
135 group("#{scope_string}#{column}").
154 group("#{scope_string}#{column}").
136 having("COUNT(#{column}) > 1").
155 having("COUNT(#{column}) > 1").
137 first.nil?
156 first.nil?
138 end
157 end
139 end
158 end
140
159
141 # Wrapper for each_root_valid? that can deal with scope.
160 # Wrapper for each_root_valid? that can deal with scope.
142 def all_roots_valid?
161 def all_roots_valid?
143 if acts_as_nested_set_options[:scope]
162 if acts_as_nested_set_options[:scope]
144 roots.group(scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
163 roots.group_by {|record| scope_column_names.collect {|col| record.send(col.to_sym) } }.all? do |scope, grouped_roots|
145 each_root_valid?(grouped_roots)
164 each_root_valid?(grouped_roots)
146 end
165 end
147 else
166 else
148 each_root_valid?(roots)
167 each_root_valid?(roots)
149 end
168 end
150 end
169 end
151
170
152 def each_root_valid?(roots_to_validate)
171 def each_root_valid?(roots_to_validate)
153 left = right = 0
172 left = right = 0
154 roots_to_validate.all? do |root|
173 roots_to_validate.all? do |root|
155 (root.left > left && root.right > right).tap do
174 (root.left > left && root.right > right).tap do
156 left = root.left
175 left = root.left
157 right = root.right
176 right = root.right
158 end
177 end
159 end
178 end
160 end
179 end
161
180
162 # Rebuilds the left & rights if unset or invalid.
181 # Rebuilds the left & rights if unset or invalid.
163 # Also very useful for converting from acts_as_tree.
182 # Also very useful for converting from acts_as_tree.
164 def rebuild!(validate_nodes = true)
183 def rebuild!(validate_nodes = true)
165 # Don't rebuild a valid tree.
184 # Don't rebuild a valid tree.
166 return true if valid?
185 return true if valid?
167
186
168 scope = lambda{|node|}
187 scope = lambda{|node|}
169 if acts_as_nested_set_options[:scope]
188 if acts_as_nested_set_options[:scope]
170 scope = lambda{|node|
189 scope = lambda{|node|
171 scope_column_names.inject(""){|str, column_name|
190 scope_column_names.inject(""){|str, column_name|
172 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
191 str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
173 }
192 }
174 }
193 }
175 end
194 end
176 indices = {}
195 indices = {}
177
196
178 set_left_and_rights = lambda do |node|
197 set_left_and_rights = lambda do |node|
179 # set left
198 # set left
180 node[left_column_name] = indices[scope.call(node)] += 1
199 node[left_column_name] = indices[scope.call(node)] += 1
181 # find
200 # find
182 where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).order(acts_as_nested_set_options[:order]).each{|n| set_left_and_rights.call(n) }
201 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) }
183 # set right
202 # set right
184 node[right_column_name] = indices[scope.call(node)] += 1
203 node[right_column_name] = indices[scope.call(node)] += 1
185 node.save!(:validate => validate_nodes)
204 node.save!(:validate => validate_nodes)
186 end
205 end
187
206
188 # Find root node(s)
207 # Find root node(s)
189 root_nodes = where("#{quoted_parent_column_name} IS NULL").order(acts_as_nested_set_options[:order]).each do |root_node|
208 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|
190 # setup index for this scope
209 # setup index for this scope
191 indices[scope.call(root_node)] ||= 0
210 indices[scope.call(root_node)] ||= 0
192 set_left_and_rights.call(root_node)
211 set_left_and_rights.call(root_node)
193 end
212 end
194 end
213 end
195
214
196 # Iterates over tree elements and determines the current level in the tree.
215 # Iterates over tree elements and determines the current level in the tree.
197 # Only accepts default ordering, odering by an other column than lft
216 # Only accepts default ordering, odering by an other column than lft
198 # does not work. This method is much more efficent than calling level
217 # does not work. This method is much more efficent than calling level
199 # because it doesn't require any additional database queries.
218 # because it doesn't require any additional database queries.
200 #
219 #
201 # Example:
220 # Example:
202 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
221 # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
203 #
222 #
204 def each_with_level(objects)
223 def each_with_level(objects)
205 path = [nil]
224 path = [nil]
206 objects.each do |o|
225 objects.each do |o|
207 if o.parent_id != path.last
226 if o.parent_id != path.last
208 # we are on a new level, did we decent or ascent?
227 # we are on a new level, did we descend or ascend?
209 if path.include?(o.parent_id)
228 if path.include?(o.parent_id)
210 # remove wrong wrong tailing paths elements
229 # remove wrong wrong tailing paths elements
211 path.pop while path.last != o.parent_id
230 path.pop while path.last != o.parent_id
212 else
231 else
213 path << o.parent_id
232 path << o.parent_id
214 end
233 end
215 end
234 end
216 yield(o, path.length - 1)
235 yield(o, path.length - 1)
217 end
236 end
218 end
237 end
238
239 # Same as each_with_level - Accepts a string as a second argument to sort the list
240 # Example:
241 # Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
242 def sorted_each_with_level(objects, order)
243 path = [nil]
244 children = []
245 objects.each do |o|
246 children << o if o.leaf?
247 if o.parent_id != path.last
248 if !children.empty? && !o.leaf?
249 children.sort_by! &order
250 children.each { |c| yield(c, path.length-1) }
251 children = []
252 end
253 # we are on a new level, did we decent or ascent?
254 if path.include?(o.parent_id)
255 # remove wrong wrong tailing paths elements
256 path.pop while path.last != o.parent_id
257 else
258 path << o.parent_id
259 end
260 end
261 yield(o,path.length-1) if !o.leaf?
262 end
263 if !children.empty?
264 children.sort_by! &order
265 children.each { |c| yield(c, path.length-1) }
266 end
267 end
268
269 def associate_parents(objects)
270 if objects.all?{|o| o.respond_to?(:association)}
271 id_indexed = objects.index_by(&:id)
272 objects.each do |object|
273 if !(association = object.association(:parent)).loaded? && (parent = id_indexed[object.parent_id])
274 association.target = parent
275 association.set_inverse_instance(parent)
276 end
277 end
278 else
279 objects
280 end
281 end
219 end
282 end
220
283
221 # 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.
284 # 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.
222 #
285 #
223 # category.self_and_descendants.count
286 # category.self_and_descendants.count
224 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
287 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
225
226 # Value of the parent column
288 # Value of the parent column
227 def parent_id
289 def parent_id
228 self[parent_column_name]
290 self[parent_column_name]
229 end
291 end
230
292
231 # Value of the left column
293 # Value of the left column
232 def left
294 def left
233 self[left_column_name]
295 self[left_column_name]
234 end
296 end
235
297
236 # Value of the right column
298 # Value of the right column
237 def right
299 def right
238 self[right_column_name]
300 self[right_column_name]
239 end
301 end
240
302
241 # Returns true if this is a root node.
303 # Returns true if this is a root node.
242 def root?
304 def root?
243 parent_id.nil?
305 parent_id.nil?
244 end
306 end
245
307
308 # Returns true if this is the end of a branch.
246 def leaf?
309 def leaf?
247 new_record? || (right - left == 1)
310 persisted? && right.to_i - left.to_i == 1
248 end
311 end
249
312
250 # Returns true is this is a child node
313 # Returns true is this is a child node
251 def child?
314 def child?
252 !parent_id.nil?
315 !root?
253 end
316 end
254
317
255 # Returns root
318 # Returns root
256 def root
319 def root
257 self_and_ancestors.where(parent_column_name => nil).first
320 if persisted?
321 self_and_ancestors.where(parent_column_name => nil).first
322 else
323 if parent_id && current_parent = nested_set_scope.find(parent_id)
324 current_parent.root
325 else
326 self
327 end
328 end
258 end
329 end
259
330
260 # Returns the array of all parents and self
331 # Returns the array of all parents and self
261 def self_and_ancestors
332 def self_and_ancestors
262 nested_set_scope.where([
333 nested_set_scope.where([
263 "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right
334 "#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
264 ])
335 ])
265 end
336 end
266
337
267 # Returns an array of all parents
338 # Returns an array of all parents
268 def ancestors
339 def ancestors
269 without_self self_and_ancestors
340 without_self self_and_ancestors
270 end
341 end
271
342
272 # Returns the array of all children of the parent, including self
343 # Returns the array of all children of the parent, including self
273 def self_and_siblings
344 def self_and_siblings
274 nested_set_scope.where(parent_column_name => parent_id)
345 nested_set_scope.where(parent_column_name => parent_id)
275 end
346 end
276
347
277 # Returns the array of all children of the parent, except self
348 # Returns the array of all children of the parent, except self
278 def siblings
349 def siblings
279 without_self self_and_siblings
350 without_self self_and_siblings
280 end
351 end
281
352
282 # Returns a set of all of its nested children which do not have children
353 # Returns a set of all of its nested children which do not have children
283 def leaves
354 def leaves
284 descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
355 descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
285 end
356 end
286
357
287 # Returns the level of this object in the tree
358 # Returns the level of this object in the tree
288 # root level is 0
359 # root level is 0
289 def level
360 def level
290 parent_id.nil? ? 0 : ancestors.count
361 parent_id.nil? ? 0 : compute_level
291 end
362 end
292
363
293 # Returns a set of itself and all of its nested children
364 # Returns a set of itself and all of its nested children
294 def self_and_descendants
365 def self_and_descendants
295 nested_set_scope.where([
366 nested_set_scope.where([
296 "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right
367 "#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
368 # using _left_ for both sides here lets us benefit from an index on that column if one exists
297 ])
369 ])
298 end
370 end
299
371
300 # Returns a set of all of its children and nested children
372 # Returns a set of all of its children and nested children
301 def descendants
373 def descendants
302 without_self self_and_descendants
374 without_self self_and_descendants
303 end
375 end
304
376
305 def is_descendant_of?(other)
377 def is_descendant_of?(other)
306 other.left < self.left && self.left < other.right && same_scope?(other)
378 other.left < self.left && self.left < other.right && same_scope?(other)
307 end
379 end
308
380
309 def is_or_is_descendant_of?(other)
381 def is_or_is_descendant_of?(other)
310 other.left <= self.left && self.left < other.right && same_scope?(other)
382 other.left <= self.left && self.left < other.right && same_scope?(other)
311 end
383 end
312
384
313 def is_ancestor_of?(other)
385 def is_ancestor_of?(other)
314 self.left < other.left && other.left < self.right && same_scope?(other)
386 self.left < other.left && other.left < self.right && same_scope?(other)
315 end
387 end
316
388
317 def is_or_is_ancestor_of?(other)
389 def is_or_is_ancestor_of?(other)
318 self.left <= other.left && other.left < self.right && same_scope?(other)
390 self.left <= other.left && other.left < self.right && same_scope?(other)
319 end
391 end
320
392
321 # Check if other model is in the same scope
393 # Check if other model is in the same scope
322 def same_scope?(other)
394 def same_scope?(other)
323 Array(acts_as_nested_set_options[:scope]).all? do |attr|
395 Array(acts_as_nested_set_options[:scope]).all? do |attr|
324 self.send(attr) == other.send(attr)
396 self.send(attr) == other.send(attr)
325 end
397 end
326 end
398 end
327
399
328 # Find the first sibling to the left
400 # Find the first sibling to the left
329 def left_sibling
401 def left_sibling
330 siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]).
402 siblings.where(["#{quoted_left_column_full_name} < ?", left]).
331 order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last
403 order("#{quoted_left_column_full_name} DESC").last
332 end
404 end
333
405
334 # Find the first sibling to the right
406 # Find the first sibling to the right
335 def right_sibling
407 def right_sibling
336 siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first
408 siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
337 end
409 end
338
410
339 # Shorthand method for finding the left sibling and moving to the left of it.
411 # Shorthand method for finding the left sibling and moving to the left of it.
340 def move_left
412 def move_left
341 move_to_left_of left_sibling
413 move_to_left_of left_sibling
342 end
414 end
343
415
344 # Shorthand method for finding the right sibling and moving to the right of it.
416 # Shorthand method for finding the right sibling and moving to the right of it.
345 def move_right
417 def move_right
346 move_to_right_of right_sibling
418 move_to_right_of right_sibling
347 end
419 end
348
420
349 # Move the node to the left of another node (you can pass id only)
421 # Move the node to the left of another node (you can pass id only)
350 def move_to_left_of(node)
422 def move_to_left_of(node)
351 move_to node, :left
423 move_to node, :left
352 end
424 end
353
425
354 # Move the node to the left of another node (you can pass id only)
426 # Move the node to the left of another node (you can pass id only)
355 def move_to_right_of(node)
427 def move_to_right_of(node)
356 move_to node, :right
428 move_to node, :right
357 end
429 end
358
430
359 # Move the node to the child of another node (you can pass id only)
431 # Move the node to the child of another node (you can pass id only)
360 def move_to_child_of(node)
432 def move_to_child_of(node)
361 move_to node, :child
433 move_to node, :child
362 end
434 end
363
435
436 # Move the node to the child of another node with specify index (you can pass id only)
437 def move_to_child_with_index(node, index)
438 if node.children.empty?
439 move_to_child_of(node)
440 elsif node.children.count == index
441 move_to_right_of(node.children.last)
442 else
443 move_to_left_of(node.children[index])
444 end
445 end
446
364 # Move the node to root nodes
447 # Move the node to root nodes
365 def move_to_root
448 def move_to_root
366 move_to nil, :root
449 move_to nil, :root
367 end
450 end
368
451
452 # Order children in a nested set by an attribute
453 # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
454 # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
455 def move_to_ordered_child_of(parent, order_attribute, ascending = true)
456 self.move_to_root and return unless parent
457 left = nil # This is needed, at least for the tests.
458 parent.children.each do |n| # Find the node immediately to the left of this node.
459 if ascending
460 left = n if n.send(order_attribute) < self.send(order_attribute)
461 else
462 left = n if n.send(order_attribute) > self.send(order_attribute)
463 end
464 end
465 self.move_to_child_of(parent)
466 return unless parent.children.count > 1 # Only need to order if there are multiple children.
467 if left # Self has a left neighbor.
468 self.move_to_right_of(left)
469 else # Self is the left most node.
470 self.move_to_left_of(parent.children[0])
471 end
472 end
473
369 def move_possible?(target)
474 def move_possible?(target)
370 self != target && # Can't target self
475 self != target && # Can't target self
371 same_scope?(target) && # can't be in different scopes
476 same_scope?(target) && # can't be in different scopes
372 # !(left..right).include?(target.left..target.right) # this needs tested more
477 # !(left..right).include?(target.left..target.right) # this needs tested more
373 # detect impossible move
478 # detect impossible move
374 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
479 !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
375 end
480 end
376
481
377 def to_text
482 def to_text
378 self_and_descendants.map do |node|
483 self_and_descendants.map do |node|
379 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
484 "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
380 end.join("\n")
485 end.join("\n")
381 end
486 end
382
487
383 protected
488 protected
489 def compute_level
490 node, nesting = self, 0
491 while (association = node.association(:parent)).loaded? && association.target
492 nesting += 1
493 node = node.parent
494 end if node.respond_to? :association
495 node == self ? ancestors.count : node.level + nesting
496 end
384
497
385 def without_self(scope)
498 def without_self(scope)
386 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
499 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
387 end
500 end
388
501
389 # All nested set queries should use this nested_set_scope, which performs finds on
502 # All nested set queries should use this nested_set_scope, which performs finds on
390 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
503 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
391 # declaration.
504 # declaration.
392 def nested_set_scope(options = {})
505 def nested_set_scope(options = {})
393 options = {:order => "#{self.class.quoted_table_name}.#{quoted_left_column_name}"}.merge(options)
506 options = {:order => quoted_left_column_full_name}.merge(options)
394 scopes = Array(acts_as_nested_set_options[:scope])
507 scopes = Array(acts_as_nested_set_options[:scope])
395 options[:conditions] = scopes.inject({}) do |conditions,attr|
508 options[:conditions] = scopes.inject({}) do |conditions,attr|
396 conditions.merge attr => self[attr]
509 conditions.merge attr => self[attr]
397 end unless scopes.empty?
510 end unless scopes.empty?
398 self.class.base_class.scoped options
511 self.class.base_class.unscoped.scoped options
399 end
512 end
400
513
401 def store_new_parent
514 def store_new_parent
402 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
515 @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
403 true # force callback to return true
516 true # force callback to return true
404 end
517 end
405
518
406 def move_to_new_parent
519 def move_to_new_parent
407 if @move_to_new_parent_id.nil?
520 if @move_to_new_parent_id.nil?
408 move_to_root
521 move_to_root
409 elsif @move_to_new_parent_id
522 elsif @move_to_new_parent_id
410 move_to_child_of(@move_to_new_parent_id)
523 move_to_child_of(@move_to_new_parent_id)
411 end
524 end
412 end
525 end
413
526
527 def set_depth!
528 if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
529 in_tenacious_transaction do
530 reload
531
532 nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
533 end
534 self[depth_column_name.to_sym] = self.level
535 end
536 end
537
414 # on creation, set automatically lft and rgt to the end of the tree
538 # on creation, set automatically lft and rgt to the end of the tree
415 def set_default_left_and_right
539 def set_default_left_and_right
416 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").limit(1).lock(true).first
540 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_full_name} desc").limit(1).lock(true).first
417 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
541 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
418 # adds the new node to the right of all existing nodes
542 # adds the new node to the right of all existing nodes
419 self[left_column_name] = maxright + 1
543 self[left_column_name] = maxright + 1
420 self[right_column_name] = maxright + 2
544 self[right_column_name] = maxright + 2
421 end
545 end
422
546
423 def in_tenacious_transaction(&block)
547 def in_tenacious_transaction(&block)
424 retry_count = 0
548 retry_count = 0
425 begin
549 begin
426 transaction(&block)
550 transaction(&block)
427 rescue ActiveRecord::StatementInvalid => error
551 rescue ActiveRecord::StatementInvalid => error
428 raise unless connection.open_transactions.zero?
552 raise unless connection.open_transactions.zero?
429 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
553 raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
430 raise unless retry_count < 10
554 raise unless retry_count < 10
431 retry_count += 1
555 retry_count += 1
432 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
556 logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
433 sleep(rand(retry_count)*0.1) # Aloha protocol
557 sleep(rand(retry_count)*0.1) # Aloha protocol
434 retry
558 retry
435 end
559 end
436 end
560 end
437
561
438 # Prunes a branch off of the tree, shifting all of the elements on the right
562 # Prunes a branch off of the tree, shifting all of the elements on the right
439 # back to the left so the counts still work.
563 # back to the left so the counts still work.
440 def destroy_descendants
564 def destroy_descendants
441 return if right.nil? || left.nil? || skip_before_destroy
565 return if right.nil? || left.nil? || skip_before_destroy
442
566
443 in_tenacious_transaction do
567 in_tenacious_transaction do
444 reload_nested_set
568 reload_nested_set
445 # select the rows in the model that extend past the deletion point and apply a lock
569 # select the rows in the model that extend past the deletion point and apply a lock
446 nested_set_scope.
570 nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
447 select("id").
571 select(id).lock(true)
448 where("#{quoted_left_column_name} >= ?", left).
449 lock(true).
450 all
451
572
452 if acts_as_nested_set_options[:dependent] == :destroy
573 if acts_as_nested_set_options[:dependent] == :destroy
453 descendants.each do |model|
574 descendants.each do |model|
454 model.skip_before_destroy = true
575 model.skip_before_destroy = true
455 model.destroy
576 model.destroy
456 end
577 end
457 else
578 else
458 nested_set_scope.delete_all(
579 nested_set_scope.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
459 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
580 delete_all
460 left, right]
461 )
462 end
581 end
463
582
464 # update lefts and rights for remaining nodes
583 # update lefts and rights for remaining nodes
465 diff = right - left + 1
584 diff = right - left + 1
466 nested_set_scope.update_all(
585 nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
467 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
586 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
468 ["#{quoted_left_column_name} > ?", right]
469 )
587 )
470 nested_set_scope.update_all(
588
471 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
589 nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
472 ["#{quoted_right_column_name} > ?", right]
590 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
473 )
591 )
474
592
475 reload
593 # Don't allow multiple calls to destroy to corrupt the set
476 # Don't allow multiple calls to destroy to corrupt the set
477 self.skip_before_destroy = true
594 self.skip_before_destroy = true
478 end
595 end
479 end
596 end
480
597
481 # reload left, right, and parent
598 # reload left, right, and parent
482 def reload_nested_set
599 def reload_nested_set
483 reload(
600 reload(
484 :select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}",
601 :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
485 :lock => true
602 :lock => true
486 )
603 )
487 end
604 end
488
605
489 def move_to(target, position)
606 def move_to(target, position)
490 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
607 raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
491 run_callbacks :move do
608 run_callbacks :move do
492 in_tenacious_transaction do
609 in_tenacious_transaction do
493 if target.is_a? self.class.base_class
610 if target.is_a? self.class.base_class
494 target.reload_nested_set
611 target.reload_nested_set
495 elsif position != :root
612 elsif position != :root
496 # load object if node is not an object
613 # load object if node is not an object
497 target = nested_set_scope.find(target)
614 target = nested_set_scope.find(target)
498 end
615 end
499 self.reload_nested_set
616 self.reload_nested_set
500
617
501 unless position == :root || move_possible?(target)
618 unless position == :root || move_possible?(target)
502 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
619 raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
503 end
620 end
504
621
505 bound = case position
622 bound = case position
506 when :child; target[right_column_name]
623 when :child; target[right_column_name]
507 when :left; target[left_column_name]
624 when :left; target[left_column_name]
508 when :right; target[right_column_name] + 1
625 when :right; target[right_column_name] + 1
509 when :root; 1
626 when :root; 1
510 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
627 else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
511 end
628 end
512
629
513 if bound > self[right_column_name]
630 if bound > self[right_column_name]
514 bound = bound - 1
631 bound = bound - 1
515 other_bound = self[right_column_name] + 1
632 other_bound = self[right_column_name] + 1
516 else
633 else
517 other_bound = self[left_column_name] - 1
634 other_bound = self[left_column_name] - 1
518 end
635 end
519
636
520 # there would be no change
637 # there would be no change
521 return if bound == self[right_column_name] || bound == self[left_column_name]
638 return if bound == self[right_column_name] || bound == self[left_column_name]
522
639
523 # we have defined the boundaries of two non-overlapping intervals,
640 # we have defined the boundaries of two non-overlapping intervals,
524 # so sorting puts both the intervals and their boundaries in order
641 # so sorting puts both the intervals and their boundaries in order
525 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
642 a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
526
643
527 # select the rows in the model between a and d, and apply a lock
644 # select the rows in the model between a and d, and apply a lock
528 self.class.base_class.select('id').lock(true).where(
645 self.class.base_class.select('id').lock(true).where(
529 ["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}]
646 ["#{quoted_left_column_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
530 )
647 )
531
648
532 new_parent = case position
649 new_parent = case position
533 when :child; target.id
650 when :child; target.id
534 when :root; nil
651 when :root; nil
535 else target[parent_column_name]
652 else target[parent_column_name]
536 end
653 end
537
654
538 self.nested_set_scope.update_all([
655 self.nested_set_scope.update_all([
539 "#{quoted_left_column_name} = CASE " +
656 "#{quoted_left_column_name} = CASE " +
540 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
657 "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
541 "THEN #{quoted_left_column_name} + :d - :b " +
658 "THEN #{quoted_left_column_name} + :d - :b " +
542 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
659 "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
543 "THEN #{quoted_left_column_name} + :a - :c " +
660 "THEN #{quoted_left_column_name} + :a - :c " +
544 "ELSE #{quoted_left_column_name} END, " +
661 "ELSE #{quoted_left_column_name} END, " +
545 "#{quoted_right_column_name} = CASE " +
662 "#{quoted_right_column_name} = CASE " +
546 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
663 "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
547 "THEN #{quoted_right_column_name} + :d - :b " +
664 "THEN #{quoted_right_column_name} + :d - :b " +
548 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
665 "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
549 "THEN #{quoted_right_column_name} + :a - :c " +
666 "THEN #{quoted_right_column_name} + :a - :c " +
550 "ELSE #{quoted_right_column_name} END, " +
667 "ELSE #{quoted_right_column_name} END, " +
551 "#{quoted_parent_column_name} = CASE " +
668 "#{quoted_parent_column_name} = CASE " +
552 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
669 "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
553 "ELSE #{quoted_parent_column_name} END",
670 "ELSE #{quoted_parent_column_name} END",
554 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
671 {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
555 ])
672 ])
556 end
673 end
557 target.reload_nested_set if target
674 target.reload_nested_set if target
675 self.set_depth!
676 self.descendants.each(&:save)
558 self.reload_nested_set
677 self.reload_nested_set
559 end
678 end
560 end
679 end
561
680
562 end
681 end
563
682
564 # Mixed into both classes and instances to provide easy access to the column names
683 # Mixed into both classes and instances to provide easy access to the column names
565 module Columns
684 module Columns
566 def left_column_name
685 def left_column_name
567 acts_as_nested_set_options[:left_column]
686 acts_as_nested_set_options[:left_column]
568 end
687 end
569
688
570 def right_column_name
689 def right_column_name
571 acts_as_nested_set_options[:right_column]
690 acts_as_nested_set_options[:right_column]
572 end
691 end
573
692
693 def depth_column_name
694 acts_as_nested_set_options[:depth_column]
695 end
696
574 def parent_column_name
697 def parent_column_name
575 acts_as_nested_set_options[:parent_column]
698 acts_as_nested_set_options[:parent_column]
576 end
699 end
577
700
701 def order_column
702 acts_as_nested_set_options[:order_column] || left_column_name
703 end
704
578 def scope_column_names
705 def scope_column_names
579 Array(acts_as_nested_set_options[:scope])
706 Array(acts_as_nested_set_options[:scope])
580 end
707 end
581
708
582 def quoted_left_column_name
709 def quoted_left_column_name
583 connection.quote_column_name(left_column_name)
710 connection.quote_column_name(left_column_name)
584 end
711 end
585
712
586 def quoted_right_column_name
713 def quoted_right_column_name
587 connection.quote_column_name(right_column_name)
714 connection.quote_column_name(right_column_name)
588 end
715 end
589
716
717 def quoted_depth_column_name
718 connection.quote_column_name(depth_column_name)
719 end
720
590 def quoted_parent_column_name
721 def quoted_parent_column_name
591 connection.quote_column_name(parent_column_name)
722 connection.quote_column_name(parent_column_name)
592 end
723 end
593
724
594 def quoted_scope_column_names
725 def quoted_scope_column_names
595 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
726 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
596 end
727 end
728
729 def quoted_left_column_full_name
730 "#{quoted_table_name}.#{quoted_left_column_name}"
731 end
732
733 def quoted_right_column_full_name
734 "#{quoted_table_name}.#{quoted_right_column_name}"
735 end
736
737 def quoted_parent_column_full_name
738 "#{quoted_table_name}.#{quoted_parent_column_name}"
739 end
597 end
740 end
598
741
599 end
742 end
600 end
743 end
601 end
744 end
@@ -1,44 +1,89
1 # -*- coding: utf-8 -*-
1 module CollectiveIdea #:nodoc:
2 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
3 module Acts #:nodoc:
3 module NestedSet #:nodoc:
4 module NestedSet #:nodoc:
4 # This module provides some helpers for the model classes using acts_as_nested_set.
5 # This module provides some helpers for the model classes using acts_as_nested_set.
5 # It is included by default in all views.
6 # It is included by default in all views.
6 #
7 #
7 module Helper
8 module Helper
8 # Returns options for select.
9 # Returns options for select.
9 # You can exclude some items from the tree.
10 # You can exclude some items from the tree.
10 # You can pass a block receiving an item and returning the string displayed in the select.
11 # You can pass a block receiving an item and returning the string displayed in the select.
11 #
12 #
12 # == Params
13 # == Params
13 # * +class_or_item+ - Class name or top level times
14 # * +class_or_item+ - Class name or top level times
14 # * +mover+ - The item that is being move, used to exlude impossible moves
15 # * +mover+ - The item that is being move, used to exlude impossible moves
15 # * +&block+ - a block that will be used to display: { |item| ... item.name }
16 # * +&block+ - a block that will be used to display: { |item| ... item.name }
16 #
17 #
17 # == Usage
18 # == Usage
18 #
19 #
19 # <%= f.select :parent_id, nested_set_options(Category, @category) {|i|
20 # <%= f.select :parent_id, nested_set_options(Category, @category) {|i|
20 # "#{'–' * i.level} #{i.name}"
21 # "#{'–' * i.level} #{i.name}"
21 # }) %>
22 # }) %>
22 #
23 #
23 def nested_set_options(class_or_item, mover = nil)
24 def nested_set_options(class_or_item, mover = nil)
24 if class_or_item.is_a? Array
25 if class_or_item.is_a? Array
25 items = class_or_item.reject { |e| !e.root? }
26 items = class_or_item.reject { |e| !e.root? }
26 else
27 else
27 class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
28 class_or_item = class_or_item.roots if class_or_item.respond_to?(:scoped)
28 items = Array(class_or_item)
29 items = Array(class_or_item)
29 end
30 end
30 result = []
31 result = []
31 items.each do |root|
32 items.each do |root|
32 result += root.self_and_descendants.map do |i|
33 result += root.class.associate_parents(root.self_and_descendants).map do |i|
33 if mover.nil? || mover.new_record? || mover.move_possible?(i)
34 if mover.nil? || mover.new_record? || mover.move_possible?(i)
34 [yield(i), i.id]
35 [yield(i), i.id]
35 end
36 end
36 end.compact
37 end.compact
37 end
38 end
38 result
39 result
39 end
40 end
40
41
42 # Returns options for select as nested_set_options, sorted by an specific column
43 # It requires passing a string with the name of the column to sort the set with
44 # You can exclude some items from the tree.
45 # You can pass a block receiving an item and returning the string displayed in the select.
46 #
47 # == Params
48 # * +class_or_item+ - Class name or top level times
49 # * +:column+ - Column to sort the set (this will sort each children for all root elements)
50 # * +mover+ - The item that is being move, used to exlude impossible moves
51 # * +&block+ - a block that will be used to display: { |item| ... item.name }
52 #
53 # == Usage
54 #
55 # <%= f.select :parent_id, nested_set_options(Category, :sort_by_this_column, @category) {|i|
56 # "#{'–' * i.level} #{i.name}"
57 # }) %>
58 #
59 def sorted_nested_set_options(class_or_item, order, mover = nil)
60 if class_or_item.is_a? Array
61 items = class_or_item.reject { |e| !e.root? }
62 else
63 class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
64 items = Array(class_or_item)
65 end
66 result = []
67 children = []
68 items.each do |root|
69 root.class.associate_parents(root.self_and_descendants).map do |i|
70 if mover.nil? || mover.new_record? || mover.move_possible?(i)
71 if !i.leaf?
72 children.sort_by! &order
73 children.each { |c| result << [yield(c), c.id] }
74 children = []
75 result << [yield(i), i.id]
76 else
77 children << i
78 end
79 end
80 end.compact
81 end
82 children.sort_by! &order
83 children.each { |c| result << [yield(c), c.id] }
84 result
85 end
41 end
86 end
42 end
87 end
43 end
88 end
44 end
89 end
@@ -1,3 +1,3
1 module AwesomeNestedSet
1 module AwesomeNestedSet
2 VERSION = '2.1.0' unless defined?(::AwesomeNestedSet::VERSION)
2 VERSION = '2.1.5' unless defined?(::AwesomeNestedSet::VERSION)
3 end
3 end
@@ -1,67 +1,95
1 require 'spec_helper'
1 require 'spec_helper'
2
2
3 describe "Helper" do
3 describe "Helper" do
4 include CollectiveIdea::Acts::NestedSet::Helper
4 include CollectiveIdea::Acts::NestedSet::Helper
5
5
6 before(:all) do
6 before(:all) do
7 self.class.fixtures :categories
7 self.class.fixtures :categories
8 end
8 end
9
9
10 describe "nested_set_options" do
10 describe "nested_set_options" do
11 it "test_nested_set_options" do
11 it "test_nested_set_options" do
12 expected = [
12 expected = [
13 [" Top Level", 1],
13 [" Top Level", 1],
14 ["- Child 1", 2],
14 ["- Child 1", 2],
15 ['- Child 2', 3],
15 ['- Child 2', 3],
16 ['-- Child 2.1', 4],
16 ['-- Child 2.1', 4],
17 ['- Child 3', 5],
17 ['- Child 3', 5],
18 [" Top Level 2", 6]
18 [" Top Level 2", 6]
19 ]
19 ]
20 actual = nested_set_options(Category) do |c|
20 actual = nested_set_options(Category.scoped) do |c|
21 "#{'-' * c.level} #{c.name}"
21 "#{'-' * c.level} #{c.name}"
22 end
22 end
23 actual.should == expected
23 actual.should == expected
24 end
24 end
25
25
26 it "test_nested_set_options_with_mover" do
26 it "test_nested_set_options_with_mover" do
27 expected = [
27 expected = [
28 [" Top Level", 1],
28 [" Top Level", 1],
29 ["- Child 1", 2],
29 ["- Child 1", 2],
30 ['- Child 3', 5],
30 ['- Child 3', 5],
31 [" Top Level 2", 6]
31 [" Top Level 2", 6]
32 ]
32 ]
33 actual = nested_set_options(Category.scoped, categories(:child_2)) do |c|
34 "#{'-' * c.level} #{c.name}"
35 end
36 actual.should == expected
37 end
38
39 it "test_nested_set_options_with_class_as_argument" do
40 expected = [
41 [" Top Level", 1],
42 ["- Child 1", 2],
43 ['- Child 2', 3],
44 ['-- Child 2.1', 4],
45 ['- Child 3', 5],
46 [" Top Level 2", 6]
47 ]
48 actual = nested_set_options(Category) do |c|
49 "#{'-' * c.level} #{c.name}"
50 end
51 actual.should == expected
52 end
53
54 it "test_nested_set_options_with_class_as_argument_with_mover" do
55 expected = [
56 [" Top Level", 1],
57 ["- Child 1", 2],
58 ['- Child 3', 5],
59 [" Top Level 2", 6]
60 ]
33 actual = nested_set_options(Category, categories(:child_2)) do |c|
61 actual = nested_set_options(Category, categories(:child_2)) do |c|
34 "#{'-' * c.level} #{c.name}"
62 "#{'-' * c.level} #{c.name}"
35 end
63 end
36 actual.should == expected
64 actual.should == expected
37 end
65 end
38
66
39 it "test_nested_set_options_with_array_as_argument_without_mover" do
67 it "test_nested_set_options_with_array_as_argument_without_mover" do
40 expected = [
68 expected = [
41 [" Top Level", 1],
69 [" Top Level", 1],
42 ["- Child 1", 2],
70 ["- Child 1", 2],
43 ['- Child 2', 3],
71 ['- Child 2', 3],
44 ['-- Child 2.1', 4],
72 ['-- Child 2.1', 4],
45 ['- Child 3', 5],
73 ['- Child 3', 5],
46 [" Top Level 2", 6]
74 [" Top Level 2", 6]
47 ]
75 ]
48 actual = nested_set_options(Category.all) do |c|
76 actual = nested_set_options(Category.all) do |c|
49 "#{'-' * c.level} #{c.name}"
77 "#{'-' * c.level} #{c.name}"
50 end
78 end
51 actual.should == expected
79 actual.should == expected
52 end
80 end
53
81
54 it "test_nested_set_options_with_array_as_argument_with_mover" do
82 it "test_nested_set_options_with_array_as_argument_with_mover" do
55 expected = [
83 expected = [
56 [" Top Level", 1],
84 [" Top Level", 1],
57 ["- Child 1", 2],
85 ["- Child 1", 2],
58 ['- Child 3', 5],
86 ['- Child 3', 5],
59 [" Top Level 2", 6]
87 [" Top Level 2", 6]
60 ]
88 ]
61 actual = nested_set_options(Category.all, categories(:child_2)) do |c|
89 actual = nested_set_options(Category.all, categories(:child_2)) do |c|
62 "#{'-' * c.level} #{c.name}"
90 "#{'-' * c.level} #{c.name}"
63 end
91 end
64 actual.should == expected
92 actual.should == expected
65 end
93 end
66 end
94 end
67 end
95 end
@@ -1,841 +1,1082
1 require 'spec_helper'
1 require 'spec_helper'
2
2
3 describe "AwesomeNestedSet" do
3 describe "AwesomeNestedSet" do
4 before(:all) do
4 before(:all) do
5 self.class.fixtures :categories, :departments, :notes, :things, :brokens
5 self.class.fixtures :categories, :departments, :notes, :things, :brokens
6 end
6 end
7
7
8 describe "defaults" do
8 describe "defaults" do
9 it "should have left_column_default" do
9 it "should have left_column_default" do
10 Default.acts_as_nested_set_options[:left_column].should == 'lft'
10 Default.acts_as_nested_set_options[:left_column].should == 'lft'
11 end
11 end
12
12
13 it "should have right_column_default" do
13 it "should have right_column_default" do
14 Default.acts_as_nested_set_options[:right_column].should == 'rgt'
14 Default.acts_as_nested_set_options[:right_column].should == 'rgt'
15 end
15 end
16
16
17 it "should have parent_column_default" do
17 it "should have parent_column_default" do
18 Default.acts_as_nested_set_options[:parent_column].should == 'parent_id'
18 Default.acts_as_nested_set_options[:parent_column].should == 'parent_id'
19 end
19 end
20
20
21 it "should have scope_default" do
21 it "should have scope_default" do
22 Default.acts_as_nested_set_options[:scope].should be_nil
22 Default.acts_as_nested_set_options[:scope].should be_nil
23 end
23 end
24
24
25 it "should have left_column_name" do
25 it "should have left_column_name" do
26 Default.left_column_name.should == 'lft'
26 Default.left_column_name.should == 'lft'
27 Default.new.left_column_name.should == 'lft'
27 Default.new.left_column_name.should == 'lft'
28 RenamedColumns.left_column_name.should == 'red'
28 RenamedColumns.left_column_name.should == 'red'
29 RenamedColumns.new.left_column_name.should == 'red'
29 RenamedColumns.new.left_column_name.should == 'red'
30 end
30 end
31
31
32 it "should have right_column_name" do
32 it "should have right_column_name" do
33 Default.right_column_name.should == 'rgt'
33 Default.right_column_name.should == 'rgt'
34 Default.new.right_column_name.should == 'rgt'
34 Default.new.right_column_name.should == 'rgt'
35 RenamedColumns.right_column_name.should == 'black'
35 RenamedColumns.right_column_name.should == 'black'
36 RenamedColumns.new.right_column_name.should == 'black'
36 RenamedColumns.new.right_column_name.should == 'black'
37 end
37 end
38
38
39 it "has a depth_column_name" do
40 Default.depth_column_name.should == 'depth'
41 Default.new.depth_column_name.should == 'depth'
42 RenamedColumns.depth_column_name.should == 'pitch'
43 RenamedColumns.depth_column_name.should == 'pitch'
44 end
45
39 it "should have parent_column_name" do
46 it "should have parent_column_name" do
40 Default.parent_column_name.should == 'parent_id'
47 Default.parent_column_name.should == 'parent_id'
41 Default.new.parent_column_name.should == 'parent_id'
48 Default.new.parent_column_name.should == 'parent_id'
42 RenamedColumns.parent_column_name.should == 'mother_id'
49 RenamedColumns.parent_column_name.should == 'mother_id'
43 RenamedColumns.new.parent_column_name.should == 'mother_id'
50 RenamedColumns.new.parent_column_name.should == 'mother_id'
44 end
51 end
45 end
52 end
46
53
47 it "creation_with_altered_column_names" do
54 it "creation_with_altered_column_names" do
48 lambda {
55 lambda {
49 RenamedColumns.create!()
56 RenamedColumns.create!()
50 }.should_not raise_exception
57 }.should_not raise_exception
51 end
58 end
52
59
53 it "creation when existing record has nil left column" do
60 it "creation when existing record has nil left column" do
54 assert_nothing_raised do
61 assert_nothing_raised do
55 Broken.create!
62 Broken.create!
56 end
63 end
57 end
64 end
58
65
59 it "quoted_left_column_name" do
66 it "quoted_left_column_name" do
60 quoted = Default.connection.quote_column_name('lft')
67 quoted = Default.connection.quote_column_name('lft')
61 Default.quoted_left_column_name.should == quoted
68 Default.quoted_left_column_name.should == quoted
62 Default.new.quoted_left_column_name.should == quoted
69 Default.new.quoted_left_column_name.should == quoted
63 end
70 end
64
71
65 it "quoted_right_column_name" do
72 it "quoted_right_column_name" do
66 quoted = Default.connection.quote_column_name('rgt')
73 quoted = Default.connection.quote_column_name('rgt')
67 Default.quoted_right_column_name.should == quoted
74 Default.quoted_right_column_name.should == quoted
68 Default.new.quoted_right_column_name.should == quoted
75 Default.new.quoted_right_column_name.should == quoted
69 end
76 end
70
77
78 it "quoted_depth_column_name" do
79 quoted = Default.connection.quote_column_name('depth')
80 Default.quoted_depth_column_name.should == quoted
81 Default.new.quoted_depth_column_name.should == quoted
82 end
83
71 it "left_column_protected_from_assignment" do
84 it "left_column_protected_from_assignment" do
72 lambda {
85 lambda {
73 Category.new.lft = 1
86 Category.new.lft = 1
74 }.should raise_exception(ActiveRecord::ActiveRecordError)
87 }.should raise_exception(ActiveRecord::ActiveRecordError)
75 end
88 end
76
89
77 it "right_column_protected_from_assignment" do
90 it "right_column_protected_from_assignment" do
78 lambda {
91 lambda {
79 Category.new.rgt = 1
92 Category.new.rgt = 1
80 }.should raise_exception(ActiveRecord::ActiveRecordError)
93 }.should raise_exception(ActiveRecord::ActiveRecordError)
81 end
94 end
82
95
96 it "depth_column_protected_from_assignment" do
97 lambda {
98 Category.new.depth = 1
99 }.should raise_exception(ActiveRecord::ActiveRecordError)
100 end
101
83 it "scoped_appends_id" do
102 it "scoped_appends_id" do
84 ScopedCategory.acts_as_nested_set_options[:scope].should == :organization_id
103 ScopedCategory.acts_as_nested_set_options[:scope].should == :organization_id
85 end
104 end
86
105
87 it "roots_class_method" do
106 it "roots_class_method" do
88 Category.roots.should == Category.find_all_by_parent_id(nil)
107 Category.roots.should == Category.find_all_by_parent_id(nil)
89 end
108 end
90
109
91 it "root_class_method" do
110 it "root_class_method" do
92 Category.root.should == categories(:top_level)
111 Category.root.should == categories(:top_level)
93 end
112 end
94
113
95 it "root" do
114 it "root" do
96 categories(:child_3).root.should == categories(:top_level)
115 categories(:child_3).root.should == categories(:top_level)
97 end
116 end
98
117
118 it "root when not persisted and parent_column_name value is self" do
119 new_category = Category.new
120 new_category.root.should == new_category
121 end
122
123 it "root when not persisted and parent_column_name value is set" do
124 last_category = Category.last
125 Category.new(Default.parent_column_name => last_category.id).root.should == last_category.root
126 end
127
99 it "root?" do
128 it "root?" do
100 categories(:top_level).root?.should be_true
129 categories(:top_level).root?.should be_true
101 categories(:top_level_2).root?.should be_true
130 categories(:top_level_2).root?.should be_true
102 end
131 end
103
132
104 it "leaves_class_method" do
133 it "leaves_class_method" do
105 Category.find(:all, :conditions => "#{Category.right_column_name} - #{Category.left_column_name} = 1").should == Category.leaves
134 Category.find(:all, :conditions => "#{Category.right_column_name} - #{Category.left_column_name} = 1").should == Category.leaves
106 Category.leaves.count.should == 4
135 Category.leaves.count.should == 4
107 Category.leaves.should include(categories(:child_1))
136 Category.leaves.should include(categories(:child_1))
108 Category.leaves.should include(categories(:child_2_1))
137 Category.leaves.should include(categories(:child_2_1))
109 Category.leaves.should include(categories(:child_3))
138 Category.leaves.should include(categories(:child_3))
110 Category.leaves.should include(categories(:top_level_2))
139 Category.leaves.should include(categories(:top_level_2))
111 end
140 end
112
141
113 it "leaf" do
142 it "leaf" do
114 categories(:child_1).leaf?.should be_true
143 categories(:child_1).leaf?.should be_true
115 categories(:child_2_1).leaf?.should be_true
144 categories(:child_2_1).leaf?.should be_true
116 categories(:child_3).leaf?.should be_true
145 categories(:child_3).leaf?.should be_true
117 categories(:top_level_2).leaf?.should be_true
146 categories(:top_level_2).leaf?.should be_true
118
147
119 categories(:top_level).leaf?.should be_false
148 categories(:top_level).leaf?.should be_false
120 categories(:child_2).leaf?.should be_false
149 categories(:child_2).leaf?.should be_false
121 Category.new.leaf?.should be_false
150 Category.new.leaf?.should be_false
122 end
151 end
123
152
124
153
125 it "parent" do
154 it "parent" do
126 categories(:child_2_1).parent.should == categories(:child_2)
155 categories(:child_2_1).parent.should == categories(:child_2)
127 end
156 end
128
157
129 it "self_and_ancestors" do
158 it "self_and_ancestors" do
130 child = categories(:child_2_1)
159 child = categories(:child_2_1)
131 self_and_ancestors = [categories(:top_level), categories(:child_2), child]
160 self_and_ancestors = [categories(:top_level), categories(:child_2), child]
132 self_and_ancestors.should == child.self_and_ancestors
161 self_and_ancestors.should == child.self_and_ancestors
133 end
162 end
134
163
135 it "ancestors" do
164 it "ancestors" do
136 child = categories(:child_2_1)
165 child = categories(:child_2_1)
137 ancestors = [categories(:top_level), categories(:child_2)]
166 ancestors = [categories(:top_level), categories(:child_2)]
138 ancestors.should == child.ancestors
167 ancestors.should == child.ancestors
139 end
168 end
140
169
141 it "self_and_siblings" do
170 it "self_and_siblings" do
142 child = categories(:child_2)
171 child = categories(:child_2)
143 self_and_siblings = [categories(:child_1), child, categories(:child_3)]
172 self_and_siblings = [categories(:child_1), child, categories(:child_3)]
144 self_and_siblings.should == child.self_and_siblings
173 self_and_siblings.should == child.self_and_siblings
145 lambda do
174 lambda do
146 tops = [categories(:top_level), categories(:top_level_2)]
175 tops = [categories(:top_level), categories(:top_level_2)]
147 assert_equal tops, categories(:top_level).self_and_siblings
176 assert_equal tops, categories(:top_level).self_and_siblings
148 end.should_not raise_exception
177 end.should_not raise_exception
149 end
178 end
150
179
151 it "siblings" do
180 it "siblings" do
152 child = categories(:child_2)
181 child = categories(:child_2)
153 siblings = [categories(:child_1), categories(:child_3)]
182 siblings = [categories(:child_1), categories(:child_3)]
154 siblings.should == child.siblings
183 siblings.should == child.siblings
155 end
184 end
156
185
157 it "leaves" do
186 it "leaves" do
158 leaves = [categories(:child_1), categories(:child_2_1), categories(:child_3)]
187 leaves = [categories(:child_1), categories(:child_2_1), categories(:child_3)]
159 categories(:top_level).leaves.should == leaves
188 categories(:top_level).leaves.should == leaves
160 end
189 end
161
190
162 it "level" do
191 describe "level" do
163 categories(:top_level).level.should == 0
192 it "returns the correct level" do
164 categories(:child_1).level.should == 1
193 categories(:top_level).level.should == 0
165 categories(:child_2_1).level.should == 2
194 categories(:child_1).level.should == 1
195 categories(:child_2_1).level.should == 2
196 end
197
198 context "given parent associations are loaded" do
199 it "returns the correct level" do
200 child = categories(:child_1)
201 if child.respond_to?(:association)
202 child.association(:parent).load_target
203 child.parent.association(:parent).load_target
204 child.level.should == 1
205 else
206 pending 'associations not used where child#association is not a method'
207 end
208 end
209 end
210 end
211
212 describe "depth" do
213 let(:lawyers) { Category.create!(:name => "lawyers") }
214 let(:us) { Category.create!(:name => "United States") }
215 let(:new_york) { Category.create!(:name => "New York") }
216 let(:patent) { Category.create!(:name => "Patent Law") }
217
218 before(:each) do
219 # lawyers > us > new_york > patent
220 us.move_to_child_of(lawyers)
221 new_york.move_to_child_of(us)
222 patent.move_to_child_of(new_york)
223 [lawyers, us, new_york, patent].each(&:reload)
224 end
225
226 it "updates depth when moved into child position" do
227 lawyers.depth.should == 0
228 us.depth.should == 1
229 new_york.depth.should == 2
230 patent.depth.should == 3
231 end
232
233 it "updates depth of all descendants when parent is moved" do
234 # lawyers
235 # us > new_york > patent
236 us.move_to_right_of(lawyers)
237 [lawyers, us, new_york, patent].each(&:reload)
238 us.depth.should == 0
239 new_york.depth.should == 1
240 patent.depth.should == 2
241 end
242 end
243
244 it "depth is magic and does not apply when column is missing" do
245 lambda { NoDepth.create!(:name => "shallow") }.should_not raise_error
246 lambda { NoDepth.first.save }.should_not raise_error
247 lambda { NoDepth.rebuild! }.should_not raise_error
248
249 NoDepth.method_defined?(:depth).should be_false
250 NoDepth.first.respond_to?(:depth).should be_false
166 end
251 end
167
252
168 it "has_children?" do
253 it "has_children?" do
169 categories(:child_2_1).children.empty?.should be_true
254 categories(:child_2_1).children.empty?.should be_true
170 categories(:child_2).children.empty?.should be_false
255 categories(:child_2).children.empty?.should be_false
171 categories(:top_level).children.empty?.should be_false
256 categories(:top_level).children.empty?.should be_false
172 end
257 end
173
258
174 it "self_and_descendents" do
259 it "self_and_descendants" do
175 parent = categories(:top_level)
260 parent = categories(:top_level)
176 self_and_descendants = [parent, categories(:child_1), categories(:child_2),
261 self_and_descendants = [
177 categories(:child_2_1), categories(:child_3)]
262 parent,
263 categories(:child_1),
264 categories(:child_2),
265 categories(:child_2_1),
266 categories(:child_3)
267 ]
178 self_and_descendants.should == parent.self_and_descendants
268 self_and_descendants.should == parent.self_and_descendants
179 self_and_descendants.count.should == parent.self_and_descendants.count
269 self_and_descendants.count.should == parent.self_and_descendants.count
180 end
270 end
181
271
182 it "descendents" do
272 it "descendants" do
183 lawyers = Category.create!(:name => "lawyers")
273 lawyers = Category.create!(:name => "lawyers")
184 us = Category.create!(:name => "United States")
274 us = Category.create!(:name => "United States")
185 us.move_to_child_of(lawyers)
275 us.move_to_child_of(lawyers)
186 patent = Category.create!(:name => "Patent Law")
276 patent = Category.create!(:name => "Patent Law")
187 patent.move_to_child_of(us)
277 patent.move_to_child_of(us)
188 lawyers.reload
278 lawyers.reload
189
279
190 lawyers.children.size.should == 1
280 lawyers.children.size.should == 1
191 us.children.size.should == 1
281 us.children.size.should == 1
192 lawyers.descendants.size.should == 2
282 lawyers.descendants.size.should == 2
193 end
283 end
194
284
195 it "self_and_descendents" do
285 it "self_and_descendants" do
196 parent = categories(:top_level)
286 parent = categories(:top_level)
197 descendants = [categories(:child_1), categories(:child_2),
287 descendants = [
198 categories(:child_2_1), categories(:child_3)]
288 categories(:child_1),
289 categories(:child_2),
290 categories(:child_2_1),
291 categories(:child_3)
292 ]
199 descendants.should == parent.descendants
293 descendants.should == parent.descendants
200 end
294 end
201
295
202 it "children" do
296 it "children" do
203 category = categories(:top_level)
297 category = categories(:top_level)
204 category.children.each {|c| category.id.should == c.parent_id }
298 category.children.each {|c| category.id.should == c.parent_id }
205 end
299 end
206
300
207 it "order_of_children" do
301 it "order_of_children" do
208 categories(:child_2).move_left
302 categories(:child_2).move_left
209 categories(:child_2).should == categories(:top_level).children[0]
303 categories(:child_2).should == categories(:top_level).children[0]
210 categories(:child_1).should == categories(:top_level).children[1]
304 categories(:child_1).should == categories(:top_level).children[1]
211 categories(:child_3).should == categories(:top_level).children[2]
305 categories(:child_3).should == categories(:top_level).children[2]
212 end
306 end
213
307
214 it "is_or_is_ancestor_of?" do
308 it "is_or_is_ancestor_of?" do
215 categories(:top_level).is_or_is_ancestor_of?(categories(:child_1)).should be_true
309 categories(:top_level).is_or_is_ancestor_of?(categories(:child_1)).should be_true
216 categories(:top_level).is_or_is_ancestor_of?(categories(:child_2_1)).should be_true
310 categories(:top_level).is_or_is_ancestor_of?(categories(:child_2_1)).should be_true
217 categories(:child_2).is_or_is_ancestor_of?(categories(:child_2_1)).should be_true
311 categories(:child_2).is_or_is_ancestor_of?(categories(:child_2_1)).should be_true
218 categories(:child_2_1).is_or_is_ancestor_of?(categories(:child_2)).should be_false
312 categories(:child_2_1).is_or_is_ancestor_of?(categories(:child_2)).should be_false
219 categories(:child_1).is_or_is_ancestor_of?(categories(:child_2)).should be_false
313 categories(:child_1).is_or_is_ancestor_of?(categories(:child_2)).should be_false
220 categories(:child_1).is_or_is_ancestor_of?(categories(:child_1)).should be_true
314 categories(:child_1).is_or_is_ancestor_of?(categories(:child_1)).should be_true
221 end
315 end
222
316
223 it "is_ancestor_of?" do
317 it "is_ancestor_of?" do
224 categories(:top_level).is_ancestor_of?(categories(:child_1)).should be_true
318 categories(:top_level).is_ancestor_of?(categories(:child_1)).should be_true
225 categories(:top_level).is_ancestor_of?(categories(:child_2_1)).should be_true
319 categories(:top_level).is_ancestor_of?(categories(:child_2_1)).should be_true
226 categories(:child_2).is_ancestor_of?(categories(:child_2_1)).should be_true
320 categories(:child_2).is_ancestor_of?(categories(:child_2_1)).should be_true
227 categories(:child_2_1).is_ancestor_of?(categories(:child_2)).should be_false
321 categories(:child_2_1).is_ancestor_of?(categories(:child_2)).should be_false
228 categories(:child_1).is_ancestor_of?(categories(:child_2)).should be_false
322 categories(:child_1).is_ancestor_of?(categories(:child_2)).should be_false
229 categories(:child_1).is_ancestor_of?(categories(:child_1)).should be_false
323 categories(:child_1).is_ancestor_of?(categories(:child_1)).should be_false
230 end
324 end
231
325
232 it "is_or_is_ancestor_of_with_scope" do
326 it "is_or_is_ancestor_of_with_scope" do
233 root = ScopedCategory.root
327 root = ScopedCategory.root
234 child = root.children.first
328 child = root.children.first
235 root.is_or_is_ancestor_of?(child).should be_true
329 root.is_or_is_ancestor_of?(child).should be_true
236 child.update_attribute :organization_id, 'different'
330 child.update_attribute :organization_id, 'different'
237 root.is_or_is_ancestor_of?(child).should be_false
331 root.is_or_is_ancestor_of?(child).should be_false
238 end
332 end
239
333
240 it "is_or_is_descendant_of?" do
334 it "is_or_is_descendant_of?" do
241 categories(:child_1).is_or_is_descendant_of?(categories(:top_level)).should be_true
335 categories(:child_1).is_or_is_descendant_of?(categories(:top_level)).should be_true
242 categories(:child_2_1).is_or_is_descendant_of?(categories(:top_level)).should be_true
336 categories(:child_2_1).is_or_is_descendant_of?(categories(:top_level)).should be_true
243 categories(:child_2_1).is_or_is_descendant_of?(categories(:child_2)).should be_true
337 categories(:child_2_1).is_or_is_descendant_of?(categories(:child_2)).should be_true
244 categories(:child_2).is_or_is_descendant_of?(categories(:child_2_1)).should be_false
338 categories(:child_2).is_or_is_descendant_of?(categories(:child_2_1)).should be_false
245 categories(:child_2).is_or_is_descendant_of?(categories(:child_1)).should be_false
339 categories(:child_2).is_or_is_descendant_of?(categories(:child_1)).should be_false
246 categories(:child_1).is_or_is_descendant_of?(categories(:child_1)).should be_true
340 categories(:child_1).is_or_is_descendant_of?(categories(:child_1)).should be_true
247 end
341 end
248
342
249 it "is_descendant_of?" do
343 it "is_descendant_of?" do
250 categories(:child_1).is_descendant_of?(categories(:top_level)).should be_true
344 categories(:child_1).is_descendant_of?(categories(:top_level)).should be_true
251 categories(:child_2_1).is_descendant_of?(categories(:top_level)).should be_true
345 categories(:child_2_1).is_descendant_of?(categories(:top_level)).should be_true
252 categories(:child_2_1).is_descendant_of?(categories(:child_2)).should be_true
346 categories(:child_2_1).is_descendant_of?(categories(:child_2)).should be_true
253 categories(:child_2).is_descendant_of?(categories(:child_2_1)).should be_false
347 categories(:child_2).is_descendant_of?(categories(:child_2_1)).should be_false
254 categories(:child_2).is_descendant_of?(categories(:child_1)).should be_false
348 categories(:child_2).is_descendant_of?(categories(:child_1)).should be_false
255 categories(:child_1).is_descendant_of?(categories(:child_1)).should be_false
349 categories(:child_1).is_descendant_of?(categories(:child_1)).should be_false
256 end
350 end
257
351
258 it "is_or_is_descendant_of_with_scope" do
352 it "is_or_is_descendant_of_with_scope" do
259 root = ScopedCategory.root
353 root = ScopedCategory.root
260 child = root.children.first
354 child = root.children.first
261 child.is_or_is_descendant_of?(root).should be_true
355 child.is_or_is_descendant_of?(root).should be_true
262 child.update_attribute :organization_id, 'different'
356 child.update_attribute :organization_id, 'different'
263 child.is_or_is_descendant_of?(root).should be_false
357 child.is_or_is_descendant_of?(root).should be_false
264 end
358 end
265
359
266 it "same_scope?" do
360 it "same_scope?" do
267 root = ScopedCategory.root
361 root = ScopedCategory.root
268 child = root.children.first
362 child = root.children.first
269 child.same_scope?(root).should be_true
363 child.same_scope?(root).should be_true
270 child.update_attribute :organization_id, 'different'
364 child.update_attribute :organization_id, 'different'
271 child.same_scope?(root).should be_false
365 child.same_scope?(root).should be_false
272 end
366 end
273
367
274 it "left_sibling" do
368 it "left_sibling" do
275 categories(:child_1).should == categories(:child_2).left_sibling
369 categories(:child_1).should == categories(:child_2).left_sibling
276 categories(:child_2).should == categories(:child_3).left_sibling
370 categories(:child_2).should == categories(:child_3).left_sibling
277 end
371 end
278
372
279 it "left_sibling_of_root" do
373 it "left_sibling_of_root" do
280 categories(:top_level).left_sibling.should be_nil
374 categories(:top_level).left_sibling.should be_nil
281 end
375 end
282
376
283 it "left_sibling_without_siblings" do
377 it "left_sibling_without_siblings" do
284 categories(:child_2_1).left_sibling.should be_nil
378 categories(:child_2_1).left_sibling.should be_nil
285 end
379 end
286
380
287 it "left_sibling_of_leftmost_node" do
381 it "left_sibling_of_leftmost_node" do
288 categories(:child_1).left_sibling.should be_nil
382 categories(:child_1).left_sibling.should be_nil
289 end
383 end
290
384
291 it "right_sibling" do
385 it "right_sibling" do
292 categories(:child_3).should == categories(:child_2).right_sibling
386 categories(:child_3).should == categories(:child_2).right_sibling
293 categories(:child_2).should == categories(:child_1).right_sibling
387 categories(:child_2).should == categories(:child_1).right_sibling
294 end
388 end
295
389
296 it "right_sibling_of_root" do
390 it "right_sibling_of_root" do
297 categories(:top_level_2).should == categories(:top_level).right_sibling
391 categories(:top_level_2).should == categories(:top_level).right_sibling
298 categories(:top_level_2).right_sibling.should be_nil
392 categories(:top_level_2).right_sibling.should be_nil
299 end
393 end
300
394
301 it "right_sibling_without_siblings" do
395 it "right_sibling_without_siblings" do
302 categories(:child_2_1).right_sibling.should be_nil
396 categories(:child_2_1).right_sibling.should be_nil
303 end
397 end
304
398
305 it "right_sibling_of_rightmost_node" do
399 it "right_sibling_of_rightmost_node" do
306 categories(:child_3).right_sibling.should be_nil
400 categories(:child_3).right_sibling.should be_nil
307 end
401 end
308
402
309 it "move_left" do
403 it "move_left" do
310 categories(:child_2).move_left
404 categories(:child_2).move_left
311 categories(:child_2).left_sibling.should be_nil
405 categories(:child_2).left_sibling.should be_nil
312 categories(:child_1).should == categories(:child_2).right_sibling
406 categories(:child_1).should == categories(:child_2).right_sibling
313 Category.valid?.should be_true
407 Category.valid?.should be_true
314 end
408 end
315
409
316 it "move_right" do
410 it "move_right" do
317 categories(:child_2).move_right
411 categories(:child_2).move_right
318 categories(:child_2).right_sibling.should be_nil
412 categories(:child_2).right_sibling.should be_nil
319 categories(:child_3).should == categories(:child_2).left_sibling
413 categories(:child_3).should == categories(:child_2).left_sibling
320 Category.valid?.should be_true
414 Category.valid?.should be_true
321 end
415 end
322
416
323 it "move_to_left_of" do
417 it "move_to_left_of" do
324 categories(:child_3).move_to_left_of(categories(:child_1))
418 categories(:child_3).move_to_left_of(categories(:child_1))
325 categories(:child_3).left_sibling.should be_nil
419 categories(:child_3).left_sibling.should be_nil
326 categories(:child_1).should == categories(:child_3).right_sibling
420 categories(:child_1).should == categories(:child_3).right_sibling
327 Category.valid?.should be_true
421 Category.valid?.should be_true
328 end
422 end
329
423
330 it "move_to_right_of" do
424 it "move_to_right_of" do
331 categories(:child_1).move_to_right_of(categories(:child_3))
425 categories(:child_1).move_to_right_of(categories(:child_3))
332 categories(:child_1).right_sibling.should be_nil
426 categories(:child_1).right_sibling.should be_nil
333 categories(:child_3).should == categories(:child_1).left_sibling
427 categories(:child_3).should == categories(:child_1).left_sibling
334 Category.valid?.should be_true
428 Category.valid?.should be_true
335 end
429 end
336
430
337 it "move_to_root" do
431 it "move_to_root" do
338 categories(:child_2).move_to_root
432 categories(:child_2).move_to_root
339 categories(:child_2).parent.should be_nil
433 categories(:child_2).parent.should be_nil
340 categories(:child_2).level.should == 0
434 categories(:child_2).level.should == 0
341 categories(:child_2_1).level.should == 1
435 categories(:child_2_1).level.should == 1
342 categories(:child_2).left.should == 1
436 categories(:child_2).left.should == 1
343 categories(:child_2).right.should == 4
437 categories(:child_2).right.should == 4
344 Category.valid?.should be_true
438 Category.valid?.should be_true
345 end
439 end
346
440
347 it "move_to_child_of" do
441 it "move_to_child_of" do
348 categories(:child_1).move_to_child_of(categories(:child_3))
442 categories(:child_1).move_to_child_of(categories(:child_3))
349 categories(:child_3).id.should == categories(:child_1).parent_id
443 categories(:child_3).id.should == categories(:child_1).parent_id
350 Category.valid?.should be_true
444 Category.valid?.should be_true
351 end
445 end
352
446
447 describe "#move_to_child_with_index" do
448 it "move to a node without child" do
449 categories(:child_1).move_to_child_with_index(categories(:child_3), 0)
450 categories(:child_3).id.should == categories(:child_1).parent_id
451 categories(:child_1).left.should == 7
452 categories(:child_1).right.should == 8
453 categories(:child_3).left.should == 6
454 categories(:child_3).right.should == 9
455 Category.valid?.should be_true
456 end
457
458 it "move to a node to the left child" do
459 categories(:child_1).move_to_child_with_index(categories(:child_2), 0)
460 categories(:child_1).parent_id.should == categories(:child_2).id
461 categories(:child_2_1).left.should == 5
462 categories(:child_2_1).right.should == 6
463 categories(:child_1).left.should == 3
464 categories(:child_1).right.should == 4
465 categories(:child_2).reload
466 categories(:child_2).left.should == 2
467 categories(:child_2).right.should == 7
468 end
469
470 it "move to a node to the right child" do
471 categories(:child_1).move_to_child_with_index(categories(:child_2), 1)
472 categories(:child_1).parent_id.should == categories(:child_2).id
473 categories(:child_2_1).left.should == 3
474 categories(:child_2_1).right.should == 4
475 categories(:child_1).left.should == 5
476 categories(:child_1).right.should == 6
477 categories(:child_2).reload
478 categories(:child_2).left.should == 2
479 categories(:child_2).right.should == 7
480 end
481
482 end
483
353 it "move_to_child_of_appends_to_end" do
484 it "move_to_child_of_appends_to_end" do
354 child = Category.create! :name => 'New Child'
485 child = Category.create! :name => 'New Child'
355 child.move_to_child_of categories(:top_level)
486 child.move_to_child_of categories(:top_level)
356 child.should == categories(:top_level).children.last
487 child.should == categories(:top_level).children.last
357 end
488 end
358
489
359 it "subtree_move_to_child_of" do
490 it "subtree_move_to_child_of" do
360 categories(:child_2).left.should == 4
491 categories(:child_2).left.should == 4
361 categories(:child_2).right.should == 7
492 categories(:child_2).right.should == 7
362
493
363 categories(:child_1).left.should == 2
494 categories(:child_1).left.should == 2
364 categories(:child_1).right.should == 3
495 categories(:child_1).right.should == 3
365
496
366 categories(:child_2).move_to_child_of(categories(:child_1))
497 categories(:child_2).move_to_child_of(categories(:child_1))
367 Category.valid?.should be_true
498 Category.valid?.should be_true
368 categories(:child_1).id.should == categories(:child_2).parent_id
499 categories(:child_1).id.should == categories(:child_2).parent_id
369
500
370 categories(:child_2).left.should == 3
501 categories(:child_2).left.should == 3
371 categories(:child_2).right.should == 6
502 categories(:child_2).right.should == 6
372 categories(:child_1).left.should == 2
503 categories(:child_1).left.should == 2
373 categories(:child_1).right.should == 7
504 categories(:child_1).right.should == 7
374 end
505 end
375
506
376 it "slightly_difficult_move_to_child_of" do
507 it "slightly_difficult_move_to_child_of" do
377 categories(:top_level_2).left.should == 11
508 categories(:top_level_2).left.should == 11
378 categories(:top_level_2).right.should == 12
509 categories(:top_level_2).right.should == 12
379
510
380 # create a new top-level node and move single-node top-level tree inside it.
511 # create a new top-level node and move single-node top-level tree inside it.
381 new_top = Category.create(:name => 'New Top')
512 new_top = Category.create(:name => 'New Top')
382 new_top.left.should == 13
513 new_top.left.should == 13
383 new_top.right.should == 14
514 new_top.right.should == 14
384
515
385 categories(:top_level_2).move_to_child_of(new_top)
516 categories(:top_level_2).move_to_child_of(new_top)
386
517
387 Category.valid?.should be_true
518 Category.valid?.should be_true
388 new_top.id.should == categories(:top_level_2).parent_id
519 new_top.id.should == categories(:top_level_2).parent_id
389
520
390 categories(:top_level_2).left.should == 12
521 categories(:top_level_2).left.should == 12
391 categories(:top_level_2).right.should == 13
522 categories(:top_level_2).right.should == 13
392 new_top.left.should == 11
523 new_top.left.should == 11
393 new_top.right.should == 14
524 new_top.right.should == 14
394 end
525 end
395
526
396 it "difficult_move_to_child_of" do
527 it "difficult_move_to_child_of" do
397 categories(:top_level).left.should == 1
528 categories(:top_level).left.should == 1
398 categories(:top_level).right.should == 10
529 categories(:top_level).right.should == 10
399 categories(:child_2_1).left.should == 5
530 categories(:child_2_1).left.should == 5
400 categories(:child_2_1).right.should == 6
531 categories(:child_2_1).right.should == 6
401
532
402 # create a new top-level node and move an entire top-level tree inside it.
533 # create a new top-level node and move an entire top-level tree inside it.
403 new_top = Category.create(:name => 'New Top')
534 new_top = Category.create(:name => 'New Top')
404 categories(:top_level).move_to_child_of(new_top)
535 categories(:top_level).move_to_child_of(new_top)
405 categories(:child_2_1).reload
536 categories(:child_2_1).reload
406 Category.valid?.should be_true
537 Category.valid?.should be_true
407 new_top.id.should == categories(:top_level).parent_id
538 new_top.id.should == categories(:top_level).parent_id
408
539
409 categories(:top_level).left.should == 4
540 categories(:top_level).left.should == 4
410 categories(:top_level).right.should == 13
541 categories(:top_level).right.should == 13
411 categories(:child_2_1).left.should == 8
542 categories(:child_2_1).left.should == 8
412 categories(:child_2_1).right.should == 9
543 categories(:child_2_1).right.should == 9
413 end
544 end
414
545
415 #rebuild swaps the position of the 2 children when added using move_to_child twice onto same parent
546 #rebuild swaps the position of the 2 children when added using move_to_child twice onto same parent
416 it "move_to_child_more_than_once_per_parent_rebuild" do
547 it "move_to_child_more_than_once_per_parent_rebuild" do
417 root1 = Category.create(:name => 'Root1')
548 root1 = Category.create(:name => 'Root1')
418 root2 = Category.create(:name => 'Root2')
549 root2 = Category.create(:name => 'Root2')
419 root3 = Category.create(:name => 'Root3')
550 root3 = Category.create(:name => 'Root3')
420
551
421 root2.move_to_child_of root1
552 root2.move_to_child_of root1
422 root3.move_to_child_of root1
553 root3.move_to_child_of root1
423
554
424 output = Category.roots.last.to_text
555 output = Category.roots.last.to_text
425 Category.update_all('lft = null, rgt = null')
556 Category.update_all('lft = null, rgt = null')
426 Category.rebuild!
557 Category.rebuild!
427
558
428 Category.roots.last.to_text.should == output
559 Category.roots.last.to_text.should == output
429 end
560 end
430
561
431 # doing move_to_child twice onto same parent from the furthest right first
562 # doing move_to_child twice onto same parent from the furthest right first
432 it "move_to_child_more_than_once_per_parent_outside_in" do
563 it "move_to_child_more_than_once_per_parent_outside_in" do
433 node1 = Category.create(:name => 'Node-1')
564 node1 = Category.create(:name => 'Node-1')
434 node2 = Category.create(:name => 'Node-2')
565 node2 = Category.create(:name => 'Node-2')
435 node3 = Category.create(:name => 'Node-3')
566 node3 = Category.create(:name => 'Node-3')
436
567
437 node2.move_to_child_of node1
568 node2.move_to_child_of node1
438 node3.move_to_child_of node1
569 node3.move_to_child_of node1
439
570
440 output = Category.roots.last.to_text
571 output = Category.roots.last.to_text
441 Category.update_all('lft = null, rgt = null')
572 Category.update_all('lft = null, rgt = null')
442 Category.rebuild!
573 Category.rebuild!
443
574
444 Category.roots.last.to_text.should == output
575 Category.roots.last.to_text.should == output
445 end
576 end
446
577
578 it "should_move_to_ordered_child" do
579 node1 = Category.create(:name => 'Node-1')
580 node2 = Category.create(:name => 'Node-2')
581 node3 = Category.create(:name => 'Node-3')
582
583 node2.move_to_ordered_child_of(node1, "name")
584
585 assert_equal node1, node2.parent
586 assert_equal 1, node1.children.count
587
588 node3.move_to_ordered_child_of(node1, "name", true) # acending
589
590 assert_equal node1, node3.parent
591 assert_equal 2, node1.children.count
592 assert_equal node2.name, node1.children[0].name
593 assert_equal node3.name, node1.children[1].name
594
595 node3.move_to_ordered_child_of(node1, "name", false) # decending
596 node1.reload
597
598 assert_equal node1, node3.parent
599 assert_equal 2, node1.children.count
600 assert_equal node3.name, node1.children[0].name
601 assert_equal node2.name, node1.children[1].name
602 end
603
447 it "should be able to rebuild without validating each record" do
604 it "should be able to rebuild without validating each record" do
448 root1 = Category.create(:name => 'Root1')
605 root1 = Category.create(:name => 'Root1')
449 root2 = Category.create(:name => 'Root2')
606 root2 = Category.create(:name => 'Root2')
450 root3 = Category.create(:name => 'Root3')
607 root3 = Category.create(:name => 'Root3')
451
608
452 root2.move_to_child_of root1
609 root2.move_to_child_of root1
453 root3.move_to_child_of root1
610 root3.move_to_child_of root1
454
611
455 root2.name = nil
612 root2.name = nil
456 root2.save!(:validate => false)
613 root2.save!(:validate => false)
457
614
458 output = Category.roots.last.to_text
615 output = Category.roots.last.to_text
459 Category.update_all('lft = null, rgt = null')
616 Category.update_all('lft = null, rgt = null')
460 Category.rebuild!(false)
617 Category.rebuild!(false)
461
618
462 Category.roots.last.to_text.should == output
619 Category.roots.last.to_text.should == output
463 end
620 end
464
621
465 it "valid_with_null_lefts" do
622 it "valid_with_null_lefts" do
466 Category.valid?.should be_true
623 Category.valid?.should be_true
467 Category.update_all('lft = null')
624 Category.update_all('lft = null')
468 Category.valid?.should be_false
625 Category.valid?.should be_false
469 end
626 end
470
627
471 it "valid_with_null_rights" do
628 it "valid_with_null_rights" do
472 Category.valid?.should be_true
629 Category.valid?.should be_true
473 Category.update_all('rgt = null')
630 Category.update_all('rgt = null')
474 Category.valid?.should be_false
631 Category.valid?.should be_false
475 end
632 end
476
633
477 it "valid_with_missing_intermediate_node" do
634 it "valid_with_missing_intermediate_node" do
478 # Even though child_2_1 will still exist, it is a sign of a sloppy delete, not an invalid tree.
635 # Even though child_2_1 will still exist, it is a sign of a sloppy delete, not an invalid tree.
479 Category.valid?.should be_true
636 Category.valid?.should be_true
480 Category.delete(categories(:child_2).id)
637 Category.delete(categories(:child_2).id)
481 Category.valid?.should be_true
638 Category.valid?.should be_true
482 end
639 end
483
640
484 it "valid_with_overlapping_and_rights" do
641 it "valid_with_overlapping_and_rights" do
485 Category.valid?.should be_true
642 Category.valid?.should be_true
486 categories(:top_level_2)['lft'] = 0
643 categories(:top_level_2)['lft'] = 0
487 categories(:top_level_2).save
644 categories(:top_level_2).save
488 Category.valid?.should be_false
645 Category.valid?.should be_false
489 end
646 end
490
647
491 it "rebuild" do
648 it "rebuild" do
492 Category.valid?.should be_true
649 Category.valid?.should be_true
493 before_text = Category.root.to_text
650 before_text = Category.root.to_text
494 Category.update_all('lft = null, rgt = null')
651 Category.update_all('lft = null, rgt = null')
495 Category.rebuild!
652 Category.rebuild!
496 Category.valid?.should be_true
653 Category.valid?.should be_true
497 before_text.should == Category.root.to_text
654 before_text.should == Category.root.to_text
498 end
655 end
499
656
500 it "move_possible_for_sibling" do
657 it "move_possible_for_sibling" do
501 categories(:child_2).move_possible?(categories(:child_1)).should be_true
658 categories(:child_2).move_possible?(categories(:child_1)).should be_true
502 end
659 end
503
660
504 it "move_not_possible_to_self" do
661 it "move_not_possible_to_self" do
505 categories(:top_level).move_possible?(categories(:top_level)).should be_false
662 categories(:top_level).move_possible?(categories(:top_level)).should be_false
506 end
663 end
507
664
508 it "move_not_possible_to_parent" do
665 it "move_not_possible_to_parent" do
509 categories(:top_level).descendants.each do |descendant|
666 categories(:top_level).descendants.each do |descendant|
510 categories(:top_level).move_possible?(descendant).should be_false
667 categories(:top_level).move_possible?(descendant).should be_false
511 descendant.move_possible?(categories(:top_level)).should be_true
668 descendant.move_possible?(categories(:top_level)).should be_true
512 end
669 end
513 end
670 end
514
671
515 it "is_or_is_ancestor_of?" do
672 it "is_or_is_ancestor_of?" do
516 [:child_1, :child_2, :child_2_1, :child_3].each do |c|
673 [:child_1, :child_2, :child_2_1, :child_3].each do |c|
517 categories(:top_level).is_or_is_ancestor_of?(categories(c)).should be_true
674 categories(:top_level).is_or_is_ancestor_of?(categories(c)).should be_true
518 end
675 end
519 categories(:top_level).is_or_is_ancestor_of?(categories(:top_level_2)).should be_false
676 categories(:top_level).is_or_is_ancestor_of?(categories(:top_level_2)).should be_false
520 end
677 end
521
678
522 it "left_and_rights_valid_with_blank_left" do
679 it "left_and_rights_valid_with_blank_left" do
523 Category.left_and_rights_valid?.should be_true
680 Category.left_and_rights_valid?.should be_true
524 categories(:child_2)[:lft] = nil
681 categories(:child_2)[:lft] = nil
525 categories(:child_2).save(:validate => false)
682 categories(:child_2).save(:validate => false)
526 Category.left_and_rights_valid?.should be_false
683 Category.left_and_rights_valid?.should be_false
527 end
684 end
528
685
529 it "left_and_rights_valid_with_blank_right" do
686 it "left_and_rights_valid_with_blank_right" do
530 Category.left_and_rights_valid?.should be_true
687 Category.left_and_rights_valid?.should be_true
531 categories(:child_2)[:rgt] = nil
688 categories(:child_2)[:rgt] = nil
532 categories(:child_2).save(:validate => false)
689 categories(:child_2).save(:validate => false)
533 Category.left_and_rights_valid?.should be_false
690 Category.left_and_rights_valid?.should be_false
534 end
691 end
535
692
536 it "left_and_rights_valid_with_equal" do
693 it "left_and_rights_valid_with_equal" do
537 Category.left_and_rights_valid?.should be_true
694 Category.left_and_rights_valid?.should be_true
538 categories(:top_level_2)[:lft] = categories(:top_level_2)[:rgt]
695 categories(:top_level_2)[:lft] = categories(:top_level_2)[:rgt]
539 categories(:top_level_2).save(:validate => false)
696 categories(:top_level_2).save(:validate => false)
540 Category.left_and_rights_valid?.should be_false
697 Category.left_and_rights_valid?.should be_false
541 end
698 end
542
699
543 it "left_and_rights_valid_with_left_equal_to_parent" do
700 it "left_and_rights_valid_with_left_equal_to_parent" do
544 Category.left_and_rights_valid?.should be_true
701 Category.left_and_rights_valid?.should be_true
545 categories(:child_2)[:lft] = categories(:top_level)[:lft]
702 categories(:child_2)[:lft] = categories(:top_level)[:lft]
546 categories(:child_2).save(:validate => false)
703 categories(:child_2).save(:validate => false)
547 Category.left_and_rights_valid?.should be_false
704 Category.left_and_rights_valid?.should be_false
548 end
705 end
549
706
550 it "left_and_rights_valid_with_right_equal_to_parent" do
707 it "left_and_rights_valid_with_right_equal_to_parent" do
551 Category.left_and_rights_valid?.should be_true
708 Category.left_and_rights_valid?.should be_true
552 categories(:child_2)[:rgt] = categories(:top_level)[:rgt]
709 categories(:child_2)[:rgt] = categories(:top_level)[:rgt]
553 categories(:child_2).save(:validate => false)
710 categories(:child_2).save(:validate => false)
554 Category.left_and_rights_valid?.should be_false
711 Category.left_and_rights_valid?.should be_false
555 end
712 end
556
713
557 it "moving_dirty_objects_doesnt_invalidate_tree" do
714 it "moving_dirty_objects_doesnt_invalidate_tree" do
558 r1 = Category.create :name => "Test 1"
715 r1 = Category.create :name => "Test 1"
559 r2 = Category.create :name => "Test 2"
716 r2 = Category.create :name => "Test 2"
560 r3 = Category.create :name => "Test 3"
717 r3 = Category.create :name => "Test 3"
561 r4 = Category.create :name => "Test 4"
718 r4 = Category.create :name => "Test 4"
562 nodes = [r1, r2, r3, r4]
719 nodes = [r1, r2, r3, r4]
563
720
564 r2.move_to_child_of(r1)
721 r2.move_to_child_of(r1)
565 Category.valid?.should be_true
722 Category.valid?.should be_true
566
723
567 r3.move_to_child_of(r1)
724 r3.move_to_child_of(r1)
568 Category.valid?.should be_true
725 Category.valid?.should be_true
569
726
570 r4.move_to_child_of(r2)
727 r4.move_to_child_of(r2)
571 Category.valid?.should be_true
728 Category.valid?.should be_true
572 end
729 end
573
730
574 it "multi_scoped_no_duplicates_for_columns?" do
731 it "multi_scoped_no_duplicates_for_columns?" do
575 lambda {
732 lambda {
576 Note.no_duplicates_for_columns?
733 Note.no_duplicates_for_columns?
577 }.should_not raise_exception
734 }.should_not raise_exception
578 end
735 end
579
736
580 it "multi_scoped_all_roots_valid?" do
737 it "multi_scoped_all_roots_valid?" do
581 lambda {
738 lambda {
582 Note.all_roots_valid?
739 Note.all_roots_valid?
583 }.should_not raise_exception
740 }.should_not raise_exception
584 end
741 end
585
742
586 it "multi_scoped" do
743 it "multi_scoped" do
587 note1 = Note.create!(:body => "A", :notable_id => 2, :notable_type => 'Category')
744 note1 = Note.create!(:body => "A", :notable_id => 2, :notable_type => 'Category')
588 note2 = Note.create!(:body => "B", :notable_id => 2, :notable_type => 'Category')
745 note2 = Note.create!(:body => "B", :notable_id => 2, :notable_type => 'Category')
589 note3 = Note.create!(:body => "C", :notable_id => 2, :notable_type => 'Default')
746 note3 = Note.create!(:body => "C", :notable_id => 2, :notable_type => 'Default')
590
747
591 [note1, note2].should == note1.self_and_siblings
748 [note1, note2].should == note1.self_and_siblings
592 [note3].should == note3.self_and_siblings
749 [note3].should == note3.self_and_siblings
593 end
750 end
594
751
595 it "multi_scoped_rebuild" do
752 it "multi_scoped_rebuild" do
596 root = Note.create!(:body => "A", :notable_id => 3, :notable_type => 'Category')
753 root = Note.create!(:body => "A", :notable_id => 3, :notable_type => 'Category')
597 child1 = Note.create!(:body => "B", :notable_id => 3, :notable_type => 'Category')
754 child1 = Note.create!(:body => "B", :notable_id => 3, :notable_type => 'Category')
598 child2 = Note.create!(:body => "C", :notable_id => 3, :notable_type => 'Category')
755 child2 = Note.create!(:body => "C", :notable_id => 3, :notable_type => 'Category')
599
756
600 child1.move_to_child_of root
757 child1.move_to_child_of root
601 child2.move_to_child_of root
758 child2.move_to_child_of root
602
759
603 Note.update_all('lft = null, rgt = null')
760 Note.update_all('lft = null, rgt = null')
604 Note.rebuild!
761 Note.rebuild!
605
762
606 Note.roots.find_by_body('A').should == root
763 Note.roots.find_by_body('A').should == root
607 [child1, child2].should == Note.roots.find_by_body('A').children
764 [child1, child2].should == Note.roots.find_by_body('A').children
608 end
765 end
609
766
610 it "same_scope_with_multi_scopes" do
767 it "same_scope_with_multi_scopes" do
611 lambda {
768 lambda {
612 notes(:scope1).same_scope?(notes(:child_1))
769 notes(:scope1).same_scope?(notes(:child_1))
613 }.should_not raise_exception
770 }.should_not raise_exception
614 notes(:scope1).same_scope?(notes(:child_1)).should be_true
771 notes(:scope1).same_scope?(notes(:child_1)).should be_true
615 notes(:child_1).same_scope?(notes(:scope1)).should be_true
772 notes(:child_1).same_scope?(notes(:scope1)).should be_true
616 notes(:scope1).same_scope?(notes(:scope2)).should be_false
773 notes(:scope1).same_scope?(notes(:scope2)).should be_false
617 end
774 end
618
775
619 it "quoting_of_multi_scope_column_names" do
776 it "quoting_of_multi_scope_column_names" do
620 ["\"notable_id\"", "\"notable_type\""].should == Note.quoted_scope_column_names
777 ## Proper Array Assignment for different DBs as per their quoting column behavior
778 if Note.connection.adapter_name.match(/Oracle/)
779 expected_quoted_scope_column_names = ["\"NOTABLE_ID\"", "\"NOTABLE_TYPE\""]
780 elsif Note.connection.adapter_name.match(/Mysql/)
781 expected_quoted_scope_column_names = ["`notable_id`", "`notable_type`"]
782 else
783 expected_quoted_scope_column_names = ["\"notable_id\"", "\"notable_type\""]
784 end
785 expected_quoted_scope_column_names.should == Note.quoted_scope_column_names
621 end
786 end
622
787
623 it "equal_in_same_scope" do
788 it "equal_in_same_scope" do
624 notes(:scope1).should == notes(:scope1)
789 notes(:scope1).should == notes(:scope1)
625 notes(:scope1).should_not == notes(:child_1)
790 notes(:scope1).should_not == notes(:child_1)
626 end
791 end
627
792
628 it "equal_in_different_scopes" do
793 it "equal_in_different_scopes" do
629 notes(:scope1).should_not == notes(:scope2)
794 notes(:scope1).should_not == notes(:scope2)
630 end
795 end
631
796
632 it "delete_does_not_invalidate" do
797 it "delete_does_not_invalidate" do
633 Category.acts_as_nested_set_options[:dependent] = :delete
798 Category.acts_as_nested_set_options[:dependent] = :delete
634 categories(:child_2).destroy
799 categories(:child_2).destroy
635 Category.valid?.should be_true
800 Category.valid?.should be_true
636 end
801 end
637
802
638 it "destroy_does_not_invalidate" do
803 it "destroy_does_not_invalidate" do
639 Category.acts_as_nested_set_options[:dependent] = :destroy
804 Category.acts_as_nested_set_options[:dependent] = :destroy
640 categories(:child_2).destroy
805 categories(:child_2).destroy
641 Category.valid?.should be_true
806 Category.valid?.should be_true
642 end
807 end
643
808
644 it "destroy_multiple_times_does_not_invalidate" do
809 it "destroy_multiple_times_does_not_invalidate" do
645 Category.acts_as_nested_set_options[:dependent] = :destroy
810 Category.acts_as_nested_set_options[:dependent] = :destroy
646 categories(:child_2).destroy
811 categories(:child_2).destroy
647 categories(:child_2).destroy
812 categories(:child_2).destroy
648 Category.valid?.should be_true
813 Category.valid?.should be_true
649 end
814 end
650
815
651 it "assigning_parent_id_on_create" do
816 it "assigning_parent_id_on_create" do
652 category = Category.create!(:name => "Child", :parent_id => categories(:child_2).id)
817 category = Category.create!(:name => "Child", :parent_id => categories(:child_2).id)
653 categories(:child_2).should == category.parent
818 categories(:child_2).should == category.parent
654 categories(:child_2).id.should == category.parent_id
819 categories(:child_2).id.should == category.parent_id
655 category.left.should_not be_nil
820 category.left.should_not be_nil
656 category.right.should_not be_nil
821 category.right.should_not be_nil
657 Category.valid?.should be_true
822 Category.valid?.should be_true
658 end
823 end
659
824
660 it "assigning_parent_on_create" do
825 it "assigning_parent_on_create" do
661 category = Category.create!(:name => "Child", :parent => categories(:child_2))
826 category = Category.create!(:name => "Child", :parent => categories(:child_2))
662 categories(:child_2).should == category.parent
827 categories(:child_2).should == category.parent
663 categories(:child_2).id.should == category.parent_id
828 categories(:child_2).id.should == category.parent_id
664 category.left.should_not be_nil
829 category.left.should_not be_nil
665 category.right.should_not be_nil
830 category.right.should_not be_nil
666 Category.valid?.should be_true
831 Category.valid?.should be_true
667 end
832 end
668
833
669 it "assigning_parent_id_to_nil_on_create" do
834 it "assigning_parent_id_to_nil_on_create" do
670 category = Category.create!(:name => "New Root", :parent_id => nil)
835 category = Category.create!(:name => "New Root", :parent_id => nil)
671 category.parent.should be_nil
836 category.parent.should be_nil
672 category.parent_id.should be_nil
837 category.parent_id.should be_nil
673 category.left.should_not be_nil
838 category.left.should_not be_nil
674 category.right.should_not be_nil
839 category.right.should_not be_nil
675 Category.valid?.should be_true
840 Category.valid?.should be_true
676 end
841 end
677
842
678 it "assigning_parent_id_on_update" do
843 it "assigning_parent_id_on_update" do
679 category = categories(:child_2_1)
844 category = categories(:child_2_1)
680 category.parent_id = categories(:child_3).id
845 category.parent_id = categories(:child_3).id
681 category.save
846 category.save
682 category.reload
847 category.reload
683 categories(:child_3).reload
848 categories(:child_3).reload
684 categories(:child_3).should == category.parent
849 categories(:child_3).should == category.parent
685 categories(:child_3).id.should == category.parent_id
850 categories(:child_3).id.should == category.parent_id
686 Category.valid?.should be_true
851 Category.valid?.should be_true
687 end
852 end
688
853
689 it "assigning_parent_on_update" do
854 it "assigning_parent_on_update" do
690 category = categories(:child_2_1)
855 category = categories(:child_2_1)
691 category.parent = categories(:child_3)
856 category.parent = categories(:child_3)
692 category.save
857 category.save
693 category.reload
858 category.reload
694 categories(:child_3).reload
859 categories(:child_3).reload
695 categories(:child_3).should == category.parent
860 categories(:child_3).should == category.parent
696 categories(:child_3).id.should == category.parent_id
861 categories(:child_3).id.should == category.parent_id
697 Category.valid?.should be_true
862 Category.valid?.should be_true
698 end
863 end
699
864
700 it "assigning_parent_id_to_nil_on_update" do
865 it "assigning_parent_id_to_nil_on_update" do
701 category = categories(:child_2_1)
866 category = categories(:child_2_1)
702 category.parent_id = nil
867 category.parent_id = nil
703 category.save
868 category.save
704 category.parent.should be_nil
869 category.parent.should be_nil
705 category.parent_id.should be_nil
870 category.parent_id.should be_nil
706 Category.valid?.should be_true
871 Category.valid?.should be_true
707 end
872 end
708
873
709 it "creating_child_from_parent" do
874 it "creating_child_from_parent" do
710 category = categories(:child_2).children.create!(:name => "Child")
875 category = categories(:child_2).children.create!(:name => "Child")
711 categories(:child_2).should == category.parent
876 categories(:child_2).should == category.parent
712 categories(:child_2).id.should == category.parent_id
877 categories(:child_2).id.should == category.parent_id
713 category.left.should_not be_nil
878 category.left.should_not be_nil
714 category.right.should_not be_nil
879 category.right.should_not be_nil
715 Category.valid?.should be_true
880 Category.valid?.should be_true
716 end
881 end
717
882
718 def check_structure(entries, structure)
883 def check_structure(entries, structure)
719 structure = structure.dup
884 structure = structure.dup
720 Category.each_with_level(entries) do |category, level|
885 Category.each_with_level(entries) do |category, level|
721 expected_level, expected_name = structure.shift
886 expected_level, expected_name = structure.shift
722 expected_name.should == category.name
887 expected_name.should == category.name
723 expected_level.should == level
888 expected_level.should == level
724 end
889 end
725 end
890 end
726
891
727 it "each_with_level" do
892 it "each_with_level" do
728 levels = [
893 levels = [
729 [0, "Top Level"],
894 [0, "Top Level"],
730 [1, "Child 1"],
895 [1, "Child 1"],
731 [1, "Child 2"],
896 [1, "Child 2"],
732 [2, "Child 2.1"],
897 [2, "Child 2.1"],
733 [1, "Child 3" ]]
898 [1, "Child 3" ]
899 ]
734
900
735 check_structure(Category.root.self_and_descendants, levels)
901 check_structure(Category.root.self_and_descendants, levels)
736
902
737 # test some deeper structures
903 # test some deeper structures
738 category = Category.find_by_name("Child 1")
904 category = Category.find_by_name("Child 1")
739 c1 = Category.new(:name => "Child 1.1")
905 c1 = Category.new(:name => "Child 1.1")
740 c2 = Category.new(:name => "Child 1.1.1")
906 c2 = Category.new(:name => "Child 1.1.1")
741 c3 = Category.new(:name => "Child 1.1.1.1")
907 c3 = Category.new(:name => "Child 1.1.1.1")
742 c4 = Category.new(:name => "Child 1.2")
908 c4 = Category.new(:name => "Child 1.2")
743 [c1, c2, c3, c4].each(&:save!)
909 [c1, c2, c3, c4].each(&:save!)
744
910
745 c1.move_to_child_of(category)
911 c1.move_to_child_of(category)
746 c2.move_to_child_of(c1)
912 c2.move_to_child_of(c1)
747 c3.move_to_child_of(c2)
913 c3.move_to_child_of(c2)
748 c4.move_to_child_of(category)
914 c4.move_to_child_of(category)
749
915
750 levels = [
916 levels = [
751 [0, "Top Level"],
917 [0, "Top Level"],
752 [1, "Child 1"],
918 [1, "Child 1"],
753 [2, "Child 1.1"],
919 [2, "Child 1.1"],
754 [3, "Child 1.1.1"],
920 [3, "Child 1.1.1"],
755 [4, "Child 1.1.1.1"],
921 [4, "Child 1.1.1.1"],
756 [2, "Child 1.2"],
922 [2, "Child 1.2"],
757 [1, "Child 2"],
923 [1, "Child 2"],
758 [2, "Child 2.1"],
924 [2, "Child 2.1"],
759 [1, "Child 3" ]]
925 [1, "Child 3" ]
926 ]
760
927
761 check_structure(Category.root.self_and_descendants, levels)
928 check_structure(Category.root.self_and_descendants, levels)
762 end
929 end
763
930
764 it "should not error on a model with attr_accessible" do
931 it "should not error on a model with attr_accessible" do
765 model = Class.new(ActiveRecord::Base)
932 model = Class.new(ActiveRecord::Base)
766 model.table_name = 'categories'
933 model.table_name = 'categories'
767 model.attr_accessible :name
934 model.attr_accessible :name
768 lambda {
935 lambda {
769 model.acts_as_nested_set
936 model.acts_as_nested_set
770 model.new(:name => 'foo')
937 model.new(:name => 'foo')
771 }.should_not raise_exception
938 }.should_not raise_exception
772 end
939 end
773
940
774 describe "before_move_callback" do
941 describe "before_move_callback" do
775 it "should fire the callback" do
942 it "should fire the callback" do
776 categories(:child_2).should_receive(:custom_before_move)
943 categories(:child_2).should_receive(:custom_before_move)
777 categories(:child_2).move_to_root
944 categories(:child_2).move_to_root
778 end
945 end
779
946
780 it "should stop move when callback returns false" do
947 it "should stop move when callback returns false" do
781 Category.test_allows_move = false
948 Category.test_allows_move = false
782 categories(:child_3).move_to_root.should be_false
949 categories(:child_3).move_to_root.should be_false
783 categories(:child_3).root?.should be_false
950 categories(:child_3).root?.should be_false
784 end
951 end
785
952
786 it "should not halt save actions" do
953 it "should not halt save actions" do
787 Category.test_allows_move = false
954 Category.test_allows_move = false
788 categories(:child_3).parent_id = nil
955 categories(:child_3).parent_id = nil
789 categories(:child_3).save.should be_true
956 categories(:child_3).save.should be_true
790 end
957 end
791 end
958 end
792
959
793 describe "counter_cache" do
960 describe "counter_cache" do
794
961
795 it "should allow use of a counter cache for children" do
962 it "should allow use of a counter cache for children" do
796 note1 = things(:parent1)
963 note1 = things(:parent1)
797 note1.children.count.should == 2
964 note1.children.count.should == 2
798 end
965 end
799
966
800 it "should increment the counter cache on create" do
967 it "should increment the counter cache on create" do
801 note1 = things(:parent1)
968 note1 = things(:parent1)
802 note1.children.count.should == 2
969 note1.children.count.should == 2
803 note1[:children_count].should == 2
970 note1[:children_count].should == 2
804 note1.children.create :body => 'Child 3'
971 note1.children.create :body => 'Child 3'
805 note1.children.count.should == 3
972 note1.children.count.should == 3
806 note1.reload
973 note1.reload
807 note1[:children_count].should == 3
974 note1[:children_count].should == 3
808 end
975 end
809
976
810 it "should decrement the counter cache on destroy" do
977 it "should decrement the counter cache on destroy" do
811 note1 = things(:parent1)
978 note1 = things(:parent1)
812 note1.children.count.should == 2
979 note1.children.count.should == 2
813 note1[:children_count].should == 2
980 note1[:children_count].should == 2
814 note1.children.last.destroy
981 note1.children.last.destroy
815 note1.children.count.should == 1
982 note1.children.count.should == 1
816 note1.reload
983 note1.reload
817 note1[:children_count].should == 1
984 note1[:children_count].should == 1
818 end
985 end
819 end
986 end
820
987
821 describe "association callbacks on children" do
988 describe "association callbacks on children" do
822 it "should call the appropriate callbacks on the children :has_many association " do
989 it "should call the appropriate callbacks on the children :has_many association " do
823 root = DefaultWithCallbacks.create
990 root = DefaultWithCallbacks.create
824 root.should_not be_new_record
991 root.should_not be_new_record
825
992
826 child = root.children.build
993 child = root.children.build
827
994
828 root.before_add.should == child
995 root.before_add.should == child
829 root.after_add.should == child
996 root.after_add.should == child
830
997
831 root.before_remove.should_not == child
998 root.before_remove.should_not == child
832 root.after_remove.should_not == child
999 root.after_remove.should_not == child
833
1000
834 child.save.should be_true
1001 child.save.should be_true
835 root.children.delete(child).should be_true
1002 root.children.delete(child).should be_true
836
1003
837 root.before_remove.should == child
1004 root.before_remove.should == child
838 root.after_remove.should == child
1005 root.after_remove.should == child
839 end
1006 end
840 end
1007 end
1008
1009 describe 'creating roots with a default scope ordering' do
1010 it "assigns rgt and lft correctly" do
1011 alpha = Order.create(:name => 'Alpha')
1012 gamma = Order.create(:name => 'Gamma')
1013 omega = Order.create(:name => 'Omega')
1014
1015 alpha.lft.should == 1
1016 alpha.rgt.should == 2
1017 gamma.lft.should == 3
1018 gamma.rgt.should == 4
1019 omega.lft.should == 5
1020 omega.rgt.should == 6
1021 end
1022 end
1023
1024 describe 'moving node from one scoped tree to another' do
1025 xit "moves single node correctly" do
1026 root1 = Note.create!(:body => "A-1", :notable_id => 4, :notable_type => 'Category')
1027 child1_1 = Note.create!(:body => "B-1", :notable_id => 4, :notable_type => 'Category')
1028 child1_2 = Note.create!(:body => "C-1", :notable_id => 4, :notable_type => 'Category')
1029 child1_1.move_to_child_of root1
1030 child1_2.move_to_child_of root1
1031
1032 root2 = Note.create!(:body => "A-2", :notable_id => 5, :notable_type => 'Category')
1033 child2_1 = Note.create!(:body => "B-2", :notable_id => 5, :notable_type => 'Category')
1034 child2_2 = Note.create!(:body => "C-2", :notable_id => 5, :notable_type => 'Category')
1035 child2_1.move_to_child_of root2
1036 child2_2.move_to_child_of root2
1037
1038 child1_1.update_attributes!(:notable_id => 5)
1039 child1_1.move_to_child_of root2
1040
1041 root1.children.should == [child1_2]
1042 root2.children.should == [child2_1, child2_2, child1_1]
1043
1044 Note.valid?.should == true
1045 end
1046
1047 xit "moves node with children correctly" do
1048 root1 = Note.create!(:body => "A-1", :notable_id => 4, :notable_type => 'Category')
1049 child1_1 = Note.create!(:body => "B-1", :notable_id => 4, :notable_type => 'Category')
1050 child1_2 = Note.create!(:body => "C-1", :notable_id => 4, :notable_type => 'Category')
1051 child1_1.move_to_child_of root1
1052 child1_2.move_to_child_of child1_1
1053
1054 root2 = Note.create!(:body => "A-2", :notable_id => 5, :notable_type => 'Category')
1055 child2_1 = Note.create!(:body => "B-2", :notable_id => 5, :notable_type => 'Category')
1056 child2_2 = Note.create!(:body => "C-2", :notable_id => 5, :notable_type => 'Category')
1057 child2_1.move_to_child_of root2
1058 child2_2.move_to_child_of root2
1059
1060 child1_1.update_attributes!(:notable_id => 5)
1061 child1_1.move_to_child_of root2
1062
1063 root1.children.should == []
1064 root2.children.should == [child2_1, child2_2, child1_1]
1065 child1_1.children should == [child1_2]
1066 root2.siblings.should == [child2_1, child2_2, child1_1, child1_2]
1067
1068 Note.valid?.should == true
1069 end
1070 end
1071
1072 describe 'specifying custom sort column' do
1073 it "should sort by the default sort column" do
1074 Category.order_column.should == 'lft'
1075 end
1076
1077 it "should sort by custom sort column" do
1078 OrderedCategory.acts_as_nested_set_options[:order_column].should == 'name'
1079 OrderedCategory.order_column.should == 'name'
1080 end
1081 end
841 end
1082 end
@@ -1,18 +1,25
1 sqlite3:
1 sqlite3:
2 adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
2 adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
3 database: awesome_nested_set.sqlite3.db
3 database: awesome_nested_set.sqlite3.db
4 sqlite3mem:
4 sqlite3mem:
5 adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
5 adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
6 database: ":memory:"
6 database: ":memory:"
7 postgresql:
7 postgresql:
8 adapter: postgresql
8 adapter: postgresql
9 username: postgres
9 username: postgres
10 password: postgres
10 password: postgres
11 database: awesome_nested_set_plugin_test
11 database: awesome_nested_set_plugin_test
12 min_messages: ERROR
12 min_messages: ERROR
13 mysql:
13 mysql:
14 adapter: mysql2
14 adapter: mysql2
15 host: localhost
15 host: localhost
16 username: root
16 username: root
17 password:
17 password:
18 database: awesome_nested_set_plugin_test No newline at end of file
18 database: awesome_nested_set_plugin_test
19 ## Add DB Configuration to run Oracle tests
20 oracle:
21 adapter: oracle_enhanced
22 host: localhost
23 username: awesome_nested_set_dev
24 password:
25 database: xe
@@ -1,45 +1,65
1 ActiveRecord::Schema.define(:version => 0) do
1 ActiveRecord::Schema.define(:version => 0) do
2
2
3 create_table :categories, :force => true do |t|
3 create_table :categories, :force => true do |t|
4 t.column :name, :string
4 t.column :name, :string
5 t.column :parent_id, :integer
5 t.column :parent_id, :integer
6 t.column :lft, :integer
6 t.column :lft, :integer
7 t.column :rgt, :integer
7 t.column :rgt, :integer
8 t.column :depth, :integer
8 t.column :organization_id, :integer
9 t.column :organization_id, :integer
9 end
10 end
10
11
11 create_table :departments, :force => true do |t|
12 create_table :departments, :force => true do |t|
12 t.column :name, :string
13 t.column :name, :string
13 end
14 end
14
15
15 create_table :notes, :force => true do |t|
16 create_table :notes, :force => true do |t|
16 t.column :body, :text
17 t.column :body, :text
17 t.column :parent_id, :integer
18 t.column :parent_id, :integer
18 t.column :lft, :integer
19 t.column :lft, :integer
19 t.column :rgt, :integer
20 t.column :rgt, :integer
21 t.column :depth, :integer
20 t.column :notable_id, :integer
22 t.column :notable_id, :integer
21 t.column :notable_type, :string
23 t.column :notable_type, :string
22 end
24 end
23
25
24 create_table :renamed_columns, :force => true do |t|
26 create_table :renamed_columns, :force => true do |t|
25 t.column :name, :string
27 t.column :name, :string
26 t.column :mother_id, :integer
28 t.column :mother_id, :integer
27 t.column :red, :integer
29 t.column :red, :integer
28 t.column :black, :integer
30 t.column :black, :integer
31 t.column :pitch, :integer
29 end
32 end
30
33
31 create_table :things, :force => true do |t|
34 create_table :things, :force => true do |t|
32 t.column :body, :text
35 t.column :body, :text
33 t.column :parent_id, :integer
36 t.column :parent_id, :integer
34 t.column :lft, :integer
37 t.column :lft, :integer
35 t.column :rgt, :integer
38 t.column :rgt, :integer
39 t.column :depth, :integer
36 t.column :children_count, :integer
40 t.column :children_count, :integer
37 end
41 end
38
42
39 create_table :brokens, :force => true do |t|
43 create_table :brokens, :force => true do |t|
40 t.column :name, :string
44 t.column :name, :string
41 t.column :parent_id, :integer
45 t.column :parent_id, :integer
42 t.column :lft, :integer
46 t.column :lft, :integer
43 t.column :rgt, :integer
47 t.column :rgt, :integer
48 t.column :depth, :integer
49 end
50
51 create_table :orders, :force => true do |t|
52 t.column :name, :string
53 t.column :parent_id, :integer
54 t.column :lft, :integer
55 t.column :rgt, :integer
56 t.column :depth, :integer
57 end
58
59 create_table :no_depths, :force => true do |t|
60 t.column :name, :string
61 t.column :parent_id, :integer
62 t.column :lft, :integer
63 t.column :rgt, :integer
44 end
64 end
45 end
65 end
@@ -1,72 +1,90
1 class Note < ActiveRecord::Base
1 class Note < ActiveRecord::Base
2 acts_as_nested_set :scope => [:notable_id, :notable_type]
2 acts_as_nested_set :scope => [:notable_id, :notable_type]
3 end
3 end
4
4
5 class Default < ActiveRecord::Base
5 class Default < ActiveRecord::Base
6 self.table_name = 'categories'
6 self.table_name = 'categories'
7 acts_as_nested_set
7 acts_as_nested_set
8 end
8 end
9
9
10 class ScopedCategory < ActiveRecord::Base
10 class ScopedCategory < ActiveRecord::Base
11 self.table_name = 'categories'
11 self.table_name = 'categories'
12 acts_as_nested_set :scope => :organization
12 acts_as_nested_set :scope => :organization
13 end
13 end
14
14
15 class OrderedCategory < ActiveRecord::Base
16 self.table_name = 'categories'
17 acts_as_nested_set :order_column => 'name'
18 end
19
15 class RenamedColumns < ActiveRecord::Base
20 class RenamedColumns < ActiveRecord::Base
16 acts_as_nested_set :parent_column => 'mother_id', :left_column => 'red', :right_column => 'black'
21 acts_as_nested_set :parent_column => 'mother_id',
22 :left_column => 'red',
23 :right_column => 'black',
24 :depth_column => 'pitch'
17 end
25 end
18
26
19 class Category < ActiveRecord::Base
27 class Category < ActiveRecord::Base
20 acts_as_nested_set
28 acts_as_nested_set
21
29
22 validates_presence_of :name
30 validates_presence_of :name
23
31
24 # Setup a callback that we can switch to true or false per-test
32 # Setup a callback that we can switch to true or false per-test
25 set_callback :move, :before, :custom_before_move
33 set_callback :move, :before, :custom_before_move
26 cattr_accessor :test_allows_move
34 cattr_accessor :test_allows_move
27 @@test_allows_move = true
35 @@test_allows_move = true
28 def custom_before_move
36 def custom_before_move
29 @@test_allows_move
37 @@test_allows_move
30 end
38 end
31
39
32 def to_s
40 def to_s
33 name
41 name
34 end
42 end
35
43
36 def recurse &block
44 def recurse &block
37 block.call self, lambda{
45 block.call self, lambda{
38 self.children.each do |child|
46 self.children.each do |child|
39 child.recurse &block
47 child.recurse &block
40 end
48 end
41 }
49 }
42 end
50 end
43 end
51 end
44
52
45 class Thing < ActiveRecord::Base
53 class Thing < ActiveRecord::Base
46 acts_as_nested_set :counter_cache => 'children_count'
54 acts_as_nested_set :counter_cache => 'children_count'
47 end
55 end
48
56
49 class DefaultWithCallbacks < ActiveRecord::Base
57 class DefaultWithCallbacks < ActiveRecord::Base
50
58
51 self.table_name = 'categories'
59 self.table_name = 'categories'
52
60
53 attr_accessor :before_add, :after_add, :before_remove, :after_remove
61 attr_accessor :before_add, :after_add, :before_remove, :after_remove
54
62
55 acts_as_nested_set :before_add => :do_before_add_stuff,
63 acts_as_nested_set :before_add => :do_before_add_stuff,
56 :after_add => :do_after_add_stuff,
64 :after_add => :do_after_add_stuff,
57 :before_remove => :do_before_remove_stuff,
65 :before_remove => :do_before_remove_stuff,
58 :after_remove => :do_after_remove_stuff
66 :after_remove => :do_after_remove_stuff
59
67
60 private
68 private
61
69
62 [ :before_add, :after_add, :before_remove, :after_remove ].each do |hook_name|
70 [ :before_add, :after_add, :before_remove, :after_remove ].each do |hook_name|
63 define_method "do_#{hook_name}_stuff" do |child_node|
71 define_method "do_#{hook_name}_stuff" do |child_node|
64 self.send("#{hook_name}=", child_node)
72 self.send("#{hook_name}=", child_node)
65 end
73 end
66 end
74 end
67
75
68 end
76 end
69
77
70 class Broken < ActiveRecord::Base
78 class Broken < ActiveRecord::Base
71 acts_as_nested_set
79 acts_as_nested_set
72 end No newline at end of file
80 end
81
82 class Order < ActiveRecord::Base
83 acts_as_nested_set
84
85 default_scope order(:name)
86 end
87
88 class NoDepth < ActiveRecord::Base
89 acts_as_nested_set
90 end
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (603 lines changed) Show them Hide them
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now