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
@@ -0,0 +1,67 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/position'
4
+ require 'acts_as_ordered_tree/transaction/create'
5
+ require 'acts_as_ordered_tree/transaction/destroy'
6
+ require 'acts_as_ordered_tree/transaction/move'
7
+ require 'acts_as_ordered_tree/transaction/passthrough'
8
+ require 'acts_as_ordered_tree/transaction/reorder'
9
+
10
+ module ActsAsOrderedTree
11
+ module Transaction
12
+ # @api private
13
+ module Factory
14
+ # Creates previous and current position objects for node
15
+ # @api private
16
+ class PositionFactory
17
+ def initialize(node)
18
+ @node = node
19
+ end
20
+
21
+ def previous
22
+ Position.new @node, @node.parent_id_was, @node.position_was
23
+ end
24
+
25
+ def current
26
+ Position.new @node, @node.parent_id, @node.position
27
+ end
28
+
29
+ def transition
30
+ Position::Transition.new(previous, current)
31
+ end
32
+ end
33
+ private_constant :PositionFactory
34
+
35
+ # Creates proper transaction according to +node+
36
+ #
37
+ # @param [ActsAsOrderedTree::Node] node
38
+ # @param [true, false] destroy set to true if node should be destroyed
39
+ # @return [ActsAsOrderedTree::Transaction::Base]
40
+ def create(node, destroy = false)
41
+ pos = PositionFactory.new(node)
42
+
43
+ case
44
+ when destroy
45
+ Destroy.new(node, pos.previous)
46
+ when node.record.new_record?
47
+ Create.new(node, pos.current)
48
+ else
49
+ create_from_transition(node, pos.transition)
50
+ end
51
+ end
52
+ module_function :create
53
+
54
+ def create_from_transition(node, transition)
55
+ case
56
+ when transition.movement?
57
+ Move.new(node, transition)
58
+ when transition.reorder?
59
+ Reorder.new(node, transition)
60
+ else
61
+ Passthrough.new
62
+ end
63
+ end
64
+ module_function :create_from_transition
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,70 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/transaction/update'
4
+
5
+ module ActsAsOrderedTree
6
+ module Transaction
7
+ class Move < Update
8
+ before 'trigger_callback(:before_remove, from.parent)'
9
+ before 'trigger_callback(:before_add, to.parent)'
10
+
11
+ after :update_descendants_depth, :if => [
12
+ 'transition.movement?',
13
+ 'tree.columns.depth?',
14
+ 'transition.level_changed?',
15
+ 'record.children.size > 0'
16
+ ]
17
+
18
+ after 'trigger_callback(:after_add, to.parent)'
19
+ after 'trigger_callback(:after_remove, from.parent)'
20
+ after 'transition.update_counters'
21
+
22
+ finalize
23
+
24
+ private
25
+ def update_values
26
+ updates = Hash[
27
+ position => position_value,
28
+ parent_id => parent_id_value
29
+ ]
30
+
31
+ updates[depth] = depth_value if tree.columns.depth? && transition.level_changed?
32
+
33
+ updates
34
+ end
35
+
36
+ # Records to be updated
37
+ def update_scope
38
+ filter = (id == record.id) | (parent_id == from.parent_id) | (parent_id == to.parent_id)
39
+ node.scope.where(filter.to_sql)
40
+ end
41
+
42
+ def parent_id_value
43
+ switch.when(id == record.id, to.parent_id).else(parent_id)
44
+ end
45
+
46
+ def position_value
47
+ switch.
48
+ when(id == record.id).
49
+ then(@to.position).
50
+ # decrement lower positions in old parent
51
+ when((parent_id == from.parent_id) & (position > from.position)).
52
+ then(position - 1).
53
+ # increment positions in new parent
54
+ when((parent_id == to.parent_id) & (position >= to.position)).
55
+ then(position + 1).
56
+ else(position)
57
+ end
58
+
59
+ def depth_value
60
+ switch.
61
+ when(id == record.id, to.depth).
62
+ else(depth)
63
+ end
64
+
65
+ def update_descendants_depth
66
+ record.descendants.update_all set depth => depth + (to.depth - from.depth)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,12 @@
1
+ # coding: utf-8
2
+
3
+ module ActsAsOrderedTree
4
+ module Transaction
5
+ # Null transaction, does nothing but delegates to caller
6
+ class Passthrough
7
+ def start(&block)
8
+ block.call if block_given?
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,42 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/transaction/update'
4
+
5
+ module ActsAsOrderedTree
6
+ module Transaction
7
+ class Reorder < Update
8
+ finalize
9
+
10
+ protected
11
+ # if we reorder node then we cannot put it to position higher than highest
12
+ def push_to_bottom
13
+ to.position = highest_position.zero? ? 1 : highest_position
14
+ end
15
+
16
+ private
17
+ def update_scope
18
+ to.siblings.where(positions_range)
19
+ end
20
+
21
+ def update_values
22
+ { position => position_value }
23
+ end
24
+
25
+ def positions_range
26
+ position.in([from.position, to.position].min..[from.position, to.position].max)
27
+ end
28
+
29
+ def position_value
30
+ expr = switch.
31
+ when(position == from.position).then(to.position).
32
+ else(position)
33
+
34
+ if to.position > from.position
35
+ expr.when(positions_range).then(position - 1)
36
+ else
37
+ expr.when(positions_range).then(position + 1)
38
+ end
39
+ end
40
+ end # class Reorder
41
+ end # module Transaction
42
+ end # module ActsAsOrderedTree
@@ -0,0 +1,64 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/transaction/base'
4
+
5
+ module ActsAsOrderedTree
6
+ module Transaction
7
+ class Save < Base
8
+ attr_reader :to
9
+
10
+ before 'to.lock!'
11
+ before :set_scope!, :if => 'to.parent?'
12
+ before :push_to_bottom, :if => :push_to_bottom?
13
+ before 'to.position = 1', :if => 'to.position <= 0'
14
+
15
+ around :copy_attributes
16
+
17
+ # @param [ActsAsOrderedTree::Node] node
18
+ # @param [ActsAsOrderedTree::Position] to to which position given +node+ is saved
19
+ def initialize(node, to)
20
+ super(node)
21
+ @to = to
22
+ end
23
+
24
+ protected
25
+ # Copies parent_id, position and depth from destination to record
26
+ def copy_attributes
27
+ record.parent = to.parent
28
+ node.position = to.position
29
+ node.depth = to.depth if tree.columns.depth?
30
+
31
+ yield
32
+ end
33
+
34
+ # Returns highest position within node's siblings
35
+ def highest_position
36
+ @highest_position ||= to.siblings.maximum(tree.columns.position) || 0
37
+ end
38
+
39
+ # Should be fired when given position is empty
40
+ def push_to_bottom
41
+ to.position = highest_position + 1
42
+ end
43
+
44
+ # Returns true if record should be pushed to bottom of list
45
+ def push_to_bottom?
46
+ to.position.blank? ||
47
+ position_out_of_bounds?
48
+ end
49
+
50
+ private
51
+ def set_scope!
52
+ tree.columns.scope.each do |column|
53
+ record[column] = to.parent[column]
54
+ end
55
+
56
+ nil
57
+ end
58
+
59
+ def position_out_of_bounds?
60
+ to.position > highest_position
61
+ end
62
+ end # class Save
63
+ end # module Transaction
64
+ end # module ActsAsOrderedTree
@@ -0,0 +1,78 @@
1
+ # coding: utf-8
2
+
3
+ require 'active_support/core_ext/object/with_options'
4
+
5
+ require 'acts_as_ordered_tree/position'
6
+ require 'acts_as_ordered_tree/transaction/save'
7
+ require 'acts_as_ordered_tree/transaction/dsl'
8
+
9
+ module ActsAsOrderedTree
10
+ module Transaction
11
+ # Update transaction includes Move and Reorder
12
+ #
13
+ # @abstract
14
+ # @api private
15
+ class Update < Save
16
+ include DSL
17
+
18
+ attr_reader :from, :transition
19
+
20
+ around :update_tree
21
+
22
+ # @param [ActsAsOrderedTree::Node] node
23
+ # @param [ActsAsOrderedTree::Position::Transition] transition
24
+ def initialize(node, transition)
25
+ @transition = transition
26
+ @from = transition.from
27
+
28
+ super(node, transition.to)
29
+ end
30
+
31
+ protected
32
+ def update_tree
33
+ callbacks = transition.reorder? ? :reorder : :move
34
+
35
+ record.run_callbacks(callbacks) do
36
+ record.hook_update do |update|
37
+ update.scope = update_scope
38
+ update.values = update_values.merge(changed_attributes)
39
+
40
+ yield
41
+ end
42
+ end
43
+ end
44
+
45
+ def update_scope
46
+ # implement in successors
47
+ end
48
+
49
+ def update_values
50
+ # implement in successors
51
+ end
52
+
53
+ private
54
+ # Returns hash of UPDATE..SET expressions for each
55
+ # changed record attribute (except tree attributes)
56
+ #
57
+ # @return [Hash<String => Arel::Nodes::Node>]
58
+ def changed_attributes
59
+ changed_attributes_names.each_with_object({}) do |attr, hash|
60
+ hash[attr] = attribute_value(attr)
61
+ end
62
+ end
63
+
64
+ def attribute_value(attr)
65
+ attr_value = record.read_attribute(attr)
66
+ quoted = record.class.connection.quote(attr_value)
67
+
68
+ switch.
69
+ when(id == record.id).then(Arel.sql(quoted)).
70
+ else(attribute(attr))
71
+ end
72
+
73
+ def changed_attributes_names
74
+ record.changed - (tree.columns.to_a - tree.columns.scope)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,148 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/compatibility'
4
+
5
+ require 'acts_as_ordered_tree/tree/callbacks'
6
+ require 'acts_as_ordered_tree/tree/columns'
7
+ require 'acts_as_ordered_tree/tree/children_association'
8
+ require 'acts_as_ordered_tree/tree/deprecated_columns_accessors'
9
+ require 'acts_as_ordered_tree/tree/parent_association'
10
+ require 'acts_as_ordered_tree/tree/perseverance'
11
+ require 'acts_as_ordered_tree/tree/scopes'
12
+
13
+ require 'acts_as_ordered_tree/hooks'
14
+
15
+ require 'acts_as_ordered_tree/adapters'
16
+ require 'acts_as_ordered_tree/validators'
17
+
18
+ require 'acts_as_ordered_tree/instance_methods'
19
+
20
+ module ActsAsOrderedTree
21
+ # ActsAsOrderedTree::Tree
22
+ class Tree
23
+ # Default ordered tree options
24
+ DEFAULT_OPTIONS = {
25
+ :parent_column => :parent_id,
26
+ :position_column => :position,
27
+ :depth_column => :depth
28
+ }.freeze
29
+
30
+ PROTECTED_ATTRIBUTES = :left_sibling, :left_sibling_id,
31
+ :higher_item, :higher_item_id,
32
+ :right_sibling, :right_sibling_id,
33
+ :lower_item, :lower_item_id
34
+
35
+ attr_reader :klass
36
+
37
+ # @!attribute [r] columns
38
+ # Columns information aggregator
39
+ #
40
+ # @return [ActsAsOrderedTree::Tree::Columns] column object
41
+ attr_reader :columns
42
+
43
+ # @!attribute [r] callbacks
44
+ # :before_add, :after_add, :before_remove and :after_remove callbacks storage
45
+ #
46
+ # @return [ActsAsOrderedTree::Tree::Callbacks] callbacks object
47
+ attr_reader :callbacks
48
+
49
+ # @!attribute [r] adapter
50
+ # Ordered tree adapter which contains implementation of some traverse methods
51
+ #
52
+ # @return [ActsAsOrderedTree::Adapters::Abstract] adapter object
53
+ attr_reader :adapter
54
+
55
+ # @!attribute [r] options
56
+ # Ordered tree options
57
+ #
58
+ # @return [Hash]
59
+ attr_reader :options
60
+
61
+ # Create and setup tree object
62
+ #
63
+ # @param [Class] klass
64
+ # @param [Hash] options
65
+ def self.setup!(klass, options)
66
+ klass.ordered_tree = new(klass, options).setup
67
+ end
68
+
69
+ # @param [Class] klass
70
+ # @param [Hash] options
71
+ def initialize(klass, options)
72
+ @klass = klass
73
+ @options = DEFAULT_OPTIONS.merge(options).freeze
74
+ @columns = Columns.new(klass, @options)
75
+ @callbacks = Callbacks.new(klass, @options)
76
+ @children = ChildrenAssociation.new(self)
77
+ @parent = ParentAssociation.new(self)
78
+ @adapter = Adapters.lookup(klass.connection.adapter_name).new(self)
79
+ end
80
+
81
+ # Builds associations, callbacks, validations etc.
82
+ def setup
83
+ setup_associations
84
+ setup_once
85
+
86
+ self
87
+ end
88
+
89
+ # Returns Class object which will be used for associations,
90
+ # scopes and tree traversals.
91
+ #
92
+ # @return [Class]
93
+ def base_class
94
+ if klass.finder_needs_type_condition?
95
+ klass.base_class
96
+ else
97
+ klass
98
+ end
99
+ end
100
+
101
+ private
102
+ def already_setup?
103
+ klass.ordered_tree?
104
+ end
105
+
106
+ def setup_once
107
+ return if already_setup?
108
+
109
+ setup_validations
110
+ setup_callbacks
111
+ protect_attributes *PROTECTED_ATTRIBUTES
112
+
113
+ klass.class_eval do
114
+ extend Scopes
115
+ extend DeprecatedColumnsAccessors
116
+
117
+ include InstanceMethods
118
+ include Perseverance
119
+ include Hooks
120
+ end
121
+ end
122
+
123
+ def setup_associations
124
+ @parent.build
125
+ @children.build
126
+ end
127
+
128
+ def setup_validations
129
+ if columns.scope?
130
+ klass.validates_with Validators::ScopeValidator, :on => :update, :if => :parent
131
+ end
132
+
133
+ klass.validates_with Validators::CyclicReferenceValidator, :on => :update, :if => :parent
134
+ end
135
+
136
+ def setup_callbacks
137
+ klass.define_model_callbacks(:move, :reorder)
138
+ klass.around_save(:save_ordered_tree_node)
139
+ klass.around_destroy(:destroy_ordered_tree_node)
140
+ end
141
+
142
+ def protect_attributes(*attributes)
143
+ Compatibility.version '< 4.0.0' do
144
+ klass.attr_protected *attributes
145
+ end
146
+ end
147
+ end
148
+ end