acts_as_ordered_tree 1.3.1 → 2.0.0.beta3

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.
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