##// END OF EJS Templates
import awesome_nested_set 2.1.5...
Toshi MARUYAMA -
r12402:6f78b3a4080f
parent child
Show More
@@ -1,14 +1,17
1 before_install: gem install bundler --pre
1 notifications:
2 notifications:
2 email:
3 email:
3 - parndt@gmail.com
4 - parndt@gmail.com
4 env:
5 env:
5 - DB=sqlite3
6 - DB=sqlite3
6 - DB=sqlite3mem
7 - DB=sqlite3mem
7 - DB=postgresql
8 - DB=mysql
9 rvm:
8 rvm:
10 - 1.8.7
9 - 1.8.7
11 - 1.9.2
10 - 1.9.2
12 - 1.9.3
11 - 1.9.3
13 - rbx-2.0
12 - rbx
14 - jruby
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,3 +1,41
1 2.1.5
2 * Worked around issues where AR#association wasn't present on Rails 3.0.x. [Philip Arndt]
3 * Adds option 'order_column' which defaults to 'left_column_name'. [gudata]
4 * Added moving with order functionality. [Sytse Sijbrandij]
5 * Use tablename in all select queries. [Mikhail Dieterle]
6 * Made sure all descendants' depths are updated when moving parent, not just immediate child. [Phil Thompson]
7 * Add documentation of the callbacks. [Tobias Maier]
8
9 2.1.4
10 * nested_set_options accept both Class & AR Relation. [Semyon Perepelitsa]
11 * Reduce the number of queries triggered by the canonical usage of `i.level` in the `nested_set` helpers. [thedarkone]
12 * Specifically require active_record [Bogdan Gusiev]
13 * compute_level now checks for a non nil association target. [Joel Nimety]
14
15 2.1.3
16 * Update child depth when parent node is moved. [Amanda Wagener]
17 * Added move_to_child_with_index. [Ben Zhang]
18 * Optimised self_and_descendants for when there's an index on lft. [Mark Torrance]
19 * Added support for an unsaved record to return the right 'root'. [Philip Arndt]
20
21 2.1.2
22 * Fixed regressions introduced. [Philip Arndt]
23
24 2.1.1
25 * Added 'depth' which indicates how many levels deep the node is.
26 This only works when you have a column called 'depth' in your table,
27 otherwise it doesn't set itself. [Philip Arndt]
28 * Rails 3.2 support added. [Gabriel Sobrinho]
29 * Oracle compatibility added. [Pikender Sharma]
30 * Adding row locking to deletion, locking source of pivot values, and adding retry on collisions. [Markus J. Q. Roberts]
31 * Added method and helper for sorting children by column. [bluegod]
32 * Fixed .all_roots_valid? to work with Postgres. [Joshua Clayton]
33 * Made compatible with polymorphic belongs_to. [Graham Randall]
34 * Added in the association callbacks to the children :has_many association. [Michael Deering]
35 * Modified helper to allow using array of objects as argument. [Rahmat Budiharso]
36 * Fixed cases where we were calling attr_protected. [Jacob Swanner]
37 * Fixed nil cases involving lft and rgt. [Stuart Coyle] and [Patrick Morgan]
38
1 2.0.2
39 2.0.2
2 * Fixed deprecation warning under Rails 3.1 [Philip Arndt]
40 * Fixed deprecation warning under Rails 3.1 [Philip Arndt]
3 * Converted Test::Unit matchers to RSpec. [Uģis Ozols]
41 * Converted Test::Unit matchers to RSpec. [Uģis Ozols]
@@ -16,7 +16,8 This is a new implementation of nested set based off of BetterNestedSet that fix
16
16
17 == Usage
17 == Usage
18
18
19 To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id:
19 To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id.
20 You can also have an optional field: depth:
20
21
21 class CreateCategories < ActiveRecord::Migration
22 class CreateCategories < ActiveRecord::Migration
22 def self.up
23 def self.up
@@ -25,6 +26,7 To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt,
25 t.integer :parent_id
26 t.integer :parent_id
26 t.integer :lft
27 t.integer :lft
27 t.integer :rgt
28 t.integer :rgt
29 t.integer :depth # this is optional.
28 end
30 end
29 end
31 end
30
32
@@ -41,6 +43,57 Enable the nested set functionality by declaring acts_as_nested_set on your mode
41
43
42 Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet for more info.
44 Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet for more info.
43
45
46 == Callbacks
47
48 There are three callbacks called when moving a node. `before_move`, `after_move` and `around_move`.
49
50 class Category < ActiveRecord::Base
51 acts_as_nested_set
52
53 after_move :rebuild_slug
54 around_move :da_fancy_things_around
55
56 private
57
58 def rebuild_slug
59 # do whatever
60 end
61
62 def da_fancy_things_around
63 # do something...
64 yield # actually moves
65 # do something else...
66 end
67 end
68
69 Beside this there are also hooks to act on the newly added or removed children.
70
71 class Category < ActiveRecord::Base
72 acts_as_nested_set :before_add => :do_before_add_stuff,
73 :after_add => :do_after_add_stuff,
74 :before_remove => :do_before_remove_stuff,
75 :after_remove => :do_after_remove_stuff
76
77 private
78
79 def do_before_add_stuff(child_node)
80 # do whatever with the child
81 end
82
83 def do_after_add_stuff(child_node)
84 # do whatever with the child
85 end
86
87 def do_before_remove_stuff(child_node)
88 # do whatever with the child
89 end
90
91 def do_after_remove_stuff(child_node)
92 # do whatever with the child
93 end
94 end
95
96
44 == Protecting attributes from mass assignment
97 == Protecting attributes from mass assignment
45
98
46 It's generally best to "white list" the attributes that can be used in mass assignment:
99 It's generally best to "white list" the attributes that can be used in mass assignment:
@@ -86,13 +139,13 You can learn more about nested sets at: http://threebit.net/tutorials/nestedset
86 If you find what you might think is a bug:
139 If you find what you might think is a bug:
87
140
88 1. Check the GitHub issue tracker to see if anyone else has had the same issue.
141 1. Check the GitHub issue tracker to see if anyone else has had the same issue.
89 http://github.com/collectiveidea/awesome_nested_set/issues/
142 https://github.com/collectiveidea/awesome_nested_set/issues/
90 2. If you don't see anything, create an issue with information on how to reproduce it.
143 2. If you don't see anything, create an issue with information on how to reproduce it.
91
144
92 If you want to contribute an enhancement or a fix:
145 If you want to contribute an enhancement or a fix:
93
146
94 1. Fork the project on github.
147 1. Fork the project on GitHub.
95 http://github.com/collectiveidea/awesome_nested_set/
148 https://github.com/collectiveidea/awesome_nested_set/
96 2. Make your changes with tests.
149 2. Make your changes with tests.
97 3. Commit the changes without making changes to the Rakefile, VERSION, or any other files that aren't related to your enhancement or fix
150 3. Commit the changes without making changes to the Rakefile, VERSION, or any other files that aren't related to your enhancement or fix
98 4. Send a pull request.
151 4. Send a pull request.
@@ -19,4 +19,5 Gem::Specification.new do |s|
19 s.rubygems_version = %q{1.3.6}
19 s.rubygems_version = %q{1.3.6}
20 s.summary = %q{An awesome nested set implementation for Active Record}
20 s.summary = %q{An awesome nested set implementation for Active Record}
21 s.add_runtime_dependency 'activerecord', '>= 3.0.0'
21 s.add_runtime_dependency 'activerecord', '>= 3.0.0'
22 s.add_development_dependency 'rspec-rails', '~> 2.8'
22 end
23 end
@@ -1,4 +1,5
1 require 'awesome_nested_set/awesome_nested_set'
1 require 'awesome_nested_set/awesome_nested_set'
2 require 'active_record'
2 ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet
3 ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet
3
4
4 if defined?(ActionView)
5 if defined?(ActionView)
@@ -23,6 +23,7 module CollectiveIdea #:nodoc:
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
23 # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
24 # * +:left_column+ - column name for left boundry data, default "lft"
24 # * +:left_column+ - column name for left boundry data, default "lft"
25 # * +:right_column+ - column name for right boundry data, default "rgt"
25 # * +:right_column+ - column name for right boundry data, default "rgt"
26 # * +:depth_column+ - column name for the depth data, default "depth"
26 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
27 # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
27 # (if it hasn't been already) and use that as the foreign key restriction. You
28 # (if it hasn't been already) and use that as the foreign key restriction. You
28 # can also pass an array to scope by multiple attributes.
29 # can also pass an array to scope by multiple attributes.
@@ -34,6 +35,8 module CollectiveIdea #:nodoc:
34 # * +:counter_cache+ adds a counter cache for the number of children.
35 # * +:counter_cache+ adds a counter cache for the number of children.
35 # defaults to false.
36 # defaults to false.
36 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
37 # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
38 # * +:order_column+ on which column to do sorting, by default it is the left_column_name
39 # Example: <tt>acts_as_nested_set :order_column => :position</tt>
37 #
40 #
38 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
41 # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
39 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
42 # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
@@ -43,9 +46,10 module CollectiveIdea #:nodoc:
43 :parent_column => 'parent_id',
46 :parent_column => 'parent_id',
44 :left_column => 'lft',
47 :left_column => 'lft',
45 :right_column => 'rgt',
48 :right_column => 'rgt',
49 :depth_column => 'depth',
46 :dependent => :delete_all, # or :destroy
50 :dependent => :delete_all, # or :destroy
47 :counter_cache => false,
51 :polymorphic => false,
48 :order => 'id'
52 :counter_cache => false
49 }.merge(options)
53 }.merge(options)
50
54
51 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
55 if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
@@ -62,24 +66,32 module CollectiveIdea #:nodoc:
62 belongs_to :parent, :class_name => self.base_class.to_s,
66 belongs_to :parent, :class_name => self.base_class.to_s,
63 :foreign_key => parent_column_name,
67 :foreign_key => parent_column_name,
64 :counter_cache => options[:counter_cache],
68 :counter_cache => options[:counter_cache],
65 :inverse_of => :children
69 :inverse_of => (:children unless options[:polymorphic]),
66 has_many :children, :class_name => self.base_class.to_s,
70 :polymorphic => options[:polymorphic]
67 :foreign_key => parent_column_name, :order => left_column_name,
71
68 :inverse_of => :parent,
72 has_many_children_options = {
69 :before_add => options[:before_add],
73 :class_name => self.base_class.to_s,
70 :after_add => options[:after_add],
74 :foreign_key => parent_column_name,
71 :before_remove => options[:before_remove],
75 :order => order_column,
72 :after_remove => options[:after_remove]
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 attr_accessor :skip_before_destroy
86 attr_accessor :skip_before_destroy
75
87
76 before_create :set_default_left_and_right
88 before_create :set_default_left_and_right
77 before_save :store_new_parent
89 before_save :store_new_parent
78 after_save :move_to_new_parent
90 after_save :move_to_new_parent, :set_depth!
79 before_destroy :destroy_descendants
91 before_destroy :destroy_descendants
80
92
81 # no assignment to structure fields
93 # no assignment to structure fields
82 [left_column_name, right_column_name].each do |column|
94 [left_column_name, right_column_name, depth_column_name].each do |column|
83 module_eval <<-"end_eval", __FILE__, __LINE__
95 module_eval <<-"end_eval", __FILE__, __LINE__
84 def #{column}=(x)
96 def #{column}=(x)
85 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
97 raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
@@ -93,6 +105,10 module CollectiveIdea #:nodoc:
93 module Model
105 module Model
94 extend ActiveSupport::Concern
106 extend ActiveSupport::Concern
95
107
108 included do
109 delegate :quoted_table_name, :to => self
110 end
111
96 module ClassMethods
112 module ClassMethods
97 # Returns the first root
113 # Returns the first root
98 def root
114 def root
@@ -100,11 +116,11 module CollectiveIdea #:nodoc:
100 end
116 end
101
117
102 def roots
118 def roots
103 where(parent_column_name => nil).order(quoted_left_column_name)
119 where(parent_column_name => nil).order(quoted_left_column_full_name)
104 end
120 end
105
121
106 def leaves
122 def leaves
107 where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
123 where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
108 end
124 end
109
125
110 def valid?
126 def valid?
@@ -112,16 +128,19 module CollectiveIdea #:nodoc:
112 end
128 end
113
129
114 def left_and_rights_valid?
130 def left_and_rights_valid?
115 joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
131 ## AS clause not supported in Oracle in FROM clause for aliasing table name
116 "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}").
132 joins("LEFT OUTER JOIN #{quoted_table_name}" +
133 (connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
134 "parent ON " +
135 "#{quoted_parent_column_full_name} = parent.#{primary_key}").
117 where(
136 where(
118 "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
137 "#{quoted_left_column_full_name} IS NULL OR " +
119 "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
138 "#{quoted_right_column_full_name} IS NULL OR " +
120 "#{quoted_table_name}.#{quoted_left_column_name} >= " +
139 "#{quoted_left_column_full_name} >= " +
121 "#{quoted_table_name}.#{quoted_right_column_name} OR " +
140 "#{quoted_right_column_full_name} OR " +
122 "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
141 "(#{quoted_parent_column_full_name} IS NOT NULL AND " +
123 "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
142 "(#{quoted_left_column_full_name} <= parent.#{quoted_left_column_name} OR " +
124 "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
143 "#{quoted_right_column_full_name} >= parent.#{quoted_right_column_name}))"
125 ).count == 0
144 ).count == 0
126 end
145 end
127
146
@@ -129,7 +148,7 module CollectiveIdea #:nodoc:
129 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
148 scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
130 connection.quote_column_name(c)
149 connection.quote_column_name(c)
131 end.push(nil).join(", ")
150 end.push(nil).join(", ")
132 [quoted_left_column_name, quoted_right_column_name].all? do |column|
151 [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
133 # No duplicates
152 # No duplicates
134 select("#{scope_string}#{column}, COUNT(#{column})").
153 select("#{scope_string}#{column}, COUNT(#{column})").
135 group("#{scope_string}#{column}").
154 group("#{scope_string}#{column}").
@@ -141,7 +160,7 module CollectiveIdea #:nodoc:
141 # Wrapper for each_root_valid? that can deal with scope.
160 # Wrapper for each_root_valid? that can deal with scope.
142 def all_roots_valid?
161 def all_roots_valid?
143 if acts_as_nested_set_options[:scope]
162 if acts_as_nested_set_options[:scope]
144 roots.group(scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
163 roots.group_by {|record| scope_column_names.collect {|col| record.send(col.to_sym) } }.all? do |scope, grouped_roots|
145 each_root_valid?(grouped_roots)
164 each_root_valid?(grouped_roots)
146 end
165 end
147 else
166 else
@@ -179,14 +198,14 module CollectiveIdea #:nodoc:
179 # set left
198 # set left
180 node[left_column_name] = indices[scope.call(node)] += 1
199 node[left_column_name] = indices[scope.call(node)] += 1
181 # find
200 # find
182 where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).order(acts_as_nested_set_options[:order]).each{|n| set_left_and_rights.call(n) }
201 where(["#{quoted_parent_column_full_name} = ? #{scope.call(node)}", node]).order("#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, id").each{|n| set_left_and_rights.call(n) }
183 # set right
202 # set right
184 node[right_column_name] = indices[scope.call(node)] += 1
203 node[right_column_name] = indices[scope.call(node)] += 1
185 node.save!(:validate => validate_nodes)
204 node.save!(:validate => validate_nodes)
186 end
205 end
187
206
188 # Find root node(s)
207 # Find root node(s)
189 root_nodes = where("#{quoted_parent_column_name} IS NULL").order(acts_as_nested_set_options[:order]).each do |root_node|
208 root_nodes = where("#{quoted_parent_column_full_name} IS NULL").order("#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, id").each do |root_node|
190 # setup index for this scope
209 # setup index for this scope
191 indices[scope.call(root_node)] ||= 0
210 indices[scope.call(root_node)] ||= 0
192 set_left_and_rights.call(root_node)
211 set_left_and_rights.call(root_node)
@@ -205,7 +224,7 module CollectiveIdea #:nodoc:
205 path = [nil]
224 path = [nil]
206 objects.each do |o|
225 objects.each do |o|
207 if o.parent_id != path.last
226 if o.parent_id != path.last
208 # we are on a new level, did we decent or ascent?
227 # we are on a new level, did we descend or ascend?
209 if path.include?(o.parent_id)
228 if path.include?(o.parent_id)
210 # remove wrong wrong tailing paths elements
229 # remove wrong wrong tailing paths elements
211 path.pop while path.last != o.parent_id
230 path.pop while path.last != o.parent_id
@@ -216,13 +235,56 module CollectiveIdea #:nodoc:
216 yield(o, path.length - 1)
235 yield(o, path.length - 1)
217 end
236 end
218 end
237 end
238
239 # Same as each_with_level - Accepts a string as a second argument to sort the list
240 # Example:
241 # Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
242 def sorted_each_with_level(objects, order)
243 path = [nil]
244 children = []
245 objects.each do |o|
246 children << o if o.leaf?
247 if o.parent_id != path.last
248 if !children.empty? && !o.leaf?
249 children.sort_by! &order
250 children.each { |c| yield(c, path.length-1) }
251 children = []
252 end
253 # we are on a new level, did we decent or ascent?
254 if path.include?(o.parent_id)
255 # remove wrong wrong tailing paths elements
256 path.pop while path.last != o.parent_id
257 else
258 path << o.parent_id
259 end
260 end
261 yield(o,path.length-1) if !o.leaf?
262 end
263 if !children.empty?
264 children.sort_by! &order
265 children.each { |c| yield(c, path.length-1) }
266 end
267 end
268
269 def associate_parents(objects)
270 if objects.all?{|o| o.respond_to?(:association)}
271 id_indexed = objects.index_by(&:id)
272 objects.each do |object|
273 if !(association = object.association(:parent)).loaded? && (parent = id_indexed[object.parent_id])
274 association.target = parent
275 association.set_inverse_instance(parent)
276 end
277 end
278 else
279 objects
280 end
281 end
219 end
282 end
220
283
221 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
284 # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
222 #
285 #
223 # category.self_and_descendants.count
286 # category.self_and_descendants.count
224 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
287 # category.ancestors.find(:all, :conditions => "name like '%foo%'")
225
226 # Value of the parent column
288 # Value of the parent column
227 def parent_id
289 def parent_id
228 self[parent_column_name]
290 self[parent_column_name]
@@ -243,24 +305,33 module CollectiveIdea #:nodoc:
243 parent_id.nil?
305 parent_id.nil?
244 end
306 end
245
307
308 # Returns true if this is the end of a branch.
246 def leaf?
309 def leaf?
247 new_record? || (right - left == 1)
310 persisted? && right.to_i - left.to_i == 1
248 end
311 end
249
312
250 # Returns true is this is a child node
313 # Returns true is this is a child node
251 def child?
314 def child?
252 !parent_id.nil?
315 !root?
253 end
316 end
254
317
255 # Returns root
318 # Returns root
256 def root
319 def root
320 if persisted?
257 self_and_ancestors.where(parent_column_name => nil).first
321 self_and_ancestors.where(parent_column_name => nil).first
322 else
323 if parent_id && current_parent = nested_set_scope.find(parent_id)
324 current_parent.root
325 else
326 self
327 end
328 end
258 end
329 end
259
330
260 # Returns the array of all parents and self
331 # Returns the array of all parents and self
261 def self_and_ancestors
332 def self_and_ancestors
262 nested_set_scope.where([
333 nested_set_scope.where([
263 "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right
334 "#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
264 ])
335 ])
265 end
336 end
266
337
@@ -281,19 +352,20 module CollectiveIdea #:nodoc:
281
352
282 # Returns a set of all of its nested children which do not have children
353 # Returns a set of all of its nested children which do not have children
283 def leaves
354 def leaves
284 descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
355 descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
285 end
356 end
286
357
287 # Returns the level of this object in the tree
358 # Returns the level of this object in the tree
288 # root level is 0
359 # root level is 0
289 def level
360 def level
290 parent_id.nil? ? 0 : ancestors.count
361 parent_id.nil? ? 0 : compute_level
291 end
362 end
292
363
293 # Returns a set of itself and all of its nested children
364 # Returns a set of itself and all of its nested children
294 def self_and_descendants
365 def self_and_descendants
295 nested_set_scope.where([
366 nested_set_scope.where([
296 "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right
367 "#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
368 # using _left_ for both sides here lets us benefit from an index on that column if one exists
297 ])
369 ])
298 end
370 end
299
371
@@ -327,13 +399,13 module CollectiveIdea #:nodoc:
327
399
328 # Find the first sibling to the left
400 # Find the first sibling to the left
329 def left_sibling
401 def left_sibling
330 siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]).
402 siblings.where(["#{quoted_left_column_full_name} < ?", left]).
331 order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last
403 order("#{quoted_left_column_full_name} DESC").last
332 end
404 end
333
405
334 # Find the first sibling to the right
406 # Find the first sibling to the right
335 def right_sibling
407 def right_sibling
336 siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first
408 siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
337 end
409 end
338
410
339 # Shorthand method for finding the left sibling and moving to the left of it.
411 # Shorthand method for finding the left sibling and moving to the left of it.
@@ -361,11 +433,44 module CollectiveIdea #:nodoc:
361 move_to node, :child
433 move_to node, :child
362 end
434 end
363
435
436 # Move the node to the child of another node with specify index (you can pass id only)
437 def move_to_child_with_index(node, index)
438 if node.children.empty?
439 move_to_child_of(node)
440 elsif node.children.count == index
441 move_to_right_of(node.children.last)
442 else
443 move_to_left_of(node.children[index])
444 end
445 end
446
364 # Move the node to root nodes
447 # Move the node to root nodes
365 def move_to_root
448 def move_to_root
366 move_to nil, :root
449 move_to nil, :root
367 end
450 end
368
451
452 # Order children in a nested set by an attribute
453 # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
454 # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
455 def move_to_ordered_child_of(parent, order_attribute, ascending = true)
456 self.move_to_root and return unless parent
457 left = nil # This is needed, at least for the tests.
458 parent.children.each do |n| # Find the node immediately to the left of this node.
459 if ascending
460 left = n if n.send(order_attribute) < self.send(order_attribute)
461 else
462 left = n if n.send(order_attribute) > self.send(order_attribute)
463 end
464 end
465 self.move_to_child_of(parent)
466 return unless parent.children.count > 1 # Only need to order if there are multiple children.
467 if left # Self has a left neighbor.
468 self.move_to_right_of(left)
469 else # Self is the left most node.
470 self.move_to_left_of(parent.children[0])
471 end
472 end
473
369 def move_possible?(target)
474 def move_possible?(target)
370 self != target && # Can't target self
475 self != target && # Can't target self
371 same_scope?(target) && # can't be in different scopes
476 same_scope?(target) && # can't be in different scopes
@@ -381,6 +486,14 module CollectiveIdea #:nodoc:
381 end
486 end
382
487
383 protected
488 protected
489 def compute_level
490 node, nesting = self, 0
491 while (association = node.association(:parent)).loaded? && association.target
492 nesting += 1
493 node = node.parent
494 end if node.respond_to? :association
495 node == self ? ancestors.count : node.level + nesting
496 end
384
497
385 def without_self(scope)
498 def without_self(scope)
386 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
499 scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
@@ -390,12 +503,12 module CollectiveIdea #:nodoc:
390 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
503 # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
391 # declaration.
504 # declaration.
392 def nested_set_scope(options = {})
505 def nested_set_scope(options = {})
393 options = {:order => "#{self.class.quoted_table_name}.#{quoted_left_column_name}"}.merge(options)
506 options = {:order => quoted_left_column_full_name}.merge(options)
394 scopes = Array(acts_as_nested_set_options[:scope])
507 scopes = Array(acts_as_nested_set_options[:scope])
395 options[:conditions] = scopes.inject({}) do |conditions,attr|
508 options[:conditions] = scopes.inject({}) do |conditions,attr|
396 conditions.merge attr => self[attr]
509 conditions.merge attr => self[attr]
397 end unless scopes.empty?
510 end unless scopes.empty?
398 self.class.base_class.scoped options
511 self.class.base_class.unscoped.scoped options
399 end
512 end
400
513
401 def store_new_parent
514 def store_new_parent
@@ -411,9 +524,20 module CollectiveIdea #:nodoc:
411 end
524 end
412 end
525 end
413
526
527 def set_depth!
528 if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
529 in_tenacious_transaction do
530 reload
531
532 nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
533 end
534 self[depth_column_name.to_sym] = self.level
535 end
536 end
537
414 # on creation, set automatically lft and rgt to the end of the tree
538 # on creation, set automatically lft and rgt to the end of the tree
415 def set_default_left_and_right
539 def set_default_left_and_right
416 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").limit(1).lock(true).first
540 highest_right_row = nested_set_scope(:order => "#{quoted_right_column_full_name} desc").limit(1).lock(true).first
417 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
541 maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
418 # adds the new node to the right of all existing nodes
542 # adds the new node to the right of all existing nodes
419 self[left_column_name] = maxright + 1
543 self[left_column_name] = maxright + 1
@@ -443,11 +567,8 module CollectiveIdea #:nodoc:
443 in_tenacious_transaction do
567 in_tenacious_transaction do
444 reload_nested_set
568 reload_nested_set
445 # select the rows in the model that extend past the deletion point and apply a lock
569 # select the rows in the model that extend past the deletion point and apply a lock
446 nested_set_scope.
570 nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
447 select("id").
571 select(id).lock(true)
448 where("#{quoted_left_column_name} >= ?", left).
449 lock(true).
450 all
451
572
452 if acts_as_nested_set_options[:dependent] == :destroy
573 if acts_as_nested_set_options[:dependent] == :destroy
453 descendants.each do |model|
574 descendants.each do |model|
@@ -455,24 +576,20 module CollectiveIdea #:nodoc:
455 model.destroy
576 model.destroy
456 end
577 end
457 else
578 else
458 nested_set_scope.delete_all(
579 nested_set_scope.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
459 ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
580 delete_all
460 left, right]
461 )
462 end
581 end
463
582
464 # update lefts and rights for remaining nodes
583 # update lefts and rights for remaining nodes
465 diff = right - left + 1
584 diff = right - left + 1
466 nested_set_scope.update_all(
585 nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
467 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
586 ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
468 ["#{quoted_left_column_name} > ?", right]
469 )
587 )
470 nested_set_scope.update_all(
588
471 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
589 nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
472 ["#{quoted_right_column_name} > ?", right]
590 ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
473 )
591 )
474
592
475 reload
476 # Don't allow multiple calls to destroy to corrupt the set
593 # Don't allow multiple calls to destroy to corrupt the set
477 self.skip_before_destroy = true
594 self.skip_before_destroy = true
478 end
595 end
@@ -481,7 +598,7 reload
481 # reload left, right, and parent
598 # reload left, right, and parent
482 def reload_nested_set
599 def reload_nested_set
483 reload(
600 reload(
484 :select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}",
601 :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
485 :lock => true
602 :lock => true
486 )
603 )
487 end
604 end
@@ -526,7 +643,7 reload
526
643
527 # select the rows in the model between a and d, and apply a lock
644 # select the rows in the model between a and d, and apply a lock
528 self.class.base_class.select('id').lock(true).where(
645 self.class.base_class.select('id').lock(true).where(
529 ["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}]
646 ["#{quoted_left_column_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
530 )
647 )
531
648
532 new_parent = case position
649 new_parent = case position
@@ -555,6 +672,8 reload
555 ])
672 ])
556 end
673 end
557 target.reload_nested_set if target
674 target.reload_nested_set if target
675 self.set_depth!
676 self.descendants.each(&:save)
558 self.reload_nested_set
677 self.reload_nested_set
559 end
678 end
560 end
679 end
@@ -571,10 +690,18 reload
571 acts_as_nested_set_options[:right_column]
690 acts_as_nested_set_options[:right_column]
572 end
691 end
573
692
693 def depth_column_name
694 acts_as_nested_set_options[:depth_column]
695 end
696
574 def parent_column_name
697 def parent_column_name
575 acts_as_nested_set_options[:parent_column]
698 acts_as_nested_set_options[:parent_column]
576 end
699 end
577
700
701 def order_column
702 acts_as_nested_set_options[:order_column] || left_column_name
703 end
704
578 def scope_column_names
705 def scope_column_names
579 Array(acts_as_nested_set_options[:scope])
706 Array(acts_as_nested_set_options[:scope])
580 end
707 end
@@ -587,6 +714,10 reload
587 connection.quote_column_name(right_column_name)
714 connection.quote_column_name(right_column_name)
588 end
715 end
589
716
717 def quoted_depth_column_name
718 connection.quote_column_name(depth_column_name)
719 end
720
590 def quoted_parent_column_name
721 def quoted_parent_column_name
591 connection.quote_column_name(parent_column_name)
722 connection.quote_column_name(parent_column_name)
592 end
723 end
@@ -594,6 +725,18 reload
594 def quoted_scope_column_names
725 def quoted_scope_column_names
595 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
726 scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
596 end
727 end
728
729 def quoted_left_column_full_name
730 "#{quoted_table_name}.#{quoted_left_column_name}"
731 end
732
733 def quoted_right_column_full_name
734 "#{quoted_table_name}.#{quoted_right_column_name}"
735 end
736
737 def quoted_parent_column_full_name
738 "#{quoted_table_name}.#{quoted_parent_column_name}"
739 end
597 end
740 end
598
741
599 end
742 end
@@ -1,3 +1,4
1 # -*- coding: utf-8 -*-
1 module CollectiveIdea #:nodoc:
2 module CollectiveIdea #:nodoc:
2 module Acts #:nodoc:
3 module Acts #:nodoc:
3 module NestedSet #:nodoc:
4 module NestedSet #:nodoc:
@@ -24,12 +25,12 module CollectiveIdea #:nodoc:
24 if class_or_item.is_a? Array
25 if class_or_item.is_a? Array
25 items = class_or_item.reject { |e| !e.root? }
26 items = class_or_item.reject { |e| !e.root? }
26 else
27 else
27 class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
28 class_or_item = class_or_item.roots if class_or_item.respond_to?(:scoped)
28 items = Array(class_or_item)
29 items = Array(class_or_item)
29 end
30 end
30 result = []
31 result = []
31 items.each do |root|
32 items.each do |root|
32 result += root.self_and_descendants.map do |i|
33 result += root.class.associate_parents(root.self_and_descendants).map do |i|
33 if mover.nil? || mover.new_record? || mover.move_possible?(i)
34 if mover.nil? || mover.new_record? || mover.move_possible?(i)
34 [yield(i), i.id]
35 [yield(i), i.id]
35 end
36 end
@@ -38,6 +39,50 module CollectiveIdea #:nodoc:
38 result
39 result
39 end
40 end
40
41
42 # Returns options for select as nested_set_options, sorted by an specific column
43 # It requires passing a string with the name of the column to sort the set with
44 # You can exclude some items from the tree.
45 # You can pass a block receiving an item and returning the string displayed in the select.
46 #
47 # == Params
48 # * +class_or_item+ - Class name or top level times
49 # * +:column+ - Column to sort the set (this will sort each children for all root elements)
50 # * +mover+ - The item that is being move, used to exlude impossible moves
51 # * +&block+ - a block that will be used to display: { |item| ... item.name }
52 #
53 # == Usage
54 #
55 # <%= f.select :parent_id, nested_set_options(Category, :sort_by_this_column, @category) {|i|
56 # "#{'–' * i.level} #{i.name}"
57 # }) %>
58 #
59 def sorted_nested_set_options(class_or_item, order, mover = nil)
60 if class_or_item.is_a? Array
61 items = class_or_item.reject { |e| !e.root? }
62 else
63 class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
64 items = Array(class_or_item)
65 end
66 result = []
67 children = []
68 items.each do |root|
69 root.class.associate_parents(root.self_and_descendants).map do |i|
70 if mover.nil? || mover.new_record? || mover.move_possible?(i)
71 if !i.leaf?
72 children.sort_by! &order
73 children.each { |c| result << [yield(c), c.id] }
74 children = []
75 result << [yield(i), i.id]
76 else
77 children << i
78 end
79 end
80 end.compact
81 end
82 children.sort_by! &order
83 children.each { |c| result << [yield(c), c.id] }
84 result
85 end
41 end
86 end
42 end
87 end
43 end
88 end
@@ -1,3 +1,3
1 module AwesomeNestedSet
1 module AwesomeNestedSet
2 VERSION = '2.1.0' unless defined?(::AwesomeNestedSet::VERSION)
2 VERSION = '2.1.5' unless defined?(::AwesomeNestedSet::VERSION)
3 end
3 end
@@ -17,7 +17,7 describe "Helper" do
17 ['- Child 3', 5],
17 ['- Child 3', 5],
18 [" Top Level 2", 6]
18 [" Top Level 2", 6]
19 ]
19 ]
20 actual = nested_set_options(Category) do |c|
20 actual = nested_set_options(Category.scoped) do |c|
21 "#{'-' * c.level} #{c.name}"
21 "#{'-' * c.level} #{c.name}"
22 end
22 end
23 actual.should == expected
23 actual.should == expected
@@ -30,6 +30,34 describe "Helper" do
30 ['- Child 3', 5],
30 ['- Child 3', 5],
31 [" Top Level 2", 6]
31 [" Top Level 2", 6]
32 ]
32 ]
33 actual = nested_set_options(Category.scoped, categories(:child_2)) do |c|
34 "#{'-' * c.level} #{c.name}"
35 end
36 actual.should == expected
37 end
38
39 it "test_nested_set_options_with_class_as_argument" do
40 expected = [
41 [" Top Level", 1],
42 ["- Child 1", 2],
43 ['- Child 2', 3],
44 ['-- Child 2.1', 4],
45 ['- Child 3', 5],
46 [" Top Level 2", 6]
47 ]
48 actual = nested_set_options(Category) do |c|
49 "#{'-' * c.level} #{c.name}"
50 end
51 actual.should == expected
52 end
53
54 it "test_nested_set_options_with_class_as_argument_with_mover" do
55 expected = [
56 [" Top Level", 1],
57 ["- Child 1", 2],
58 ['- Child 3', 5],
59 [" Top Level 2", 6]
60 ]
33 actual = nested_set_options(Category, categories(:child_2)) do |c|
61 actual = nested_set_options(Category, categories(:child_2)) do |c|
34 "#{'-' * c.level} #{c.name}"
62 "#{'-' * c.level} #{c.name}"
35 end
63 end
@@ -36,6 +36,13 describe "AwesomeNestedSet" do
36 RenamedColumns.new.right_column_name.should == 'black'
36 RenamedColumns.new.right_column_name.should == 'black'
37 end
37 end
38
38
39 it "has a depth_column_name" do
40 Default.depth_column_name.should == 'depth'
41 Default.new.depth_column_name.should == 'depth'
42 RenamedColumns.depth_column_name.should == 'pitch'
43 RenamedColumns.depth_column_name.should == 'pitch'
44 end
45
39 it "should have parent_column_name" do
46 it "should have parent_column_name" do
40 Default.parent_column_name.should == 'parent_id'
47 Default.parent_column_name.should == 'parent_id'
41 Default.new.parent_column_name.should == 'parent_id'
48 Default.new.parent_column_name.should == 'parent_id'
@@ -68,6 +75,12 describe "AwesomeNestedSet" do
68 Default.new.quoted_right_column_name.should == quoted
75 Default.new.quoted_right_column_name.should == quoted
69 end
76 end
70
77
78 it "quoted_depth_column_name" do
79 quoted = Default.connection.quote_column_name('depth')
80 Default.quoted_depth_column_name.should == quoted
81 Default.new.quoted_depth_column_name.should == quoted
82 end
83
71 it "left_column_protected_from_assignment" do
84 it "left_column_protected_from_assignment" do
72 lambda {
85 lambda {
73 Category.new.lft = 1
86 Category.new.lft = 1
@@ -80,6 +93,12 describe "AwesomeNestedSet" do
80 }.should raise_exception(ActiveRecord::ActiveRecordError)
93 }.should raise_exception(ActiveRecord::ActiveRecordError)
81 end
94 end
82
95
96 it "depth_column_protected_from_assignment" do
97 lambda {
98 Category.new.depth = 1
99 }.should raise_exception(ActiveRecord::ActiveRecordError)
100 end
101
83 it "scoped_appends_id" do
102 it "scoped_appends_id" do
84 ScopedCategory.acts_as_nested_set_options[:scope].should == :organization_id
103 ScopedCategory.acts_as_nested_set_options[:scope].should == :organization_id
85 end
104 end
@@ -96,6 +115,16 describe "AwesomeNestedSet" do
96 categories(:child_3).root.should == categories(:top_level)
115 categories(:child_3).root.should == categories(:top_level)
97 end
116 end
98
117
118 it "root when not persisted and parent_column_name value is self" do
119 new_category = Category.new
120 new_category.root.should == new_category
121 end
122
123 it "root when not persisted and parent_column_name value is set" do
124 last_category = Category.last
125 Category.new(Default.parent_column_name => last_category.id).root.should == last_category.root
126 end
127
99 it "root?" do
128 it "root?" do
100 categories(:top_level).root?.should be_true
129 categories(:top_level).root?.should be_true
101 categories(:top_level_2).root?.should be_true
130 categories(:top_level_2).root?.should be_true
@@ -159,27 +188,88 describe "AwesomeNestedSet" do
159 categories(:top_level).leaves.should == leaves
188 categories(:top_level).leaves.should == leaves
160 end
189 end
161
190
162 it "level" do
191 describe "level" do
192 it "returns the correct level" do
163 categories(:top_level).level.should == 0
193 categories(:top_level).level.should == 0
164 categories(:child_1).level.should == 1
194 categories(:child_1).level.should == 1
165 categories(:child_2_1).level.should == 2
195 categories(:child_2_1).level.should == 2
166 end
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 it "has_children?" do
253 it "has_children?" do
169 categories(:child_2_1).children.empty?.should be_true
254 categories(:child_2_1).children.empty?.should be_true
170 categories(:child_2).children.empty?.should be_false
255 categories(:child_2).children.empty?.should be_false
171 categories(:top_level).children.empty?.should be_false
256 categories(:top_level).children.empty?.should be_false
172 end
257 end
173
258
174 it "self_and_descendents" do
259 it "self_and_descendants" do
175 parent = categories(:top_level)
260 parent = categories(:top_level)
176 self_and_descendants = [parent, categories(:child_1), categories(:child_2),
261 self_and_descendants = [
177 categories(:child_2_1), categories(:child_3)]
262 parent,
263 categories(:child_1),
264 categories(:child_2),
265 categories(:child_2_1),
266 categories(:child_3)
267 ]
178 self_and_descendants.should == parent.self_and_descendants
268 self_and_descendants.should == parent.self_and_descendants
179 self_and_descendants.count.should == parent.self_and_descendants.count
269 self_and_descendants.count.should == parent.self_and_descendants.count
180 end
270 end
181
271
182 it "descendents" do
272 it "descendants" do
183 lawyers = Category.create!(:name => "lawyers")
273 lawyers = Category.create!(:name => "lawyers")
184 us = Category.create!(:name => "United States")
274 us = Category.create!(:name => "United States")
185 us.move_to_child_of(lawyers)
275 us.move_to_child_of(lawyers)
@@ -192,10 +282,14 describe "AwesomeNestedSet" do
192 lawyers.descendants.size.should == 2
282 lawyers.descendants.size.should == 2
193 end
283 end
194
284
195 it "self_and_descendents" do
285 it "self_and_descendants" do
196 parent = categories(:top_level)
286 parent = categories(:top_level)
197 descendants = [categories(:child_1), categories(:child_2),
287 descendants = [
198 categories(:child_2_1), categories(:child_3)]
288 categories(:child_1),
289 categories(:child_2),
290 categories(:child_2_1),
291 categories(:child_3)
292 ]
199 descendants.should == parent.descendants
293 descendants.should == parent.descendants
200 end
294 end
201
295
@@ -350,6 +444,43 describe "AwesomeNestedSet" do
350 Category.valid?.should be_true
444 Category.valid?.should be_true
351 end
445 end
352
446
447 describe "#move_to_child_with_index" do
448 it "move to a node without child" do
449 categories(:child_1).move_to_child_with_index(categories(:child_3), 0)
450 categories(:child_3).id.should == categories(:child_1).parent_id
451 categories(:child_1).left.should == 7
452 categories(:child_1).right.should == 8
453 categories(:child_3).left.should == 6
454 categories(:child_3).right.should == 9
455 Category.valid?.should be_true
456 end
457
458 it "move to a node to the left child" do
459 categories(:child_1).move_to_child_with_index(categories(:child_2), 0)
460 categories(:child_1).parent_id.should == categories(:child_2).id
461 categories(:child_2_1).left.should == 5
462 categories(:child_2_1).right.should == 6
463 categories(:child_1).left.should == 3
464 categories(:child_1).right.should == 4
465 categories(:child_2).reload
466 categories(:child_2).left.should == 2
467 categories(:child_2).right.should == 7
468 end
469
470 it "move to a node to the right child" do
471 categories(:child_1).move_to_child_with_index(categories(:child_2), 1)
472 categories(:child_1).parent_id.should == categories(:child_2).id
473 categories(:child_2_1).left.should == 3
474 categories(:child_2_1).right.should == 4
475 categories(:child_1).left.should == 5
476 categories(:child_1).right.should == 6
477 categories(:child_2).reload
478 categories(:child_2).left.should == 2
479 categories(:child_2).right.should == 7
480 end
481
482 end
483
353 it "move_to_child_of_appends_to_end" do
484 it "move_to_child_of_appends_to_end" do
354 child = Category.create! :name => 'New Child'
485 child = Category.create! :name => 'New Child'
355 child.move_to_child_of categories(:top_level)
486 child.move_to_child_of categories(:top_level)
@@ -444,6 +575,32 describe "AwesomeNestedSet" do
444 Category.roots.last.to_text.should == output
575 Category.roots.last.to_text.should == output
445 end
576 end
446
577
578 it "should_move_to_ordered_child" do
579 node1 = Category.create(:name => 'Node-1')
580 node2 = Category.create(:name => 'Node-2')
581 node3 = Category.create(:name => 'Node-3')
582
583 node2.move_to_ordered_child_of(node1, "name")
584
585 assert_equal node1, node2.parent
586 assert_equal 1, node1.children.count
587
588 node3.move_to_ordered_child_of(node1, "name", true) # acending
589
590 assert_equal node1, node3.parent
591 assert_equal 2, node1.children.count
592 assert_equal node2.name, node1.children[0].name
593 assert_equal node3.name, node1.children[1].name
594
595 node3.move_to_ordered_child_of(node1, "name", false) # decending
596 node1.reload
597
598 assert_equal node1, node3.parent
599 assert_equal 2, node1.children.count
600 assert_equal node3.name, node1.children[0].name
601 assert_equal node2.name, node1.children[1].name
602 end
603
447 it "should be able to rebuild without validating each record" do
604 it "should be able to rebuild without validating each record" do
448 root1 = Category.create(:name => 'Root1')
605 root1 = Category.create(:name => 'Root1')
449 root2 = Category.create(:name => 'Root2')
606 root2 = Category.create(:name => 'Root2')
@@ -617,7 +774,15 describe "AwesomeNestedSet" do
617 end
774 end
618
775
619 it "quoting_of_multi_scope_column_names" do
776 it "quoting_of_multi_scope_column_names" do
620 ["\"notable_id\"", "\"notable_type\""].should == Note.quoted_scope_column_names
777 ## Proper Array Assignment for different DBs as per their quoting column behavior
778 if Note.connection.adapter_name.match(/Oracle/)
779 expected_quoted_scope_column_names = ["\"NOTABLE_ID\"", "\"NOTABLE_TYPE\""]
780 elsif Note.connection.adapter_name.match(/Mysql/)
781 expected_quoted_scope_column_names = ["`notable_id`", "`notable_type`"]
782 else
783 expected_quoted_scope_column_names = ["\"notable_id\"", "\"notable_type\""]
784 end
785 expected_quoted_scope_column_names.should == Note.quoted_scope_column_names
621 end
786 end
622
787
623 it "equal_in_same_scope" do
788 it "equal_in_same_scope" do
@@ -730,7 +895,8 describe "AwesomeNestedSet" do
730 [1, "Child 1"],
895 [1, "Child 1"],
731 [1, "Child 2"],
896 [1, "Child 2"],
732 [2, "Child 2.1"],
897 [2, "Child 2.1"],
733 [1, "Child 3" ]]
898 [1, "Child 3" ]
899 ]
734
900
735 check_structure(Category.root.self_and_descendants, levels)
901 check_structure(Category.root.self_and_descendants, levels)
736
902
@@ -756,7 +922,8 describe "AwesomeNestedSet" do
756 [2, "Child 1.2"],
922 [2, "Child 1.2"],
757 [1, "Child 2"],
923 [1, "Child 2"],
758 [2, "Child 2.1"],
924 [2, "Child 2.1"],
759 [1, "Child 3" ]]
925 [1, "Child 3" ]
926 ]
760
927
761 check_structure(Category.root.self_and_descendants, levels)
928 check_structure(Category.root.self_and_descendants, levels)
762 end
929 end
@@ -838,4 +1005,78 describe "AwesomeNestedSet" do
838 root.after_remove.should == child
1005 root.after_remove.should == child
839 end
1006 end
840 end
1007 end
1008
1009 describe 'creating roots with a default scope ordering' do
1010 it "assigns rgt and lft correctly" do
1011 alpha = Order.create(:name => 'Alpha')
1012 gamma = Order.create(:name => 'Gamma')
1013 omega = Order.create(:name => 'Omega')
1014
1015 alpha.lft.should == 1
1016 alpha.rgt.should == 2
1017 gamma.lft.should == 3
1018 gamma.rgt.should == 4
1019 omega.lft.should == 5
1020 omega.rgt.should == 6
1021 end
1022 end
1023
1024 describe 'moving node from one scoped tree to another' do
1025 xit "moves single node correctly" do
1026 root1 = Note.create!(:body => "A-1", :notable_id => 4, :notable_type => 'Category')
1027 child1_1 = Note.create!(:body => "B-1", :notable_id => 4, :notable_type => 'Category')
1028 child1_2 = Note.create!(:body => "C-1", :notable_id => 4, :notable_type => 'Category')
1029 child1_1.move_to_child_of root1
1030 child1_2.move_to_child_of root1
1031
1032 root2 = Note.create!(:body => "A-2", :notable_id => 5, :notable_type => 'Category')
1033 child2_1 = Note.create!(:body => "B-2", :notable_id => 5, :notable_type => 'Category')
1034 child2_2 = Note.create!(:body => "C-2", :notable_id => 5, :notable_type => 'Category')
1035 child2_1.move_to_child_of root2
1036 child2_2.move_to_child_of root2
1037
1038 child1_1.update_attributes!(:notable_id => 5)
1039 child1_1.move_to_child_of root2
1040
1041 root1.children.should == [child1_2]
1042 root2.children.should == [child2_1, child2_2, child1_1]
1043
1044 Note.valid?.should == true
1045 end
1046
1047 xit "moves node with children correctly" do
1048 root1 = Note.create!(:body => "A-1", :notable_id => 4, :notable_type => 'Category')
1049 child1_1 = Note.create!(:body => "B-1", :notable_id => 4, :notable_type => 'Category')
1050 child1_2 = Note.create!(:body => "C-1", :notable_id => 4, :notable_type => 'Category')
1051 child1_1.move_to_child_of root1
1052 child1_2.move_to_child_of child1_1
1053
1054 root2 = Note.create!(:body => "A-2", :notable_id => 5, :notable_type => 'Category')
1055 child2_1 = Note.create!(:body => "B-2", :notable_id => 5, :notable_type => 'Category')
1056 child2_2 = Note.create!(:body => "C-2", :notable_id => 5, :notable_type => 'Category')
1057 child2_1.move_to_child_of root2
1058 child2_2.move_to_child_of root2
1059
1060 child1_1.update_attributes!(:notable_id => 5)
1061 child1_1.move_to_child_of root2
1062
1063 root1.children.should == []
1064 root2.children.should == [child2_1, child2_2, child1_1]
1065 child1_1.children should == [child1_2]
1066 root2.siblings.should == [child2_1, child2_2, child1_1, child1_2]
1067
1068 Note.valid?.should == true
1069 end
1070 end
1071
1072 describe 'specifying custom sort column' do
1073 it "should sort by the default sort column" do
1074 Category.order_column.should == 'lft'
1075 end
1076
1077 it "should sort by custom sort column" do
1078 OrderedCategory.acts_as_nested_set_options[:order_column].should == 'name'
1079 OrderedCategory.order_column.should == 'name'
1080 end
1081 end
841 end
1082 end
@@ -16,3 +16,10 mysql:
16 username: root
16 username: root
17 password:
17 password:
18 database: awesome_nested_set_plugin_test
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
@@ -5,6 +5,7 ActiveRecord::Schema.define(:version => 0) do
5 t.column :parent_id, :integer
5 t.column :parent_id, :integer
6 t.column :lft, :integer
6 t.column :lft, :integer
7 t.column :rgt, :integer
7 t.column :rgt, :integer
8 t.column :depth, :integer
8 t.column :organization_id, :integer
9 t.column :organization_id, :integer
9 end
10 end
10
11
@@ -17,6 +18,7 ActiveRecord::Schema.define(:version => 0) do
17 t.column :parent_id, :integer
18 t.column :parent_id, :integer
18 t.column :lft, :integer
19 t.column :lft, :integer
19 t.column :rgt, :integer
20 t.column :rgt, :integer
21 t.column :depth, :integer
20 t.column :notable_id, :integer
22 t.column :notable_id, :integer
21 t.column :notable_type, :string
23 t.column :notable_type, :string
22 end
24 end
@@ -26,6 +28,7 ActiveRecord::Schema.define(:version => 0) do
26 t.column :mother_id, :integer
28 t.column :mother_id, :integer
27 t.column :red, :integer
29 t.column :red, :integer
28 t.column :black, :integer
30 t.column :black, :integer
31 t.column :pitch, :integer
29 end
32 end
30
33
31 create_table :things, :force => true do |t|
34 create_table :things, :force => true do |t|
@@ -33,6 +36,7 ActiveRecord::Schema.define(:version => 0) do
33 t.column :parent_id, :integer
36 t.column :parent_id, :integer
34 t.column :lft, :integer
37 t.column :lft, :integer
35 t.column :rgt, :integer
38 t.column :rgt, :integer
39 t.column :depth, :integer
36 t.column :children_count, :integer
40 t.column :children_count, :integer
37 end
41 end
38
42
@@ -41,5 +45,21 ActiveRecord::Schema.define(:version => 0) do
41 t.column :parent_id, :integer
45 t.column :parent_id, :integer
42 t.column :lft, :integer
46 t.column :lft, :integer
43 t.column :rgt, :integer
47 t.column :rgt, :integer
48 t.column :depth, :integer
49 end
50
51 create_table :orders, :force => true do |t|
52 t.column :name, :string
53 t.column :parent_id, :integer
54 t.column :lft, :integer
55 t.column :rgt, :integer
56 t.column :depth, :integer
57 end
58
59 create_table :no_depths, :force => true do |t|
60 t.column :name, :string
61 t.column :parent_id, :integer
62 t.column :lft, :integer
63 t.column :rgt, :integer
44 end
64 end
45 end
65 end
@@ -12,8 +12,16 class ScopedCategory < ActiveRecord::Base
12 acts_as_nested_set :scope => :organization
12 acts_as_nested_set :scope => :organization
13 end
13 end
14
14
15 class OrderedCategory < ActiveRecord::Base
16 self.table_name = 'categories'
17 acts_as_nested_set :order_column => 'name'
18 end
19
15 class RenamedColumns < ActiveRecord::Base
20 class RenamedColumns < ActiveRecord::Base
16 acts_as_nested_set :parent_column => 'mother_id', :left_column => 'red', :right_column => 'black'
21 acts_as_nested_set :parent_column => 'mother_id',
22 :left_column => 'red',
23 :right_column => 'black',
24 :depth_column => 'pitch'
17 end
25 end
18
26
19 class Category < ActiveRecord::Base
27 class Category < ActiveRecord::Base
@@ -70,3 +78,13 end
70 class Broken < ActiveRecord::Base
78 class Broken < ActiveRecord::Base
71 acts_as_nested_set
79 acts_as_nested_set
72 end
80 end
81
82 class Order < ActiveRecord::Base
83 acts_as_nested_set
84
85 default_scope order(:name)
86 end
87
88 class NoDepth < ActiveRecord::Base
89 acts_as_nested_set
90 end
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (603 lines changed) Show them Hide them
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now