acts_as_ordered_tree 1.0.5 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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