tiny_support 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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