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.
@@ -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
- extend ActiveSupport::Concern
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::FakeScope.new(self.class, nodes, :where => {:id => nodes.map(&:id)})
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
- ActsAsOrderedTree::FakeScope.new(scope, records)
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::FakeScope.new self.class, records, :where => {:id => records.map(&:id)}
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::FakeScope.new self.class, records, :where => {:id => records.map(&:id)}
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
- move_to_left_of left_sibling
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
- move_to_right_of right_sibling
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 cant be nil" unless index
182
- new_siblings = (node.try(:children) || self.class.roots).reject { |root_node| root_node == self }
183
-
184
- if new_siblings.empty?
185
- node ? move_to_child_of(node) : move_to_root
186
- elsif new_siblings.count <= index
187
- move_to_right_of(new_siblings.last)
188
- elsif
189
- index >= 0 ? move_to_left_of(new_siblings[index]) : move_to_right_of(new_siblings[index])
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 children of target node
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
- if target.is_a? self.class.base_class
264
- target.reload
265
- elsif pos != :root && target
266
- # load object if node is not an object
267
- target = self.class.find(target)
268
- end
269
-
270
- unless pos == :root || target && move_possible?(target)
271
- raise ActiveRecord::ActiveRecordError, "Impossible move"
272
- end
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
- position_was = send "#{position_column}_was".intern
275
- parent_id_was = send "#{parent_column}_was".intern
276
- parent_id, position, depth = compute_ordered_tree_columns(target, pos)
298
+ unless pos == :root || target && move_possible?(target)
299
+ raise ActiveRecord::ActiveRecordError, "Impossible move"
300
+ end
277
301
 
278
- # nothing changed - quit
279
- return if parent_id == parent_id_was && position == position_was
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
- update = proc do
282
- decrement_lower_positions parent_id_was, position_was if position_was
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
- columns = {parent_column => parent_id, position_column => position}
286
- columns[depth_column] = depth if depth_column
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
- ordered_tree_scope.update_all(columns, :id => id)
289
- reload_node
290
- end
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
- move_kind = case
293
- when id_was && parent_id != parent_id_was then :move
294
- when id_was && position != position_was then :reorder
295
- else nil
296
- end
322
+ reload_node
323
+ end
297
324
 
298
- if move_kind
299
- run_callbacks move_kind, &update
300
- else
301
- update.call
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 "#{position_column} = #{position_column} - 1", conditions
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
- def increment_lower_positions(parent_id, position) #:nodoc:
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 "#{position_column} = #{position_column} + 1", conditions
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
@@ -1,3 +1,3 @@
1
1
  module ActsAsOrderedTree
2
- VERSION = "1.0.5"
2
+ VERSION = "1.1.1"
3
3
  end
@@ -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
- # protect position&depth from mass-assignment
65
- attr_protected depth_column, position_column
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
@@ -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.establish_connection(:adapter => "sqlite3", :database => ":memory:")
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.around :each do |example|
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
@@ -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.0.5
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-08-22 00:00:00.000000000 Z
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: bundler
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: '1.0'
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: '1.0'
46
+ version: 0.9.2
47
47
  - !ruby/object:Gem::Dependency
48
- name: rails
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: 3.0.0
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: 3.0.0
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: -3932473491474199545
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: -3932473491474199545
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