acts_as_ordered_tree 1.3.1 → 2.0.0.beta3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/lib/acts_as_ordered_tree.rb +22 -100
  3. data/lib/acts_as_ordered_tree/adapters.rb +17 -0
  4. data/lib/acts_as_ordered_tree/adapters/abstract.rb +23 -0
  5. data/lib/acts_as_ordered_tree/adapters/postgresql.rb +150 -0
  6. data/lib/acts_as_ordered_tree/adapters/recursive.rb +157 -0
  7. data/lib/acts_as_ordered_tree/compatibility.rb +22 -0
  8. data/lib/acts_as_ordered_tree/compatibility/active_record/association_scope.rb +9 -0
  9. data/lib/acts_as_ordered_tree/compatibility/active_record/default_scoped.rb +19 -0
  10. data/lib/acts_as_ordered_tree/compatibility/active_record/null_relation.rb +71 -0
  11. data/lib/acts_as_ordered_tree/compatibility/features.rb +153 -0
  12. data/lib/acts_as_ordered_tree/deprecate.rb +24 -0
  13. data/lib/acts_as_ordered_tree/hooks.rb +38 -0
  14. data/lib/acts_as_ordered_tree/hooks/update.rb +86 -0
  15. data/lib/acts_as_ordered_tree/instance_methods.rb +92 -453
  16. data/lib/acts_as_ordered_tree/iterators/arranger.rb +35 -0
  17. data/lib/acts_as_ordered_tree/iterators/level_calculator.rb +52 -0
  18. data/lib/acts_as_ordered_tree/iterators/orphans_pruner.rb +58 -0
  19. data/lib/acts_as_ordered_tree/node.rb +78 -0
  20. data/lib/acts_as_ordered_tree/node/attributes.rb +48 -0
  21. data/lib/acts_as_ordered_tree/node/movement.rb +62 -0
  22. data/lib/acts_as_ordered_tree/node/movements.rb +111 -0
  23. data/lib/acts_as_ordered_tree/node/predicates.rb +98 -0
  24. data/lib/acts_as_ordered_tree/node/reloading.rb +49 -0
  25. data/lib/acts_as_ordered_tree/node/siblings.rb +139 -0
  26. data/lib/acts_as_ordered_tree/node/traversals.rb +53 -0
  27. data/lib/acts_as_ordered_tree/persevering_transaction.rb +93 -0
  28. data/lib/acts_as_ordered_tree/position.rb +143 -0
  29. data/lib/acts_as_ordered_tree/relation/arrangeable.rb +33 -0
  30. data/lib/acts_as_ordered_tree/relation/iterable.rb +41 -0
  31. data/lib/acts_as_ordered_tree/relation/preloaded.rb +46 -11
  32. data/lib/acts_as_ordered_tree/transaction/base.rb +57 -0
  33. data/lib/acts_as_ordered_tree/transaction/callbacks.rb +67 -0
  34. data/lib/acts_as_ordered_tree/transaction/create.rb +68 -0
  35. data/lib/acts_as_ordered_tree/transaction/destroy.rb +34 -0
  36. data/lib/acts_as_ordered_tree/transaction/dsl.rb +214 -0
  37. data/lib/acts_as_ordered_tree/transaction/factory.rb +67 -0
  38. data/lib/acts_as_ordered_tree/transaction/move.rb +70 -0
  39. data/lib/acts_as_ordered_tree/transaction/passthrough.rb +12 -0
  40. data/lib/acts_as_ordered_tree/transaction/reorder.rb +42 -0
  41. data/lib/acts_as_ordered_tree/transaction/save.rb +64 -0
  42. data/lib/acts_as_ordered_tree/transaction/update.rb +78 -0
  43. data/lib/acts_as_ordered_tree/tree.rb +148 -0
  44. data/lib/acts_as_ordered_tree/tree/association.rb +20 -0
  45. data/lib/acts_as_ordered_tree/tree/callbacks.rb +57 -0
  46. data/lib/acts_as_ordered_tree/tree/children_association.rb +120 -0
  47. data/lib/acts_as_ordered_tree/tree/columns.rb +102 -0
  48. data/lib/acts_as_ordered_tree/tree/deprecated_columns_accessors.rb +24 -0
  49. data/lib/acts_as_ordered_tree/tree/parent_association.rb +31 -0
  50. data/lib/acts_as_ordered_tree/tree/perseverance.rb +19 -0
  51. data/lib/acts_as_ordered_tree/tree/scopes.rb +56 -0
  52. data/lib/acts_as_ordered_tree/validators.rb +1 -1
  53. data/lib/acts_as_ordered_tree/version.rb +1 -1
  54. data/spec/acts_as_ordered_tree_spec.rb +80 -909
  55. data/spec/adapters/postgresql_spec.rb +14 -0
  56. data/spec/adapters/recursive_spec.rb +12 -0
  57. data/spec/adapters/shared.rb +272 -0
  58. data/spec/callbacks_spec.rb +177 -0
  59. data/spec/counter_cache_spec.rb +31 -0
  60. data/spec/create_spec.rb +110 -0
  61. data/spec/destroy_spec.rb +57 -0
  62. data/spec/inheritance_spec.rb +176 -0
  63. data/spec/move_spec.rb +94 -0
  64. data/spec/node/movements/concurrent_movements_spec.rb +354 -0
  65. data/spec/node/movements/move_higher_spec.rb +46 -0
  66. data/spec/node/movements/move_lower_spec.rb +46 -0
  67. data/spec/node/movements/move_to_child_of_spec.rb +147 -0
  68. data/spec/node/movements/move_to_child_with_index_spec.rb +124 -0
  69. data/spec/node/movements/move_to_child_with_position_spec.rb +85 -0
  70. data/spec/node/movements/move_to_left_of_spec.rb +120 -0
  71. data/spec/node/movements/move_to_right_of_spec.rb +120 -0
  72. data/spec/node/movements/move_to_root_spec.rb +67 -0
  73. data/spec/node/predicates_spec.rb +211 -0
  74. data/spec/node/reloading_spec.rb +42 -0
  75. data/spec/node/siblings_spec.rb +193 -0
  76. data/spec/node/traversals_spec.rb +71 -0
  77. data/spec/persevering_transaction_spec.rb +98 -0
  78. data/spec/relation/arrangeable_spec.rb +88 -0
  79. data/spec/relation/iterable_spec.rb +104 -0
  80. data/spec/relation/preloaded_spec.rb +57 -0
  81. data/spec/reorder_spec.rb +83 -0
  82. data/spec/spec_helper.rb +30 -38
  83. data/spec/support/db/boot.rb +22 -0
  84. data/spec/{db → support/db}/config.travis.yml +2 -0
  85. data/spec/{db → support/db}/config.yml +1 -0
  86. data/spec/{db → support/db}/schema.rb +9 -0
  87. data/spec/support/factories.rb +2 -2
  88. data/spec/support/matchers.rb +67 -58
  89. data/spec/support/models.rb +6 -14
  90. data/spec/support/tree_factory.rb +315 -0
  91. data/spec/tree/children_association_spec.rb +72 -0
  92. data/spec/tree/columns_spec.rb +65 -0
  93. data/spec/tree/scopes_spec.rb +39 -0
  94. metadata +161 -43
  95. data/lib/acts_as_ordered_tree/adapters/postgresql_adapter.rb +0 -104
  96. data/lib/acts_as_ordered_tree/arrangeable.rb +0 -80
  97. data/lib/acts_as_ordered_tree/class_methods.rb +0 -72
  98. data/lib/acts_as_ordered_tree/relation/base.rb +0 -26
  99. data/lib/acts_as_ordered_tree/tenacious_transaction.rb +0 -30
  100. data/spec/concurrency_support_spec.rb +0 -156
@@ -1,104 +0,0 @@
1
- require 'acts_as_ordered_tree/arrangeable'
2
- require 'acts_as_ordered_tree/relation/preloaded'
3
-
4
- module ActsAsOrderedTree
5
- module Adapters
6
- module PostgreSQLAdapter
7
- # Recursive ancestors fetcher
8
- def self_and_ancestors
9
- if persisted? && !send("#{parent_column}_changed?")
10
- query = <<-QUERY
11
- SELECT id, #{parent_column}, 1 AS _depth
12
- FROM #{self.class.quoted_table_name}
13
- WHERE #{arel[:id].eq(id).to_sql}
14
- UNION ALL
15
- SELECT alias1.id, alias1.#{parent_column}, _depth + 1
16
- FROM #{self.class.quoted_table_name} alias1
17
- INNER JOIN self_and_ancestors ON alias1.id = self_and_ancestors.#{parent_column}
18
- QUERY
19
-
20
- with_recursive_join(query, 'self_and_ancestors').
21
- order('self_and_ancestors._depth DESC').
22
- extending(Arrangeable)
23
- else
24
- (ancestors + [self]).tap { |ary| ary.extend(Arrangeable) }
25
- end
26
- end
27
-
28
- # Recursive ancestors fetcher
29
- def ancestors
30
- query = <<-QUERY
31
- SELECT id, #{parent_column}, 1 AS _depth
32
- FROM #{self.class.quoted_table_name}
33
- WHERE #{arel[:id].eq(parent.try(:id)).to_sql}
34
- UNION ALL
35
- SELECT alias1.id, alias1.#{parent_column}, _depth + 1
36
- FROM #{self.class.quoted_table_name} alias1
37
- INNER JOIN ancestors ON alias1.id = ancestors.#{parent_column}
38
- QUERY
39
-
40
- with_recursive_join(query, 'ancestors').
41
- order('ancestors._depth DESC').
42
- extending(Arrangeable)
43
- end
44
-
45
- def root
46
- root? ? self : ancestors.first
47
- end
48
-
49
- def self_and_descendants
50
- query = <<-QUERY
51
- SELECT id, #{parent_column}, ARRAY[#{position_column}] AS _positions
52
- FROM #{self.class.quoted_table_name}
53
- WHERE #{arel[:id].eq(id).to_sql}
54
- UNION ALL
55
- SELECT alias1.id, alias1.#{parent_column}, _positions || alias1.#{position_column}
56
- FROM descendants INNER JOIN
57
- #{self.class.quoted_table_name} alias1 ON alias1.parent_id = descendants.id
58
- QUERY
59
-
60
- with_recursive_join(query, 'descendants').
61
- order('descendants._positions ASC').
62
- extending(Arrangeable)
63
- end
64
-
65
- def descendants
66
- self_and_descendants.where(arel[:id].not_eq(id))
67
- end
68
-
69
- private
70
- def recursive_scope
71
- ActsAsOrderedTree::Relation::Recursive.new(ordered_tree_scope)
72
- end
73
-
74
- def with_recursive_join(recursive_query_sql, aliaz)
75
- join_sql = 'INNER JOIN (' +
76
- "WITH RECURSIVE #{aliaz} AS (" +
77
- recursive_query_sql +
78
- ") SELECT * FROM #{aliaz} " +
79
- ") #{aliaz} ON #{aliaz}.id = #{self.class.quoted_table_name}.id"
80
-
81
- ordered_tree_scope.joins(join_sql)
82
- end
83
-
84
- # Rails 3.0 does not support update_all with joins, so we patch it :(
85
- if ActiveRecord::VERSION::STRING <= '3.1.0'
86
- module Rails30UpdateAllPatch
87
- def update_all(updates, conditions = nil, options = {})
88
- relation = except(:joins, :where).
89
- where(:id => select(klass.arel_table[:id]).except(:order, :limit).arel)
90
- relation.update_all(updates, conditions, options)
91
- end
92
- end
93
-
94
- def with_recursive_join_30(recursive_query_sql, aliaz)
95
- relation = with_recursive_join_31(recursive_query_sql, aliaz)
96
- relation.extend(Rails30UpdateAllPatch)
97
- relation
98
- end
99
- alias_method :with_recursive_join_31, :with_recursive_join
100
- alias_method :with_recursive_join, :with_recursive_join_30
101
- end
102
- end
103
- end
104
- end
@@ -1,80 +0,0 @@
1
- module ActsAsOrderedTree
2
- module Arrangeable
3
- # @api private
4
- class Arranger
5
- attr_reader :collection, :cache
6
-
7
- def initialize(collection, options = {})
8
- @collection = collection
9
- @discard_orphans = options[:orphans] == :discard
10
- @min_level = nil
11
-
12
- if discard_orphans? && !collection.klass.depth_column && ActiveRecord::Base.logger
13
- ActiveRecord::Base.logger.warn {
14
- '%s model has no `depth` column, '\
15
- 'it can lead to N+1 queries during #arrange method invocation' % collection.klass
16
- }
17
- end
18
-
19
- @cache = Hash.new
20
- @prepared = false
21
- end
22
-
23
- def arrange
24
- prepare unless prepared?
25
-
26
- @arranged ||= collection.each_with_object(Hash.new) do |node, result|
27
- ancestors = ancestors(node)
28
-
29
- if discard_orphans?
30
- root = ancestors.first || node
31
-
32
- next if root.level > @min_level
33
- end
34
-
35
- insertion_point = result
36
-
37
- ancestors.each { |a| insertion_point = (insertion_point[a] ||= {}) }
38
-
39
- insertion_point[node] = {}
40
- end
41
- end
42
-
43
- private
44
- def prepare
45
- collection.each do |node|
46
- cache[node.id] = node if node.id
47
- @min_level = [@min_level, node.level].compact.min
48
- end
49
-
50
- @prepared = true
51
- end
52
-
53
- def discard_orphans?
54
- @discard_orphans
55
- end
56
-
57
- def prepared?
58
- @prepared
59
- end
60
-
61
- # get parent node of +node+
62
- def parent(node)
63
- cache[node[node.parent_column]]
64
- end
65
-
66
- def ancestors(node)
67
- parent = parent(node)
68
- parent ? ancestors(parent) + [parent] : []
69
- end
70
- end
71
- private_constant :Arranger
72
-
73
- # Arrange associated collection into a nested hash of the form
74
- # {node => children}, where children = {} if the node has no children.
75
- def arrange(options = {})
76
- @arranger ||= Arranger.new(self, options)
77
- @arranger.arrange
78
- end
79
- end
80
- end
@@ -1,72 +0,0 @@
1
- require "acts_as_ordered_tree/adapters/postgresql_adapter"
2
-
3
- module ActsAsOrderedTree
4
- module ClassMethods
5
- extend ActiveSupport::Concern
6
-
7
- included do
8
- scope :preorder, -> { order(arel_table[position_column].asc) }
9
- scope :roots, -> { where(arel_table[parent_column].eq(nil)).preorder }
10
-
11
- # add +leaves+ scope only if counter_cache column present
12
- scope :leaves, -> { where(arel_table[children_counter_cache_column].eq(0)) } if
13
- children_counter_cache?
14
-
15
- # when default value for counter_cache is absent we should set it manually
16
- before_create "self.#{children_counter_cache_column} = 0" if children_counter_cache?
17
- end
18
-
19
- module ClassMethods
20
- # Returns the first root
21
- def root
22
- roots.first
23
- end
24
-
25
- private
26
- def children_counter_cache? #:nodoc:
27
- children_counter_cache_column && columns_hash.key?(children_counter_cache_column.to_s)
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
70
- end # module ClassMethods
71
- end # module ClassMethods
72
- end # module ActsAsOrderedTree
@@ -1,26 +0,0 @@
1
- module ActsAsOrderedTree
2
- module Relation
3
- class Base < ActiveRecord::Relation
4
- EMPTY_SCOPE_METHOD = ActiveRecord::VERSION::STRING < '4.0.0' ? :scoped : :all
5
-
6
- # Create from existing +relation+ or from +class+ and +table+
7
- def initialize(class_or_relation, table = nil)
8
- relation = class_or_relation
9
-
10
- if class_or_relation.is_a?(Class)
11
- relation = class_or_relation.send(EMPTY_SCOPE_METHOD)
12
- table ||= class_or_relation.arel_table
13
-
14
- super(class_or_relation, table)
15
- else
16
- super(class_or_relation.klass, class_or_relation.table)
17
- end
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
- end
24
- end
25
- end
26
- end
@@ -1,30 +0,0 @@
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 self.class.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,156 +0,0 @@
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
- size.times.map { |x|
11
- Thread.new do
12
- ActiveRecord::Base.connection_pool.with_connection { yield x }
13
- end
14
- }.each(&:join)
15
- end
16
- end
17
- include Concurrency
18
-
19
- let!(:root) { create :default }
20
-
21
- it "should not create nodes with same position" do
22
- pool(3) do
23
- create :default, :parent => root
24
- end
25
-
26
- root.children.map(&:position).should eq [1, 2, 3]
27
- end
28
-
29
- it "should not move nodes to same position when moving to child of certain node" do
30
- nodes = create_list :default, 3
31
-
32
- pool(3) do |x|
33
- nodes[x].move_to_child_of(root)
34
- end
35
-
36
- root.children.map(&:position).should eq [1, 2, 3]
37
- end
38
-
39
- it "should not move nodes to same position when moving to left of root node" do
40
- nodes = create_list :default, 3, :parent => root
41
-
42
- pool(3) do |x|
43
- nodes[x].move_to_left_of(root)
44
- end
45
-
46
- Default.roots.map(&:position).should eq [1, 2, 3, 4]
47
- end
48
-
49
- it "should not move nodes to same position when moving to left of child node" do
50
- child = create :default, :parent => root
51
- nodes = create_list :default, 3, :parent => child
52
-
53
- pool(3) do |x|
54
- nodes[x].move_to_left_of(child)
55
- end
56
-
57
- root.children.map(&:position).should eq [1, 2, 3, 4]
58
- root.children.last.should eq child
59
- end
60
-
61
- it "should not move nodes to same position when moving to right of child node" do
62
- child = create :default, :parent => root
63
- nodes = create_list :default, 3, :parent => child
64
-
65
- pool(3) do |x|
66
- nodes[x].move_to_right_of(child)
67
- end
68
-
69
- root.children.map(&:position).should eq [1, 2, 3, 4]
70
- root.children.first.should eq child
71
- end
72
-
73
- it "should not move nodes to same position when moving to root" do
74
- nodes = create_list :default, 3, :parent => root
75
-
76
- pool(3) do |x|
77
- nodes[x].move_to_root
78
- end
79
-
80
- Default.roots.map(&:position).should eq [1, 2, 3, 4]
81
- end
82
-
83
- # checking deadlock also
84
- it "should not move nodes to same position when moving to specified index" do
85
- # root
86
- # * child1
87
- # * nodes1_1
88
- # * nodes1_2
89
- # * child2
90
- # * nodes2_1
91
- # * nodes2_2
92
- child1, child2 = create_list :default, 2, :parent => root
93
-
94
- nodes1, nodes2 = create_list(:default, 2, :parent => child1),
95
- create_list(:default, 2, :parent => child2)
96
-
97
- nodes1_1, nodes1_2 = nodes1
98
- nodes2_1, nodes2_2 = nodes2
99
-
100
- # nodes2_2 -> child1[0]
101
- thread1 = Thread.new do
102
- ActiveRecord::Base.connection_pool.with_connection do
103
- nodes2_2.move_to_child_with_index(child1, 0)
104
- end
105
- end
106
- # nodes1_1 -> child2[2]
107
- thread2 = Thread.new do
108
- ActiveRecord::Base.connection_pool.with_connection do
109
- nodes1_1.move_to_child_with_index(child2, 2)
110
- end
111
- end
112
- [thread1, thread2].map(&:join)
113
-
114
- child1.children.reload.should == [nodes2_2, nodes1_2]
115
- child2.children.reload.should == [nodes2_1, nodes1_1]
116
- end
117
-
118
- it "should not move nodes to same position when moving higher" do
119
- child1, child2, child3 = create_list :default, 3, :parent => root
120
-
121
- thread1 = Thread.new do
122
- ActiveRecord::Base.connection_pool.with_connection do
123
- child2.move_higher
124
- end
125
- end
126
- thread2 = Thread.new do
127
- ActiveRecord::Base.connection_pool.with_connection do
128
- child3.move_higher
129
- end
130
- end
131
-
132
- [thread1, thread2].map(&:join)
133
-
134
- root.children.map(&:position).should eq [1, 2, 3]
135
- end
136
-
137
- it "should not move nodes to same position when moving lower" do
138
- child1, child2, child3 = create_list :default, 3, :parent => root
139
-
140
- thread1 = Thread.new do
141
- ActiveRecord::Base.connection_pool.with_connection do
142
- child1.move_lower
143
- end
144
- end
145
- thread2 = Thread.new do
146
- ActiveRecord::Base.connection_pool.with_connection do
147
- child2.move_lower
148
- end
149
- end
150
-
151
- [thread1, thread2].map(&:join)
152
-
153
- root.children.map(&:position).should eq [1, 2, 3]
154
- end
155
- end
156
- end