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