awesome_nested_set 2.0.1 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG CHANGED
@@ -1,3 +1,27 @@
1
+ 2.1.1
2
+ * Added 'depth' which indicates how many levels deep the node is.
3
+ This only works when you have a column called 'depth' in your table,
4
+ otherwise it doesn't set itself. [Philip Arndt]
5
+ * Rails 3.2 support added. [Gabriel Sobrinho]
6
+ * Oracle compatibility added. [Pikender Sharma]
7
+ * Adding row locking to deletion, locking source of pivot values, and adding retry on collisions. [Markus J. Q. Roberts]
8
+ * Added method and helper for sorting children by column. [bluegod]
9
+ * Fixed .all_roots_valid? to work with Postgres. [Joshua Clayton]
10
+ * Made compatible with polymorphic belongs_to. [Graham Randall]
11
+ * Added in the association callbacks to the children :has_many association. [Michael Deering]
12
+ * Modified helper to allow using array of objects as argument. [Rahmat Budiharso]
13
+ * Fixed cases where we were calling attr_protected. [Jacob Swanner]
14
+ * Fixed nil cases involving lft and rgt. [Stuart Coyle] and [Patrick Morgan]
15
+
16
+ 2.0.2
17
+ * Fixed deprecation warning under Rails 3.1 [Philip Arndt]
18
+ * Converted Test::Unit matchers to RSpec. [Uģis Ozols]
19
+ * Added inverse_of to associations to improve performance rendering trees. [Sergio Cambra]
20
+ * Added row locking and fixed some race conditions. [Markus J. Q. Roberts]
21
+
22
+ 2.0.1
23
+ * Fixed a bug with move_to not using nested_set_scope [Andreas Sekine]
24
+
1
25
  2.0.0.pre
2
26
  * Expect Rails 3
3
27
  * 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.
data/README.rdoc CHANGED
@@ -16,7 +16,8 @@ This is a new implementation of nested set based off of BetterNestedSet that fix
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
@@ -25,6 +26,7 @@ To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt,
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
 
@@ -39,7 +41,23 @@ Enable the nested set functionality by declaring acts_as_nested_set on your mode
39
41
  acts_as_nested_set
40
42
  end
41
43
 
42
- Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet::Model::SingletonMethods for more info.
44
+ Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet for more info.
45
+
46
+ == Protecting attributes from mass assignment
47
+
48
+ It's generally best to "white list" the attributes that can be used in mass assignment:
49
+
50
+ class Category < ActiveRecord::Base
51
+ acts_as_nested_set
52
+ attr_accessible :name, :parent_id
53
+ end
54
+
55
+ If for some reason that is not possible, you will probably want to protect the lft and rgt attributes:
56
+
57
+ class Category < ActiveRecord::Base
58
+ acts_as_nested_set
59
+ attr_protected :lft, :rgt
60
+ end
43
61
 
44
62
  == Conversion from other trees
45
63
 
@@ -70,15 +88,15 @@ You can learn more about nested sets at: http://threebit.net/tutorials/nestedset
70
88
  If you find what you might think is a bug:
71
89
 
72
90
  1. Check the GitHub issue tracker to see if anyone else has had the same issue.
73
- http://github.com/collectiveidea/awesome_nested_set/issues/
91
+ https://github.com/collectiveidea/awesome_nested_set/issues/
74
92
  2. If you don't see anything, create an issue with information on how to reproduce it.
75
93
 
76
94
  If you want to contribute an enhancement or a fix:
77
95
 
78
- 1. Fork the project on github.
79
- http://github.com/collectiveidea/awesome_nested_set/
96
+ 1. Fork the project on GitHub.
97
+ https://github.com/collectiveidea/awesome_nested_set/
80
98
  2. Make your changes with tests.
81
99
  3. Commit the changes without making changes to the Rakefile, VERSION, or any other files that aren't related to your enhancement or fix
82
100
  4. Send a pull request.
83
101
 
84
- Copyright ©2008 Collective Idea, released under the MIT license
102
+ Copyright ©2008 Collective Idea, released under the MIT license
@@ -23,6 +23,7 @@ module CollectiveIdea #:nodoc:
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.
@@ -36,14 +37,16 @@ module CollectiveIdea #:nodoc:
36
37
  # Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
37
38
  #
38
39
  # See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
39
- # CollectiveIdea::Acts::NestedSet::Model::InstanceMethods for a list of instance methods added
40
+ # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
40
41
  # to acts_as_nested_set models
41
42
  def acts_as_nested_set(options = {})
42
43
  options = {
43
44
  :parent_column => 'parent_id',
44
45
  :left_column => 'lft',
45
46
  :right_column => 'rgt',
47
+ :depth_column => 'depth',
46
48
  :dependent => :delete_all, # or :destroy
49
+ :polymorphic => false,
47
50
  :counter_cache => false
48
51
  }.merge(options)
49
52
 
@@ -51,8 +54,8 @@ module CollectiveIdea #:nodoc:
51
54
  options[:scope] = "#{options[:scope]}_id".intern
52
55
  end
53
56
 
54
- write_inheritable_attribute :acts_as_nested_set_options, options
55
- class_inheritable_reader :acts_as_nested_set_options
57
+ class_attribute :acts_as_nested_set_options
58
+ self.acts_as_nested_set_options = options
56
59
 
57
60
  include CollectiveIdea::Acts::NestedSet::Model
58
61
  include Columns
@@ -60,24 +63,26 @@ module CollectiveIdea #:nodoc:
60
63
 
61
64
  belongs_to :parent, :class_name => self.base_class.to_s,
62
65
  :foreign_key => parent_column_name,
63
- :counter_cache => options[:counter_cache]
66
+ :counter_cache => options[:counter_cache],
67
+ :inverse_of => (:children unless options[:polymorphic]),
68
+ :polymorphic => options[:polymorphic]
64
69
  has_many :children, :class_name => self.base_class.to_s,
65
- :foreign_key => parent_column_name, :order => quoted_left_column_name
70
+ :foreign_key => parent_column_name, :order => left_column_name,
71
+ :inverse_of => (:parent unless options[:polymorphic]),
72
+ :before_add => options[:before_add],
73
+ :after_add => options[:after_add],
74
+ :before_remove => options[:before_remove],
75
+ :after_remove => options[:after_remove]
66
76
 
67
77
  attr_accessor :skip_before_destroy
68
78
 
69
- # no bulk assignment
70
- if accessible_attributes.blank?
71
- attr_protected left_column_name.intern, right_column_name.intern
72
- end
73
-
74
79
  before_create :set_default_left_and_right
75
80
  before_save :store_new_parent
76
- after_save :move_to_new_parent
81
+ after_save :move_to_new_parent, :set_depth!
77
82
  before_destroy :destroy_descendants
78
83
 
79
84
  # no assignment to structure fields
80
- [left_column_name, right_column_name].each do |column|
85
+ [left_column_name, right_column_name, depth_column_name].each do |column|
81
86
  module_eval <<-"end_eval", __FILE__, __LINE__
82
87
  def #{column}=(x)
83
88
  raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
@@ -85,10 +90,7 @@ module CollectiveIdea #:nodoc:
85
90
  end_eval
86
91
  end
87
92
 
88
- scope :roots, where(parent_column_name => nil).order(quoted_left_column_name)
89
- scope :leaves, where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
90
-
91
- define_callbacks :move, :terminator => "result == false"
93
+ define_model_callbacks :move
92
94
  end
93
95
 
94
96
  module Model
@@ -100,12 +102,23 @@ module CollectiveIdea #:nodoc:
100
102
  roots.first
101
103
  end
102
104
 
105
+ def roots
106
+ where(parent_column_name => nil).order(quoted_left_column_name)
107
+ end
108
+
109
+ def leaves
110
+ where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
111
+ end
112
+
103
113
  def valid?
104
114
  left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
105
115
  end
106
116
 
107
- def left_and_rights_valid?
108
- joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
117
+ def left_and_rights_valid?
118
+ ## AS clause not supported in Oracle in FROM clause for aliasing table name
119
+ joins("LEFT OUTER JOIN #{quoted_table_name}" +
120
+ (connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
121
+ "parent ON " +
109
122
  "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}").
110
123
  where(
111
124
  "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
@@ -134,7 +147,7 @@ module CollectiveIdea #:nodoc:
134
147
  # Wrapper for each_root_valid? that can deal with scope.
135
148
  def all_roots_valid?
136
149
  if acts_as_nested_set_options[:scope]
137
- roots.group(scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
150
+ roots.group_by {|record| scope_column_names.collect {|col| record.send(col.to_sym) } }.all? do |scope, grouped_roots|
138
151
  each_root_valid?(grouped_roots)
139
152
  end
140
153
  else
@@ -198,7 +211,7 @@ module CollectiveIdea #:nodoc:
198
211
  path = [nil]
199
212
  objects.each do |o|
200
213
  if o.parent_id != path.last
201
- # we are on a new level, did we decent or ascent?
214
+ # we are on a new level, did we descend or ascend?
202
215
  if path.include?(o.parent_id)
203
216
  # remove wrong wrong tailing paths elements
204
217
  path.pop while path.last != o.parent_id
@@ -209,317 +222,388 @@ module CollectiveIdea #:nodoc:
209
222
  yield(o, path.length - 1)
210
223
  end
211
224
  end
225
+
226
+ # Same as each_with_level - Accepts a string as a second argument to sort the list
227
+ # Example:
228
+ # Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
229
+ def sorted_each_with_level(objects, order)
230
+ path = [nil]
231
+ children = []
232
+ objects.each do |o|
233
+ children << o if o.leaf?
234
+ if o.parent_id != path.last
235
+ if !children.empty? && !o.leaf?
236
+ children.sort_by! &order
237
+ children.each { |c| yield(c, path.length-1) }
238
+ children = []
239
+ end
240
+ # we are on a new level, did we decent or ascent?
241
+ if path.include?(o.parent_id)
242
+ # remove wrong wrong tailing paths elements
243
+ path.pop while path.last != o.parent_id
244
+ else
245
+ path << o.parent_id
246
+ end
247
+ end
248
+ yield(o,path.length-1) if !o.leaf?
249
+ end
250
+ if !children.empty?
251
+ children.sort_by! &order
252
+ children.each { |c| yield(c, path.length-1) }
253
+ end
254
+ end
212
255
  end
213
-
256
+
214
257
  # 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.
215
258
  #
216
259
  # category.self_and_descendants.count
217
260
  # category.ancestors.find(:all, :conditions => "name like '%foo%'")
218
- module InstanceMethods
219
- # Value of the parent column
220
- def parent_id
221
- self[parent_column_name]
222
- end
261
+ # Value of the parent column
262
+ def parent_id
263
+ self[parent_column_name]
264
+ end
223
265
 
224
- # Value of the left column
225
- def left
226
- self[left_column_name]
227
- end
266
+ # Value of the left column
267
+ def left
268
+ self[left_column_name]
269
+ end
228
270
 
229
- # Value of the right column
230
- def right
231
- self[right_column_name]
232
- end
271
+ # Value of the right column
272
+ def right
273
+ self[right_column_name]
274
+ end
233
275
 
234
- # Returns true if this is a root node.
235
- def root?
236
- parent_id.nil?
237
- end
276
+ # Returns true if this is a root node.
277
+ def root?
278
+ parent_id.nil?
279
+ end
238
280
 
239
- def leaf?
240
- !new_record? && right - left == 1
241
- end
281
+ # Returns true if this is the end of a branch.
282
+ def leaf?
283
+ persisted? && right.to_i - left.to_i == 1
284
+ end
242
285
 
243
- # Returns true is this is a child node
244
- def child?
245
- !parent_id.nil?
246
- end
286
+ # Returns true is this is a child node
287
+ def child?
288
+ !parent_id.nil?
289
+ end
247
290
 
248
- # Returns root
249
- def root
250
- self_and_ancestors.where(parent_column_name => nil).first
251
- end
291
+ # Returns root
292
+ def root
293
+ self_and_ancestors.where(parent_column_name => nil).first
294
+ end
252
295
 
253
- # Returns the array of all parents and self
254
- def self_and_ancestors
255
- nested_set_scope.where([
256
- "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right
257
- ])
258
- end
296
+ # Returns the array of all parents and self
297
+ def self_and_ancestors
298
+ nested_set_scope.where([
299
+ "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right
300
+ ])
301
+ end
259
302
 
260
- # Returns an array of all parents
261
- def ancestors
262
- without_self self_and_ancestors
263
- end
303
+ # Returns an array of all parents
304
+ def ancestors
305
+ without_self self_and_ancestors
306
+ end
264
307
 
265
- # Returns the array of all children of the parent, including self
266
- def self_and_siblings
267
- nested_set_scope.where(parent_column_name => parent_id)
268
- end
308
+ # Returns the array of all children of the parent, including self
309
+ def self_and_siblings
310
+ nested_set_scope.where(parent_column_name => parent_id)
311
+ end
269
312
 
270
- # Returns the array of all children of the parent, except self
271
- def siblings
272
- without_self self_and_siblings
273
- end
313
+ # Returns the array of all children of the parent, except self
314
+ def siblings
315
+ without_self self_and_siblings
316
+ end
274
317
 
275
- # Returns a set of all of its nested children which do not have children
276
- def leaves
277
- descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
278
- end
318
+ # Returns a set of all of its nested children which do not have children
319
+ def leaves
320
+ descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
321
+ end
279
322
 
280
- # Returns the level of this object in the tree
281
- # root level is 0
282
- def level
283
- parent_id.nil? ? 0 : ancestors.count
284
- end
323
+ # Returns the level of this object in the tree
324
+ # root level is 0
325
+ def level
326
+ parent_id.nil? ? 0 : ancestors.count
327
+ end
285
328
 
286
- # Returns a set of itself and all of its nested children
287
- def self_and_descendants
288
- nested_set_scope.where([
289
- "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right
290
- ])
291
- end
329
+ # Returns a set of itself and all of its nested children
330
+ def self_and_descendants
331
+ nested_set_scope.where([
332
+ "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right
333
+ ])
334
+ end
292
335
 
293
- # Returns a set of all of its children and nested children
294
- def descendants
295
- without_self self_and_descendants
296
- end
336
+ # Returns a set of all of its children and nested children
337
+ def descendants
338
+ without_self self_and_descendants
339
+ end
297
340
 
298
- def is_descendant_of?(other)
299
- other.left < self.left && self.left < other.right && same_scope?(other)
300
- end
341
+ def is_descendant_of?(other)
342
+ other.left < self.left && self.left < other.right && same_scope?(other)
343
+ end
301
344
 
302
- def is_or_is_descendant_of?(other)
303
- other.left <= self.left && self.left < other.right && same_scope?(other)
304
- end
345
+ def is_or_is_descendant_of?(other)
346
+ other.left <= self.left && self.left < other.right && same_scope?(other)
347
+ end
305
348
 
306
- def is_ancestor_of?(other)
307
- self.left < other.left && other.left < self.right && same_scope?(other)
308
- end
349
+ def is_ancestor_of?(other)
350
+ self.left < other.left && other.left < self.right && same_scope?(other)
351
+ end
309
352
 
310
- def is_or_is_ancestor_of?(other)
311
- self.left <= other.left && other.left < self.right && same_scope?(other)
312
- end
353
+ def is_or_is_ancestor_of?(other)
354
+ self.left <= other.left && other.left < self.right && same_scope?(other)
355
+ end
313
356
 
314
- # Check if other model is in the same scope
315
- def same_scope?(other)
316
- Array(acts_as_nested_set_options[:scope]).all? do |attr|
317
- self.send(attr) == other.send(attr)
318
- end
357
+ # Check if other model is in the same scope
358
+ def same_scope?(other)
359
+ Array(acts_as_nested_set_options[:scope]).all? do |attr|
360
+ self.send(attr) == other.send(attr)
319
361
  end
362
+ end
320
363
 
321
- # Find the first sibling to the left
322
- def left_sibling
323
- siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]).
324
- order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last
325
- end
364
+ # Find the first sibling to the left
365
+ def left_sibling
366
+ siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]).
367
+ order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last
368
+ end
326
369
 
327
- # Find the first sibling to the right
328
- def right_sibling
329
- siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first
330
- end
370
+ # Find the first sibling to the right
371
+ def right_sibling
372
+ siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first
373
+ end
331
374
 
332
- # Shorthand method for finding the left sibling and moving to the left of it.
333
- def move_left
334
- move_to_left_of left_sibling
335
- end
375
+ # Shorthand method for finding the left sibling and moving to the left of it.
376
+ def move_left
377
+ move_to_left_of left_sibling
378
+ end
336
379
 
337
- # Shorthand method for finding the right sibling and moving to the right of it.
338
- def move_right
339
- move_to_right_of right_sibling
340
- end
380
+ # Shorthand method for finding the right sibling and moving to the right of it.
381
+ def move_right
382
+ move_to_right_of right_sibling
383
+ end
341
384
 
342
- # Move the node to the left of another node (you can pass id only)
343
- def move_to_left_of(node)
344
- move_to node, :left
345
- end
385
+ # Move the node to the left of another node (you can pass id only)
386
+ def move_to_left_of(node)
387
+ move_to node, :left
388
+ end
346
389
 
347
- # Move the node to the left of another node (you can pass id only)
348
- def move_to_right_of(node)
349
- move_to node, :right
350
- end
390
+ # Move the node to the left of another node (you can pass id only)
391
+ def move_to_right_of(node)
392
+ move_to node, :right
393
+ end
351
394
 
352
- # Move the node to the child of another node (you can pass id only)
353
- def move_to_child_of(node)
354
- move_to node, :child
355
- end
395
+ # Move the node to the child of another node (you can pass id only)
396
+ def move_to_child_of(node)
397
+ move_to node, :child
398
+ end
356
399
 
357
- # Move the node to root nodes
358
- def move_to_root
359
- move_to nil, :root
360
- end
400
+ # Move the node to root nodes
401
+ def move_to_root
402
+ move_to nil, :root
403
+ end
361
404
 
362
- def move_possible?(target)
363
- self != target && # Can't target self
364
- same_scope?(target) && # can't be in different scopes
365
- # !(left..right).include?(target.left..target.right) # this needs tested more
366
- # detect impossible move
367
- !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
368
- end
405
+ def move_possible?(target)
406
+ self != target && # Can't target self
407
+ same_scope?(target) && # can't be in different scopes
408
+ # !(left..right).include?(target.left..target.right) # this needs tested more
409
+ # detect impossible move
410
+ !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
411
+ end
369
412
 
370
- def to_text
371
- self_and_descendants.map do |node|
372
- "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
373
- end.join("\n")
374
- end
413
+ def to_text
414
+ self_and_descendants.map do |node|
415
+ "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
416
+ end.join("\n")
417
+ end
375
418
 
376
- protected
419
+ protected
377
420
 
378
- def without_self(scope)
379
- scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
380
- end
421
+ def without_self(scope)
422
+ scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
423
+ end
381
424
 
382
- # All nested set queries should use this nested_set_scope, which performs finds on
383
- # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
384
- # declaration.
385
- def nested_set_scope
386
- options = {:order => quoted_left_column_name}
387
- scopes = Array(acts_as_nested_set_options[:scope])
388
- options[:conditions] = scopes.inject({}) do |conditions,attr|
389
- conditions.merge attr => self[attr]
390
- end unless scopes.empty?
391
- self.class.base_class.scoped options
392
- end
425
+ # All nested set queries should use this nested_set_scope, which performs finds on
426
+ # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
427
+ # declaration.
428
+ def nested_set_scope(options = {})
429
+ options = {:order => quoted_left_column_name}.merge(options)
430
+ scopes = Array(acts_as_nested_set_options[:scope])
431
+ options[:conditions] = scopes.inject({}) do |conditions,attr|
432
+ conditions.merge attr => self[attr]
433
+ end unless scopes.empty?
434
+ self.class.base_class.unscoped.scoped options
435
+ end
436
+
437
+ def store_new_parent
438
+ @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
439
+ true # force callback to return true
440
+ end
393
441
 
394
- def store_new_parent
395
- @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
396
- true # force callback to return true
442
+ def move_to_new_parent
443
+ if @move_to_new_parent_id.nil?
444
+ move_to_root
445
+ elsif @move_to_new_parent_id
446
+ move_to_child_of(@move_to_new_parent_id)
397
447
  end
448
+ end
398
449
 
399
- def move_to_new_parent
400
- if @move_to_new_parent_id.nil?
401
- move_to_root
402
- elsif @move_to_new_parent_id
403
- move_to_child_of(@move_to_new_parent_id)
450
+ def set_depth!
451
+ if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
452
+ in_tenacious_transaction do
453
+ reload
454
+
455
+ nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
404
456
  end
457
+ self[:depth] = self.level
405
458
  end
459
+ end
406
460
 
407
- # on creation, set automatically lft and rgt to the end of the tree
408
- def set_default_left_and_right
409
- maxright = nested_set_scope.maximum(right_column_name) || 0
410
- # adds the new node to the right of all existing nodes
411
- self[left_column_name] = maxright + 1
412
- self[right_column_name] = maxright + 2
413
- end
461
+ # on creation, set automatically lft and rgt to the end of the tree
462
+ def set_default_left_and_right
463
+ highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").find(:first, :limit => 1,:lock => true )
464
+ maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
465
+ # adds the new node to the right of all existing nodes
466
+ self[left_column_name] = maxright + 1
467
+ self[right_column_name] = maxright + 2
468
+ end
414
469
 
415
- # Prunes a branch off of the tree, shifting all of the elements on the right
416
- # back to the left so the counts still work.
417
- def destroy_descendants
418
- return if right.nil? || left.nil? || skip_before_destroy
470
+ def in_tenacious_transaction(&block)
471
+ retry_count = 0
472
+ begin
473
+ transaction(&block)
474
+ rescue ActiveRecord::StatementInvalid => error
475
+ raise unless connection.open_transactions.zero?
476
+ raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
477
+ raise unless retry_count < 10
478
+ retry_count += 1
479
+ logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
480
+ sleep(rand(retry_count)*0.1) # Aloha protocol
481
+ retry
482
+ end
483
+ end
419
484
 
420
- self.class.base_class.transaction do
421
- if acts_as_nested_set_options[:dependent] == :destroy
422
- descendants.each do |model|
423
- model.skip_before_destroy = true
424
- model.destroy
425
- end
426
- else
427
- nested_set_scope.delete_all(
428
- ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
429
- left, right]
430
- )
485
+ # Prunes a branch off of the tree, shifting all of the elements on the right
486
+ # back to the left so the counts still work.
487
+ def destroy_descendants
488
+ return if right.nil? || left.nil? || skip_before_destroy
489
+
490
+ in_tenacious_transaction do
491
+ reload_nested_set
492
+ # select the rows in the model that extend past the deletion point and apply a lock
493
+ self.class.base_class.find(:all,
494
+ :select => "id",
495
+ :conditions => ["#{quoted_left_column_name} >= ?", left],
496
+ :lock => true
497
+ )
498
+
499
+ if acts_as_nested_set_options[:dependent] == :destroy
500
+ descendants.each do |model|
501
+ model.skip_before_destroy = true
502
+ model.destroy
431
503
  end
432
-
433
- # update lefts and rights for remaining nodes
434
- diff = right - left + 1
435
- nested_set_scope.update_all(
436
- ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
437
- ["#{quoted_left_column_name} > ?", right]
438
- )
439
- nested_set_scope.update_all(
440
- ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
441
- ["#{quoted_right_column_name} > ?", right]
504
+ else
505
+ nested_set_scope.delete_all(
506
+ ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
507
+ left, right]
442
508
  )
443
-
444
- # Don't allow multiple calls to destroy to corrupt the set
445
- self.skip_before_destroy = true
446
509
  end
447
- end
448
510
 
449
- # reload left, right, and parent
450
- def reload_nested_set
451
- reload(:select => "#{quoted_left_column_name}, " +
452
- "#{quoted_right_column_name}, #{quoted_parent_column_name}")
511
+ # update lefts and rights for remaining nodes
512
+ diff = right - left + 1
513
+ nested_set_scope.update_all(
514
+ ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
515
+ ["#{quoted_left_column_name} > ?", right]
516
+ )
517
+ nested_set_scope.update_all(
518
+ ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
519
+ ["#{quoted_right_column_name} > ?", right]
520
+ )
521
+
522
+ # Don't allow multiple calls to destroy to corrupt the set
523
+ self.skip_before_destroy = true
453
524
  end
525
+ end
454
526
 
455
- def move_to(target, position)
456
- raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
457
- run_callbacks :move do
458
- transaction do
459
- if target.is_a? self.class.base_class
460
- target.reload_nested_set
461
- elsif position != :root
462
- # load object if node is not an object
463
- target = nested_set_scope.find(target)
464
- end
465
- self.reload_nested_set
527
+ # reload left, right, and parent
528
+ def reload_nested_set
529
+ reload(
530
+ :select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}",
531
+ :lock => true
532
+ )
533
+ end
466
534
 
467
- unless position == :root || move_possible?(target)
468
- raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
469
- end
535
+ def move_to(target, position)
536
+ raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
537
+ run_callbacks :move do
538
+ in_tenacious_transaction do
539
+ if target.is_a? self.class.base_class
540
+ target.reload_nested_set
541
+ elsif position != :root
542
+ # load object if node is not an object
543
+ target = nested_set_scope.find(target)
544
+ end
545
+ self.reload_nested_set
470
546
 
471
- bound = case position
472
- when :child; target[right_column_name]
473
- when :left; target[left_column_name]
474
- when :right; target[right_column_name] + 1
475
- when :root; 1
476
- else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
477
- end
547
+ unless position == :root || move_possible?(target)
548
+ raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
549
+ end
478
550
 
479
- if bound > self[right_column_name]
480
- bound = bound - 1
481
- other_bound = self[right_column_name] + 1
482
- else
483
- other_bound = self[left_column_name] - 1
484
- end
551
+ bound = case position
552
+ when :child; target[right_column_name]
553
+ when :left; target[left_column_name]
554
+ when :right; target[right_column_name] + 1
555
+ when :root; 1
556
+ else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
557
+ end
558
+
559
+ if bound > self[right_column_name]
560
+ bound = bound - 1
561
+ other_bound = self[right_column_name] + 1
562
+ else
563
+ other_bound = self[left_column_name] - 1
564
+ end
485
565
 
486
- # there would be no change
487
- return if bound == self[right_column_name] || bound == self[left_column_name]
566
+ # there would be no change
567
+ return if bound == self[right_column_name] || bound == self[left_column_name]
488
568
 
489
- # we have defined the boundaries of two non-overlapping intervals,
490
- # so sorting puts both the intervals and their boundaries in order
491
- a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
569
+ # we have defined the boundaries of two non-overlapping intervals,
570
+ # so sorting puts both the intervals and their boundaries in order
571
+ a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
492
572
 
493
- new_parent = case position
494
- when :child; target.id
495
- when :root; nil
496
- else target[parent_column_name]
497
- end
573
+ # select the rows in the model between a and d, and apply a lock
574
+ self.class.base_class.select('id').lock(true).where(
575
+ ["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}]
576
+ )
498
577
 
499
- self.nested_set_scope.update_all([
500
- "#{quoted_left_column_name} = CASE " +
501
- "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
502
- "THEN #{quoted_left_column_name} + :d - :b " +
503
- "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
504
- "THEN #{quoted_left_column_name} + :a - :c " +
505
- "ELSE #{quoted_left_column_name} END, " +
506
- "#{quoted_right_column_name} = CASE " +
507
- "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
508
- "THEN #{quoted_right_column_name} + :d - :b " +
509
- "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
510
- "THEN #{quoted_right_column_name} + :a - :c " +
511
- "ELSE #{quoted_right_column_name} END, " +
512
- "#{quoted_parent_column_name} = CASE " +
513
- "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
514
- "ELSE #{quoted_parent_column_name} END",
515
- {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
516
- ])
578
+ new_parent = case position
579
+ when :child; target.id
580
+ when :root; nil
581
+ else target[parent_column_name]
517
582
  end
518
- target.reload_nested_set if target
519
- self.reload_nested_set
583
+
584
+ self.nested_set_scope.update_all([
585
+ "#{quoted_left_column_name} = CASE " +
586
+ "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
587
+ "THEN #{quoted_left_column_name} + :d - :b " +
588
+ "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
589
+ "THEN #{quoted_left_column_name} + :a - :c " +
590
+ "ELSE #{quoted_left_column_name} END, " +
591
+ "#{quoted_right_column_name} = CASE " +
592
+ "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
593
+ "THEN #{quoted_right_column_name} + :d - :b " +
594
+ "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
595
+ "THEN #{quoted_right_column_name} + :a - :c " +
596
+ "ELSE #{quoted_right_column_name} END, " +
597
+ "#{quoted_parent_column_name} = CASE " +
598
+ "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
599
+ "ELSE #{quoted_parent_column_name} END",
600
+ {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
601
+ ])
520
602
  end
603
+ target.reload_nested_set if target
604
+ self.set_depth!
605
+ self.reload_nested_set
521
606
  end
522
-
523
607
  end
524
608
 
525
609
  end
@@ -534,6 +618,10 @@ module CollectiveIdea #:nodoc:
534
618
  acts_as_nested_set_options[:right_column]
535
619
  end
536
620
 
621
+ def depth_column_name
622
+ acts_as_nested_set_options[:depth_column]
623
+ end
624
+
537
625
  def parent_column_name
538
626
  acts_as_nested_set_options[:parent_column]
539
627
  end
@@ -550,6 +638,10 @@ module CollectiveIdea #:nodoc:
550
638
  connection.quote_column_name(right_column_name)
551
639
  end
552
640
 
641
+ def quoted_depth_column_name
642
+ connection.quote_column_name(depth_column_name)
643
+ end
644
+
553
645
  def quoted_parent_column_name
554
646
  connection.quote_column_name(parent_column_name)
555
647
  end
@@ -561,4 +653,5 @@ module CollectiveIdea #:nodoc:
561
653
 
562
654
  end
563
655
  end
564
- end
656
+ end
657
+
@@ -1,3 +1,4 @@
1
+ # -*- coding: utf-8 -*-
1
2
  module CollectiveIdea #:nodoc:
2
3
  module Acts #:nodoc:
3
4
  module NestedSet #:nodoc:
@@ -21,8 +22,12 @@ module CollectiveIdea #:nodoc:
21
22
  # }) %>
22
23
  #
23
24
  def nested_set_options(class_or_item, mover = nil)
24
- class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
25
- items = Array(class_or_item)
25
+ if class_or_item.is_a? Array
26
+ items = class_or_item.reject { |e| !e.root? }
27
+ else
28
+ class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
29
+ items = Array(class_or_item)
30
+ end
26
31
  result = []
27
32
  items.each do |root|
28
33
  result += root.self_and_descendants.map do |i|
@@ -33,8 +38,52 @@ module CollectiveIdea #:nodoc:
33
38
  end
34
39
  result
35
40
  end
36
-
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.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
37
86
  end
38
87
  end
39
88
  end
40
- end
89
+ end
@@ -1,3 +1,3 @@
1
1
  module AwesomeNestedSet
2
- VERSION = '2.0.1' unless defined?(::AwesomeNestedSet::VERSION)
3
- end
2
+ VERSION = '2.1.1' unless defined?(::AwesomeNestedSet::VERSION)
3
+ end
metadata CHANGED
@@ -1,18 +1,23 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: awesome_nested_set
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 9
4
5
  prerelease:
5
- version: 2.0.1
6
+ segments:
7
+ - 2
8
+ - 1
9
+ - 1
10
+ version: 2.1.1
6
11
  platform: ruby
7
12
  authors:
8
13
  - Brandon Keepers
9
14
  - Daniel Morrison
15
+ - Philip Arndt
10
16
  autorequire:
11
17
  bindir: bin
12
18
  cert_chain: []
13
19
 
14
- date: 2011-06-16 00:00:00 -04:00
15
- default_executable:
20
+ date: 2012-01-25 00:00:00 Z
16
21
  dependencies:
17
22
  - !ruby/object:Gem::Dependency
18
23
  name: activerecord
@@ -22,6 +27,11 @@ dependencies:
22
27
  requirements:
23
28
  - - ">="
24
29
  - !ruby/object:Gem::Version
30
+ hash: 7
31
+ segments:
32
+ - 3
33
+ - 0
34
+ - 0
25
35
  version: 3.0.0
26
36
  type: :runtime
27
37
  version_requirements: *id001
@@ -41,7 +51,6 @@ files:
41
51
  - MIT-LICENSE
42
52
  - README.rdoc
43
53
  - CHANGELOG
44
- has_rdoc: true
45
54
  homepage: http://github.com/collectiveidea/awesome_nested_set
46
55
  licenses: []
47
56
 
@@ -58,17 +67,23 @@ required_ruby_version: !ruby/object:Gem::Requirement
58
67
  requirements:
59
68
  - - ">="
60
69
  - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
61
73
  version: "0"
62
74
  required_rubygems_version: !ruby/object:Gem::Requirement
63
75
  none: false
64
76
  requirements:
65
77
  - - ">="
66
78
  - !ruby/object:Gem::Version
79
+ hash: 3
80
+ segments:
81
+ - 0
67
82
  version: "0"
68
83
  requirements: []
69
84
 
70
85
  rubyforge_project:
71
- rubygems_version: 1.5.2
86
+ rubygems_version: 1.8.10
72
87
  signing_key:
73
88
  specification_version: 3
74
89
  summary: An awesome nested set implementation for Active Record