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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8d1d945d226f1b6846fa1b58f24a6adab1d41425
4
- data.tar.gz: a894af6acac2e1d1bbc044cc008effc846b5b42a
3
+ metadata.gz: 43c7e01b6a7953300b7be948777fc396f0c3d00a
4
+ data.tar.gz: 73aa9566feb83f5edbafaa39b9a01d3438cd9934
5
5
  SHA512:
6
- metadata.gz: 458af4339a7cdb37ab604344970a7c83264827b62923b90f90e5e607355babd918df26b96897ff91ca2f1b6064f8d0cb6f2d95d1af14c3da6d67cb9cd3ecd060
7
- data.tar.gz: 62cd0756ebb2c63518cd73ef53b6163eac7bf1716ebb3d3614217bd2bebe2eefa14109dd87a53ae755032f63ce0c75a24a4f43fd1069b8d5dbca0c7edd970f78
6
+ metadata.gz: 35b94b1c3a68e9f11ac53fc6ebe07cf88635d0955dfa3ac6af11172766c57f9be7597c0ee1a139567ff1c7c7ee6316cb9e1b6409139dab120cbfa6ac267561f1
7
+ data.tar.gz: 08ff712d44b295769c4761d9bed62cb722c6ab3c5e7b6b0cf63cf7c2e77bfb389cfd452da9dfe1ae52caabd19fcf364d2d2e0b244900f3e2564a46741e2b62e8
@@ -1,15 +1,11 @@
1
- require 'active_record'
2
1
  require 'acts_as_ordered_tree/version'
3
- require 'acts_as_ordered_tree/class_methods'
4
- require 'acts_as_ordered_tree/instance_methods'
5
- require 'acts_as_ordered_tree/validators'
2
+ require 'active_support/lazy_load_hooks'
6
3
 
7
4
  module ActsAsOrderedTree
8
- PROTECTED_ATTRIBUTES_SUPPORTED = ActiveRecord::VERSION::STRING < '4.0.0' ||
9
- defined?(ProtectedAttributes)
5
+ autoload :Tree, 'acts_as_ordered_tree/tree'
10
6
 
11
- # can we use has_many :children, :order => :position
12
- PLAIN_ORDER_OPTION_SUPPORTED = ActiveRecord::VERSION::STRING < '4.0.0'
7
+ # @!attribute [r] ordered_tree
8
+ # @return [ActsAsOrderedTree::Tree] ordered tree object
13
9
 
14
10
  # == Usage
15
11
  # class Category < ActiveRecord::Base
@@ -19,101 +15,27 @@ module ActsAsOrderedTree
19
15
  # :counter_cache => :children_count
20
16
  # end
21
17
  def acts_as_ordered_tree(options = {})
22
- options = {
23
- :parent_column => :parent_id,
24
- :position_column => :position,
25
- :depth_column => :depth
26
- }.merge(options)
27
-
28
- class_attribute :acts_as_ordered_tree_options, :instance_writer => false
29
- self.acts_as_ordered_tree_options = options
30
-
31
- acts_as_ordered_tree_options[:depth_column] = nil unless
32
- columns_hash.include?(acts_as_ordered_tree_options[:depth_column].to_s)
33
-
34
- extend Columns
35
- include Columns
36
-
37
- has_many_children_options = {
38
- :class_name => "::#{base_class.name}",
39
- :foreign_key => options[:parent_column],
40
- :inverse_of => (:parent unless options[:polymorphic]),
41
- :dependent => :destroy
42
- }
43
-
44
- [:before_add, :after_add, :before_remove, :after_remove].each do |callback|
45
- has_many_children_options[callback] = options[callback] if options.key?(callback)
46
- end
47
-
48
- if PLAIN_ORDER_OPTION_SUPPORTED
49
- has_many_children_options[:order] = options[:position_column]
50
-
51
- if scope_column_names.any?
52
- has_many_children_options[:conditions] = proc do
53
- [scope_column_names.map { |c| "#{c} = ?" }.join(' AND '),
54
- scope_column_names.map { |c| self[c] }]
55
- end
56
- end
57
-
58
- has_many :children, has_many_children_options
59
- else
60
- scope = ->(parent) {
61
- relation = order(options[:position_column])
62
-
63
- if scope_column_names.any?
64
- relation = relation.where(
65
- Hash[scope_column_names.map { |c| [c, parent[c]]}]
66
- )
67
- end
68
-
69
- relation
70
- }
71
-
72
- has_many :children, scope, has_many_children_options
73
- end
74
-
75
- # create parent association
76
- belongs_to :parent,
77
- :class_name => "::#{base_class.name}",
78
- :foreign_key => options[:parent_column],
79
- :counter_cache => options[:counter_cache],
80
- :inverse_of => (:children unless options[:polymorphic])
81
-
82
- include ClassMethods
83
- include InstanceMethods
84
- setup_ordered_tree_adapter
85
- setup_ordered_tree_callbacks
86
- setup_ordered_tree_validations
87
- end # def acts_as_ordered_tree
88
-
89
- # Mixed into both classes and instances to provide easy access to the column names
90
- module Columns
91
- extend ActiveSupport::Concern
92
-
93
- included do
94
- attr_protected depth_column, position_column if PROTECTED_ATTRIBUTES_SUPPORTED
95
- end
96
-
97
- def parent_column
98
- acts_as_ordered_tree_options[:parent_column]
99
- end
100
-
101
- def position_column
102
- acts_as_ordered_tree_options[:position_column]
103
- end
18
+ Tree.setup!(self, options)
19
+ end
104
20
 
105
- def depth_column
106
- acts_as_ordered_tree_options[:depth_column] || nil
107
- end
21
+ # @api private
22
+ def self.extended(base)
23
+ base.class_attribute :ordered_tree, :instance_writer => false
24
+ end
108
25
 
109
- def children_counter_cache_column
110
- acts_as_ordered_tree_options[:counter_cache] || nil
111
- end
26
+ # Rebuild ordered tree structure for subclasses. It needs to be rebuilt
27
+ # mainly because of :children and :parent associations, which are created
28
+ # with option :class_name. It matters for class hierarchies without STI,
29
+ # they can't work properly with associations inherited from superclass.
30
+ #
31
+ # @api private
32
+ def inherited(subclass)
33
+ super
112
34
 
113
- def scope_column_names
114
- Array(acts_as_ordered_tree_options[:scope]).compact
115
- end
35
+ subclass.acts_as_ordered_tree(ordered_tree.options) if ordered_tree?
116
36
  end
117
37
  end # module ActsAsOrderedTree
118
38
 
119
- ActiveRecord::Base.extend(ActsAsOrderedTree)
39
+ ActiveSupport.on_load(:active_record) do
40
+ extend ActsAsOrderedTree
41
+ end
@@ -0,0 +1,17 @@
1
+ # coding: utf-8
2
+
3
+ require 'active_support/hash_with_indifferent_access'
4
+ require 'acts_as_ordered_tree/adapters/recursive'
5
+ require 'acts_as_ordered_tree/adapters/postgresql'
6
+
7
+ module ActsAsOrderedTree
8
+ module Adapters
9
+ # adapters map
10
+ ADAPTERS = HashWithIndifferentAccess['PostgreSQL' => PostgreSQL]
11
+ ADAPTERS.default = Recursive
12
+
13
+ def self.lookup(name)
14
+ ADAPTERS[name]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+
3
+ module ActsAsOrderedTree
4
+ module Adapters
5
+ class Abstract
6
+ attr_reader :tree
7
+
8
+ # @param [ActsAsOrderedTree::Tree] tree
9
+ def initialize(tree)
10
+ @tree = tree
11
+ end
12
+
13
+ protected
14
+ def preloaded(records)
15
+ tree.klass.where(nil).extending(Relation::Preloaded).records(records)
16
+ end
17
+
18
+ def none
19
+ tree.klass.where(nil).none
20
+ end
21
+ end # class Abstract
22
+ end # module Adapters
23
+ end # module ActsAsOrderedTree
@@ -0,0 +1,150 @@
1
+ # coding: utf-8
2
+
3
+ require 'active_record/hierarchical_query'
4
+
5
+ require 'acts_as_ordered_tree/adapters/abstract'
6
+ require 'acts_as_ordered_tree/relation/preloaded'
7
+
8
+ module ActsAsOrderedTree
9
+ module Adapters
10
+ # PostgreSQL adapter implements traverse operations with CTEs
11
+ class PostgreSQL < Abstract
12
+ attr_reader :tree
13
+
14
+ delegate :columns, :to => :tree
15
+ delegate :quote_column_name, :to => 'tree.klass.connection'
16
+
17
+ def self_and_descendants(node, &block)
18
+ traverse_down(node) do
19
+ descendants_scope(node.ordered_tree_node, &block)
20
+ end
21
+ end
22
+
23
+ def descendants(node, &block)
24
+ traverse_down(node) do
25
+ without(node) { self_and_descendants(node, &block) }
26
+ end
27
+ end
28
+
29
+ def self_and_ancestors(node, &block)
30
+ traverse_up(node, [node]) do
31
+ ancestors_scope(node.ordered_tree_node, &block)
32
+ end
33
+ end
34
+
35
+ def ancestors(node, &block)
36
+ traverse_up(node) do
37
+ without(node) { self_and_ancestors(node, &block) }
38
+ end
39
+ end
40
+
41
+ private
42
+ def without(node)
43
+ scope = yield
44
+ scope.where(scope.table[columns.id].not_eq(node.id))
45
+ end
46
+
47
+ def traverse_down(node)
48
+ if node && node.persisted?
49
+ yield
50
+ else
51
+ none
52
+ end
53
+ end
54
+
55
+ # Yields to block if record is persisted and its parent was not changed.
56
+ # Returns empty scope (or scope with +including+ records) if record is root.
57
+ # Otherwise recursively fetches ancestors and returns preloaded relation.
58
+ def traverse_up(node, including = [])
59
+ return none unless node
60
+
61
+ if can_traverse_up?(node)
62
+ if node.ordered_tree_node.has_parent?
63
+ yield
64
+ else
65
+ including.empty? ? none : preloaded(including)
66
+ end
67
+ else
68
+ preloaded(persisted_ancestors(node) + including)
69
+ end
70
+ end
71
+
72
+ # Generates scope that traverses tree down to deep, starting from given +scope+
73
+ def descendants_scope(node)
74
+ node.scope.join_recursive do |query|
75
+ query.connect_by(join_columns(columns.id => columns.parent))
76
+ .start_with(node.to_relation)
77
+
78
+ yield query if block_given?
79
+
80
+ query.order_siblings(position)
81
+ end
82
+ end
83
+
84
+ # Generates scope that traverses tree up to root, starting from given +scope+
85
+ def ancestors_scope(node, &block)
86
+ if columns.depth?
87
+ build_ancestors_query(node, &block).reorder(depth)
88
+ else
89
+ build_ancestors_query(node) do |query|
90
+ query.start_with { |start| start.select Arel.sql('0').as('__depth') }
91
+ .select(query.prior['__depth'] - 1, :start_with => false)
92
+
93
+ yield query if block_given?
94
+ end.reorder('__depth')
95
+ end
96
+ end
97
+
98
+ def build_ancestors_query(node)
99
+ node.scope.join_recursive do |query|
100
+ query.connect_by(join_columns(columns.parent => columns.id))
101
+ .start_with(node.to_relation)
102
+
103
+ yield query if block_given?
104
+ end
105
+ end
106
+
107
+ def attribute(name)
108
+ @tree.klass.arel_table[name]
109
+ end
110
+
111
+ def depth
112
+ attribute(columns.depth)
113
+ end
114
+
115
+ def position
116
+ attribute(columns.position)
117
+ end
118
+
119
+ def can_traverse_up?(node)
120
+ node.persisted? && !node.ordered_tree_node.parent_id_changed?
121
+ end
122
+
123
+ # Recursively fetches node's parents until one of them will be persisted.
124
+ # Returns persisted ancestor and array of non-persistent ancestors
125
+ def persisted_ancestors(node)
126
+ queue = []
127
+
128
+ parent = node
129
+
130
+ while (parent = parent.parent)
131
+ break if parent && parent.persisted?
132
+
133
+ queue.unshift(parent)
134
+ end
135
+
136
+ ancestors(parent) + [parent].compact + queue
137
+ end
138
+
139
+ def scope_columns_hash
140
+ Hash[tree.columns.scope.map { |x| [x, x] }]
141
+ end
142
+
143
+ def join_columns(hash)
144
+ scope_columns_hash.merge(hash).each_with_object({}) do |(k, v), h|
145
+ h[k.to_sym] = v.to_sym
146
+ end
147
+ end
148
+ end # class PostgreSQL
149
+ end # module Adapters
150
+ end # module ActsAsOrderedTree
@@ -0,0 +1,157 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/adapters/abstract'
4
+
5
+ module ActsAsOrderedTree
6
+ module Adapters
7
+ # Recursive adapter implements tree traversal in pure Ruby.
8
+ class Recursive < Abstract
9
+ def self_and_ancestors(node, &block)
10
+ return none unless node
11
+
12
+ ancestors_scope(node, :include_first => true, &block)
13
+ end
14
+
15
+ def ancestors(node, &block)
16
+ ancestors_scope(node, :include_first => false, &block)
17
+ end
18
+
19
+ def descendants(node, &block)
20
+ descendants_scope(node, :include_first => false, &block)
21
+ end
22
+
23
+ def self_and_descendants(node, &block)
24
+ descendants_scope(node, :include_first => true, &block)
25
+ end
26
+
27
+ private
28
+ def ancestors_scope(node, options, &block)
29
+ traversal = Traversal.new(node, options, &block)
30
+ traversal.follow :parent
31
+ traversal.to_scope.reverse_order!
32
+ end
33
+
34
+ def descendants_scope(node, options, &block)
35
+ return none unless node.persisted?
36
+
37
+ traversal = Traversal.new(node, options, &block)
38
+ traversal.follow :children
39
+ traversal.to_scope
40
+ end
41
+
42
+ class Traversal
43
+ delegate :klass, :to => :@start_record
44
+ attr_accessor :include_first
45
+
46
+ def initialize(start_record, options = {})
47
+ @start_record = start_record
48
+ @start_with = nil
49
+ @order_values = []
50
+ @where_values = []
51
+ @include_first = options[:include_first]
52
+ follow(options[:follow]) if options.key?(:follow)
53
+
54
+ yield self if block_given?
55
+ end
56
+
57
+ def follow(association_name)
58
+ @association = association_name
59
+
60
+ self
61
+ end
62
+
63
+ def start_with(scope = nil, &block)
64
+ @start_with = scope || block
65
+
66
+ self
67
+ end
68
+
69
+ def order_siblings(*values)
70
+ @order_values << values
71
+
72
+ self
73
+ end
74
+ alias_method :order, :order_siblings
75
+
76
+ def where(*values)
77
+ @where_values << values
78
+
79
+ self
80
+ end
81
+
82
+ def table
83
+ klass.arel_table
84
+ end
85
+
86
+ def klass
87
+ @start_record.class
88
+ end
89
+
90
+ def to_scope
91
+ null_scope.records(to_enum.to_a)
92
+ end
93
+
94
+ private
95
+ def each(&block)
96
+ return unless validate_start_conditions
97
+
98
+ yield @start_record if include_first
99
+
100
+ expand(@start_record, &block)
101
+ end
102
+
103
+ def validate_start_conditions
104
+ start_scope ? start_scope.exists? : true
105
+ end
106
+
107
+ def start_scope
108
+ return nil unless @start_with
109
+
110
+ if @start_with.is_a?(Proc)
111
+ @start_with.call klass.where(klass.primary_key => @start_record.id)
112
+ else
113
+ @start_with
114
+ end
115
+ end
116
+
117
+ def expand(record, &block)
118
+ expand_association(record).each do |child|
119
+ yield child
120
+
121
+ expand(child, &block)
122
+ end
123
+ end
124
+
125
+ def expand_association(record)
126
+ if constraints?
127
+ build_scope(record)
128
+ else
129
+ follow_association(record)
130
+ end
131
+ end
132
+
133
+ def build_scope(record)
134
+ scope = record.association(@association).scope
135
+
136
+ @where_values.each { |v| scope = scope.where(*v) }
137
+ scope = scope.except(:order).order(*@order_values.flatten) if @order_values.any?
138
+
139
+ scope
140
+ end
141
+
142
+ def follow_association(record)
143
+ Array.wrap(record.send(@association))
144
+ end
145
+
146
+ def null_scope
147
+ klass.where(nil).extending(Relation::Preloaded)
148
+ end
149
+
150
+ def constraints?
151
+ @where_values.any? || @order_values.any?
152
+ end
153
+ end
154
+ private_constant :Traversal
155
+ end # class Recursive
156
+ end # module Adapters
157
+ end # module ActsAsOrderedTree