acts_as_ordered_tree 1.0.5 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/acts_as_ordered_tree/adapters/postgresql_adapter.rb +71 -0
- data/lib/acts_as_ordered_tree/class_methods.rb +43 -0
- data/lib/acts_as_ordered_tree/instance_methods.rb +157 -56
- data/lib/acts_as_ordered_tree/relation/base.rb +24 -0
- data/lib/acts_as_ordered_tree/relation/preloaded.rb +16 -0
- data/lib/acts_as_ordered_tree/relation/recursive.rb +57 -0
- data/lib/acts_as_ordered_tree/tenacious_transaction.rb +30 -0
- data/lib/acts_as_ordered_tree/version.rb +1 -1
- data/lib/acts_as_ordered_tree.rb +9 -32
- data/spec/acts_as_ordered_tree_spec.rb +21 -10
- data/spec/concurrency_support_spec.rb +159 -0
- data/spec/db/config.yml +16 -0
- data/spec/spec_helper.rb +16 -6
- data/spec/support/matchers.rb +22 -1
- metadata +19 -43
- data/lib/acts_as_ordered_tree/fake_scope.rb +0 -28
@@ -0,0 +1,71 @@
|
|
1
|
+
require "acts_as_ordered_tree/relation/recursive"
|
2
|
+
|
3
|
+
module ActsAsOrderedTree
|
4
|
+
module Adapters
|
5
|
+
module PostgreSQLAdapter
|
6
|
+
# Recursive ancestors fetcher
|
7
|
+
def self_and_ancestors
|
8
|
+
if persisted? && !send("#{parent_column}_changed?")
|
9
|
+
query = <<-QUERY
|
10
|
+
SELECT id, #{parent_column}, 1 AS _depth
|
11
|
+
FROM #{self.class.quoted_table_name}
|
12
|
+
WHERE #{arel[:id].eq(id).to_sql}
|
13
|
+
UNION ALL
|
14
|
+
SELECT alias1.id, alias1.#{parent_column}, _depth + 1
|
15
|
+
FROM #{self.class.quoted_table_name} alias1
|
16
|
+
INNER JOIN self_and_ancestors ON alias1.id = self_and_ancestors.#{parent_column}
|
17
|
+
QUERY
|
18
|
+
|
19
|
+
recursive_scope.with_recursive("self_and_ancestors", query).
|
20
|
+
order("self_and_ancestors._depth DESC")
|
21
|
+
else
|
22
|
+
ancestors + [self]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Recursive ancestors fetcher
|
27
|
+
def ancestors
|
28
|
+
query = <<-QUERY
|
29
|
+
SELECT id, #{parent_column}, 1 AS _depth
|
30
|
+
FROM #{self.class.quoted_table_name}
|
31
|
+
WHERE #{arel[:id].eq(parent.try(:id)).to_sql}
|
32
|
+
UNION ALL
|
33
|
+
SELECT alias1.id, alias1.#{parent_column}, _depth + 1
|
34
|
+
FROM #{self.class.quoted_table_name} alias1
|
35
|
+
INNER JOIN ancestors ON alias1.id = ancestors.#{parent_column}
|
36
|
+
QUERY
|
37
|
+
|
38
|
+
recursive_scope.with_recursive("ancestors", query).
|
39
|
+
order("ancestors._depth DESC")
|
40
|
+
end
|
41
|
+
|
42
|
+
def root
|
43
|
+
root? ? self : ancestors.first
|
44
|
+
end
|
45
|
+
|
46
|
+
def self_and_descendants
|
47
|
+
query = <<-QUERY
|
48
|
+
SELECT id, #{parent_column}, ARRAY[#{position_column}] AS _positions
|
49
|
+
FROM #{self.class.quoted_table_name}
|
50
|
+
WHERE #{arel[:id].eq(id).to_sql}
|
51
|
+
UNION ALL
|
52
|
+
SELECT alias1.id, alias1.#{parent_column}, _positions || alias1.#{position_column}
|
53
|
+
FROM descendants INNER JOIN
|
54
|
+
#{self.class.quoted_table_name} alias1 ON alias1.parent_id = descendants.id
|
55
|
+
QUERY
|
56
|
+
|
57
|
+
recursive_scope.with_recursive("descendants", query).
|
58
|
+
order("descendants._positions ASC")
|
59
|
+
end
|
60
|
+
|
61
|
+
def descendants
|
62
|
+
self_and_descendants.where(arel[:id].not_eq(id))
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def recursive_scope
|
67
|
+
ActsAsOrderedTree::Relation::Recursive.new(ordered_tree_scope)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require "acts_as_ordered_tree/adapters/postgresql_adapter"
|
2
|
+
|
1
3
|
module ActsAsOrderedTree
|
2
4
|
module ClassMethods
|
3
5
|
extend ActiveSupport::Concern
|
@@ -24,6 +26,47 @@ module ActsAsOrderedTree
|
|
24
26
|
def children_counter_cache? #:nodoc:
|
25
27
|
children_counter_cache_column && columns_hash.key?(children_counter_cache_column.to_s)
|
26
28
|
end
|
29
|
+
|
30
|
+
def setup_ordered_tree_adapter #:nodoc:
|
31
|
+
include "ActsAsOrderedTree::Adapters::#{connection.class.name.demodulize}".constantize
|
32
|
+
rescue NameError, LoadError
|
33
|
+
# ignore
|
34
|
+
end
|
35
|
+
|
36
|
+
def setup_ordered_tree_callbacks #:nodoc:
|
37
|
+
define_model_callbacks :move, :reorder
|
38
|
+
|
39
|
+
if depth_column
|
40
|
+
before_create :set_depth!
|
41
|
+
before_save :set_depth!, :if => "#{parent_column}_changed?".to_sym
|
42
|
+
around_move :update_descendants_depth
|
43
|
+
end
|
44
|
+
|
45
|
+
if children_counter_cache_column
|
46
|
+
around_move :update_counter_cache
|
47
|
+
end
|
48
|
+
|
49
|
+
unless scope_column_names.empty?
|
50
|
+
before_save :set_scope!, :unless => :root?
|
51
|
+
end
|
52
|
+
|
53
|
+
after_save :move_to_root, :unless => [position_column, parent_column]
|
54
|
+
after_save 'move_to_child_of(parent)', :if => parent_column, :unless => position_column
|
55
|
+
after_save "move_to_child_with_index(parent, #{position_column})",
|
56
|
+
:if => "#{position_column} && (#{position_column}_changed? || #{parent_column}_changed?)"
|
57
|
+
|
58
|
+
before_destroy :flush_descendants
|
59
|
+
after_destroy "decrement_lower_positions(#{parent_column}_was, #{position_column}_was)", :if => position_column
|
60
|
+
end
|
61
|
+
|
62
|
+
def setup_ordered_tree_validations #:nodoc:
|
63
|
+
unless scope_column_names.empty?
|
64
|
+
validates_with Validators::ScopeValidator, :on => :update, :unless => :root?
|
65
|
+
end
|
66
|
+
|
67
|
+
# setup validations
|
68
|
+
validates_with Validators::CyclicReferenceValidator, :on => :update, :if => :parent
|
69
|
+
end
|
27
70
|
end # module ClassMethods
|
28
71
|
end # module ClassMethods
|
29
72
|
end # module ActsAsOrderedTree
|
@@ -1,7 +1,10 @@
|
|
1
1
|
# coding: utf-8
|
2
|
+
require "acts_as_ordered_tree/tenacious_transaction"
|
3
|
+
require "acts_as_ordered_tree/relation/preloaded"
|
4
|
+
|
2
5
|
module ActsAsOrderedTree
|
3
6
|
module InstanceMethods
|
4
|
-
|
7
|
+
include ActsAsOrderedTree::TenaciousTransaction
|
5
8
|
|
6
9
|
# Returns true if this is a root node.
|
7
10
|
def root?
|
@@ -46,7 +49,9 @@ module ActsAsOrderedTree
|
|
46
49
|
nodes.reverse!
|
47
50
|
|
48
51
|
# 3. create fake scope
|
49
|
-
ActsAsOrderedTree::
|
52
|
+
ActsAsOrderedTree::Relation::Preloaded.new(self.class).
|
53
|
+
where(:id => nodes.map(&:id)).
|
54
|
+
records(nodes)
|
50
55
|
end
|
51
56
|
|
52
57
|
# Returns the array of all parents starting from root
|
@@ -54,7 +59,7 @@ module ActsAsOrderedTree
|
|
54
59
|
records = self_and_ancestors - [self]
|
55
60
|
|
56
61
|
scope = self_and_ancestors.where(arel[:id].not_eq(id))
|
57
|
-
|
62
|
+
scope.records(records)
|
58
63
|
end
|
59
64
|
|
60
65
|
# Returns the array of all children of the parent, including self
|
@@ -87,14 +92,18 @@ module ActsAsOrderedTree
|
|
87
92
|
def descendants
|
88
93
|
records = fetch_self_and_descendants - [self]
|
89
94
|
|
90
|
-
ActsAsOrderedTree::
|
95
|
+
ActsAsOrderedTree::Relation::Preloaded.new(self.class).
|
96
|
+
where(:id => records.map(&:id)).
|
97
|
+
records(records)
|
91
98
|
end
|
92
99
|
|
93
100
|
# Returns a set of itself and all of its nested children
|
94
101
|
def self_and_descendants
|
95
102
|
records = fetch_self_and_descendants
|
96
103
|
|
97
|
-
ActsAsOrderedTree::
|
104
|
+
ActsAsOrderedTree::Relation::Preloaded.new(self.class).
|
105
|
+
where(:id => records.map(&:id)).
|
106
|
+
records(records)
|
98
107
|
end
|
99
108
|
|
100
109
|
def is_descendant_of?(other)
|
@@ -149,13 +158,17 @@ module ActsAsOrderedTree
|
|
149
158
|
|
150
159
|
# Shorthand method for finding the left sibling and moving to the left of it.
|
151
160
|
def move_left
|
152
|
-
|
161
|
+
tenacious_transaction do
|
162
|
+
move_to_left_of left_sibling.try(:lock!)
|
163
|
+
end
|
153
164
|
end
|
154
165
|
alias move_higher move_left
|
155
166
|
|
156
167
|
# Shorthand method for finding the right sibling and moving to the right of it.
|
157
168
|
def move_right
|
158
|
-
|
169
|
+
tenacious_transaction do
|
170
|
+
move_to_right_of right_sibling.try(:lock!)
|
171
|
+
end
|
159
172
|
end
|
160
173
|
alias move_lower move_right
|
161
174
|
|
@@ -178,15 +191,21 @@ module ActsAsOrderedTree
|
|
178
191
|
|
179
192
|
# Move the node to the child of another node with specify index
|
180
193
|
def move_to_child_with_index(node, index)
|
181
|
-
raise ActiveRecord::ActiveRecordError, "index
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
194
|
+
raise ActiveRecord::ActiveRecordError, "index can't be nil" unless index
|
195
|
+
|
196
|
+
tenacious_transaction do
|
197
|
+
new_siblings = (node.try(:children) || self.class.roots).
|
198
|
+
reload.
|
199
|
+
lock(true).
|
200
|
+
reject { |root_node| root_node == self }
|
201
|
+
|
202
|
+
if new_siblings.empty?
|
203
|
+
node ? move_to_child_of(node) : move_to_root
|
204
|
+
elsif new_siblings.count <= index
|
205
|
+
move_to_right_of(new_siblings.last)
|
206
|
+
elsif
|
207
|
+
index >= 0 ? move_to_left_of(new_siblings[index]) : move_to_right_of(new_siblings[index])
|
208
|
+
end
|
190
209
|
end
|
191
210
|
end
|
192
211
|
|
@@ -247,9 +266,10 @@ module ActsAsOrderedTree
|
|
247
266
|
when :child then
|
248
267
|
parent_id = target.id
|
249
268
|
position = if self[parent_column] == parent_id && self[position_column]
|
250
|
-
# already
|
269
|
+
# already child of target node
|
251
270
|
self[position_column]
|
252
271
|
else
|
272
|
+
# lock should be obtained on target
|
253
273
|
target.children.maximum(position_column).try(:succ) || 1
|
254
274
|
end
|
255
275
|
depth = target.level + 1
|
@@ -260,58 +280,138 @@ module ActsAsOrderedTree
|
|
260
280
|
|
261
281
|
# This method do real node movements
|
262
282
|
def move_to(target, pos) #:nodoc:
|
263
|
-
|
264
|
-
target.
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
283
|
+
tenacious_transaction do
|
284
|
+
if target.is_a? self.class.base_class
|
285
|
+
# lock obtained here
|
286
|
+
target.send(:reload_node)
|
287
|
+
elsif pos != :root && target
|
288
|
+
# load object if node is not an object
|
289
|
+
target = self.class.find(target, :lock => true)
|
290
|
+
elsif pos == :root
|
291
|
+
# Obtain lock on all root nodes
|
292
|
+
ordered_tree_scope.
|
293
|
+
roots.
|
294
|
+
lock(true).
|
295
|
+
reload
|
296
|
+
end
|
273
297
|
|
274
|
-
|
275
|
-
|
276
|
-
|
298
|
+
unless pos == :root || target && move_possible?(target)
|
299
|
+
raise ActiveRecord::ActiveRecordError, "Impossible move"
|
300
|
+
end
|
277
301
|
|
278
|
-
|
279
|
-
|
302
|
+
position_was = send "#{position_column}_was".intern
|
303
|
+
parent_id_was = send "#{parent_column}_was".intern
|
304
|
+
parent_id, position, depth = compute_ordered_tree_columns(target, pos)
|
280
305
|
|
281
|
-
|
282
|
-
|
283
|
-
increment_lower_positions parent_id, position
|
306
|
+
# nothing changed - quit
|
307
|
+
return if parent_id == parent_id_was && position == position_was
|
284
308
|
|
285
|
-
|
286
|
-
|
309
|
+
move_kind = case
|
310
|
+
when id_was && parent_id != parent_id_was then :move
|
311
|
+
when id_was && position != position_was then :reorder
|
312
|
+
else nil
|
313
|
+
end
|
287
314
|
|
288
|
-
|
289
|
-
|
290
|
-
|
315
|
+
update = proc do
|
316
|
+
if move_kind == :move
|
317
|
+
move!(id, parent_id_was, parent_id, position_was, position, depth)
|
318
|
+
else
|
319
|
+
reorder!(parent_id, position_was, position)
|
320
|
+
end
|
291
321
|
|
292
|
-
|
293
|
-
|
294
|
-
when id_was && position != position_was then :reorder
|
295
|
-
else nil
|
296
|
-
end
|
322
|
+
reload_node
|
323
|
+
end
|
297
324
|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
325
|
+
if move_kind
|
326
|
+
run_callbacks move_kind, &update
|
327
|
+
else
|
328
|
+
update.call
|
329
|
+
end
|
302
330
|
end
|
303
331
|
end
|
304
332
|
|
305
333
|
def decrement_lower_positions(parent_id, position) #:nodoc:
|
306
334
|
conditions = arel[parent_column].eq(parent_id).and(arel[position_column].gt(position))
|
307
335
|
|
308
|
-
ordered_tree_scope.update_all
|
309
|
-
end
|
336
|
+
ordered_tree_scope.where(conditions).update_all("#{position_column} = #{position_column} - 1")
|
337
|
+
end
|
338
|
+
|
339
|
+
# Internal
|
340
|
+
def move!(id, parent_id_was, parent_id, position_was, position, depth) #:nodoc:
|
341
|
+
pk = self.class.primary_key
|
342
|
+
|
343
|
+
assignments = [
|
344
|
+
"#{parent_column} = CASE " +
|
345
|
+
"WHEN #{pk} = :id " +
|
346
|
+
"THEN :parent_id " +
|
347
|
+
"ELSE #{parent_column} " +
|
348
|
+
"END",
|
349
|
+
"#{position_column} = CASE " +
|
350
|
+
# set new position
|
351
|
+
"WHEN #{pk} = :id " +
|
352
|
+
"THEN :position " +
|
353
|
+
# decrement lower positions within old parent
|
354
|
+
"WHEN #{parent_column} #{parent_id_was.nil? ? " IS NULL" : " = :parent_id_was"} AND #{position_column} > :position_was " +
|
355
|
+
"THEN #{position_column} - 1 " +
|
356
|
+
# increment lower positions within new parent
|
357
|
+
"WHEN #{parent_column} #{parent_id.nil? ? "IS NULL" : " = :parent_id"} AND #{position_column} >= :position " +
|
358
|
+
"THEN #{position_column} + 1 " +
|
359
|
+
"ELSE #{position_column} " +
|
360
|
+
"END",
|
361
|
+
("#{depth_column} = CASE " +
|
362
|
+
"WHEN #{pk} = :id " +
|
363
|
+
"THEN :depth " +
|
364
|
+
"ELSE #{depth_column} " +
|
365
|
+
"END" if depth_column)
|
366
|
+
].compact.join(', ')
|
367
|
+
|
368
|
+
conditions = arel[pk].eq(id).or(
|
369
|
+
arel[parent_column].eq(parent_id_was)
|
370
|
+
).or(
|
371
|
+
arel[parent_column].eq(parent_id)
|
372
|
+
)
|
373
|
+
|
374
|
+
binds = {:id => id,
|
375
|
+
:parent_id_was => parent_id_was,
|
376
|
+
:parent_id => parent_id,
|
377
|
+
:position_was => position_was,
|
378
|
+
:position => position,
|
379
|
+
:depth => depth}
|
380
|
+
|
381
|
+
ordered_tree_scope.where(conditions).update_all([assignments, binds])
|
382
|
+
end
|
383
|
+
|
384
|
+
# Internal
|
385
|
+
def reorder!(parent_id, position_was, position)
|
386
|
+
assignments = if position_was
|
387
|
+
<<-SQL
|
388
|
+
#{position_column} = CASE
|
389
|
+
WHEN #{position_column} = :position_was
|
390
|
+
THEN :position
|
391
|
+
WHEN #{position_column} <= :position AND #{position_column} > :position_was AND :position > :position_was
|
392
|
+
THEN #{position_column} - 1
|
393
|
+
WHEN #{position_column} >= :position AND #{position_column} < :position_was AND :position < :position_was
|
394
|
+
THEN #{position_column} + 1
|
395
|
+
ELSE #{position_column}
|
396
|
+
END
|
397
|
+
SQL
|
398
|
+
else
|
399
|
+
<<-SQL
|
400
|
+
#{position_column} = CASE
|
401
|
+
WHEN #{position_column} > :position
|
402
|
+
THEN #{position_column} + 1
|
403
|
+
WHEN #{position_column} IS NULL
|
404
|
+
THEN :position
|
405
|
+
ELSE #{position_column}
|
406
|
+
END
|
407
|
+
SQL
|
408
|
+
end
|
409
|
+
|
410
|
+
conditions = arel[parent_column].eq(parent_id)
|
310
411
|
|
311
|
-
|
312
|
-
conditions = arel[parent_column].eq(parent_id).and(arel[position_column].gteq(position))
|
412
|
+
binds = {:position_was => position_was, :position => position}
|
313
413
|
|
314
|
-
ordered_tree_scope.update_all
|
414
|
+
ordered_tree_scope.where(conditions).update_all([assignments, binds])
|
315
415
|
end
|
316
416
|
|
317
417
|
# recursively load descendants
|
@@ -342,10 +442,11 @@ module ActsAsOrderedTree
|
|
342
442
|
if diff != 0
|
343
443
|
sign = diff > 0 ? "+" : "-"
|
344
444
|
# update categories set depth = depth - 1 where id in (...)
|
345
|
-
descendants.update_all(["#{depth_column} = #{depth_column} #{sign} ?", diff.abs])
|
445
|
+
descendants.update_all(["#{depth_column} = #{depth_column} #{sign} ?", diff.abs]) if descendants.count > 0
|
346
446
|
end
|
347
447
|
end
|
348
448
|
|
449
|
+
# Used in built-in around_move routine
|
349
450
|
def update_counter_cache #:nodoc:
|
350
451
|
parent_id_was = self[parent_column]
|
351
452
|
|
@@ -362,7 +463,7 @@ module ActsAsOrderedTree
|
|
362
463
|
self.class.arel_table
|
363
464
|
end
|
364
465
|
|
365
|
-
def ordered_tree_scope
|
466
|
+
def ordered_tree_scope #:nodoc:
|
366
467
|
if scope_column_names.empty?
|
367
468
|
self.class.base_class.scoped
|
368
469
|
else
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActsAsOrderedTree
|
2
|
+
module Relation
|
3
|
+
class Base < ActiveRecord::Relation
|
4
|
+
# Create from existing +relation+ or from +class+ and +table+
|
5
|
+
def initialize(class_or_relation, table = nil)
|
6
|
+
relation = class_or_relation
|
7
|
+
|
8
|
+
if class_or_relation.is_a?(Class)
|
9
|
+
relation = class_or_relation.scoped
|
10
|
+
table ||= class_or_relation.arel_table
|
11
|
+
|
12
|
+
super(class_or_relation, table)
|
13
|
+
else
|
14
|
+
super(class_or_relation.klass, class_or_relation.table)
|
15
|
+
end
|
16
|
+
|
17
|
+
# copy instance variables from real relation
|
18
|
+
relation.instance_variables.each do |ivar|
|
19
|
+
instance_variable_set(ivar, relation.instance_variable_get(ivar))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require "acts_as_ordered_tree/relation/base"
|
2
|
+
|
3
|
+
module ActsAsOrderedTree
|
4
|
+
module Relation
|
5
|
+
# Common relation, but with already loaded records
|
6
|
+
class Preloaded < Base
|
7
|
+
# Set loaded records to +records+
|
8
|
+
def records(records)
|
9
|
+
relation = clone
|
10
|
+
relation.instance_variable_set :@records, records
|
11
|
+
relation.instance_variable_set :@loaded, true
|
12
|
+
relation
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require "acts_as_ordered_tree/relation/base"
|
2
|
+
|
3
|
+
module ActsAsOrderedTree
|
4
|
+
module Relation
|
5
|
+
# Recursive relation fixes Rails3.0 issue https://github.com/rails/rails/issues/522 for
|
6
|
+
# relations with joins to subqueries
|
7
|
+
class Recursive < Base
|
8
|
+
attr_accessor :recursive_table_value, :recursive_query_value
|
9
|
+
|
10
|
+
# relation.with_recursive("table_name", "SELECT * FROM table_name")
|
11
|
+
def with_recursive(recursive_table_name, query)
|
12
|
+
relation = clone
|
13
|
+
relation.recursive_table_value = recursive_table_name
|
14
|
+
relation.recursive_query_value = query
|
15
|
+
relation
|
16
|
+
end
|
17
|
+
|
18
|
+
def build_arel
|
19
|
+
if recursive_table_value && recursive_query_value
|
20
|
+
join_sql = "INNER JOIN (" +
|
21
|
+
recursive_query_sql +
|
22
|
+
") AS #{recursive_table_value} ON #{recursive_table_value}.id = #{table.name}.id"
|
23
|
+
|
24
|
+
except(:recursive_table, :recursive_query).joins(join_sql).build_arel
|
25
|
+
else
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def update_all(updates, conditions = nil, options = {})
|
31
|
+
if recursive_table_value && recursive_query_value
|
32
|
+
scope = where("id IN (SELECT id FROM (#{recursive_query_sql}) AS #{recursive_table_value})").
|
33
|
+
except(:recursive_table, :recursive_query, :limit, :order)
|
34
|
+
|
35
|
+
scope.update_all(updates, conditions, options)
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def except(*skips)
|
42
|
+
result = super
|
43
|
+
([:recursive_table, :recursive_query] - skips).each do |method|
|
44
|
+
result.send("#{method}_value=", send(:"#{method}_value"))
|
45
|
+
end
|
46
|
+
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def recursive_query_sql
|
52
|
+
"WITH RECURSIVE #{recursive_table_value} AS (#{recursive_query_value}) " +
|
53
|
+
"SELECT * FROM #{recursive_table_value}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module ActsAsOrderedTree
|
2
|
+
module TenaciousTransaction
|
3
|
+
DEADLOCK_MESSAGES = /Deadlock found when trying to get lock|Lock wait timeout exceeded|deadlock detected/.freeze
|
4
|
+
RETRY_COUNT = 10
|
5
|
+
|
6
|
+
# Partially borrowed from awesome_nested_set
|
7
|
+
def tenacious_transaction(&block) #:nodoc:
|
8
|
+
return transaction(&block) if @in_tenacious_transaction
|
9
|
+
|
10
|
+
@in_tenacious_transaction = true
|
11
|
+
retry_count = 0
|
12
|
+
begin
|
13
|
+
transaction(&block)
|
14
|
+
rescue ActiveRecord::StatementInvalid => error
|
15
|
+
raise unless connection.open_transactions.zero?
|
16
|
+
raise unless error.message =~ DEADLOCK_MESSAGES
|
17
|
+
raise unless retry_count < RETRY_COUNT
|
18
|
+
retry_count += 1
|
19
|
+
|
20
|
+
logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
|
21
|
+
|
22
|
+
sleep(rand(retry_count)*0.1) # Aloha protocol
|
23
|
+
|
24
|
+
retry
|
25
|
+
ensure
|
26
|
+
@in_tenacious_transaction = false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/acts_as_ordered_tree.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require "active_record"
|
2
2
|
require "acts_as_ordered_tree/version"
|
3
3
|
require "acts_as_ordered_tree/class_methods"
|
4
|
-
require "acts_as_ordered_tree/fake_scope"
|
5
4
|
require "acts_as_ordered_tree/instance_methods"
|
6
5
|
require "acts_as_ordered_tree/validators"
|
7
6
|
|
@@ -56,43 +55,21 @@ module ActsAsOrderedTree
|
|
56
55
|
:counter_cache => options[:counter_cache],
|
57
56
|
:inverse_of => (:children unless options[:polymorphic])
|
58
57
|
|
59
|
-
define_model_callbacks :move, :reorder
|
60
|
-
|
61
58
|
include ClassMethods
|
62
59
|
include InstanceMethods
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
if depth_column
|
68
|
-
before_create :set_depth!
|
69
|
-
before_save :set_depth!, :if => "#{parent_column}_changed?".to_sym
|
70
|
-
around_move :update_descendants_depth
|
71
|
-
end
|
72
|
-
|
73
|
-
if children_counter_cache_column
|
74
|
-
around_move :update_counter_cache
|
75
|
-
end
|
76
|
-
|
77
|
-
unless scope_column_names.empty?
|
78
|
-
before_save :set_scope!, :unless => :root?
|
79
|
-
validates_with Validators::ScopeValidator, :on => :update, :unless => :root?
|
80
|
-
end
|
81
|
-
|
82
|
-
after_save :move_to_root, :unless => [position_column, parent_column]
|
83
|
-
after_save 'move_to_child_of(parent)', :if => parent_column, :unless => position_column
|
84
|
-
after_save "move_to_child_with_index(parent, #{position_column})",
|
85
|
-
:if => "#{position_column} && (#{position_column}_changed? || #{parent_column}_changed?)"
|
86
|
-
|
87
|
-
before_destroy :flush_descendants
|
88
|
-
after_destroy "decrement_lower_positions(#{parent_column}_was, #{position_column}_was)", :if => position_column
|
89
|
-
|
90
|
-
# setup validations
|
91
|
-
validates_with Validators::CyclicReferenceValidator, :on => :update, :if => :parent
|
60
|
+
setup_ordered_tree_adapter
|
61
|
+
setup_ordered_tree_callbacks
|
62
|
+
setup_ordered_tree_validations
|
92
63
|
end # def acts_as_ordered_tree
|
93
64
|
|
94
65
|
# Mixed into both classes and instances to provide easy access to the column names
|
95
66
|
module Columns
|
67
|
+
extend ActiveSupport::Concern
|
68
|
+
|
69
|
+
included do
|
70
|
+
attr_protected depth_column, position_column
|
71
|
+
end
|
72
|
+
|
96
73
|
def parent_column
|
97
74
|
acts_as_ordered_tree_options[:parent_column]
|
98
75
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require File.expand_path('../spec_helper', __FILE__)
|
2
2
|
|
3
|
-
describe ActsAsOrderedTree do
|
3
|
+
describe ActsAsOrderedTree, :transactional do
|
4
4
|
describe "defaults" do
|
5
5
|
subject { Default }
|
6
6
|
|
@@ -207,7 +207,6 @@ describe ActsAsOrderedTree do
|
|
207
207
|
subject { grandchild.self_and_ancestors }
|
208
208
|
|
209
209
|
it { should be_a ActiveRecord::Relation }
|
210
|
-
it { should be_loaded }
|
211
210
|
it { should have(3).items }
|
212
211
|
its(:first) { should eq root }
|
213
212
|
its(:last) { should eq subject }
|
@@ -217,7 +216,6 @@ describe ActsAsOrderedTree do
|
|
217
216
|
subject { child.self_and_ancestors }
|
218
217
|
|
219
218
|
it { should be_a ActiveRecord::Relation }
|
220
|
-
it { should be_loaded }
|
221
219
|
it { should have(2).items }
|
222
220
|
its(:first) { should eq root }
|
223
221
|
its(:last) { should eq subject }
|
@@ -227,10 +225,29 @@ describe ActsAsOrderedTree do
|
|
227
225
|
subject { root.self_and_ancestors }
|
228
226
|
|
229
227
|
it { should be_a ActiveRecord::Relation }
|
230
|
-
it { should be_loaded }
|
231
228
|
it { should have(1).item }
|
232
229
|
its(:first) { should eq root }
|
233
230
|
end
|
231
|
+
|
232
|
+
context "when record is new" do
|
233
|
+
let(:record) { build(:default, :parent => grandchild) }
|
234
|
+
subject { record.self_and_ancestors }
|
235
|
+
|
236
|
+
it { should have(4).items }
|
237
|
+
it { should include root }
|
238
|
+
it { should include child }
|
239
|
+
it { should include grandchild }
|
240
|
+
it { should include record }
|
241
|
+
end
|
242
|
+
|
243
|
+
context "when parent is changed" do
|
244
|
+
before { grandchild.parent = root }
|
245
|
+
subject { grandchild.self_and_ancestors }
|
246
|
+
|
247
|
+
it { should include root }
|
248
|
+
it { should_not include child }
|
249
|
+
it { should include grandchild }
|
250
|
+
end
|
234
251
|
end
|
235
252
|
|
236
253
|
describe "#ancestors" do
|
@@ -243,7 +260,6 @@ describe ActsAsOrderedTree do
|
|
243
260
|
subject { grandchild.ancestors }
|
244
261
|
|
245
262
|
it { should be_a ActiveRecord::Relation }
|
246
|
-
it { should be_loaded }
|
247
263
|
it { should have(2).items }
|
248
264
|
its(:first) { should eq root }
|
249
265
|
its(:last) { should eq child }
|
@@ -253,7 +269,6 @@ describe ActsAsOrderedTree do
|
|
253
269
|
subject { child.ancestors }
|
254
270
|
|
255
271
|
it { should be_a ActiveRecord::Relation }
|
256
|
-
it { should be_loaded }
|
257
272
|
it { should have(1).item }
|
258
273
|
its(:first) { should eq root }
|
259
274
|
end
|
@@ -262,7 +277,6 @@ describe ActsAsOrderedTree do
|
|
262
277
|
subject { root.ancestors }
|
263
278
|
|
264
279
|
it { should be_a ActiveRecord::Relation }
|
265
|
-
it { should be_loaded }
|
266
280
|
it { should be_empty }
|
267
281
|
end
|
268
282
|
end
|
@@ -277,7 +291,6 @@ describe ActsAsOrderedTree do
|
|
277
291
|
subject { grandchild.self_and_descendants }
|
278
292
|
|
279
293
|
it { should be_a ActiveRecord::Relation }
|
280
|
-
it { should be_loaded }
|
281
294
|
it { should have(1).item }
|
282
295
|
its(:first) { should eq grandchild }
|
283
296
|
end
|
@@ -286,7 +299,6 @@ describe ActsAsOrderedTree do
|
|
286
299
|
subject { child.self_and_descendants }
|
287
300
|
|
288
301
|
it { should be_a ActiveRecord::Relation }
|
289
|
-
it { should be_loaded }
|
290
302
|
it { should have(2).items }
|
291
303
|
its(:first) { should eq child }
|
292
304
|
its(:last) { should eq grandchild }
|
@@ -296,7 +308,6 @@ describe ActsAsOrderedTree do
|
|
296
308
|
subject { root.self_and_descendants }
|
297
309
|
|
298
310
|
it { should be_a ActiveRecord::Relation }
|
299
|
-
it { should be_loaded }
|
300
311
|
it { should have(3).items }
|
301
312
|
its(:first) { should eq root }
|
302
313
|
its(:last) { should eq grandchild }
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
# FIXME: These tests are buggy on Rails 3.0
|
4
|
+
# Sqlite is not concurrent database
|
5
|
+
if ActiveRecord::VERSION::STRING >= "3.1" && ENV['DB'] != 'sqlite3'
|
6
|
+
describe ActsAsOrderedTree, :non_transactional do
|
7
|
+
module Concurrency
|
8
|
+
# run block in its own thread, create +size+ threads
|
9
|
+
def pool(size)
|
10
|
+
body = proc do |x|
|
11
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
12
|
+
yield x
|
13
|
+
end
|
14
|
+
end
|
15
|
+
threads = size.times.map { |x| Thread.new { body.call(x) } }
|
16
|
+
|
17
|
+
threads.each(&:join)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
include Concurrency
|
21
|
+
|
22
|
+
let!(:root) { create :default }
|
23
|
+
|
24
|
+
it "should not create nodes with same position" do
|
25
|
+
pool(3) do
|
26
|
+
create :default, :parent => root
|
27
|
+
end
|
28
|
+
|
29
|
+
root.children.map(&:position).should eq [1, 2, 3]
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should not move nodes to same position when moving to child of certain node" do
|
33
|
+
nodes = create_list :default, 3
|
34
|
+
|
35
|
+
pool(3) do |x|
|
36
|
+
nodes[x].move_to_child_of(root)
|
37
|
+
end
|
38
|
+
|
39
|
+
root.children.map(&:position).should eq [1, 2, 3]
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should not move nodes to same position when moving to left of root node" do
|
43
|
+
nodes = create_list :default, 3, :parent => root
|
44
|
+
|
45
|
+
pool(3) do |x|
|
46
|
+
nodes[x].move_to_left_of(root)
|
47
|
+
end
|
48
|
+
|
49
|
+
Default.roots.map(&:position).should eq [1, 2, 3, 4]
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should not move nodes to same position when moving to left of child node" do
|
53
|
+
child = create :default, :parent => root
|
54
|
+
nodes = create_list :default, 3, :parent => child
|
55
|
+
|
56
|
+
pool(3) do |x|
|
57
|
+
nodes[x].move_to_left_of(child)
|
58
|
+
end
|
59
|
+
|
60
|
+
root.children.map(&:position).should eq [1, 2, 3, 4]
|
61
|
+
root.children.last.should eq child
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should not move nodes to same position when moving to right of child node" do
|
65
|
+
child = create :default, :parent => root
|
66
|
+
nodes = create_list :default, 3, :parent => child
|
67
|
+
|
68
|
+
pool(3) do |x|
|
69
|
+
nodes[x].move_to_right_of(child)
|
70
|
+
end
|
71
|
+
|
72
|
+
root.children.map(&:position).should eq [1, 2, 3, 4]
|
73
|
+
root.children.first.should eq child
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should not move nodes to same position when moving to root" do
|
77
|
+
nodes = create_list :default, 3, :parent => root
|
78
|
+
|
79
|
+
pool(3) do |x|
|
80
|
+
nodes[x].move_to_root
|
81
|
+
end
|
82
|
+
|
83
|
+
Default.roots.map(&:position).should eq [1, 2, 3, 4]
|
84
|
+
end
|
85
|
+
|
86
|
+
# checking deadlock also
|
87
|
+
it "should not move nodes to same position when moving to specified index" do
|
88
|
+
# root
|
89
|
+
# * child1
|
90
|
+
# * nodes1_1
|
91
|
+
# * nodes1_2
|
92
|
+
# * child2
|
93
|
+
# * nodes2_1
|
94
|
+
# * nodes2_2
|
95
|
+
child1, child2 = create_list :default, 2, :parent => root
|
96
|
+
|
97
|
+
nodes1, nodes2 = create_list(:default, 2, :parent => child1),
|
98
|
+
create_list(:default, 2, :parent => child2)
|
99
|
+
|
100
|
+
nodes1_1, nodes1_2 = nodes1
|
101
|
+
nodes2_1, nodes2_2 = nodes2
|
102
|
+
|
103
|
+
# nodes2_2 -> child1[0]
|
104
|
+
thread1 = Thread.new do
|
105
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
106
|
+
nodes2_2.move_to_child_with_index(child1, 0)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
# nodes1_1 -> child2[2]
|
110
|
+
thread2 = Thread.new do
|
111
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
112
|
+
nodes1_1.move_to_child_with_index(child2, 2)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
[thread1, thread2].map(&:join)
|
116
|
+
|
117
|
+
child1.children.reload.should == [nodes2_2, nodes1_2]
|
118
|
+
child2.children.reload.should == [nodes2_1, nodes1_1]
|
119
|
+
end
|
120
|
+
|
121
|
+
it "should not move nodes to same position when moving higher" do
|
122
|
+
child1, child2, child3 = create_list :default, 3, :parent => root
|
123
|
+
|
124
|
+
thread1 = Thread.new do
|
125
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
126
|
+
child2.move_higher
|
127
|
+
end
|
128
|
+
end
|
129
|
+
thread2 = Thread.new do
|
130
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
131
|
+
child3.move_higher
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
[thread1, thread2].map(&:join)
|
136
|
+
|
137
|
+
root.children.map(&:position).should eq [1, 2, 3]
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should not move nodes to same position when moving lower" do
|
141
|
+
child1, child2, child3 = create_list :default, 3, :parent => root
|
142
|
+
|
143
|
+
thread1 = Thread.new do
|
144
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
145
|
+
child1.move_lower
|
146
|
+
end
|
147
|
+
end
|
148
|
+
thread2 = Thread.new do
|
149
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
150
|
+
child2.move_lower
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
[thread1, thread2].map(&:join)
|
155
|
+
|
156
|
+
root.children.map(&:position).should eq [1, 2, 3]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
data/spec/db/config.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
pg:
|
2
|
+
adapter: postgresql
|
3
|
+
username: postgres
|
4
|
+
database: acts_as_ordered_tree_test
|
5
|
+
host: 127.0.0.1
|
6
|
+
encoding: unicode
|
7
|
+
min_messages: ERROR
|
8
|
+
mysql:
|
9
|
+
adapter: mysql2
|
10
|
+
database: acts_as_ordered_tree_test
|
11
|
+
username: root
|
12
|
+
password:
|
13
|
+
encoding: utf8
|
14
|
+
sqlite3:
|
15
|
+
adapter: sqlite3
|
16
|
+
database: acts_as_ordered_tree.sqlite3.db
|
data/spec/spec_helper.rb
CHANGED
@@ -13,20 +13,19 @@ rescue LoadError
|
|
13
13
|
#ignore
|
14
14
|
end
|
15
15
|
|
16
|
-
require "active_model"
|
17
16
|
require "active_record"
|
18
|
-
require "action_controller"
|
19
17
|
require "factory_girl"
|
20
18
|
|
21
19
|
require "acts_as_ordered_tree"
|
22
20
|
require "logger"
|
21
|
+
require "yaml"
|
23
22
|
|
24
|
-
ActiveRecord::Base.
|
23
|
+
ActiveRecord::Base.configurations = YAML::load(IO.read(test_dir + "/db/config.yml"))
|
24
|
+
ActiveRecord::Base.establish_connection(ENV['DB'] || "pg")
|
25
25
|
ActiveRecord::Base.logger = Logger.new(ENV['DEBUG'] ? $stderr : '/dev/null')
|
26
26
|
ActiveRecord::Migration.verbose = false
|
27
27
|
load(File.join(test_dir, "db", "schema.rb"))
|
28
28
|
|
29
|
-
require "rspec/rails"
|
30
29
|
require "shoulda-matchers"
|
31
30
|
require "support/models"
|
32
31
|
require "support/factories"
|
@@ -34,13 +33,24 @@ require "support/matchers"
|
|
34
33
|
|
35
34
|
RSpec.configure do |config|
|
36
35
|
config.include FactoryGirl::Syntax::Methods
|
37
|
-
config.use_transactional_fixtures = true
|
38
36
|
|
39
|
-
config.
|
37
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
38
|
+
|
39
|
+
config.around :each, :transactional do |example|
|
40
40
|
ActiveRecord::Base.transaction do
|
41
41
|
example.run
|
42
42
|
|
43
43
|
raise ActiveRecord::Rollback
|
44
44
|
end
|
45
45
|
end
|
46
|
+
|
47
|
+
config.around :each, :non_transactional do |example|
|
48
|
+
begin
|
49
|
+
example.run
|
50
|
+
ensure
|
51
|
+
Default.delete_all
|
52
|
+
DefaultWithCounterCache.delete_all
|
53
|
+
Scoped.delete_all
|
54
|
+
end
|
55
|
+
end
|
46
56
|
end
|
data/spec/support/matchers.rb
CHANGED
@@ -118,7 +118,7 @@ module RSpec::Matchers
|
|
118
118
|
def matches?(*records)
|
119
119
|
@records = Array.wrap(records).flatten
|
120
120
|
|
121
|
-
@records.sort_by { |record| record[record.position_column] } == @records
|
121
|
+
@records.sort_by { |record| record.reload[record.position_column] } == @records
|
122
122
|
end
|
123
123
|
|
124
124
|
def failure_message_for_should
|
@@ -129,4 +129,25 @@ module RSpec::Matchers
|
|
129
129
|
"expected #{@records.inspect} not to be ordered by position, but they are"
|
130
130
|
end
|
131
131
|
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Taken from rspec-rails
|
135
|
+
module ::ActiveModel::Validations
|
136
|
+
# Extension to enhance `should have` on AR Model instances. Calls
|
137
|
+
# model.valid? in order to prepare the object's errors object.
|
138
|
+
#
|
139
|
+
# You can also use this to specify the content of the error messages.
|
140
|
+
#
|
141
|
+
# @example
|
142
|
+
#
|
143
|
+
# model.should have(:no).errors_on(:attribute)
|
144
|
+
# model.should have(1).error_on(:attribute)
|
145
|
+
# model.should have(n).errors_on(:attribute)
|
146
|
+
#
|
147
|
+
# model.errors_on(:attribute).should include("can't be blank")
|
148
|
+
def errors_on(attribute)
|
149
|
+
self.valid?
|
150
|
+
[self.errors[attribute]].flatten.compact
|
151
|
+
end
|
152
|
+
alias :error_on :errors_on
|
132
153
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: acts_as_ordered_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2012-
|
13
|
+
date: 2012-09-14 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -29,13 +29,13 @@ dependencies:
|
|
29
29
|
- !ruby/object:Gem::Version
|
30
30
|
version: 3.0.0
|
31
31
|
- !ruby/object:Gem::Dependency
|
32
|
-
name:
|
32
|
+
name: rake
|
33
33
|
requirement: !ruby/object:Gem::Requirement
|
34
34
|
none: false
|
35
35
|
requirements:
|
36
36
|
- - ! '>='
|
37
37
|
- !ruby/object:Gem::Version
|
38
|
-
version:
|
38
|
+
version: 0.9.2
|
39
39
|
type: :development
|
40
40
|
prerelease: false
|
41
41
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -43,15 +43,15 @@ dependencies:
|
|
43
43
|
requirements:
|
44
44
|
- - ! '>='
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version:
|
46
|
+
version: 0.9.2
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
48
|
+
name: bundler
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: '1.0'
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
57
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -59,7 +59,7 @@ dependencies:
|
|
59
59
|
requirements:
|
60
60
|
- - ! '>='
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version:
|
62
|
+
version: '1.0'
|
63
63
|
- !ruby/object:Gem::Dependency
|
64
64
|
name: rspec
|
65
65
|
requirement: !ruby/object:Gem::Requirement
|
@@ -76,22 +76,6 @@ dependencies:
|
|
76
76
|
- - ! '>='
|
77
77
|
- !ruby/object:Gem::Version
|
78
78
|
version: '2.11'
|
79
|
-
- !ruby/object:Gem::Dependency
|
80
|
-
name: rspec-rails
|
81
|
-
requirement: !ruby/object:Gem::Requirement
|
82
|
-
none: false
|
83
|
-
requirements:
|
84
|
-
- - ! '>='
|
85
|
-
- !ruby/object:Gem::Version
|
86
|
-
version: '2.11'
|
87
|
-
type: :development
|
88
|
-
prerelease: false
|
89
|
-
version_requirements: !ruby/object:Gem::Requirement
|
90
|
-
none: false
|
91
|
-
requirements:
|
92
|
-
- - ! '>='
|
93
|
-
- !ruby/object:Gem::Version
|
94
|
-
version: '2.11'
|
95
79
|
- !ruby/object:Gem::Dependency
|
96
80
|
name: shoulda-matchers
|
97
81
|
requirement: !ruby/object:Gem::Requirement
|
@@ -124,22 +108,6 @@ dependencies:
|
|
124
108
|
- - <
|
125
109
|
- !ruby/object:Gem::Version
|
126
110
|
version: '3'
|
127
|
-
- !ruby/object:Gem::Dependency
|
128
|
-
name: factory_girl_rails
|
129
|
-
requirement: !ruby/object:Gem::Requirement
|
130
|
-
none: false
|
131
|
-
requirements:
|
132
|
-
- - <
|
133
|
-
- !ruby/object:Gem::Version
|
134
|
-
version: '3'
|
135
|
-
type: :development
|
136
|
-
prerelease: false
|
137
|
-
version_requirements: !ruby/object:Gem::Requirement
|
138
|
-
none: false
|
139
|
-
requirements:
|
140
|
-
- - <
|
141
|
-
- !ruby/object:Gem::Version
|
142
|
-
version: '3'
|
143
111
|
- !ruby/object:Gem::Dependency
|
144
112
|
name: appraisal
|
145
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -165,12 +133,18 @@ extensions: []
|
|
165
133
|
extra_rdoc_files: []
|
166
134
|
files:
|
167
135
|
- lib/acts_as_ordered_tree.rb
|
136
|
+
- lib/acts_as_ordered_tree/adapters/postgresql_adapter.rb
|
168
137
|
- lib/acts_as_ordered_tree/class_methods.rb
|
169
|
-
- lib/acts_as_ordered_tree/fake_scope.rb
|
170
138
|
- lib/acts_as_ordered_tree/instance_methods.rb
|
139
|
+
- lib/acts_as_ordered_tree/relation/base.rb
|
140
|
+
- lib/acts_as_ordered_tree/relation/preloaded.rb
|
141
|
+
- lib/acts_as_ordered_tree/relation/recursive.rb
|
142
|
+
- lib/acts_as_ordered_tree/tenacious_transaction.rb
|
171
143
|
- lib/acts_as_ordered_tree/validators.rb
|
172
144
|
- lib/acts_as_ordered_tree/version.rb
|
173
145
|
- spec/acts_as_ordered_tree_spec.rb
|
146
|
+
- spec/concurrency_support_spec.rb
|
147
|
+
- spec/db/config.yml
|
174
148
|
- spec/db/schema.rb
|
175
149
|
- spec/spec_helper.rb
|
176
150
|
- spec/support/factories.rb
|
@@ -190,7 +164,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
190
164
|
version: '0'
|
191
165
|
segments:
|
192
166
|
- 0
|
193
|
-
hash:
|
167
|
+
hash: 2475944794341416152
|
194
168
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
195
169
|
none: false
|
196
170
|
requirements:
|
@@ -199,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
199
173
|
version: '0'
|
200
174
|
segments:
|
201
175
|
- 0
|
202
|
-
hash:
|
176
|
+
hash: 2475944794341416152
|
203
177
|
requirements: []
|
204
178
|
rubyforge_project: acts_as_ordered_tree
|
205
179
|
rubygems_version: 1.8.24
|
@@ -208,6 +182,8 @@ specification_version: 3
|
|
208
182
|
summary: ActiveRecord extension for sorted adjacency lists support
|
209
183
|
test_files:
|
210
184
|
- spec/acts_as_ordered_tree_spec.rb
|
185
|
+
- spec/concurrency_support_spec.rb
|
186
|
+
- spec/db/config.yml
|
211
187
|
- spec/db/schema.rb
|
212
188
|
- spec/spec_helper.rb
|
213
189
|
- spec/support/factories.rb
|
@@ -1,28 +0,0 @@
|
|
1
|
-
# coding: utf-8
|
2
|
-
module ActsAsOrderedTree
|
3
|
-
class FakeScope < ActiveRecord::Relation
|
4
|
-
# create fake relation, with loaded records
|
5
|
-
#
|
6
|
-
# == Usage
|
7
|
-
# FakeScope.new(Category.where(:id => 1), [record])
|
8
|
-
# FakeScope.new(Category, [record]) { where(:id => 1) }
|
9
|
-
# FakeScope.new(Category, [record], :where => {:id => 1}, :order => "id desc")
|
10
|
-
def initialize(relation, records, conditions = {})
|
11
|
-
relation = relation.scoped if relation.is_a?(Class)
|
12
|
-
|
13
|
-
conditions.each do |method, arg|
|
14
|
-
relation = relation.send(method, arg)
|
15
|
-
end
|
16
|
-
|
17
|
-
super(relation.klass, relation.table)
|
18
|
-
|
19
|
-
# copy instance variables from real relation
|
20
|
-
relation.instance_variables.each do |ivar|
|
21
|
-
instance_variable_set(ivar, relation.instance_variable_get(ivar))
|
22
|
-
end
|
23
|
-
|
24
|
-
@loaded = true
|
25
|
-
@records = records
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|