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