mongo_nested_set 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ begin
2
+ require 'jeweler'
3
+ rescue LoadError
4
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
5
+ exit 1
6
+ end
7
+ require 'rake/testtask'
8
+ require 'rake/rdoctask'
9
+ require 'rcov/rcovtask'
10
+ require "load_multi_rails_rake_tasks"
11
+
12
+ Jeweler::Tasks.new do |s|
13
+ s.name = "mongo_nested_set"
14
+ s.summary = "Port of awesome_nested_set for MongoMapper"
15
+ s.description = s.summary
16
+ s.email = "fauxparse@gmail.com"
17
+ s.homepage = "http://github.com/fauxparse/mongo_nested_set"
18
+ s.authors = ["Matt Powell", "Brandon Keepers", "Daniel Morrison"]
19
+ s.add_dependency "mongo_mapper", ['>= 0.6.10']
20
+ s.has_rdoc = true
21
+ s.extra_rdoc_files = [ "README.rdoc"]
22
+ s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
23
+ s.test_files = Dir['test/**/*.{yml,rb}']
24
+ end
25
+ Jeweler::GemcutterTasks.new
26
+
27
+ desc 'Default: run unit tests.'
28
+ task :default => :test
29
+
30
+ desc 'Test the mongo_nested_set plugin.'
31
+ Rake::TestTask.new(:test) do |t|
32
+ t.libs += ['lib', 'test']
33
+ t.pattern = 'test/**/*_test.rb'
34
+ t.verbose = true
35
+ end
36
+
37
+ desc 'Generate documentation for the awesome_nested_set plugin.'
38
+ Rake::RDocTask.new(:rdoc) do |rdoc|
39
+ rdoc.rdoc_dir = 'rdoc'
40
+ rdoc.title = 'MongoNestedSet'
41
+ rdoc.options << '--line-numbers' << '--inline-source'
42
+ rdoc.rdoc_files.include('README.rdoc')
43
+ rdoc.rdoc_files.include('lib/**/*.rb')
44
+ end
45
+
46
+ namespace :test do
47
+ desc "just rcov minus html output"
48
+ Rcov::RcovTask.new(:coverage) do |t|
49
+ t.libs << 'test'
50
+ t.test_files = FileList['test/**/*_test.rb']
51
+ t.output_dir = 'coverage'
52
+ t.verbose = true
53
+ t.rcov_opts = %w(--exclude test,/usr/lib/ruby,/Library/Ruby,lib/mongo_nested_set/named_scope.rb --sort coverage)
54
+ end
55
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + "/rails/init"
@@ -0,0 +1,606 @@
1
+ module MongoNestedSet
2
+ def self.included(base)
3
+ base.extend(SingletonMethods)
4
+ end
5
+
6
+ module SingletonMethods
7
+ def acts_as_nested_set(options = {})
8
+ options = {
9
+ :parent_column => 'parent_id',
10
+ :left_column => 'lft',
11
+ :right_column => 'rgt',
12
+ :dependent => :delete_all, # or :destroy
13
+ }.merge(options)
14
+
15
+ if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
16
+ options[:scope] = "#{options[:scope]}_id".intern
17
+ end
18
+
19
+ write_inheritable_attribute :acts_as_nested_set_options, options
20
+ class_inheritable_reader :acts_as_nested_set_options
21
+
22
+ unless self.is_a?(ClassMethods)
23
+ include Comparable
24
+ include Columns
25
+ include InstanceMethods
26
+ extend Columns
27
+ extend ClassMethods
28
+
29
+ belongs_to :parent, :class_name => self.base_class.to_s,
30
+ :foreign_key => parent_column_name
31
+ many :children, :class_name => self.base_class.to_s,
32
+ :foreign_key => parent_column_name, :order => quoted_left_column_name
33
+
34
+ attr_accessor :skip_before_destroy
35
+
36
+ key left_column_name.intern, Integer
37
+ key right_column_name.intern, Integer
38
+ key parent_column_name.intern, ObjectId
39
+
40
+ # no bulk assignment
41
+ # if accessible_attributes.blank?
42
+ # attr_protected left_column_name.intern, right_column_name.intern
43
+ # end
44
+
45
+ before_create :set_default_left_and_right
46
+ before_save :store_new_parent
47
+ after_save :move_to_new_parent
48
+ before_destroy :destroy_descendants
49
+
50
+ # no assignment to structure fields
51
+ # [left_column_name, right_column_name].each do |column|
52
+ # module_eval <<-"end_eval", __FILE__, __LINE__
53
+ # def #{column}=(x)
54
+ # raise "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
55
+ # end
56
+ # end_eval
57
+ # end
58
+
59
+ # named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
60
+ # named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
61
+
62
+ define_callbacks("before_move", "after_move")
63
+ end
64
+ end
65
+ end
66
+
67
+ module ClassMethods
68
+ def base_class
69
+ if superclass == Object
70
+ self
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+ # Returns the first root
77
+ def root
78
+ find :first, :parent_id => nil
79
+ end
80
+
81
+ def roots
82
+ find :all, :parent_id => nil, :order => "#{left_column_name} ASC"
83
+ end
84
+
85
+ def valid?
86
+ left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
87
+ end
88
+
89
+ def left_and_rights_valid?
90
+ find(:all).detect { |node|
91
+ node.send(left_column_name).nil? ||
92
+ node.send(right_column_name).nil? ||
93
+ node.send(left_column_name) >= node.send(right_column_name) ||
94
+ !node.parent.nil? && (
95
+ node.send(left_column_name) <= node.parent.send(left_column_name) ||
96
+ node.send(right_column_name) >= node.parent.send(right_column_name)
97
+ )
98
+ }.nil?
99
+ end
100
+
101
+ def no_duplicates_for_columns?
102
+ find(:all).inject(true) { |memo, node|
103
+ memo && [left_column_name, right_column_name].inject(true) { |v, column|
104
+ v && count(node.scoped(column.to_sym => node.send(column))) == 1
105
+ }
106
+ }
107
+ end
108
+
109
+ # Wrapper for each_root_valid? that can deal with scope.
110
+ def all_roots_valid?
111
+ if acts_as_nested_set_options[:scope]
112
+ roots.group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
113
+ each_root_valid?(grouped_roots)
114
+ end
115
+ else
116
+ each_root_valid?(roots)
117
+ end
118
+ end
119
+
120
+ def each_root_valid?(roots_to_validate)
121
+ left = right = 0
122
+ roots_to_validate.all? do |root|
123
+ returning(root.left > left && root.right > right) do
124
+ left = root.left
125
+ right = root.right
126
+ end
127
+ end
128
+ end
129
+
130
+ # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
131
+ def rebuild!
132
+ # Don't rebuild a valid tree.
133
+ return true if valid?
134
+
135
+ scope = lambda{ |node| {} }
136
+ if acts_as_nested_set_options[:scope]
137
+ scope = lambda { |node|
138
+ scope_column_names.inject({}) { |hash, column_name|
139
+ hash[column_name] = node.send(column_name.to_sym)
140
+ hash
141
+ }
142
+ }
143
+ end
144
+ indices = {}
145
+
146
+ set_left_and_rights = lambda do |node|
147
+ # set left
148
+ node.send(:"#{left_column_name}=", (indices[scope.call(node)] += 1))
149
+ # find
150
+ find(:all, scope.call(node).merge(parent_column_name => node.id)).each{|n| set_left_and_rights.call(n) }
151
+ # set right
152
+ node.send(:"#{right_column_name}=", (indices[scope.call(node)] += 1))
153
+ node.save!
154
+ end
155
+
156
+ # Find root node(s)
157
+ root_nodes = find(:all, { parent_column_name => nil, :order => "#{left_column_name}, #{right_column_name}, id" }).each do |root_node|
158
+ # setup index for this scope
159
+ indices[scope.call(root_node)] ||= 0
160
+ set_left_and_rights.call(root_node)
161
+ end
162
+ end
163
+
164
+ # Iterates over tree elements and determines the current level in the tree.
165
+ # Only accepts default ordering, odering by an other column than lft
166
+ # does not work. This method is much more efficent than calling level
167
+ # because it doesn't require any additional database queries.
168
+ #
169
+ # Example:
170
+ # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
171
+ #
172
+ def each_with_level(objects)
173
+ path = [nil]
174
+ objects.sort_by(&left_column_name.to_sym).each do |o|
175
+ if o._parent_id != path.last
176
+ # we are on a new level, did we decent or ascent?
177
+ if path.include?(o._parent_id)
178
+ # remove wrong wrong tailing paths elements
179
+ path.pop while path.last != o._parent_id
180
+ else
181
+ path << o._parent_id
182
+ end
183
+ end
184
+ yield(o, path.length - 1)
185
+ end
186
+ end
187
+ end
188
+
189
+ # Mixed into both classes and instances to provide easy access to the column names
190
+ module Columns
191
+ def left_column_name
192
+ acts_as_nested_set_options[:left_column]
193
+ end
194
+
195
+ def right_column_name
196
+ acts_as_nested_set_options[:right_column]
197
+ end
198
+
199
+ def parent_column_name
200
+ acts_as_nested_set_options[:parent_column]
201
+ end
202
+
203
+ def scope_column_names
204
+ Array(acts_as_nested_set_options[:scope])
205
+ end
206
+
207
+ def quoted_left_column_name
208
+ left_column_name
209
+ end
210
+
211
+ def quoted_right_column_name
212
+ right_column_name
213
+ end
214
+
215
+ def quoted_parent_column_name
216
+ parent_column_name
217
+ end
218
+
219
+ def quoted_scope_column_names
220
+ scope_column_names
221
+ end
222
+ end
223
+
224
+ module InstanceMethods
225
+ def base_class
226
+ self.class.base_class
227
+ end
228
+
229
+ # Value of the parent column
230
+ def _parent_id
231
+ send parent_column_name
232
+ end
233
+
234
+ # Value of the left column
235
+ def left
236
+ send left_column_name
237
+ end
238
+
239
+ # Value of the right column
240
+ def right
241
+ send right_column_name
242
+ end
243
+
244
+ # Returns true if this is a root node.
245
+ def root?
246
+ _parent_id.nil?
247
+ end
248
+
249
+ def leaf?
250
+ !new? && right - left == 1
251
+ end
252
+
253
+ # Returns true is this is a child node
254
+ def child?
255
+ !_parent_id.nil?
256
+ end
257
+
258
+ # order by left column
259
+ def <=>(x)
260
+ left <=> x.left
261
+ end
262
+
263
+ # Redefine to act like active record
264
+ def ==(comparison_object)
265
+ comparison_object.equal?(self) ||
266
+ (comparison_object.instance_of?(self.class) &&
267
+ comparison_object.id == id &&
268
+ !comparison_object.new?)
269
+ end
270
+
271
+ def scope_hash
272
+ Hash[*Array(acts_as_nested_set_options[:scope]).collect { |s| [s, send(s)] }.flatten]
273
+ end
274
+
275
+ def scoped(conditions = {})
276
+ conditions.reverse_merge(scope_hash)
277
+ end
278
+
279
+ # Returns root
280
+ def root
281
+ base_class.find :first, scoped(left_column_name => { '$lte' => left }, right_column_name => { '$gte' => right })
282
+ end
283
+
284
+ # Returns the array of all parents and self
285
+ def self_and_ancestors
286
+ base_class.find :all, scoped(left_column_name => { '$lte' => left }, right_column_name => { '$gte' => right })
287
+ end
288
+
289
+ # Returns an array of all parents
290
+ def ancestors
291
+ without_self self_and_ancestors
292
+ end
293
+
294
+ # Returns the array of all children of the parent, including self
295
+ def self_and_siblings
296
+ base_class.find :all, scoped(parent_column_name => _parent_id)
297
+ end
298
+
299
+ # Returns the array of all children of the parent, except self
300
+ def siblings
301
+ without_self self_and_siblings
302
+ end
303
+
304
+ # Returns a set of all of its nested children which do not have children
305
+ # def leaves
306
+ # descendants.scoped :conditions => "#{self.class.collection_name}.#{quoted_right_column_name} - #{self.class.collection_name}.#{quoted_left_column_name} = 1"
307
+ # end
308
+
309
+ # Returns the level of this object in the tree
310
+ # root level is 0
311
+ def level
312
+ _parent_id.nil? ? 0 : ancestors.count
313
+ end
314
+
315
+ # Returns a set of itself and all of its nested children
316
+ def self_and_descendants
317
+ base_class.find :all, scoped(left_column_name => { '$gte' => left }, right_column_name => { '$lte' => right })
318
+ end
319
+
320
+ # Returns a set of all of its children and nested children
321
+ def descendants
322
+ without_self self_and_descendants
323
+ end
324
+
325
+ def is_descendant_of?(other)
326
+ other.left < self.left && self.left < other.right && same_scope?(other)
327
+ end
328
+
329
+ def is_or_is_descendant_of?(other)
330
+ other.left <= self.left && self.left < other.right && same_scope?(other)
331
+ end
332
+
333
+ def is_ancestor_of?(other)
334
+ self.left < other.left && other.left < self.right && same_scope?(other)
335
+ end
336
+
337
+ def is_or_is_ancestor_of?(other)
338
+ self.left <= other.left && other.left < self.right && same_scope?(other)
339
+ end
340
+
341
+ # Check if other model is in the same scope
342
+ def same_scope?(other)
343
+ Array(acts_as_nested_set_options[:scope]).all? do |attr|
344
+ self.send(attr) == other.send(attr)
345
+ end
346
+ end
347
+
348
+ # Find the first sibling to the left
349
+ def left_sibling
350
+ base_class.find :first, scoped(parent_column_name => _parent_id, left_column_name => { '$lt' => left }, :order => "#{left_column_name} DESC")
351
+ end
352
+
353
+ # Find the first sibling to the right
354
+ def right_sibling
355
+ base_class.find :first, scoped(parent_column_name => _parent_id, left_column_name => { '$gt' => right }, :order => "#{left_column_name}")
356
+ end
357
+
358
+ # Shorthand method for finding the left sibling and moving to the left of it.
359
+ def move_left
360
+ move_to_left_of left_sibling
361
+ end
362
+
363
+ # Shorthand method for finding the right sibling and moving to the right of it.
364
+ def move_right
365
+ move_to_right_of right_sibling
366
+ end
367
+
368
+ # Move the node to the left of another node (you can pass id only)
369
+ def move_to_left_of(node)
370
+ move_to node, :left
371
+ end
372
+
373
+ # Move the node to the left of another node (you can pass id only)
374
+ def move_to_right_of(node)
375
+ move_to node, :right
376
+ end
377
+
378
+ # Move the node to the child of another node (you can pass id only)
379
+ def move_to_child_of(node)
380
+ move_to node, :child
381
+ end
382
+
383
+ # Move the node to root nodes
384
+ def move_to_root
385
+ move_to nil, :root
386
+ end
387
+
388
+ def move_possible?(target)
389
+ self != target && # Can't target self
390
+ same_scope?(target) && # can't be in different scopes
391
+ # !(left..right).include?(target.left..target.right) # this needs tested more
392
+ # detect impossible move
393
+ !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
394
+ end
395
+
396
+ def to_text
397
+ self_and_descendants.map do |node|
398
+ "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node._parent_id}, #{node.left}, #{node.right})"
399
+ end.join("\n")
400
+ end
401
+
402
+ protected
403
+ def without_self(set)
404
+ set.reject { |node| node.id == id }
405
+ end
406
+
407
+ # All nested set queries should use this nested_set_scope, which performs finds on
408
+ # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
409
+ # declaration.
410
+ def nested_set_scope
411
+ raise "called nested_set_scope"
412
+ options = {:order => quoted_left_column_name}
413
+ scopes = Array(acts_as_nested_set_options[:scope])
414
+ options[:conditions] = scopes.inject({}) do |conditions,attr|
415
+ conditions.merge attr => self[attr]
416
+ end unless scopes.empty?
417
+ self.class.base_class.scoped options
418
+ end
419
+
420
+ def store_new_parent
421
+ unless @skip_nested_set_callbacks
422
+ @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? _parent_id : false
423
+ end
424
+ true # force callback to return true
425
+ end
426
+
427
+ def move_to_new_parent
428
+ unless @skip_nested_set_callbacks
429
+ if @move_to_new_parent_id.nil?
430
+ move_to_root
431
+ elsif @move_to_new_parent_id
432
+ move_to_child_of(@move_to_new_parent_id)
433
+ end
434
+ end
435
+ end
436
+
437
+ # on creation, set automatically lft and rgt to the end of the tree
438
+ def set_default_left_and_right
439
+ unless @skip_nested_set_callbacks
440
+ maxright = base_class.find(:first, scoped(:order => "#{right_column_name} DESC")).try(right_column_name) || 0
441
+ # adds the new node to the right of all existing nodes
442
+ self[left_column_name] = maxright + 1
443
+ self[right_column_name] = maxright + 2
444
+ end
445
+ end
446
+
447
+ # Prunes a branch off of the tree, shifting all of the elements on the right
448
+ # back to the left so the counts still work.
449
+ def destroy_descendants
450
+ return if right.nil? || left.nil? || skip_before_destroy
451
+
452
+ if acts_as_nested_set_options[:dependent] == :destroy
453
+ descendants.each do |model|
454
+ model.skip_before_destroy = true
455
+ model.destroy
456
+ end
457
+ else
458
+ base_class.delete_all scoped(left_column_name => { '$gt' => left }, right_column_name => { '$lt' => right })
459
+ end
460
+
461
+ # update lefts and rights for remaining nodes
462
+ diff = right - left + 1
463
+ base_class.find(:all, scoped(left_column_name => { '$gt' => right })).each do |node|
464
+ node.update_attributes left_column_name => node.left - diff
465
+ end
466
+ base_class.find(:all, scoped(right_column_name => { '$gt' => right })).each do |node|
467
+ node.update_attributes right_column_name => node.right - diff
468
+ end
469
+
470
+ # Don't allow multiple calls to destroy to corrupt the set
471
+ self.skip_before_destroy = true
472
+ end
473
+
474
+ # reload left, right, and parent
475
+ def reload_nested_set
476
+ doc = self.class.find(_id)
477
+ self.class.associations.each { |name, assoc| send(name).reset if respond_to?(name) }
478
+ [ left_column_name, right_column_name, parent_column_name ].each do |column|
479
+ send :"#{column}=", doc.send(column.to_sym)
480
+ end
481
+ self
482
+ end
483
+
484
+ def move_to(target, position)
485
+ raise ArgumentError, "You cannot move a new node" if self.new_record?
486
+ return if run_callbacks(:before_move) == false
487
+
488
+ if target.is_a? base_class
489
+ target.reload_nested_set
490
+ elsif position != :root
491
+ # load object if node is not an object
492
+ target = base_class.find(target, scoped)
493
+ end
494
+ self.reload_nested_set
495
+
496
+ unless position == :root || move_possible?(target)
497
+ raise ArgumentError, "Impossible move, target node cannot be inside moved tree."
498
+ end
499
+
500
+ bound = case position
501
+ when :child; target.send(right_column_name)
502
+ when :left; target.send(left_column_name)
503
+ when :right; target.send(right_column_name) + 1
504
+ when :root; 1
505
+ else raise ArgumentError, "Position should be :child, :left, :right or :root ('#{position}' received)."
506
+ end
507
+
508
+ if bound > self.send(right_column_name)
509
+ bound = bound - 1
510
+ other_bound = self.send(right_column_name) + 1
511
+ else
512
+ other_bound = self.send(left_column_name) - 1
513
+ end
514
+
515
+ # there would be no change
516
+ return if bound == self.send(right_column_name) || bound == self.send(left_column_name)
517
+
518
+ # we have defined the boundaries of two non-overlapping intervals,
519
+ # so sorting puts both the intervals and their boundaries in order
520
+ a, b, c, d = [self.send(left_column_name), self.send(right_column_name), bound, other_bound].sort
521
+
522
+ new_parent = case position
523
+ when :child; target.id
524
+ when :root; nil
525
+ else target.send(parent_column_name)
526
+ end
527
+
528
+ # base_class.collection.update({
529
+ # left_column_name => { '$gte' => a },
530
+ # left_column_name => { '$lte' => b }
531
+ # }, {
532
+ # '$inc' => { left_column_name => d - b }
533
+ # }, :multi => true)
534
+ # base_class.collection.update({
535
+ # left_column_name => { '$gte' => c },
536
+ # left_column_name => { '$lte' => d }
537
+ # }, {
538
+ # '$inc' => { left_column_name => a - c }
539
+ # }, :multi => true)
540
+ # base_class.collection.update({
541
+ # right_column_name => { '$gte' => a },
542
+ # right_column_name => { '$lte' => b }
543
+ # }, {
544
+ # '$inc' => { right_column_name => d - b }
545
+ # }, :multi => true)
546
+ # base_class.collection.update({
547
+ # right_column_name => { '$gte' => c },
548
+ # right_column_name => { '$lte' => d }
549
+ # }, {
550
+ # '$inc' => { right_column_name => a - c }
551
+ # }, :multi => true)
552
+ # base_class.collection.update({
553
+ # :_id => self.id
554
+ # }, {
555
+ # parent_column_name => new_parent
556
+ # })
557
+
558
+ to_update = {}
559
+
560
+ base_class.find(:all, scoped).each do |node|
561
+ to_update_this_node = {}
562
+ if (a..b).include? node.left
563
+ node.send :"#{left_column_name}=", node.left + d - b
564
+ elsif (c..d).include? node.left
565
+ node.send :"#{left_column_name}=", node.left + a - c
566
+ end
567
+ if (a..b).include? node.right
568
+ node.send :"#{right_column_name}=", node.right + d - b
569
+ elsif (c..d).include? node.right
570
+ node.send :"#{right_column_name}=", node.right + a - c
571
+ end
572
+ node.send :"#{parent_column_name}=", new_parent if self.id == node.id
573
+ node.save_without_nested_set_callbacks if node.changed?
574
+ end
575
+
576
+ # self.class.base_class.update_all([
577
+ # "#{quoted_left_column_name} = CASE " +
578
+ # "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
579
+ # "THEN #{quoted_left_column_name} + :d - :b " +
580
+ # "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
581
+ # "THEN #{quoted_left_column_name} + :a - :c " +
582
+ # "ELSE #{quoted_left_column_name} END, " +
583
+ # "#{quoted_right_column_name} = CASE " +
584
+ # "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
585
+ # "THEN #{quoted_right_column_name} + :d - :b " +
586
+ # "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
587
+ # "THEN #{quoted_right_column_name} + :a - :c " +
588
+ # "ELSE #{quoted_right_column_name} END, " +
589
+ # "#{quoted_parent_column_name} = CASE " +
590
+ # "WHEN id = :id THEN :new_parent " +
591
+ # "ELSE #{quoted_parent_column_name} END",
592
+ # {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
593
+ # ], nested_set_scope.proxy_options[:conditions])
594
+
595
+ target.reload_nested_set if target
596
+ self.reload_nested_set
597
+ run_callbacks(:after_move)
598
+ end
599
+
600
+ def save_without_nested_set_callbacks
601
+ @skip_nested_set_callbacks = true
602
+ save!
603
+ @skip_nested_set_callbacks = false
604
+ end
605
+ end
606
+ end