awesome_nested_set 2.1.6 → 3.0.0.rc.6

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.
@@ -1,3 +1,6 @@
1
+ require 'awesome_nested_set/columns'
2
+ require 'awesome_nested_set/model'
3
+
1
4
  module CollectiveIdea #:nodoc:
2
5
  module Acts #:nodoc:
3
6
  module NestedSet #:nodoc:
@@ -21,6 +24,7 @@ module CollectiveIdea #:nodoc:
21
24
  # Configuration options are:
22
25
  #
23
26
  # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
27
+ # * +:primary_column+ - specifies the column name to use as the inverse of the parent column (default: id)
24
28
  # * +:left_column+ - column name for left boundry data, default "lft"
25
29
  # * +:right_column+ - column name for right boundry data, default "rgt"
26
30
  # * +:depth_column+ - column name for the depth data, default "depth"
@@ -42,726 +46,96 @@ module CollectiveIdea #:nodoc:
42
46
  # CollectiveIdea::Acts::NestedSet::Model for a list of instance methods added
43
47
  # to acts_as_nested_set models
44
48
  def acts_as_nested_set(options = {})
45
- options = {
46
- :parent_column => 'parent_id',
47
- :left_column => 'lft',
48
- :right_column => 'rgt',
49
- :depth_column => 'depth',
50
- :dependent => :delete_all, # or :destroy
51
- :polymorphic => false,
52
- :counter_cache => false
53
- }.merge(options)
54
-
55
- if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
56
- options[:scope] = "#{options[:scope]}_id".intern
57
- end
58
-
59
- class_attribute :acts_as_nested_set_options
60
- self.acts_as_nested_set_options = options
49
+ acts_as_nested_set_parse_options! options
61
50
 
62
- include CollectiveIdea::Acts::NestedSet::Model
51
+ include Model
63
52
  include Columns
64
53
  extend Columns
65
54
 
66
- belongs_to :parent, :class_name => self.base_class.to_s,
67
- :foreign_key => parent_column_name,
68
- :counter_cache => options[:counter_cache],
69
- :inverse_of => (:children unless options[:polymorphic]),
70
- :polymorphic => options[:polymorphic]
71
-
72
- has_many_children_options = {
73
- :class_name => self.base_class.to_s,
74
- :foreign_key => parent_column_name,
75
- :order => order_column,
76
- :inverse_of => (:parent unless options[:polymorphic]),
77
- }
78
-
79
- # Add callbacks, if they were supplied.. otherwise, we don't want them.
80
- [:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
81
- has_many_children_options.update(ar_callback => options[ar_callback]) if options[ar_callback]
82
- end
83
-
84
- has_many :children, has_many_children_options
55
+ acts_as_nested_set_relate_parent!
56
+ acts_as_nested_set_relate_children!
85
57
 
86
58
  attr_accessor :skip_before_destroy
87
59
 
60
+ acts_as_nested_set_prevent_assignment_to_reserved_columns!
61
+ acts_as_nested_set_define_callbacks!
62
+ end
63
+
64
+ private
65
+ def acts_as_nested_set_define_callbacks!
66
+ # on creation, set automatically lft and rgt to the end of the tree
88
67
  before_create :set_default_left_and_right
89
68
  before_save :store_new_parent
90
69
  after_save :move_to_new_parent, :set_depth!
91
70
  before_destroy :destroy_descendants
92
71
 
93
- # no assignment to structure fields
94
- [left_column_name, right_column_name, depth_column_name].each do |column|
95
- module_eval <<-"end_eval", __FILE__, __LINE__
96
- def #{column}=(x)
97
- raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
98
- end
99
- end_eval
100
- end
101
-
102
72
  define_model_callbacks :move
103
73
  end
104
74
 
105
- module Model
106
- extend ActiveSupport::Concern
107
-
108
- included do
109
- delegate :quoted_table_name, :to => self
110
- end
111
-
112
- module ClassMethods
113
- # Returns the first root
114
- def root
115
- roots.first
116
- end
117
-
118
- def roots
119
- where(parent_column_name => nil).order(quoted_left_column_full_name)
120
- end
121
-
122
- def leaves
123
- where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1").order(quoted_left_column_full_name)
124
- end
125
-
126
- def valid?
127
- left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
128
- end
129
-
130
- def left_and_rights_valid?
131
- ## AS clause not supported in Oracle in FROM clause for aliasing table name
132
- joins("LEFT OUTER JOIN #{quoted_table_name}" +
133
- (connection.adapter_name.match(/Oracle/).nil? ? " AS " : " ") +
134
- "parent ON " +
135
- "#{quoted_parent_column_full_name} = parent.#{primary_key}").
136
- where(
137
- "#{quoted_left_column_full_name} IS NULL OR " +
138
- "#{quoted_right_column_full_name} IS NULL OR " +
139
- "#{quoted_left_column_full_name} >= " +
140
- "#{quoted_right_column_full_name} OR " +
141
- "(#{quoted_parent_column_full_name} IS NOT NULL AND " +
142
- "(#{quoted_left_column_full_name} <= parent.#{quoted_left_column_name} OR " +
143
- "#{quoted_right_column_full_name} >= parent.#{quoted_right_column_name}))"
144
- ).count == 0
145
- end
146
-
147
- def no_duplicates_for_columns?
148
- scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
149
- connection.quote_column_name(c)
150
- end.push(nil).join(", ")
151
- [quoted_left_column_full_name, quoted_right_column_full_name].all? do |column|
152
- # No duplicates
153
- select("#{scope_string}#{column}, COUNT(#{column})").
154
- group("#{scope_string}#{column}").
155
- having("COUNT(#{column}) > 1").
156
- first.nil?
157
- end
158
- end
159
-
160
- # Wrapper for each_root_valid? that can deal with scope.
161
- def all_roots_valid?
162
- if acts_as_nested_set_options[:scope]
163
- roots.group_by {|record| scope_column_names.collect {|col| record.send(col.to_sym) } }.all? do |scope, grouped_roots|
164
- each_root_valid?(grouped_roots)
165
- end
166
- else
167
- each_root_valid?(roots)
168
- end
169
- end
170
-
171
- def each_root_valid?(roots_to_validate)
172
- left = right = 0
173
- roots_to_validate.all? do |root|
174
- (root.left > left && root.right > right).tap do
175
- left = root.left
176
- right = root.right
177
- end
178
- end
179
- end
180
-
181
- # Rebuilds the left & rights if unset or invalid.
182
- # Also very useful for converting from acts_as_tree.
183
- def rebuild!(validate_nodes = true)
184
- # default_scope with order may break database queries so we do all operation without scope
185
- unscoped do
186
- # Don't rebuild a valid tree.
187
- return true if valid?
188
-
189
- scope = lambda{|node|}
190
- if acts_as_nested_set_options[:scope]
191
- scope = lambda{|node|
192
- scope_column_names.inject(""){|str, column_name|
193
- str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
194
- }
195
- }
196
- end
197
- indices = {}
198
-
199
- set_left_and_rights = lambda do |node|
200
- # set left
201
- node[left_column_name] = indices[scope.call(node)] += 1
202
- # find
203
- where(["#{quoted_parent_column_full_name} = ? #{scope.call(node)}", node]).order("#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, id").each{|n| set_left_and_rights.call(n) }
204
- # set right
205
- node[right_column_name] = indices[scope.call(node)] += 1
206
- node.save!(:validate => validate_nodes)
207
- end
208
-
209
- # Find root node(s)
210
- root_nodes = where("#{quoted_parent_column_full_name} IS NULL").order("#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, id").each do |root_node|
211
- # setup index for this scope
212
- indices[scope.call(root_node)] ||= 0
213
- set_left_and_rights.call(root_node)
214
- end
215
- end
216
- end
217
-
218
- # Iterates over tree elements and determines the current level in the tree.
219
- # Only accepts default ordering, odering by an other column than lft
220
- # does not work. This method is much more efficent than calling level
221
- # because it doesn't require any additional database queries.
222
- #
223
- # Example:
224
- # Category.each_with_level(Category.root.self_and_descendants) do |o, level|
225
- #
226
- def each_with_level(objects)
227
- path = [nil]
228
- objects.each do |o|
229
- if o.parent_id != path.last
230
- # we are on a new level, did we descend or ascend?
231
- if path.include?(o.parent_id)
232
- # remove wrong wrong tailing paths elements
233
- path.pop while path.last != o.parent_id
234
- else
235
- path << o.parent_id
236
- end
237
- end
238
- yield(o, path.length - 1)
239
- end
240
- end
241
-
242
- # Same as each_with_level - Accepts a string as a second argument to sort the list
243
- # Example:
244
- # Category.each_with_level(Category.root.self_and_descendants, :sort_by_this_column) do |o, level|
245
- def sorted_each_with_level(objects, order)
246
- path = [nil]
247
- children = []
248
- objects.each do |o|
249
- children << o if o.leaf?
250
- if o.parent_id != path.last
251
- if !children.empty? && !o.leaf?
252
- children.sort_by! &order
253
- children.each { |c| yield(c, path.length-1) }
254
- children = []
255
- end
256
- # we are on a new level, did we decent or ascent?
257
- if path.include?(o.parent_id)
258
- # remove wrong wrong tailing paths elements
259
- path.pop while path.last != o.parent_id
260
- else
261
- path << o.parent_id
262
- end
263
- end
264
- yield(o,path.length-1) if !o.leaf?
265
- end
266
- if !children.empty?
267
- children.sort_by! &order
268
- children.each { |c| yield(c, path.length-1) }
269
- end
270
- end
271
-
272
- def associate_parents(objects)
273
- if objects.all?{|o| o.respond_to?(:association)}
274
- id_indexed = objects.index_by(&:id)
275
- objects.each do |object|
276
- if !(association = object.association(:parent)).loaded? && (parent = id_indexed[object.parent_id])
277
- association.target = parent
278
- association.set_inverse_instance(parent)
279
- end
280
- end
281
- else
282
- objects
283
- end
284
- end
285
- end
286
-
287
- # 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.
288
- #
289
- # category.self_and_descendants.count
290
- # category.ancestors.find(:all, :conditions => "name like '%foo%'")
291
- # Value of the parent column
292
- def parent_id
293
- self[parent_column_name]
294
- end
295
-
296
- # Value of the left column
297
- def left
298
- self[left_column_name]
299
- end
300
-
301
- # Value of the right column
302
- def right
303
- self[right_column_name]
304
- end
305
-
306
- # Returns true if this is a root node.
307
- def root?
308
- parent_id.nil?
309
- end
310
-
311
- # Returns true if this is the end of a branch.
312
- def leaf?
313
- persisted? && right.to_i - left.to_i == 1
314
- end
315
-
316
- # Returns true is this is a child node
317
- def child?
318
- !root?
319
- end
320
-
321
- # Returns root
322
- def root
323
- if persisted?
324
- self_and_ancestors.where(parent_column_name => nil).first
325
- else
326
- if parent_id && current_parent = nested_set_scope.find(parent_id)
327
- current_parent.root
328
- else
329
- self
330
- end
331
- end
332
- end
333
-
334
- # Returns the array of all parents and self
335
- def self_and_ancestors
336
- nested_set_scope.where([
337
- "#{quoted_left_column_full_name} <= ? AND #{quoted_right_column_full_name} >= ?", left, right
338
- ])
339
- end
340
-
341
- # Returns an array of all parents
342
- def ancestors
343
- without_self self_and_ancestors
344
- end
345
-
346
- # Returns the array of all children of the parent, including self
347
- def self_and_siblings
348
- nested_set_scope.where(parent_column_name => parent_id)
349
- end
350
-
351
- # Returns the array of all children of the parent, except self
352
- def siblings
353
- without_self self_and_siblings
354
- end
355
-
356
- # Returns a set of all of its nested children which do not have children
357
- def leaves
358
- descendants.where("#{quoted_right_column_full_name} - #{quoted_left_column_full_name} = 1")
359
- end
360
-
361
- # Returns the level of this object in the tree
362
- # root level is 0
363
- def level
364
- parent_id.nil? ? 0 : compute_level
365
- end
366
-
367
- # Returns a set of itself and all of its nested children
368
- def self_and_descendants
369
- nested_set_scope.where([
370
- "#{quoted_left_column_full_name} >= ? AND #{quoted_left_column_full_name} < ?", left, right
371
- # using _left_ for both sides here lets us benefit from an index on that column if one exists
372
- ])
373
- end
374
-
375
- # Returns a set of all of its children and nested children
376
- def descendants
377
- without_self self_and_descendants
378
- end
379
-
380
- def is_descendant_of?(other)
381
- other.left < self.left && self.left < other.right && same_scope?(other)
382
- end
383
-
384
- def is_or_is_descendant_of?(other)
385
- other.left <= self.left && self.left < other.right && same_scope?(other)
386
- end
387
-
388
- def is_ancestor_of?(other)
389
- self.left < other.left && other.left < self.right && same_scope?(other)
390
- end
391
-
392
- def is_or_is_ancestor_of?(other)
393
- self.left <= other.left && other.left < self.right && same_scope?(other)
394
- end
395
-
396
- # Check if other model is in the same scope
397
- def same_scope?(other)
398
- Array(acts_as_nested_set_options[:scope]).all? do |attr|
399
- self.send(attr) == other.send(attr)
400
- end
401
- end
402
-
403
- # Find the first sibling to the left
404
- def left_sibling
405
- siblings.where(["#{quoted_left_column_full_name} < ?", left]).
406
- order("#{quoted_left_column_full_name} DESC").last
407
- end
408
-
409
- # Find the first sibling to the right
410
- def right_sibling
411
- siblings.where(["#{quoted_left_column_full_name} > ?", left]).first
412
- end
413
-
414
- # Shorthand method for finding the left sibling and moving to the left of it.
415
- def move_left
416
- move_to_left_of left_sibling
417
- end
418
-
419
- # Shorthand method for finding the right sibling and moving to the right of it.
420
- def move_right
421
- move_to_right_of right_sibling
422
- end
423
-
424
- # Move the node to the left of another node (you can pass id only)
425
- def move_to_left_of(node)
426
- move_to node, :left
427
- end
428
-
429
- # Move the node to the left of another node (you can pass id only)
430
- def move_to_right_of(node)
431
- move_to node, :right
432
- end
433
-
434
- # Move the node to the child of another node (you can pass id only)
435
- def move_to_child_of(node)
436
- move_to node, :child
437
- end
438
-
439
- # Move the node to the child of another node with specify index (you can pass id only)
440
- def move_to_child_with_index(node, index)
441
- if node.children.empty?
442
- move_to_child_of(node)
443
- elsif node.children.count == index
444
- move_to_right_of(node.children.last)
445
- else
446
- move_to_left_of(node.children[index])
447
- end
448
- end
449
-
450
- # Move the node to root nodes
451
- def move_to_root
452
- move_to nil, :root
453
- end
454
-
455
- # Order children in a nested set by an attribute
456
- # Can order by any attribute class that uses the Comparable mixin, for example a string or integer
457
- # Usage example when sorting categories alphabetically: @new_category.move_to_ordered_child_of(@root, "name")
458
- def move_to_ordered_child_of(parent, order_attribute, ascending = true)
459
- self.move_to_root and return unless parent
460
- left = nil # This is needed, at least for the tests.
461
- parent.children.each do |n| # Find the node immediately to the left of this node.
462
- if ascending
463
- left = n if n.send(order_attribute) < self.send(order_attribute)
464
- else
465
- left = n if n.send(order_attribute) > self.send(order_attribute)
466
- end
467
- end
468
- self.move_to_child_of(parent)
469
- return unless parent.children.count > 1 # Only need to order if there are multiple children.
470
- if left # Self has a left neighbor.
471
- self.move_to_right_of(left)
472
- else # Self is the left most node.
473
- self.move_to_left_of(parent.children[0])
474
- end
475
- end
476
-
477
- def move_possible?(target)
478
- self != target && # Can't target self
479
- same_scope?(target) && # can't be in different scopes
480
- # !(left..right).include?(target.left..target.right) # this needs tested more
481
- # detect impossible move
482
- !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
483
- end
484
-
485
- def to_text
486
- self_and_descendants.map do |node|
487
- "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
488
- end.join("\n")
489
- end
490
-
491
- protected
492
- def compute_level
493
- node, nesting = self, 0
494
- while (association = node.association(:parent)).loaded? && association.target
495
- nesting += 1
496
- node = node.parent
497
- end if node.respond_to? :association
498
- node == self ? ancestors.count : node.level + nesting
499
- end
500
-
501
- def without_self(scope)
502
- scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
503
- end
504
-
505
- # All nested set queries should use this nested_set_scope, which performs finds on
506
- # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
507
- # declaration.
508
- def nested_set_scope(options = {})
509
- options = {:order => quoted_left_column_full_name}.merge(options)
510
- scopes = Array(acts_as_nested_set_options[:scope])
511
- options[:conditions] = scopes.inject({}) do |conditions,attr|
512
- conditions.merge attr => self[attr]
513
- end unless scopes.empty?
514
- self.class.base_class.unscoped.scoped options
515
- end
516
-
517
- def store_new_parent
518
- @move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
519
- true # force callback to return true
520
- end
521
-
522
- def move_to_new_parent
523
- if @move_to_new_parent_id.nil?
524
- move_to_root
525
- elsif @move_to_new_parent_id
526
- move_to_child_of(@move_to_new_parent_id)
527
- end
528
- end
529
-
530
- def set_depth!
531
- if nested_set_scope.column_names.map(&:to_s).include?(depth_column_name.to_s)
532
- in_tenacious_transaction do
533
- reload
534
-
535
- nested_set_scope.where(:id => id).update_all(["#{quoted_depth_column_name} = ?", level])
536
- end
537
- self[depth_column_name.to_sym] = self.level
538
- end
539
- end
540
-
541
- # on creation, set automatically lft and rgt to the end of the tree
542
- def set_default_left_and_right
543
- highest_right_row = nested_set_scope(:order => "#{quoted_right_column_full_name} desc").limit(1).lock(true).first
544
- maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
545
- # adds the new node to the right of all existing nodes
546
- self[left_column_name] = maxright + 1
547
- self[right_column_name] = maxright + 2
548
- end
549
-
550
- def in_tenacious_transaction(&block)
551
- retry_count = 0
552
- begin
553
- transaction(&block)
554
- rescue ActiveRecord::StatementInvalid => error
555
- raise unless connection.open_transactions.zero?
556
- raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
557
- raise unless retry_count < 10
558
- retry_count += 1
559
- logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
560
- sleep(rand(retry_count)*0.1) # Aloha protocol
561
- retry
562
- end
563
- end
564
-
565
- # Prunes a branch off of the tree, shifting all of the elements on the right
566
- # back to the left so the counts still work.
567
- def destroy_descendants
568
- return if right.nil? || left.nil? || skip_before_destroy
569
-
570
- in_tenacious_transaction do
571
- reload_nested_set
572
- # select the rows in the model that extend past the deletion point and apply a lock
573
- nested_set_scope.where(["#{quoted_left_column_full_name} >= ?", left]).
574
- select(id).lock(true)
575
-
576
- if acts_as_nested_set_options[:dependent] == :destroy
577
- descendants.each do |model|
578
- model.skip_before_destroy = true
579
- model.destroy
580
- end
581
- else
582
- nested_set_scope.where(["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?", left, right]).
583
- delete_all
584
- end
585
-
586
- # update lefts and rights for remaining nodes
587
- diff = right - left + 1
588
- nested_set_scope.where(["#{quoted_left_column_full_name} > ?", right]).update_all(
589
- ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff]
590
- )
591
-
592
- nested_set_scope.where(["#{quoted_right_column_full_name} > ?", right]).update_all(
593
- ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff]
594
- )
595
-
596
- # Don't allow multiple calls to destroy to corrupt the set
597
- self.skip_before_destroy = true
598
- end
599
- end
600
-
601
- # reload left, right, and parent
602
- def reload_nested_set
603
- reload(
604
- :select => "#{quoted_left_column_full_name}, #{quoted_right_column_full_name}, #{quoted_parent_column_full_name}",
605
- :lock => true
606
- )
607
- end
608
-
609
- def move_to(target, position)
610
- raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
611
- run_callbacks :move do
612
- in_tenacious_transaction do
613
- if target.is_a? self.class.base_class
614
- target.reload_nested_set
615
- elsif position != :root
616
- # load object if node is not an object
617
- target = nested_set_scope.find(target)
618
- end
619
- self.reload_nested_set
620
-
621
- unless position == :root || move_possible?(target)
622
- raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
623
- end
624
-
625
- bound = case position
626
- when :child; target[right_column_name]
627
- when :left; target[left_column_name]
628
- when :right; target[right_column_name] + 1
629
- when :root; 1
630
- else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
631
- end
632
-
633
- if bound > self[right_column_name]
634
- bound = bound - 1
635
- other_bound = self[right_column_name] + 1
636
- else
637
- other_bound = self[left_column_name] - 1
638
- end
639
-
640
- # there would be no change
641
- return if bound == self[right_column_name] || bound == self[left_column_name]
642
-
643
- # we have defined the boundaries of two non-overlapping intervals,
644
- # so sorting puts both the intervals and their boundaries in order
645
- a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
646
-
647
- # select the rows in the model between a and d, and apply a lock
648
- self.class.base_class.select('id').lock(true).where(
649
- ["#{quoted_left_column_full_name} >= :a and #{quoted_right_column_full_name} <= :d", {:a => a, :d => d}]
650
- )
651
-
652
- new_parent = case position
653
- when :child; target.id
654
- when :root; nil
655
- else target[parent_column_name]
656
- end
657
-
658
- where_statement = ["not (#{quoted_left_column_name} = CASE " +
659
- "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
660
- "THEN #{quoted_left_column_name} + :d - :b " +
661
- "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
662
- "THEN #{quoted_left_column_name} + :a - :c " +
663
- "ELSE #{quoted_left_column_name} END AND " +
664
- "#{quoted_right_column_name} = CASE " +
665
- "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
666
- "THEN #{quoted_right_column_name} + :d - :b " +
667
- "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
668
- "THEN #{quoted_right_column_name} + :a - :c " +
669
- "ELSE #{quoted_right_column_name} END AND " +
670
- "#{quoted_parent_column_name} = CASE " +
671
- "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
672
- "ELSE #{quoted_parent_column_name} END)" ,
673
- {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent} ]
674
-
675
-
676
-
75
+ def acts_as_nested_set_relate_children!
76
+ has_many_children_options = {
77
+ :class_name => self.base_class.to_s,
78
+ :foreign_key => parent_column_name,
79
+ :primary_key => primary_column_name,
80
+ :inverse_of => (:parent unless acts_as_nested_set_options[:polymorphic]),
81
+ }
677
82
 
678
- self.nested_set_scope.where(*where_statement).update_all([
679
- "#{quoted_left_column_name} = CASE " +
680
- "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
681
- "THEN #{quoted_left_column_name} + :d - :b " +
682
- "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
683
- "THEN #{quoted_left_column_name} + :a - :c " +
684
- "ELSE #{quoted_left_column_name} END, " +
685
- "#{quoted_right_column_name} = CASE " +
686
- "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
687
- "THEN #{quoted_right_column_name} + :d - :b " +
688
- "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
689
- "THEN #{quoted_right_column_name} + :a - :c " +
690
- "ELSE #{quoted_right_column_name} END, " +
691
- "#{quoted_parent_column_name} = CASE " +
692
- "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
693
- "ELSE #{quoted_parent_column_name} END",
694
- {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
695
- ])
696
- end
697
- target.reload_nested_set if target
698
- self.set_depth!
699
- self.descendants.each(&:save)
700
- self.reload_nested_set
701
- end
83
+ # Add callbacks, if they were supplied.. otherwise, we don't want them.
84
+ [:before_add, :after_add, :before_remove, :after_remove].each do |ar_callback|
85
+ has_many_children_options.update(
86
+ ar_callback => acts_as_nested_set_options[ar_callback]
87
+ ) if acts_as_nested_set_options[ar_callback]
702
88
  end
703
89
 
90
+ has_many :children, -> { order(quoted_order_column_name) },
91
+ has_many_children_options
704
92
  end
705
93
 
706
- # Mixed into both classes and instances to provide easy access to the column names
707
- module Columns
708
- def left_column_name
709
- acts_as_nested_set_options[:left_column]
710
- end
711
-
712
- def right_column_name
713
- acts_as_nested_set_options[:right_column]
714
- end
715
-
716
- def depth_column_name
717
- acts_as_nested_set_options[:depth_column]
718
- end
719
-
720
- def parent_column_name
721
- acts_as_nested_set_options[:parent_column]
722
- end
723
-
724
- def order_column
725
- acts_as_nested_set_options[:order_column] || left_column_name
726
- end
727
-
728
- def scope_column_names
729
- Array(acts_as_nested_set_options[:scope])
730
- end
731
-
732
- def quoted_left_column_name
733
- connection.quote_column_name(left_column_name)
734
- end
735
-
736
- def quoted_right_column_name
737
- connection.quote_column_name(right_column_name)
738
- end
739
-
740
- def quoted_depth_column_name
741
- connection.quote_column_name(depth_column_name)
742
- end
94
+ def acts_as_nested_set_relate_parent!
95
+ belongs_to :parent, :class_name => self.base_class.to_s,
96
+ :foreign_key => parent_column_name,
97
+ :primary_key => primary_column_name,
98
+ :counter_cache => acts_as_nested_set_options[:counter_cache],
99
+ :inverse_of => (:children unless acts_as_nested_set_options[:polymorphic]),
100
+ :polymorphic => acts_as_nested_set_options[:polymorphic],
101
+ :touch => acts_as_nested_set_options[:touch]
102
+ end
743
103
 
744
- def quoted_parent_column_name
745
- connection.quote_column_name(parent_column_name)
746
- end
104
+ def acts_as_nested_set_default_options
105
+ {
106
+ :parent_column => 'parent_id',
107
+ :primary_column => 'id',
108
+ :left_column => 'lft',
109
+ :right_column => 'rgt',
110
+ :depth_column => 'depth',
111
+ :dependent => :delete_all, # or :destroy
112
+ :polymorphic => false,
113
+ :counter_cache => false,
114
+ :touch => false
115
+ }.freeze
116
+ end
747
117
 
748
- def quoted_scope_column_names
749
- scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
750
- end
118
+ def acts_as_nested_set_parse_options!(options)
119
+ options = acts_as_nested_set_default_options.merge(options)
751
120
 
752
- def quoted_left_column_full_name
753
- "#{quoted_table_name}.#{quoted_left_column_name}"
121
+ if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
122
+ options[:scope] = "#{options[:scope]}_id".intern
754
123
  end
755
124
 
756
- def quoted_right_column_full_name
757
- "#{quoted_table_name}.#{quoted_right_column_name}"
758
- end
125
+ class_attribute :acts_as_nested_set_options
126
+ self.acts_as_nested_set_options = options
127
+ end
759
128
 
760
- def quoted_parent_column_full_name
761
- "#{quoted_table_name}.#{quoted_parent_column_name}"
129
+ def acts_as_nested_set_prevent_assignment_to_reserved_columns!
130
+ # no assignment to structure fields
131
+ [left_column_name, right_column_name, depth_column_name].each do |column|
132
+ module_eval <<-"end_eval", __FILE__, __LINE__
133
+ def #{column}=(x)
134
+ raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
135
+ end
136
+ end_eval
762
137
  end
763
138
  end
764
-
765
139
  end
766
140
  end
767
141
  end