awesome_nested_set 2.0.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -41,6 +41,22 @@ Enable the nested set functionality by declaring acts_as_nested_set on your mode
41
41
 
42
42
  Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet for more info.
43
43
 
44
+ == Protecting attributes from mass assignment
45
+
46
+ It's generally best to "white list" the attributes that can be used in mass assignment:
47
+
48
+ class Category < ActiveRecord::Base
49
+ acts_as_nested_set
50
+ attr_accessible :name, :parent_id
51
+ end
52
+
53
+ If for some reason that is not possible, you will probably want to protect the lft and rgt attributes:
54
+
55
+ class Category < ActiveRecord::Base
56
+ acts_as_nested_set
57
+ attr_protected :lft, :rgt
58
+ end
59
+
44
60
  == Conversion from other trees
45
61
 
46
62
  Coming from acts_as_tree or another system where you only have a parent_id? No problem. Simply add the lft & rgt fields as above, and then run
@@ -81,4 +97,4 @@ If you want to contribute an enhancement or a fix:
81
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
82
98
  4. Send a pull request.
83
99
 
84
- Copyright ©2008 Collective Idea, released under the MIT license
100
+ 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
 
@@ -61,25 +64,25 @@ module CollectiveIdea #:nodoc:
61
64
  belongs_to :parent, :class_name => self.base_class.to_s,
62
65
  :foreign_key => parent_column_name,
63
66
  :counter_cache => options[:counter_cache],
64
- :inverse_of => :children
67
+ :inverse_of => (:children unless options[:polymorphic]),
68
+ :polymorphic => options[:polymorphic]
65
69
  has_many :children, :class_name => self.base_class.to_s,
66
- :foreign_key => parent_column_name, :order => quoted_left_column_name,
67
- :inverse_of => :parent
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]
68
76
 
69
77
  attr_accessor :skip_before_destroy
70
78
 
71
- # no bulk assignment
72
- if accessible_attributes.blank?
73
- attr_protected left_column_name.intern, right_column_name.intern
74
- end
75
-
76
79
  before_create :set_default_left_and_right
77
80
  before_save :store_new_parent
78
- after_save :move_to_new_parent
81
+ after_save :move_to_new_parent, :set_depth!
79
82
  before_destroy :destroy_descendants
80
83
 
81
84
  # no assignment to structure fields
82
- [left_column_name, right_column_name].each do |column|
85
+ [left_column_name, right_column_name, depth_column_name].each do |column|
83
86
  module_eval <<-"end_eval", __FILE__, __LINE__
84
87
  def #{column}=(x)
85
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."
@@ -87,9 +90,6 @@ module CollectiveIdea #:nodoc:
87
90
  end_eval
88
91
  end
89
92
 
90
- scope :roots, where(parent_column_name => nil).order(quoted_left_column_name)
91
- scope :leaves, where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
92
-
93
93
  define_model_callbacks :move
94
94
  end
95
95
 
@@ -102,12 +102,23 @@ module CollectiveIdea #:nodoc:
102
102
  roots.first
103
103
  end
104
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
+
105
113
  def valid?
106
114
  left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
107
115
  end
108
116
 
109
- def left_and_rights_valid?
110
- 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 " +
111
122
  "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}").
112
123
  where(
113
124
  "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
@@ -136,7 +147,7 @@ module CollectiveIdea #:nodoc:
136
147
  # Wrapper for each_root_valid? that can deal with scope.
137
148
  def all_roots_valid?
138
149
  if acts_as_nested_set_options[:scope]
139
- 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|
140
151
  each_root_valid?(grouped_roots)
141
152
  end
142
153
  else
@@ -200,7 +211,7 @@ module CollectiveIdea #:nodoc:
200
211
  path = [nil]
201
212
  objects.each do |o|
202
213
  if o.parent_id != path.last
203
- # we are on a new level, did we decent or ascent?
214
+ # we are on a new level, did we descend or ascend?
204
215
  if path.include?(o.parent_id)
205
216
  # remove wrong wrong tailing paths elements
206
217
  path.pop while path.last != o.parent_id
@@ -211,323 +222,386 @@ module CollectiveIdea #:nodoc:
211
222
  yield(o, path.length - 1)
212
223
  end
213
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
214
255
  end
215
-
256
+
216
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.
217
258
  #
218
259
  # category.self_and_descendants.count
219
260
  # category.ancestors.find(:all, :conditions => "name like '%foo%'")
220
- module InstanceMethods
221
- # Value of the parent column
222
- def parent_id
223
- self[parent_column_name]
224
- end
261
+ # Value of the parent column
262
+ def parent_id
263
+ self[parent_column_name]
264
+ end
225
265
 
226
- # Value of the left column
227
- def left
228
- self[left_column_name]
229
- end
266
+ # Value of the left column
267
+ def left
268
+ self[left_column_name]
269
+ end
230
270
 
231
- # Value of the right column
232
- def right
233
- self[right_column_name]
234
- end
271
+ # Value of the right column
272
+ def right
273
+ self[right_column_name]
274
+ end
235
275
 
236
- # Returns true if this is a root node.
237
- def root?
238
- parent_id.nil?
239
- end
276
+ # Returns true if this is a root node.
277
+ def root?
278
+ parent_id.nil?
279
+ end
240
280
 
241
- def leaf?
242
- !new_record? && right - left == 1
243
- 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
244
285
 
245
- # Returns true is this is a child node
246
- def child?
247
- !parent_id.nil?
248
- end
286
+ # Returns true is this is a child node
287
+ def child?
288
+ !parent_id.nil?
289
+ end
249
290
 
250
- # Returns root
251
- def root
252
- self_and_ancestors.where(parent_column_name => nil).first
253
- end
291
+ # Returns root
292
+ def root
293
+ self_and_ancestors.where(parent_column_name => nil).first
294
+ end
254
295
 
255
- # Returns the array of all parents and self
256
- def self_and_ancestors
257
- nested_set_scope.where([
258
- "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right
259
- ])
260
- 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
261
302
 
262
- # Returns an array of all parents
263
- def ancestors
264
- without_self self_and_ancestors
265
- end
303
+ # Returns an array of all parents
304
+ def ancestors
305
+ without_self self_and_ancestors
306
+ end
266
307
 
267
- # Returns the array of all children of the parent, including self
268
- def self_and_siblings
269
- nested_set_scope.where(parent_column_name => parent_id)
270
- 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
271
312
 
272
- # Returns the array of all children of the parent, except self
273
- def siblings
274
- without_self self_and_siblings
275
- end
313
+ # Returns the array of all children of the parent, except self
314
+ def siblings
315
+ without_self self_and_siblings
316
+ end
276
317
 
277
- # Returns a set of all of its nested children which do not have children
278
- def leaves
279
- descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
280
- 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
281
322
 
282
- # Returns the level of this object in the tree
283
- # root level is 0
284
- def level
285
- parent_id.nil? ? 0 : ancestors.count
286
- 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
287
328
 
288
- # Returns a set of itself and all of its nested children
289
- def self_and_descendants
290
- nested_set_scope.where([
291
- "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right
292
- ])
293
- 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
294
335
 
295
- # Returns a set of all of its children and nested children
296
- def descendants
297
- without_self self_and_descendants
298
- end
336
+ # Returns a set of all of its children and nested children
337
+ def descendants
338
+ without_self self_and_descendants
339
+ end
299
340
 
300
- def is_descendant_of?(other)
301
- other.left < self.left && self.left < other.right && same_scope?(other)
302
- end
341
+ def is_descendant_of?(other)
342
+ other.left < self.left && self.left < other.right && same_scope?(other)
343
+ end
303
344
 
304
- def is_or_is_descendant_of?(other)
305
- other.left <= self.left && self.left < other.right && same_scope?(other)
306
- end
345
+ def is_or_is_descendant_of?(other)
346
+ other.left <= self.left && self.left < other.right && same_scope?(other)
347
+ end
307
348
 
308
- def is_ancestor_of?(other)
309
- self.left < other.left && other.left < self.right && same_scope?(other)
310
- end
349
+ def is_ancestor_of?(other)
350
+ self.left < other.left && other.left < self.right && same_scope?(other)
351
+ end
311
352
 
312
- def is_or_is_ancestor_of?(other)
313
- self.left <= other.left && other.left < self.right && same_scope?(other)
314
- end
353
+ def is_or_is_ancestor_of?(other)
354
+ self.left <= other.left && other.left < self.right && same_scope?(other)
355
+ end
315
356
 
316
- # Check if other model is in the same scope
317
- def same_scope?(other)
318
- Array(acts_as_nested_set_options[:scope]).all? do |attr|
319
- self.send(attr) == other.send(attr)
320
- 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)
321
361
  end
362
+ end
322
363
 
323
- # Find the first sibling to the left
324
- def left_sibling
325
- siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]).
326
- order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last
327
- 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
328
369
 
329
- # Find the first sibling to the right
330
- def right_sibling
331
- siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first
332
- 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
333
374
 
334
- # Shorthand method for finding the left sibling and moving to the left of it.
335
- def move_left
336
- move_to_left_of left_sibling
337
- 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
338
379
 
339
- # Shorthand method for finding the right sibling and moving to the right of it.
340
- def move_right
341
- move_to_right_of right_sibling
342
- 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
343
384
 
344
- # Move the node to the left of another node (you can pass id only)
345
- def move_to_left_of(node)
346
- move_to node, :left
347
- 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
348
389
 
349
- # Move the node to the left of another node (you can pass id only)
350
- def move_to_right_of(node)
351
- move_to node, :right
352
- 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
353
394
 
354
- # Move the node to the child of another node (you can pass id only)
355
- def move_to_child_of(node)
356
- move_to node, :child
357
- 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
358
399
 
359
- # Move the node to root nodes
360
- def move_to_root
361
- move_to nil, :root
362
- end
400
+ # Move the node to root nodes
401
+ def move_to_root
402
+ move_to nil, :root
403
+ end
363
404
 
364
- def move_possible?(target)
365
- self != target && # Can't target self
366
- same_scope?(target) && # can't be in different scopes
367
- # !(left..right).include?(target.left..target.right) # this needs tested more
368
- # detect impossible move
369
- !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
370
- 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
371
412
 
372
- def to_text
373
- self_and_descendants.map do |node|
374
- "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
375
- end.join("\n")
376
- 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
377
418
 
378
- protected
419
+ protected
379
420
 
380
- def without_self(scope)
381
- scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
382
- end
421
+ def without_self(scope)
422
+ scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
423
+ end
383
424
 
384
- # All nested set queries should use this nested_set_scope, which performs finds on
385
- # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
386
- # declaration.
387
- def nested_set_scope(options = {})
388
- options = {:order => quoted_left_column_name}.merge(options)
389
- scopes = Array(acts_as_nested_set_options[:scope])
390
- options[:conditions] = scopes.inject({}) do |conditions,attr|
391
- conditions.merge attr => self[attr]
392
- end unless scopes.empty?
393
- self.class.base_class.scoped options
394
- 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
395
436
 
396
- def store_new_parent
397
- @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
398
- true # force callback to return true
399
- end
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
400
441
 
401
- def move_to_new_parent
402
- if @move_to_new_parent_id.nil?
403
- move_to_root
404
- elsif @move_to_new_parent_id
405
- move_to_child_of(@move_to_new_parent_id)
406
- end
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)
407
447
  end
448
+ end
449
+
450
+ def set_depth!
451
+ in_tenacious_transaction do
452
+ reload
408
453
 
409
- # on creation, set automatically lft and rgt to the end of the tree
410
- def set_default_left_and_right
411
- highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").find(:first, :limit => 1,:lock => true )
412
- maxright = highest_right_row ? highest_right_row[right_column_name] : 0
413
- # adds the new node to the right of all existing nodes
414
- self[left_column_name] = maxright + 1
415
- self[right_column_name] = maxright + 2
454
+ nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
416
455
  end
456
+ self[:depth] = self.level
457
+ end
417
458
 
418
- # Prunes a branch off of the tree, shifting all of the elements on the right
419
- # back to the left so the counts still work.
420
- def destroy_descendants
421
- return if right.nil? || left.nil? || skip_before_destroy
459
+ # on creation, set automatically lft and rgt to the end of the tree
460
+ def set_default_left_and_right
461
+ highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").find(:first, :limit => 1,:lock => true )
462
+ maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
463
+ # adds the new node to the right of all existing nodes
464
+ self[left_column_name] = maxright + 1
465
+ self[right_column_name] = maxright + 2
466
+ end
422
467
 
423
- self.class.base_class.transaction do
424
- if acts_as_nested_set_options[:dependent] == :destroy
425
- descendants.each do |model|
426
- model.skip_before_destroy = true
427
- model.destroy
428
- end
429
- else
430
- nested_set_scope.delete_all(
431
- ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
432
- left, right]
433
- )
434
- end
468
+ def in_tenacious_transaction(&block)
469
+ retry_count = 0
470
+ begin
471
+ transaction(&block)
472
+ rescue ActiveRecord::StatementInvalid => error
473
+ raise unless connection.open_transactions.zero?
474
+ raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
475
+ raise unless retry_count < 10
476
+ retry_count += 1
477
+ logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
478
+ sleep(rand(retry_count)*0.1) # Aloha protocol
479
+ retry
480
+ end
481
+ end
435
482
 
436
- # update lefts and rights for remaining nodes
437
- diff = right - left + 1
438
- nested_set_scope.update_all(
439
- ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
440
- ["#{quoted_left_column_name} > ?", right]
441
- )
442
- nested_set_scope.update_all(
443
- ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
444
- ["#{quoted_right_column_name} > ?", right]
483
+ # Prunes a branch off of the tree, shifting all of the elements on the right
484
+ # back to the left so the counts still work.
485
+ def destroy_descendants
486
+ return if right.nil? || left.nil? || skip_before_destroy
487
+
488
+ in_tenacious_transaction do
489
+ reload_nested_set
490
+ # select the rows in the model that extend past the deletion point and apply a lock
491
+ self.class.base_class.find(:all,
492
+ :select => "id",
493
+ :conditions => ["#{quoted_left_column_name} >= ?", left],
494
+ :lock => true
495
+ )
496
+
497
+ if acts_as_nested_set_options[:dependent] == :destroy
498
+ descendants.each do |model|
499
+ model.skip_before_destroy = true
500
+ model.destroy
501
+ end
502
+ else
503
+ nested_set_scope.delete_all(
504
+ ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
505
+ left, right]
445
506
  )
446
-
447
- # Don't allow multiple calls to destroy to corrupt the set
448
- self.skip_before_destroy = true
449
507
  end
450
- end
451
508
 
452
- # reload left, right, and parent
453
- def reload_nested_set
454
- reload(:select => "#{quoted_left_column_name}, " +
455
- "#{quoted_right_column_name}, #{quoted_parent_column_name}")
509
+ # update lefts and rights for remaining nodes
510
+ diff = right - left + 1
511
+ nested_set_scope.update_all(
512
+ ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
513
+ ["#{quoted_left_column_name} > ?", right]
514
+ )
515
+ nested_set_scope.update_all(
516
+ ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
517
+ ["#{quoted_right_column_name} > ?", right]
518
+ )
519
+
520
+ # Don't allow multiple calls to destroy to corrupt the set
521
+ self.skip_before_destroy = true
456
522
  end
523
+ end
457
524
 
458
- def move_to(target, position)
459
- raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
460
- run_callbacks :move do
461
- transaction do
462
- if target.is_a? self.class.base_class
463
- target.reload_nested_set
464
- elsif position != :root
465
- # load object if node is not an object
466
- target = nested_set_scope.find(target)
467
- end
468
- self.reload_nested_set
525
+ # reload left, right, and parent
526
+ def reload_nested_set
527
+ reload(
528
+ :select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}",
529
+ :lock => true
530
+ )
531
+ end
469
532
 
470
- unless position == :root || move_possible?(target)
471
- raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
472
- end
533
+ def move_to(target, position)
534
+ raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
535
+ run_callbacks :move do
536
+ in_tenacious_transaction do
537
+ if target.is_a? self.class.base_class
538
+ target.reload_nested_set
539
+ elsif position != :root
540
+ # load object if node is not an object
541
+ target = nested_set_scope.find(target)
542
+ end
543
+ self.reload_nested_set
473
544
 
474
- bound = case position
475
- when :child; target[right_column_name]
476
- when :left; target[left_column_name]
477
- when :right; target[right_column_name] + 1
478
- when :root; 1
479
- else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
480
- end
545
+ unless position == :root || move_possible?(target)
546
+ raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
547
+ end
481
548
 
482
- if bound > self[right_column_name]
483
- bound = bound - 1
484
- other_bound = self[right_column_name] + 1
485
- else
486
- other_bound = self[left_column_name] - 1
487
- end
549
+ bound = case position
550
+ when :child; target[right_column_name]
551
+ when :left; target[left_column_name]
552
+ when :right; target[right_column_name] + 1
553
+ when :root; 1
554
+ else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
555
+ end
488
556
 
489
- # there would be no change
490
- return if bound == self[right_column_name] || bound == self[left_column_name]
557
+ if bound > self[right_column_name]
558
+ bound = bound - 1
559
+ other_bound = self[right_column_name] + 1
560
+ else
561
+ other_bound = self[left_column_name] - 1
562
+ end
491
563
 
492
- # we have defined the boundaries of two non-overlapping intervals,
493
- # so sorting puts both the intervals and their boundaries in order
494
- a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
564
+ # there would be no change
565
+ return if bound == self[right_column_name] || bound == self[left_column_name]
495
566
 
496
- # select the rows in the model between a and d, and apply a lock
497
- self.class.base_class.select('id').lock(true).where(
498
- ["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}]
499
- )
567
+ # we have defined the boundaries of two non-overlapping intervals,
568
+ # so sorting puts both the intervals and their boundaries in order
569
+ a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
500
570
 
501
- new_parent = case position
502
- when :child; target.id
503
- when :root; nil
504
- else target[parent_column_name]
505
- end
571
+ # select the rows in the model between a and d, and apply a lock
572
+ self.class.base_class.select('id').lock(true).where(
573
+ ["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}]
574
+ )
506
575
 
507
- self.nested_set_scope.update_all([
508
- "#{quoted_left_column_name} = CASE " +
509
- "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
510
- "THEN #{quoted_left_column_name} + :d - :b " +
511
- "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
512
- "THEN #{quoted_left_column_name} + :a - :c " +
513
- "ELSE #{quoted_left_column_name} END, " +
514
- "#{quoted_right_column_name} = CASE " +
515
- "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
516
- "THEN #{quoted_right_column_name} + :d - :b " +
517
- "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
518
- "THEN #{quoted_right_column_name} + :a - :c " +
519
- "ELSE #{quoted_right_column_name} END, " +
520
- "#{quoted_parent_column_name} = CASE " +
521
- "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
522
- "ELSE #{quoted_parent_column_name} END",
523
- {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
524
- ])
576
+ new_parent = case position
577
+ when :child; target.id
578
+ when :root; nil
579
+ else target[parent_column_name]
525
580
  end
526
- target.reload_nested_set if target
527
- self.reload_nested_set
581
+
582
+ self.nested_set_scope.update_all([
583
+ "#{quoted_left_column_name} = CASE " +
584
+ "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
585
+ "THEN #{quoted_left_column_name} + :d - :b " +
586
+ "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
587
+ "THEN #{quoted_left_column_name} + :a - :c " +
588
+ "ELSE #{quoted_left_column_name} END, " +
589
+ "#{quoted_right_column_name} = CASE " +
590
+ "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
591
+ "THEN #{quoted_right_column_name} + :d - :b " +
592
+ "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
593
+ "THEN #{quoted_right_column_name} + :a - :c " +
594
+ "ELSE #{quoted_right_column_name} END, " +
595
+ "#{quoted_parent_column_name} = CASE " +
596
+ "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
597
+ "ELSE #{quoted_parent_column_name} END",
598
+ {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
599
+ ])
528
600
  end
601
+ target.reload_nested_set if target
602
+ self.set_depth!
603
+ self.reload_nested_set
529
604
  end
530
-
531
605
  end
532
606
 
533
607
  end
@@ -542,6 +616,10 @@ module CollectiveIdea #:nodoc:
542
616
  acts_as_nested_set_options[:right_column]
543
617
  end
544
618
 
619
+ def depth_column_name
620
+ acts_as_nested_set_options[:depth_column]
621
+ end
622
+
545
623
  def parent_column_name
546
624
  acts_as_nested_set_options[:parent_column]
547
625
  end
@@ -558,6 +636,10 @@ module CollectiveIdea #:nodoc:
558
636
  connection.quote_column_name(right_column_name)
559
637
  end
560
638
 
639
+ def quoted_depth_column_name
640
+ connection.quote_column_name(depth_column_name)
641
+ end
642
+
561
643
  def quoted_parent_column_name
562
644
  connection.quote_column_name(parent_column_name)
563
645
  end
@@ -570,3 +652,4 @@ module CollectiveIdea #:nodoc:
570
652
  end
571
653
  end
572
654
  end
655
+
@@ -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.2' unless defined?(::AwesomeNestedSet::VERSION)
2
+ VERSION = '2.1.0' unless defined?(::AwesomeNestedSet::VERSION)
3
3
  end
metadata CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
5
5
  prerelease:
6
6
  segments:
7
7
  - 2
8
+ - 1
8
9
  - 0
9
- - 2
10
- version: 2.0.2
10
+ version: 2.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Brandon Keepers
@@ -17,7 +17,7 @@ autorequire:
17
17
  bindir: bin
18
18
  cert_chain: []
19
19
 
20
- date: 2011-09-06 00:00:00 Z
20
+ date: 2012-01-25 00:00:00 Z
21
21
  dependencies:
22
22
  - !ruby/object:Gem::Dependency
23
23
  name: activerecord