closure_tree 3.7.3 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -20,9 +20,11 @@ closure_tree has some great features:
20
20
  * 2 SQL INSERTs on node creation
21
21
  * 3 SQL INSERT/UPDATEs on node reparenting
22
22
  * Support for reparenting children (and all their progeny)
23
+ * Support for [concurrency](#concurrency) (using [with_advisory_lock](https://github/mceachen/with_advisory_lock))
23
24
  * Support for polymorphism [STI](#sti) within the hierarchy
24
25
  * ```find_or_create_by_path``` for [building out hierarchies quickly and conveniently](#find_or_create_by_path)
25
26
  * Support for [deterministic ordering](#deterministic-ordering) of children
27
+ * Support for [preordered](http://en.wikipedia.org/wiki/Tree_traversal#Pre-order) traversal of descendants
26
28
  * Support for single-select depth-limited [nested hashes](#nested-hashes)
27
29
  * Excellent [test coverage](#testing) in a variety of environments
28
30
 
@@ -37,6 +39,7 @@ for a description of different tree storage algorithms.
37
39
  - [Accessing Data](#accessing-data)
38
40
  - [Polymorphic hierarchies with STI](#polymorphic-hierarchies-with-sti)
39
41
  - [Deterministic ordering](#deterministic-ordering)
42
+ - [Concurrency](#concurrency)
40
43
  - [FAQ](#faq)
41
44
  - [Testing](#testing)
42
45
  - [Change log](#change-log)
@@ -317,6 +320,9 @@ When you enable ```order```, you'll also have the following new methods injected
317
320
 
318
321
  If your ```order``` column is an integer attribute, you'll also have these:
319
322
 
323
+ * ```node1.self_and_descendants_preordered``` which will return descendants,
324
+ [pre-ordered](http://en.wikipedia.org/wiki/Tree_traversal#Pre-order).
325
+
320
326
  * ```node1.prepend_sibling(node2)``` which will
321
327
  1. set ```node2``` to the same parent as ```node1```,
322
328
  2. set ```node2```'s order column to 1 less than ```node1```'s value, and
@@ -353,6 +359,29 @@ root.reload.children.collect(&:name)
353
359
  => ["b", "c", "a"]
354
360
  ```
355
361
 
362
+ ## Concurrency
363
+
364
+ Several methods, especially ```#rebuild``` and ```#find_or_create_by_path```, cannot run concurrently correctly.
365
+ ```#find_or_create_by_path```, for example, may create duplicate nodes.
366
+
367
+ Database row-level locks work correctly with PostgreSQL, but MySQL's row-level locking is broken, and
368
+ erroneously reports deadlocks where there are none. To work around this, and have a consistent implementation
369
+ for both MySQL and PostgreSQL, [with_advisory_lock](https://github.com/mceachen/with_advisory_lock)
370
+ is used automatically to ensure correctness.
371
+
372
+ If you are already managing concurrency elsewhere in your application, and want to disable the use
373
+ of with_advisory_lock, pass ```:with_advisory_lock => false``` in the options hash:
374
+
375
+ ```ruby
376
+ class Tag
377
+ acts_as_tree :with_advisory_lock => false
378
+ end
379
+ ```
380
+
381
+ Note that you *will eventually have data corruption* if you disable advisory locks, write to your
382
+ database with multiple threads, and don't provide an alternative mutex.
383
+
384
+
356
385
  ## FAQ
357
386
 
358
387
  ### Does this gem support multiple parents?
@@ -396,6 +425,12 @@ Parallelism is not tested with Rails 3.0.x nor 3.1.x due to this
396
425
 
397
426
  ## Change log
398
427
 
428
+ ### 3.8.0
429
+
430
+ * Support for preordered descendants. This requires a numeric sort order column.
431
+ Resolves [feature request 38](https://github.com/mceachen/closure_tree/issues/38).
432
+ * Moved modules from ```acts_as_tree``` into separate files
433
+
399
434
  ### 3.7.3
400
435
 
401
436
  Due to MySQL's inability to lock rows properly, I've switched to advisory_locks for
data/lib/closure_tree.rb CHANGED
@@ -2,7 +2,6 @@ require 'active_support'
2
2
  require 'active_record'
3
3
 
4
4
  ActiveSupport.on_load :active_record do
5
- require 'with_advisory_lock'
6
5
  require 'closure_tree/acts_as_tree'
7
6
 
8
7
  ActiveRecord::Base.send :extend, ClosureTree::ActsAsTree
@@ -1,3 +1,9 @@
1
+ require 'closure_tree/columns'
2
+ require 'closure_tree/deterministic_ordering'
3
+ require 'closure_tree/model'
4
+ require 'closure_tree/numeric_deterministic_ordering'
5
+ require 'closure_tree/with_advisory_lock'
6
+
1
7
  module ClosureTree
2
8
  module ActsAsTree
3
9
  def acts_as_tree(options = {})
@@ -8,7 +14,8 @@ module ClosureTree
8
14
  :ct_base_class => self,
9
15
  :parent_column_name => 'parent_id',
10
16
  :dependent => :nullify, # or :destroy or :delete_all -- see the README
11
- :name_column => 'name'
17
+ :name_column => 'name',
18
+ :with_advisory_lock => true
12
19
  }.merge(options)
13
20
 
14
21
  raise IllegalArgumentException, "name_column can't be 'path'" if closure_tree_options[:name_column] == 'path'
@@ -16,6 +23,9 @@ module ClosureTree
16
23
  include ClosureTree::Columns
17
24
  extend ClosureTree::Columns
18
25
 
26
+ include ClosureTree::WithAdvisoryLock
27
+ extend ClosureTree::WithAdvisoryLock
28
+
19
29
  # Auto-inject the hierarchy table
20
30
  # See https://github.com/patshaughnessy/class_factory/blob/master/lib/class_factory/class_factory.rb
21
31
  class_attribute :hierarchy_class
@@ -34,573 +44,11 @@ module ClosureTree
34
44
 
35
45
  self.hierarchy_class.table_name = hierarchy_table_name
36
46
 
47
+ include ClosureTree::Model
37
48
  unless order_option.nil?
38
49
  include ClosureTree::DeterministicOrdering
39
50
  include ClosureTree::DeterministicNumericOrdering if order_is_numeric
40
51
  end
41
-
42
- include ClosureTree::Model
43
-
44
- validate :ct_validate
45
- before_save :ct_before_save
46
- after_save :ct_after_save
47
- before_destroy :ct_before_destroy
48
-
49
- belongs_to :parent,
50
- :class_name => ct_class.to_s,
51
- :foreign_key => parent_column_name
52
-
53
- unless defined?(ActiveModel::ForbiddenAttributesProtection) && ancestors.include?(ActiveModel::ForbiddenAttributesProtection)
54
- attr_accessible :parent
55
- end
56
-
57
- has_many :children, with_order_option(
58
- :class_name => ct_class.to_s,
59
- :foreign_key => parent_column_name,
60
- :dependent => closure_tree_options[:dependent]
61
- )
62
-
63
- has_many :ancestor_hierarchies,
64
- :class_name => hierarchy_class_name,
65
- :foreign_key => "descendant_id",
66
- :order => "#{quoted_hierarchy_table_name}.generations asc",
67
- :dependent => :destroy
68
-
69
- has_many :self_and_ancestors,
70
- :through => :ancestor_hierarchies,
71
- :source => :ancestor,
72
- :order => "#{quoted_hierarchy_table_name}.generations asc"
73
-
74
- has_many :descendant_hierarchies,
75
- :class_name => hierarchy_class_name,
76
- :foreign_key => "ancestor_id",
77
- :order => "#{quoted_hierarchy_table_name}.generations asc",
78
- :dependent => :destroy
79
- # TODO: FIXME: this collection currently ignores sort_order
80
- # (because the quoted_table_named would need to be joined in to get to the order column)
81
-
82
- has_many :self_and_descendants,
83
- :through => :descendant_hierarchies,
84
- :source => :descendant,
85
- :order => append_order("#{quoted_hierarchy_table_name}.generations asc")
86
-
87
- def self.roots
88
- where(parent_column_name => nil)
89
- end
90
-
91
- # There is no default depth limit. This might be crazy-big, depending
92
- # on your tree shape. Hash huge trees at your own peril!
93
- def self.hash_tree(options = {})
94
- build_hash_tree(hash_tree_scope(options[:limit_depth]))
95
- end
96
-
97
- def find_all_by_generation(generation_level)
98
- s = joins(<<-SQL)
99
- INNER JOIN (
100
- SELECT #{primary_key} as root_id
101
- FROM #{quoted_table_name}
102
- WHERE #{quoted_parent_column_name} IS NULL
103
- ) AS roots ON (1 = 1)
104
- INNER JOIN (
105
- SELECT ancestor_id, descendant_id
106
- FROM #{quoted_hierarchy_table_name}
107
- GROUP BY 1, 2
108
- HAVING MAX(generations) = #{generation_level.to_i}
109
- ) AS descendants ON (
110
- #{quoted_table_name}.#{primary_key} = descendants.descendant_id
111
- AND roots.root_id = descendants.ancestor_id
112
- )
113
- SQL
114
- order_option ? s.order(order_option) : s
115
- end
116
-
117
- def self.leaves
118
- s = joins(<<-SQL)
119
- INNER JOIN (
120
- SELECT ancestor_id
121
- FROM #{quoted_hierarchy_table_name}
122
- GROUP BY 1
123
- HAVING MAX(#{quoted_hierarchy_table_name}.generations) = 0
124
- ) AS leaves ON (#{quoted_table_name}.#{primary_key} = leaves.ancestor_id)
125
- SQL
126
- order_option ? s.order(order_option) : s
127
- end
128
- end
129
- end
130
-
131
- module Model
132
- extend ActiveSupport::Concern
133
-
134
- # Returns true if this node has no parents.
135
- def root?
136
- ct_parent_id.nil?
137
- end
138
-
139
- # Returns true if this node has a parent, and is not a root.
140
- def child?
141
- !parent.nil?
142
- end
143
-
144
- # Returns true if this node has no children.
145
- def leaf?
146
- children.empty?
147
- end
148
-
149
- # Returns the farthest ancestor, or self if +root?+
150
- def root
151
- self_and_ancestors.where(parent_column_name.to_sym => nil).first
152
- end
153
-
154
- def leaves
155
- self_and_descendants.leaves
156
- end
157
-
158
- def depth
159
- ancestors.size
160
- end
161
-
162
- alias :level :depth
163
-
164
- def ancestors
165
- without_self(self_and_ancestors)
166
- end
167
-
168
- def ancestor_ids
169
- ids_from(ancestors)
170
- end
171
-
172
- # Returns an array, root first, of self_and_ancestors' values of the +to_s_column+, which defaults
173
- # to the +name_column+.
174
- # (so child.ancestry_path == +%w{grandparent parent child}+
175
- def ancestry_path(to_s_column = name_column)
176
- self_and_ancestors.reverse.collect { |n| n.send to_s_column.to_sym }
177
- end
178
-
179
- def descendants
180
- without_self(self_and_descendants)
181
- end
182
-
183
- def descendant_ids
184
- ids_from(descendants)
185
- end
186
-
187
- def self_and_siblings
188
- s = ct_base_class.where(parent_column_sym => parent)
189
- order_option.present? ? s.order(quoted_order_column) : s
190
- end
191
-
192
- def siblings
193
- without_self(self_and_siblings)
194
- end
195
-
196
- def sibling_ids
197
- ids_from(siblings)
198
- end
199
-
200
- # Alias for appending to the children collection.
201
- # You can also add directly to the children collection, if you'd prefer.
202
- def add_child(child_node)
203
- children << child_node
204
- child_node
205
- end
206
-
207
- # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+.
208
- def find_by_path(path)
209
- path = path.is_a?(Enumerable) ? path.dup : [path]
210
- node = self
211
- while !path.empty? && node
212
- node = node.children.where(name_sym => path.shift).first
213
- end
214
- node
215
- end
216
-
217
- # Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
218
- def find_or_create_by_path(path, attributes = {})
219
- with_advisory_lock("closure_tree") do
220
- transaction do
221
- subpath = path.is_a?(Enumerable) ? path.dup : [path]
222
- child_name = subpath.shift
223
- return self unless child_name
224
- child = transaction do
225
- attrs = {name_sym => child_name}
226
- attrs[:type] = self.type if ct_subclass? && ct_has_type?
227
- self.children.where(attrs).first || begin
228
- child = self.class.new(attributes.merge(attrs))
229
- self.children << child
230
- child
231
- end
232
- end
233
- child.find_or_create_by_path(subpath, attributes)
234
- end
235
- end
236
- end
237
-
238
- def find_all_by_generation(generation_level)
239
- s = ct_base_class.joins(<<-SQL)
240
- INNER JOIN (
241
- SELECT descendant_id
242
- FROM #{quoted_hierarchy_table_name}
243
- WHERE ancestor_id = #{ct_quote(self.id)}
244
- GROUP BY 1
245
- HAVING MAX(#{quoted_hierarchy_table_name}.generations) = #{generation_level.to_i}
246
- ) AS descendants ON (#{quoted_table_name}.#{ct_base_class.primary_key} = descendants.descendant_id)
247
- SQL
248
- order_option ? s.order(order_option) : s
249
- end
250
-
251
- def hash_tree_scope(limit_depth = nil)
252
- scope = self_and_descendants
253
- if limit_depth
254
- scope.where("#{quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}")
255
- else
256
- scope
257
- end
258
- end
259
-
260
- def hash_tree(options = {})
261
- self.class.build_hash_tree(hash_tree_scope(options[:limit_depth]))
262
- end
263
-
264
- def ct_parent_id
265
- read_attribute(parent_column_sym)
266
- end
267
-
268
- protected
269
-
270
- def ct_validate
271
- if changes[parent_column_name] &&
272
- parent.present? &&
273
- parent.self_and_ancestors.include?(self)
274
- errors.add(parent_column_sym, "You cannot add an ancestor as a descendant")
275
- end
276
- end
277
-
278
- def ct_before_save
279
- @was_new_record = new_record?
280
- true # don't cancel the save
281
- end
282
-
283
- def ct_after_save
284
- rebuild! if changes[parent_column_name] || @was_new_record
285
- @was_new_record = false # we aren't new anymore.
286
- true # don't cancel anything.
287
- end
288
-
289
- def rebuild!
290
- with_advisory_lock("closure_tree") do
291
- transaction do
292
- delete_hierarchy_references unless @was_new_record
293
- hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
294
- unless root?
295
- connection.execute <<-SQL
296
- INSERT INTO #{quoted_hierarchy_table_name}
297
- (ancestor_id, descendant_id, generations)
298
- SELECT x.ancestor_id, #{ct_quote(id)}, x.generations + 1
299
- FROM #{quoted_hierarchy_table_name} x
300
- WHERE x.descendant_id = #{ct_quote(self.ct_parent_id)}
301
- SQL
302
- end
303
- children.each { |c| c.rebuild! }
304
- end
305
- end
306
- end
307
-
308
- def ct_before_destroy
309
- delete_hierarchy_references
310
- if closure_tree_options[:dependent] == :nullify
311
- children.each { |c| c.rebuild! }
312
- end
313
- end
314
-
315
- def delete_hierarchy_references
316
- # The crazy double-wrapped sub-subselect works around MySQL's limitation of subselects on the same table that is being mutated.
317
- # It shouldn't affect performance of postgresql.
318
- # See http://dev.mysql.com/doc/refman/5.0/en/subquery-errors.html
319
- # Also: PostgreSQL doesn't support INNER JOIN on DELETE, so we can't use that.
320
- connection.execute <<-SQL
321
- DELETE FROM #{quoted_hierarchy_table_name}
322
- WHERE descendant_id IN (
323
- SELECT DISTINCT descendant_id
324
- FROM ( SELECT descendant_id
325
- FROM #{quoted_hierarchy_table_name}
326
- WHERE ancestor_id = #{ct_quote(id)}
327
- ) AS x )
328
- OR descendant_id = #{ct_quote(id)}
329
- SQL
330
- end
331
-
332
- def without_self(scope)
333
- scope.where(["#{quoted_table_name}.#{ct_base_class.primary_key} != ?", self])
334
- end
335
-
336
- def ids_from(scope)
337
- if scope.respond_to? :pluck
338
- scope.pluck(:id)
339
- else
340
- scope.select(:id).collect(&:id)
341
- end
342
- end
343
-
344
- def ct_quote(field)
345
- self.class.connection.quote(field)
346
- end
347
-
348
- # TODO: _parent_id will be removed in the next major version
349
- alias :_parent_id :ct_parent_id
350
-
351
- module ClassMethods
352
-
353
- # Returns an arbitrary node that has no parents.
354
- def root
355
- roots.first
356
- end
357
-
358
- # Rebuilds the hierarchy table based on the parent_id column in the database.
359
- # Note that the hierarchy table will be truncated.
360
- def rebuild!
361
- with_advisory_lock("closure_tree") do
362
- transaction do
363
- hierarchy_class.delete_all # not destroy_all -- we just want a simple truncate.
364
- roots.each { |n| n.send(:rebuild!) } # roots just uses the parent_id column, so this is safe.
365
- end
366
- end
367
- nil
368
- end
369
-
370
- # Find the node whose +ancestry_path+ is +path+
371
- def find_by_path(path)
372
- subpath = path.dup
373
- root = roots.where(name_sym => subpath.shift).first
374
- root.find_by_path(subpath) if root
375
- end
376
-
377
- # Find or create nodes such that the +ancestry_path+ is +path+
378
- def find_or_create_by_path(path, attributes = {})
379
- subpath = path.dup
380
- root_name = subpath.shift
381
- with_advisory_lock("closure_tree") do
382
- transaction do
383
- # shenanigans because find_or_create can't infer we want the same class as this:
384
- # Note that roots will already be constrained to this subclass (in the case of polymorphism):
385
- root = roots.where(name_sym => root_name).first
386
- root ||= create!(attributes.merge(name_sym => root_name))
387
- root.find_or_create_by_path(subpath, attributes)
388
- end
389
- end
390
- end
391
-
392
- def hash_tree_scope(limit_depth = nil)
393
- # Deepest generation, within limit, for each descendant
394
- # NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
395
- generation_depth = <<-SQL
396
- INNER JOIN (
397
- SELECT descendant_id, MAX(generations) as depth
398
- FROM #{quoted_hierarchy_table_name}
399
- GROUP BY descendant_id
400
- #{"HAVING MAX(generations) <= #{limit_depth - 1}" if limit_depth}
401
- ) AS generation_depth
402
- ON #{quoted_table_name}.#{primary_key} = generation_depth.descendant_id
403
- SQL
404
- scoped.joins(generation_depth).order(append_order("generation_depth.depth"))
405
- end
406
-
407
- # Builds nested hash structure using the scope returned from the passed in scope
408
- def build_hash_tree(tree_scope)
409
- tree = ActiveSupport::OrderedHash.new
410
- id_to_hash = {}
411
-
412
- tree_scope.each do |ea|
413
- h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
414
- if ea.root? || tree.empty? # We're at the top of the tree.
415
- tree[ea] = h
416
- else
417
- id_to_hash[ea.ct_parent_id][ea] = h
418
- end
419
- end
420
-
421
- tree
422
- end
423
- end
424
- end
425
-
426
- # Mixed into both classes and instances to provide easy access to the column names
427
- module Columns
428
-
429
- def parent_column_name
430
- closure_tree_options[:parent_column_name]
431
- end
432
-
433
- def parent_column_sym
434
- parent_column_name.to_sym
435
- end
436
-
437
- def has_name?
438
- ct_class.new.attributes.include? closure_tree_options[:name_column]
439
- end
440
-
441
- def name_column
442
- closure_tree_options[:name_column]
443
- end
444
-
445
- def name_sym
446
- name_column.to_sym
447
- end
448
-
449
- def hierarchy_table_name
450
- # We need to use the table_name, not something like ct_class.to_s.demodulize + "_hierarchies",
451
- # because they may have overridden the table name, which is what we want to be consistent with
452
- # in order for the schema to make sense.
453
- tablename = closure_tree_options[:hierarchy_table_name] ||
454
- remove_prefix_and_suffix(ct_table_name).singularize + "_hierarchies"
455
-
456
- ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix
457
- end
458
-
459
- def hierarchy_class_name
460
- closure_tree_options[:hierarchy_class_name] || ct_class.to_s + "Hierarchy"
461
- end
462
-
463
- def quoted_hierarchy_table_name
464
- connection.quote_table_name hierarchy_table_name
465
- end
466
-
467
- def quoted_parent_column_name
468
- connection.quote_column_name parent_column_name
469
- end
470
-
471
- def order_option
472
- closure_tree_options[:order]
473
- end
474
-
475
- def with_order_option(options)
476
- order_option ? options.merge(:order => order_option) : options
477
- end
478
-
479
- def append_order(order_by)
480
- order_option ? "#{order_by}, #{order_option}" : order_by
481
- end
482
-
483
- def order_is_numeric
484
- # The table might not exist yet (in the case of ActiveRecord::Observer use, see issue 32)
485
- return false if order_option.nil? || !self.table_exists?
486
- c = ct_class.columns_hash[order_option]
487
- c && c.type == :integer
488
- end
489
-
490
- def ct_class
491
- (self.is_a?(Class) ? self : self.class)
492
- end
493
-
494
- # This is the "topmost" class. This will only potentially not be ct_class if you are using STI.
495
- def ct_base_class
496
- ct_class.closure_tree_options[:ct_base_class]
497
- end
498
-
499
- def ct_subclass?
500
- ct_class != ct_class.base_class
501
- end
502
-
503
- def ct_attribute_names
504
- @ct_attr_names ||= ct_class.new.attributes.keys - ct_class.protected_attributes.to_a
505
- end
506
-
507
- def ct_has_type?
508
- ct_attribute_names.include? 'type'
509
- end
510
-
511
- def ct_table_name
512
- ct_class.table_name
513
- end
514
-
515
- def quoted_table_name
516
- connection.quote_table_name ct_table_name
517
- end
518
-
519
- def remove_prefix_and_suffix(table_name)
520
- prefix = Regexp.escape(ActiveRecord::Base.table_name_prefix)
521
- suffix = Regexp.escape(ActiveRecord::Base.table_name_suffix)
522
- table_name.gsub(/^#{prefix}(.+)#{suffix}$/, "\\1")
523
- end
524
- end
525
-
526
- module DeterministicOrdering
527
- def order_column
528
- o = order_option
529
- o.split(' ', 2).first if o
530
- end
531
-
532
- def require_order_column
533
- raise ":order value, '#{order_option}', isn't a column" if order_column.nil?
534
- end
535
-
536
- def order_column_sym
537
- require_order_column
538
- order_column.to_sym
539
- end
540
-
541
- def order_value
542
- read_attribute(order_column_sym)
543
- end
544
-
545
- def order_value=(new_order_value)
546
- write_attribute(order_column_sym, new_order_value)
547
- end
548
-
549
- def quoted_order_column(include_table_name = true)
550
- require_order_column
551
- prefix = include_table_name ? "#{quoted_table_name}." : ""
552
- "#{prefix}#{connection.quote_column_name(order_column)}"
553
- end
554
-
555
- def siblings_before
556
- siblings.where(["#{quoted_order_column} < ?", order_value])
557
- end
558
-
559
- def siblings_after
560
- siblings.where(["#{quoted_order_column} > ?", order_value])
561
- end
562
- end
563
-
564
- # This module is only included if the order column is an integer.
565
- module DeterministicNumericOrdering
566
- def append_sibling(sibling_node, use_update_all = true)
567
- add_sibling(sibling_node, use_update_all, true)
568
- end
569
-
570
- def prepend_sibling(sibling_node, use_update_all = true)
571
- add_sibling(sibling_node, use_update_all, false)
572
- end
573
-
574
- def add_sibling(sibling_node, use_update_all = true, add_after = true)
575
- fail "can't add self as sibling" if self == sibling_node
576
- # issue 40: we need to lock the parent to prevent deadlocks on parallel sibling additions
577
- with_advisory_lock("closure_tree") do
578
- transaction do
579
- # issue 18: we need to set the order_value explicitly so subsequent orders will work.
580
- update_attribute(:order_value, 0) if self.order_value.nil?
581
- sibling_node.order_value = self.order_value.to_i + (add_after ? 1 : -1)
582
- # We need to incr the before_siblings to make room for sibling_node:
583
- if use_update_all
584
- col = quoted_order_column(false)
585
- # issue 21: we have to use the base class, so STI doesn't get in the way of only updating the child class instances:
586
- ct_base_class.update_all(
587
- ["#{col} = #{col} #{add_after ? '+' : '-'} 1", "updated_at = now()"],
588
- ["#{quoted_parent_column_name} = ? AND #{col} #{add_after ? '>=' : '<='} ?",
589
- ct_parent_id,
590
- sibling_node.order_value])
591
- else
592
- last_value = sibling_node.order_value.to_i
593
- (add_after ? siblings_after : siblings_before.reverse).each do |ea|
594
- last_value += (add_after ? 1 : -1)
595
- ea.order_value = last_value
596
- ea.save!
597
- end
598
- end
599
- sibling_node.parent = self.parent
600
- sibling_node.save!
601
- sibling_node.reload
602
- end
603
- end
604
52
  end
605
53
  end
606
54
  end