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.
- data/README.rdoc +17 -1
- data/lib/awesome_nested_set/awesome_nested_set.rb +356 -273
- data/lib/awesome_nested_set/helper.rb +53 -4
- data/lib/awesome_nested_set/version.rb +1 -1
- metadata +3 -3
data/README.rdoc
CHANGED
@@ -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
|
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 =>
|
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
|
-
|
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.
|
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
|
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
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
end
|
261
|
+
# Value of the parent column
|
262
|
+
def parent_id
|
263
|
+
self[parent_column_name]
|
264
|
+
end
|
225
265
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
266
|
+
# Value of the left column
|
267
|
+
def left
|
268
|
+
self[left_column_name]
|
269
|
+
end
|
230
270
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
271
|
+
# Value of the right column
|
272
|
+
def right
|
273
|
+
self[right_column_name]
|
274
|
+
end
|
235
275
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
276
|
+
# Returns true if this is a root node.
|
277
|
+
def root?
|
278
|
+
parent_id.nil?
|
279
|
+
end
|
240
280
|
|
241
|
-
|
242
|
-
|
243
|
-
|
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
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
286
|
+
# Returns true is this is a child node
|
287
|
+
def child?
|
288
|
+
!parent_id.nil?
|
289
|
+
end
|
249
290
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
291
|
+
# Returns root
|
292
|
+
def root
|
293
|
+
self_and_ancestors.where(parent_column_name => nil).first
|
294
|
+
end
|
254
295
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
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
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
303
|
+
# Returns an array of all parents
|
304
|
+
def ancestors
|
305
|
+
without_self self_and_ancestors
|
306
|
+
end
|
266
307
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
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
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
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
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
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
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
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
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
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
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
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
|
-
|
301
|
-
|
302
|
-
|
341
|
+
def is_descendant_of?(other)
|
342
|
+
other.left < self.left && self.left < other.right && same_scope?(other)
|
343
|
+
end
|
303
344
|
|
304
|
-
|
305
|
-
|
306
|
-
|
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
|
-
|
309
|
-
|
310
|
-
|
349
|
+
def is_ancestor_of?(other)
|
350
|
+
self.left < other.left && other.left < self.right && same_scope?(other)
|
351
|
+
end
|
311
352
|
|
312
|
-
|
313
|
-
|
314
|
-
|
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
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
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
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
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
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
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
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
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
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
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
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
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
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
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
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
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
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
400
|
+
# Move the node to root nodes
|
401
|
+
def move_to_root
|
402
|
+
move_to nil, :root
|
403
|
+
end
|
363
404
|
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
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
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
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
|
-
|
419
|
+
protected
|
379
420
|
|
380
|
-
|
381
|
-
|
382
|
-
|
421
|
+
def without_self(scope)
|
422
|
+
scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
|
423
|
+
end
|
383
424
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
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
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
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
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
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
|
-
|
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
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
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
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
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
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
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
|
-
|
453
|
-
|
454
|
-
|
455
|
-
"#{
|
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
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
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
|
-
|
471
|
-
|
472
|
-
|
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
|
-
|
475
|
-
|
476
|
-
|
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
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
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
|
-
|
490
|
-
|
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
|
-
|
493
|
-
|
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
|
-
|
497
|
-
|
498
|
-
|
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
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
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
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
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
|
-
|
527
|
-
self.
|
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
|
-
|
25
|
-
|
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
|
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
|
-
|
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:
|
20
|
+
date: 2012-01-25 00:00:00 Z
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|
23
23
|
name: activerecord
|