awesome_nested_set 2.0.2 → 2.1.0

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.
@@ -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