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