awesome_nested_set 2.0.1 → 2.1.2

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,33 +54,42 @@ 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
59
62
  extend Columns
60
63
 
61
64
  belongs_to :parent, :class_name => self.base_class.to_s,
62
- :foreign_key => parent_column_name,
63
- :counter_cache => options[:counter_cache]
64
- has_many :children, :class_name => self.base_class.to_s,
65
- :foreign_key => parent_column_name, :order => quoted_left_column_name
65
+ :foreign_key => parent_column_name,
66
+ :counter_cache => options[:counter_cache],
67
+ :inverse_of => (:children unless options[:polymorphic]),
68
+ :polymorphic => options[:polymorphic]
66
69
 
67
- attr_accessor :skip_before_destroy
70
+ has_many_children_options = {
71
+ :class_name => self.base_class.to_s,
72
+ :foreign_key => parent_column_name,
73
+ :order => left_column_name,
74
+ :inverse_of => (:parent unless options[:polymorphic]),
75
+ }
68
76
 
69
- # no bulk assignment
70
- if accessible_attributes.blank?
71
- attr_protected left_column_name.intern, right_column_name.intern
77
+ # Add callbacks, if they were supplied.. otherwise, we don't want them.
78
+ [:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
79
+ has_many_children_options.update(ar_callback => options[ar_callback]) if options[ar_callback]
72
80
  end
73
81
 
82
+ has_many :children, has_many_children_options
83
+
84
+ attr_accessor :skip_before_destroy
85
+
74
86
  before_create :set_default_left_and_right
75
87
  before_save :store_new_parent
76
- after_save :move_to_new_parent
88
+ after_save :move_to_new_parent, :set_depth!
77
89
  before_destroy :destroy_descendants
78
90
 
79
91
  # no assignment to structure fields
80
- [left_column_name, right_column_name].each do |column|
92
+ [left_column_name, right_column_name, depth_column_name].each do |column|
81
93
  module_eval <<-"end_eval", __FILE__, __LINE__
82
94
  def #{column}=(x)
83
95
  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 +97,7 @@ module CollectiveIdea #:nodoc:
85
97
  end_eval
86
98
  end
87
99
 
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"
100
+ define_model_callbacks :move
92
101
  end
93
102
 
94
103
  module Model
@@ -100,12 +109,23 @@ module CollectiveIdea #:nodoc:
100
109
  roots.first
101
110
  end
102
111
 
112
+ def roots
113
+ where(parent_column_name => nil).order(quoted_left_column_name)
114
+ end
115
+
116
+ def leaves
117
+ where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
118
+ end
119
+
103
120
  def valid?
104
121
  left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
105
122
  end
106
123
 
107
124
  def left_and_rights_valid?
108
- joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
125
+ ## AS clause not supported in Oracle in FROM clause for aliasing table name
126
+ joins("LEFT OUTER JOIN #{quoted_table_name}" +
127
+ (connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
128
+ "parent ON " +
109
129
  "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}").
110
130
  where(
111
131
  "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
@@ -134,7 +154,7 @@ module CollectiveIdea #:nodoc:
134
154
  # Wrapper for each_root_valid? that can deal with scope.
135
155
  def all_roots_valid?
136
156
  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|
157
+ roots.group_by {|record| scope_column_names.collect {|col| record.send(col.to_sym) } }.all? do |scope, grouped_roots|
138
158
  each_root_valid?(grouped_roots)
139
159
  end
140
160
  else
@@ -198,7 +218,7 @@ module CollectiveIdea #:nodoc:
198
218
  path = [nil]
199
219
  objects.each do |o|
200
220
  if o.parent_id != path.last
201
- # we are on a new level, did we decent or ascent?
221
+ # we are on a new level, did we descend or ascend?
202
222
  if path.include?(o.parent_id)
203
223
  # remove wrong wrong tailing paths elements
204
224
  path.pop while path.last != o.parent_id
@@ -209,317 +229,388 @@ module CollectiveIdea #:nodoc:
209
229
  yield(o, path.length - 1)
210
230
  end
211
231
  end
232
+
233
+ # Same as each_with_level - Accepts a string as a second argument to sort the list
234
+ # Example:
235
+ # Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
236
+ def sorted_each_with_level(objects, order)
237
+ path = [nil]
238
+ children = []
239
+ objects.each do |o|
240
+ children << o if o.leaf?
241
+ if o.parent_id != path.last
242
+ if !children.empty? && !o.leaf?
243
+ children.sort_by! &order
244
+ children.each { |c| yield(c, path.length-1) }
245
+ children = []
246
+ end
247
+ # we are on a new level, did we decent or ascent?
248
+ if path.include?(o.parent_id)
249
+ # remove wrong wrong tailing paths elements
250
+ path.pop while path.last != o.parent_id
251
+ else
252
+ path << o.parent_id
253
+ end
254
+ end
255
+ yield(o,path.length-1) if !o.leaf?
256
+ end
257
+ if !children.empty?
258
+ children.sort_by! &order
259
+ children.each { |c| yield(c, path.length-1) }
260
+ end
261
+ end
212
262
  end
213
263
 
214
264
  # 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
265
  #
216
266
  # category.self_and_descendants.count
217
267
  # 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
268
+ # Value of the parent column
269
+ def parent_id
270
+ self[parent_column_name]
271
+ end
223
272
 
224
- # Value of the left column
225
- def left
226
- self[left_column_name]
227
- end
273
+ # Value of the left column
274
+ def left
275
+ self[left_column_name]
276
+ end
228
277
 
229
- # Value of the right column
230
- def right
231
- self[right_column_name]
232
- end
278
+ # Value of the right column
279
+ def right
280
+ self[right_column_name]
281
+ end
233
282
 
234
- # Returns true if this is a root node.
235
- def root?
236
- parent_id.nil?
237
- end
283
+ # Returns true if this is a root node.
284
+ def root?
285
+ parent_id.nil?
286
+ end
238
287
 
239
- def leaf?
240
- !new_record? && right - left == 1
241
- end
288
+ # Returns true if this is the end of a branch.
289
+ def leaf?
290
+ persisted? && right.to_i - left.to_i == 1
291
+ end
242
292
 
243
- # Returns true is this is a child node
244
- def child?
245
- !parent_id.nil?
246
- end
293
+ # Returns true is this is a child node
294
+ def child?
295
+ !parent_id.nil?
296
+ end
247
297
 
248
- # Returns root
249
- def root
250
- self_and_ancestors.where(parent_column_name => nil).first
251
- end
298
+ # Returns root
299
+ def root
300
+ self_and_ancestors.where(parent_column_name => nil).first
301
+ end
252
302
 
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
303
+ # Returns the array of all parents and self
304
+ def self_and_ancestors
305
+ nested_set_scope.where([
306
+ "#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right
307
+ ])
308
+ end
259
309
 
260
- # Returns an array of all parents
261
- def ancestors
262
- without_self self_and_ancestors
263
- end
310
+ # Returns an array of all parents
311
+ def ancestors
312
+ without_self self_and_ancestors
313
+ end
264
314
 
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
315
+ # Returns the array of all children of the parent, including self
316
+ def self_and_siblings
317
+ nested_set_scope.where(parent_column_name => parent_id)
318
+ end
269
319
 
270
- # Returns the array of all children of the parent, except self
271
- def siblings
272
- without_self self_and_siblings
273
- end
320
+ # Returns the array of all children of the parent, except self
321
+ def siblings
322
+ without_self self_and_siblings
323
+ end
274
324
 
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
325
+ # Returns a set of all of its nested children which do not have children
326
+ def leaves
327
+ descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
328
+ end
279
329
 
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
330
+ # Returns the level of this object in the tree
331
+ # root level is 0
332
+ def level
333
+ parent_id.nil? ? 0 : ancestors.count
334
+ end
285
335
 
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
336
+ # Returns a set of itself and all of its nested children
337
+ def self_and_descendants
338
+ nested_set_scope.where([
339
+ "#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right
340
+ ])
341
+ end
292
342
 
293
- # Returns a set of all of its children and nested children
294
- def descendants
295
- without_self self_and_descendants
296
- end
343
+ # Returns a set of all of its children and nested children
344
+ def descendants
345
+ without_self self_and_descendants
346
+ end
297
347
 
298
- def is_descendant_of?(other)
299
- other.left < self.left && self.left < other.right && same_scope?(other)
300
- end
348
+ def is_descendant_of?(other)
349
+ other.left < self.left && self.left < other.right && same_scope?(other)
350
+ end
301
351
 
302
- def is_or_is_descendant_of?(other)
303
- other.left <= self.left && self.left < other.right && same_scope?(other)
304
- end
352
+ def is_or_is_descendant_of?(other)
353
+ other.left <= self.left && self.left < other.right && same_scope?(other)
354
+ end
305
355
 
306
- def is_ancestor_of?(other)
307
- self.left < other.left && other.left < self.right && same_scope?(other)
308
- end
356
+ def is_ancestor_of?(other)
357
+ self.left < other.left && other.left < self.right && same_scope?(other)
358
+ end
309
359
 
310
- def is_or_is_ancestor_of?(other)
311
- self.left <= other.left && other.left < self.right && same_scope?(other)
312
- end
360
+ def is_or_is_ancestor_of?(other)
361
+ self.left <= other.left && other.left < self.right && same_scope?(other)
362
+ end
313
363
 
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
364
+ # Check if other model is in the same scope
365
+ def same_scope?(other)
366
+ Array(acts_as_nested_set_options[:scope]).all? do |attr|
367
+ self.send(attr) == other.send(attr)
319
368
  end
369
+ end
320
370
 
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
371
+ # Find the first sibling to the left
372
+ def left_sibling
373
+ siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]).
374
+ order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last
375
+ end
326
376
 
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
377
+ # Find the first sibling to the right
378
+ def right_sibling
379
+ siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first
380
+ end
331
381
 
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
382
+ # Shorthand method for finding the left sibling and moving to the left of it.
383
+ def move_left
384
+ move_to_left_of left_sibling
385
+ end
336
386
 
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
387
+ # Shorthand method for finding the right sibling and moving to the right of it.
388
+ def move_right
389
+ move_to_right_of right_sibling
390
+ end
341
391
 
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
392
+ # Move the node to the left of another node (you can pass id only)
393
+ def move_to_left_of(node)
394
+ move_to node, :left
395
+ end
346
396
 
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
397
+ # Move the node to the left of another node (you can pass id only)
398
+ def move_to_right_of(node)
399
+ move_to node, :right
400
+ end
351
401
 
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
402
+ # Move the node to the child of another node (you can pass id only)
403
+ def move_to_child_of(node)
404
+ move_to node, :child
405
+ end
356
406
 
357
- # Move the node to root nodes
358
- def move_to_root
359
- move_to nil, :root
360
- end
407
+ # Move the node to root nodes
408
+ def move_to_root
409
+ move_to nil, :root
410
+ end
361
411
 
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
412
+ def move_possible?(target)
413
+ self != target && # Can't target self
414
+ same_scope?(target) && # can't be in different scopes
415
+ # !(left..right).include?(target.left..target.right) # this needs tested more
416
+ # detect impossible move
417
+ !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
418
+ end
369
419
 
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
420
+ def to_text
421
+ self_and_descendants.map do |node|
422
+ "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
423
+ end.join("\n")
424
+ end
375
425
 
376
- protected
426
+ protected
377
427
 
378
- def without_self(scope)
379
- scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
380
- end
428
+ def without_self(scope)
429
+ scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
430
+ end
381
431
 
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
432
+ # All nested set queries should use this nested_set_scope, which performs finds on
433
+ # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
434
+ # declaration.
435
+ def nested_set_scope(options = {})
436
+ options = {:order => quoted_left_column_name}.merge(options)
437
+ scopes = Array(acts_as_nested_set_options[:scope])
438
+ options[:conditions] = scopes.inject({}) do |conditions,attr|
439
+ conditions.merge attr => self[attr]
440
+ end unless scopes.empty?
441
+ self.class.base_class.unscoped.scoped options
442
+ end
443
+
444
+ def store_new_parent
445
+ @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
446
+ true # force callback to return true
447
+ end
393
448
 
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
449
+ def move_to_new_parent
450
+ if @move_to_new_parent_id.nil?
451
+ move_to_root
452
+ elsif @move_to_new_parent_id
453
+ move_to_child_of(@move_to_new_parent_id)
397
454
  end
455
+ end
398
456
 
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)
457
+ def set_depth!
458
+ if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
459
+ in_tenacious_transaction do
460
+ reload
461
+
462
+ nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
404
463
  end
464
+ self[depth_column_name.to_sym] = self.level
405
465
  end
466
+ end
406
467
 
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
468
+ # on creation, set automatically lft and rgt to the end of the tree
469
+ def set_default_left_and_right
470
+ highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").find(:first, :limit => 1,:lock => true )
471
+ maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
472
+ # adds the new node to the right of all existing nodes
473
+ self[left_column_name] = maxright + 1
474
+ self[right_column_name] = maxright + 2
475
+ end
414
476
 
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
477
+ def in_tenacious_transaction(&block)
478
+ retry_count = 0
479
+ begin
480
+ transaction(&block)
481
+ rescue ActiveRecord::StatementInvalid => error
482
+ raise unless connection.open_transactions.zero?
483
+ raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
484
+ raise unless retry_count < 10
485
+ retry_count += 1
486
+ logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
487
+ sleep(rand(retry_count)*0.1) # Aloha protocol
488
+ retry
489
+ end
490
+ end
419
491
 
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
- )
492
+ # Prunes a branch off of the tree, shifting all of the elements on the right
493
+ # back to the left so the counts still work.
494
+ def destroy_descendants
495
+ return if right.nil? || left.nil? || skip_before_destroy
496
+
497
+ in_tenacious_transaction do
498
+ reload_nested_set
499
+ # select the rows in the model that extend past the deletion point and apply a lock
500
+ self.class.base_class.find(:all,
501
+ :select => "id",
502
+ :conditions => ["#{quoted_left_column_name} >= ?", left],
503
+ :lock => true
504
+ )
505
+
506
+ if acts_as_nested_set_options[:dependent] == :destroy
507
+ descendants.each do |model|
508
+ model.skip_before_destroy = true
509
+ model.destroy
431
510
  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]
511
+ else
512
+ nested_set_scope.delete_all(
513
+ ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
514
+ left, right]
442
515
  )
443
-
444
- # Don't allow multiple calls to destroy to corrupt the set
445
- self.skip_before_destroy = true
446
516
  end
447
- end
448
517
 
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}")
518
+ # update lefts and rights for remaining nodes
519
+ diff = right - left + 1
520
+ nested_set_scope.update_all(
521
+ ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
522
+ ["#{quoted_left_column_name} > ?", right]
523
+ )
524
+ nested_set_scope.update_all(
525
+ ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
526
+ ["#{quoted_right_column_name} > ?", right]
527
+ )
528
+
529
+ # Don't allow multiple calls to destroy to corrupt the set
530
+ self.skip_before_destroy = true
453
531
  end
532
+ end
454
533
 
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
534
+ # reload left, right, and parent
535
+ def reload_nested_set
536
+ reload(
537
+ :select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}",
538
+ :lock => true
539
+ )
540
+ end
466
541
 
467
- unless position == :root || move_possible?(target)
468
- raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
469
- end
542
+ def move_to(target, position)
543
+ raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
544
+ run_callbacks :move do
545
+ in_tenacious_transaction do
546
+ if target.is_a? self.class.base_class
547
+ target.reload_nested_set
548
+ elsif position != :root
549
+ # load object if node is not an object
550
+ target = nested_set_scope.find(target)
551
+ end
552
+ self.reload_nested_set
470
553
 
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
554
+ unless position == :root || move_possible?(target)
555
+ raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
556
+ end
478
557
 
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
558
+ bound = case position
559
+ when :child; target[right_column_name]
560
+ when :left; target[left_column_name]
561
+ when :right; target[right_column_name] + 1
562
+ when :root; 1
563
+ else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
564
+ end
485
565
 
486
- # there would be no change
487
- return if bound == self[right_column_name] || bound == self[left_column_name]
566
+ if bound > self[right_column_name]
567
+ bound = bound - 1
568
+ other_bound = self[right_column_name] + 1
569
+ else
570
+ other_bound = self[left_column_name] - 1
571
+ end
488
572
 
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
573
+ # there would be no change
574
+ return if bound == self[right_column_name] || bound == self[left_column_name]
492
575
 
493
- new_parent = case position
494
- when :child; target.id
495
- when :root; nil
496
- else target[parent_column_name]
497
- end
576
+ # we have defined the boundaries of two non-overlapping intervals,
577
+ # so sorting puts both the intervals and their boundaries in order
578
+ a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
579
+
580
+ # select the rows in the model between a and d, and apply a lock
581
+ self.class.base_class.select('id').lock(true).where(
582
+ ["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}]
583
+ )
498
584
 
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
- ])
585
+ new_parent = case position
586
+ when :child; target.id
587
+ when :root; nil
588
+ else target[parent_column_name]
517
589
  end
518
- target.reload_nested_set if target
519
- self.reload_nested_set
590
+
591
+ self.nested_set_scope.update_all([
592
+ "#{quoted_left_column_name} = CASE " +
593
+ "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
594
+ "THEN #{quoted_left_column_name} + :d - :b " +
595
+ "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
596
+ "THEN #{quoted_left_column_name} + :a - :c " +
597
+ "ELSE #{quoted_left_column_name} END, " +
598
+ "#{quoted_right_column_name} = CASE " +
599
+ "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
600
+ "THEN #{quoted_right_column_name} + :d - :b " +
601
+ "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
602
+ "THEN #{quoted_right_column_name} + :a - :c " +
603
+ "ELSE #{quoted_right_column_name} END, " +
604
+ "#{quoted_parent_column_name} = CASE " +
605
+ "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
606
+ "ELSE #{quoted_parent_column_name} END",
607
+ {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
608
+ ])
520
609
  end
610
+ target.reload_nested_set if target
611
+ self.set_depth!
612
+ self.reload_nested_set
521
613
  end
522
-
523
614
  end
524
615
 
525
616
  end
@@ -534,6 +625,10 @@ module CollectiveIdea #:nodoc:
534
625
  acts_as_nested_set_options[:right_column]
535
626
  end
536
627
 
628
+ def depth_column_name
629
+ acts_as_nested_set_options[:depth_column]
630
+ end
631
+
537
632
  def parent_column_name
538
633
  acts_as_nested_set_options[:parent_column]
539
634
  end
@@ -550,6 +645,10 @@ module CollectiveIdea #:nodoc:
550
645
  connection.quote_column_name(right_column_name)
551
646
  end
552
647
 
648
+ def quoted_depth_column_name
649
+ connection.quote_column_name(depth_column_name)
650
+ end
651
+
553
652
  def quoted_parent_column_name
554
653
  connection.quote_column_name(parent_column_name)
555
654
  end
@@ -561,4 +660,5 @@ module CollectiveIdea #:nodoc:
561
660
 
562
661
  end
563
662
  end
564
- end
663
+ end
664
+
@@ -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.2' 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: 15
4
5
  prerelease:
5
- version: 2.0.1
6
+ segments:
7
+ - 2
8
+ - 1
9
+ - 2
10
+ version: 2.1.2
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-26 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