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,98 @@
1
+ # coding: utf-8
2
+
3
+ module ActsAsOrderedTree
4
+ class Node
5
+ module Predicates
6
+ # Returns true if this is a root node.
7
+ def root?
8
+ !parent_id?
9
+ end
10
+
11
+ # Returns true if this is the end of a branch.
12
+ def leaf?
13
+ record.persisted? && if children.loaded? || tree.columns.counter_cache?
14
+ # no SQL-queries here
15
+ children.empty?
16
+ else
17
+ !children.exists?
18
+ end
19
+ end
20
+
21
+ # Returns true if node contains any children.
22
+ def has_children?
23
+ !leaf?
24
+ end
25
+
26
+ # Returns true is node is not a root node.
27
+ def has_parent?
28
+ !root?
29
+ end
30
+
31
+ # Returns true if current node is descendant of +other+ node.
32
+ #
33
+ # @param [ActiveRecord::Base] other
34
+ def is_descendant_of?(other)
35
+ same_scope?(other) &&
36
+ ancestors.include?(other) &&
37
+ ancestors.all? { |x| x.ordered_tree_node.same_scope?(record) }
38
+ end
39
+
40
+ # Returns true if current node is equal to +other+ node or is descendant of +other+ node.
41
+ #
42
+ # @param [ActiveRecord::Base] other
43
+ def is_or_is_descendant_of?(other)
44
+ record == other || is_descendant_of?(other)
45
+ end
46
+
47
+ # Returns true if current node is ancestor of +other+ node.
48
+ #
49
+ # @param [ActiveRecord::Base] other
50
+ def is_ancestor_of?(other)
51
+ same_scope?(other) && other.is_descendant_of?(record)
52
+ end
53
+
54
+ # Returns true if current node is equal to +other+ node or is ancestor of +other+ node.
55
+ #
56
+ # @param [ActiveRecord::Base] other
57
+ def is_or_is_ancestor_of?(other)
58
+ same_scope?(other) && other.is_or_is_descendant_of?(record)
59
+ end
60
+
61
+ # Return +true+ if this object is the first in the list.
62
+ def first?
63
+ position <= 1
64
+ end
65
+
66
+ # Return +true+ if this object is the last in the list.
67
+ def last?
68
+ if tree.columns.counter_cache? && parent
69
+ parent.children.size == position
70
+ else
71
+ !right_sibling
72
+ end
73
+ end
74
+
75
+ # Check if other node is in the same scope.
76
+ #
77
+ # @api private
78
+ def same_scope?(other)
79
+ same_kind?(other) && tree.columns.scope.all? do |attr|
80
+ record[attr] == other[attr]
81
+ end
82
+ end
83
+
84
+ # Check if other node has the same parent
85
+ #
86
+ # @api private
87
+ def same_parent?(other)
88
+ same_scope?(other) && parent_id == other.ordered_tree_node.parent_id
89
+ end
90
+
91
+ private
92
+ # Check if other node belongs to same class hierarchy.
93
+ def same_kind?(other)
94
+ other.ordered_tree && other.ordered_tree.base_class == tree.base_class
95
+ end
96
+ end # module Predicates
97
+ end # class Node
98
+ end # module ActsAsOrderedTree
@@ -0,0 +1,49 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/compatibility'
4
+
5
+ module ActsAsOrderedTree
6
+ class Node
7
+ module Reloading
8
+ Compatibility.version '< 4.0.0' do
9
+ # Reloads node's attributes related to tree structure
10
+ def reload(options = {})
11
+ record.reload(options.merge(:select => tree_columns))
12
+ end
13
+ end
14
+
15
+ Compatibility.version '>= 4.0.0' do
16
+ # Reloads node's attributes related to tree structure
17
+ def reload(options = {})
18
+ record.association_cache.delete(:parent)
19
+ record.association_cache.delete(:children)
20
+
21
+ fresh_object = reload_scope(options).find(record.id)
22
+
23
+ fresh_object.attributes.each_pair do |key, value|
24
+ record[key] = value
25
+ end
26
+
27
+ record.instance_eval do
28
+ # @attributes.update(fresh_object.instance_variable_get(:@attributes))
29
+ @attributes_cache = {}
30
+ end
31
+
32
+ record
33
+ end
34
+
35
+ private
36
+ def reload_scope(options)
37
+ options ||= {}
38
+ lock_value = options.fetch(:lock, false)
39
+ record.class.unscoped.select(tree_columns).lock(lock_value)
40
+ end
41
+ end
42
+
43
+ private
44
+ def tree_columns
45
+ tree.columns.to_a
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,139 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/node/predicates'
4
+
5
+ module ActsAsOrderedTree
6
+ class Node
7
+ module Siblings
8
+ include Predicates
9
+
10
+ # Returns collection of all children of the parent, including self
11
+ #
12
+ # @return [ActiveRecord::Relation]
13
+ def self_and_siblings
14
+ scope.where( tree.columns.parent => parent_id ).preorder
15
+ end
16
+
17
+ # Returns collection of all children of the parent, except self
18
+ #
19
+ # @return [ActiveRecord::Relation]
20
+ def siblings
21
+ self_and_siblings.where( table[tree.columns.id].not_eq(id) )
22
+ end
23
+
24
+ # Returns siblings lying to the left of (upper than) current node.
25
+ #
26
+ # @return [ActiveRecord::Relation]
27
+ def left_siblings
28
+ siblings.where( table[tree.columns.position].lteq(position) )
29
+ end
30
+ alias :higher_items :left_siblings
31
+
32
+ # Returns a left (upper) sibling of node.
33
+ #
34
+ # @return [ActiveRecord::Base, nil]
35
+ def left_sibling
36
+ higher_items.last
37
+ end
38
+ alias :higher_item :left_sibling
39
+
40
+ # Set node new left (upper) sibling.
41
+ # Just changes node's parent_id and position attributes.
42
+ #
43
+ # @param [ActiveRecord::Base] node new left sibling
44
+ # @raise [ActiveRecord::AssociationTypeMismatch] if +node+ class does not
45
+ # match current node class.
46
+ def left_sibling=(node)
47
+ return node if record == node
48
+
49
+ to = validate_sibling!(node)
50
+
51
+ self.position = higher_than?(node) ? to.position : to.position + 1
52
+ self.parent_id = to.parent_id
53
+
54
+ node
55
+ end
56
+ alias :higher_item= :left_sibling=
57
+
58
+ # Set node new left sibling by its ID.
59
+ # Changes node's parent_id and position.
60
+ #
61
+ # @param [Fixnum] id new left sibling ID
62
+ # @raise [ActiveRecord::RecordNotFound] if given +id+ was not found
63
+ def left_sibling_id=(id)
64
+ assign_sibling_by_id(id, :left)
65
+ end
66
+ alias :higher_item_id= :left_sibling_id=
67
+
68
+ # Returns siblings lying to the right of (lower than) current node.
69
+ #
70
+ # @return [ActiveRecord::Relation]
71
+ def right_siblings
72
+ siblings.where( table[tree.columns.position].gteq(position) )
73
+ end
74
+ alias :lower_items :right_siblings
75
+
76
+ # Returns a right (lower) sibling of the node
77
+ #
78
+ # @return [ActiveRecord::Base, nil]
79
+ def right_sibling
80
+ right_siblings.first
81
+ end
82
+ alias :lower_item :right_sibling
83
+
84
+ # Set node new right (lower) sibling.
85
+ # Just changes node's parent_id and position attributes.
86
+ #
87
+ # @param [ActiveRecord::Base] node new right sibling
88
+ # @raise [ActiveRecord::AssociationTypeMismatch] if +node+ class does not
89
+ # match current node class.
90
+ def right_sibling=(node)
91
+ to = validate_sibling!(node)
92
+
93
+ self.position = higher_than?(node) ? to.position - 1 : to.position
94
+ self.parent_id = to.parent_id
95
+
96
+ node
97
+ end
98
+ alias :lower_item= :right_sibling=
99
+
100
+ # Set node new right sibling by its ID.
101
+ # Changes node's parent_id and position.
102
+ #
103
+ # @param [Fixnum] id new right sibling ID
104
+ # @raise [ActiveRecord::RecordNotFound] if given +id+ was not found
105
+ def right_sibling_id=(id)
106
+ assign_sibling_by_id(id, :right)
107
+ end
108
+ alias :lower_item_id= :right_sibling_id=
109
+
110
+ private
111
+ def higher_than?(other)
112
+ same_parent?(other) && position < other.ordered_tree_node.position
113
+ end
114
+
115
+ # Raises exception if +other+ is kind of wrong class
116
+ #
117
+ # @return [ActsAsOrderedTree::Node]
118
+ def validate_sibling!(other)
119
+ unless other.is_a?(tree.base_class)
120
+ message = "#{tree.base_class.name} expected, got #{other.class.name}"
121
+ raise ActiveRecord::AssociationTypeMismatch, message
122
+ end
123
+
124
+ other.ordered_tree_node
125
+ end
126
+
127
+ # @api private
128
+ def assign_sibling_by_id(id, position)
129
+ node = tree.base_class.find(id)
130
+
131
+ case position
132
+ when :left, :higher then self.left_sibling = node
133
+ when :right, :lower then self.right_sibling = node
134
+ else raise RuntimeError, 'Unknown sibling position'
135
+ end
136
+ end
137
+ end # module Siblings
138
+ end # class Node
139
+ end # module ActsAsOrderedTree
@@ -0,0 +1,53 @@
1
+ # coding: utf-8
2
+
3
+ require 'acts_as_ordered_tree/relation/arrangeable'
4
+ require 'acts_as_ordered_tree/relation/iterable'
5
+
6
+ module ActsAsOrderedTree
7
+ class Node
8
+ module Traversals
9
+ # Returns relation that contains all node's parents, starting from root.
10
+ #
11
+ # @return [ActiveRecord::Relation]
12
+ def ancestors
13
+ iterable tree.adapter.ancestors(record)
14
+ end
15
+
16
+ # Returns relation that containt all node's parents
17
+ # and node itself, starting from root.
18
+ #
19
+ # @return [ActiveRecord::Relation]
20
+ def self_and_ancestors
21
+ iterable tree.adapter.self_and_ancestors(record)
22
+ end
23
+
24
+ # Returns collection of all node's children including their nested children.
25
+ #
26
+ # @return [ActiveRecord::Relation]
27
+ def descendants
28
+ iterable tree.adapter.descendants(record)
29
+ end
30
+
31
+ # Returns collection of all node's children including their
32
+ # nested children, and node itself.
33
+ #
34
+ # @return [ActiveRecord::Relation]
35
+ def self_and_descendants
36
+ iterable tree.adapter.self_and_descendants(record)
37
+ end
38
+
39
+ # Returns very first ancestor of current node. If current node is root,
40
+ # then method returns node itself.
41
+ #
42
+ # @return [ActiveRecord::Base]
43
+ def root
44
+ root? ? record : ancestors.first
45
+ end
46
+
47
+ private
48
+ def iterable(scope)
49
+ scope.extending(Relation::Arrangeable, Relation::Iterable)
50
+ end
51
+ end # module Traversals
52
+ end # module Node
53
+ end # module ActsAsOrderedTree
@@ -0,0 +1,93 @@
1
+ # coding: utf-8
2
+
3
+ module ActsAsOrderedTree
4
+ class PerseveringTransaction
5
+ module State
6
+ # Generate helper methods for given +state+.
7
+ # AR adapter calls :committed! and :rolledback! methods
8
+ #
9
+ # @api private
10
+ def state_method(state)
11
+ define_method "#{state}!" do |*|
12
+ @state = state
13
+ end
14
+
15
+ define_method "#{state}?" do
16
+ @state == state
17
+ end
18
+ end
19
+ end
20
+ extend State
21
+
22
+ # Which errors should be treated as deadlocks
23
+ DEADLOCK_MESSAGES = Regexp.new [
24
+ 'Deadlock found when trying to get lock',
25
+ 'Lock wait timeout exceeded',
26
+ 'deadlock detected',
27
+ 'database is locked'
28
+ ].join(?|).freeze
29
+ # How many times we should retry transaction
30
+ RETRY_COUNT = 10
31
+
32
+ attr_reader :connection, :attempts
33
+ delegate :logger, :to => :connection
34
+
35
+ state_method :committed
36
+ state_method :rolledback
37
+
38
+ def initialize(connection)
39
+ @connection = connection
40
+ @attempts = 0
41
+ @callbacks = []
42
+ @state = nil
43
+ end
44
+
45
+ # Starts persevering transaction
46
+ def start(&block)
47
+ @attempts += 1
48
+
49
+ with_transaction_state(&block)
50
+ rescue ActiveRecord::StatementInvalid => error
51
+ raise unless connection.open_transactions.zero?
52
+ raise unless error.message =~ DEADLOCK_MESSAGES
53
+ raise if attempts >= RETRY_COUNT
54
+
55
+ logger.info "Deadlock detected on attempt #{attempts}, restarting transaction"
56
+
57
+ pause and retry
58
+ end
59
+
60
+ # Execute given +block+ when after transaction _real_ commit
61
+ def after_commit(&block)
62
+ @callbacks << block if block_given?
63
+ end
64
+
65
+ # This method is called by AR adapter
66
+ # @api private
67
+ def has_transactional_callbacks?
68
+ true
69
+ end
70
+
71
+ # Marks this transaction as committed and executes its commit callbacks
72
+ # @api private
73
+ def committed_with_callbacks!
74
+ committed_without_callbacks!
75
+ @callbacks.each { |callback| callback.call }
76
+ end
77
+ alias_method_chain :committed!, :callbacks
78
+
79
+ private
80
+ def pause
81
+ sleep(rand(attempts) * 0.1)
82
+ end
83
+
84
+ # Runs real transaction and remembers its state
85
+ def with_transaction_state
86
+ connection.transaction do
87
+ connection.add_transaction_record(self)
88
+
89
+ yield
90
+ end
91
+ end
92
+ end # class PerseveringTransaction
93
+ end # module ActsAsOrderedTree
@@ -0,0 +1,143 @@
1
+ # coding: utf-8
2
+
3
+ module ActsAsOrderedTree
4
+ # Position structure aggregates knowledge about node's position in the tree
5
+ #
6
+ # @api private
7
+ class Position
8
+ # This class represents node position change
9
+ #
10
+ # @api private
11
+ class Transition
12
+ # @return [ActsAsOrderedTree::Position]
13
+ attr_reader :from
14
+
15
+ # @return [ActsAsOrderedTree::Position]
16
+ attr_reader :to
17
+
18
+ # @param [ActsAsOrderedTree::Position] from
19
+ # @param [ActsAsOrderedTree::Position] to
20
+ def initialize(from, to)
21
+ @from, @to = from, to
22
+ end
23
+
24
+ def changed?
25
+ from != to
26
+ end
27
+
28
+ def reorder?
29
+ changed? && from.parent_id == to.parent_id
30
+ end
31
+
32
+ def movement?
33
+ changed? && from.parent_id != to.parent_id
34
+ end
35
+
36
+ def level_changed?
37
+ from.depth != to.depth
38
+ end
39
+
40
+ def update_counters
41
+ if movement?
42
+ from.decrement_counter
43
+ to.increment_counter
44
+ end
45
+ end
46
+ end
47
+
48
+ attr_reader :node, :position
49
+ attr_accessor :parent_id
50
+
51
+ delegate :record, :to => :node
52
+
53
+ # @param [ActsAsOrderedTree::Node] node
54
+ # @param [Integer] parent_id
55
+ # @param [Integer] position
56
+ def initialize(node, parent_id, position)
57
+ @node, @parent_id, self.position = node, parent_id, position
58
+ end
59
+
60
+ # attr_writer with coercion to [nil or Integer]
61
+ def position=(value)
62
+ @position = value.presence && value.to_i
63
+ end
64
+
65
+ def klass
66
+ record.class
67
+ end
68
+
69
+ def parent
70
+ return @parent if defined?(@parent)
71
+
72
+ @parent = parent_id ? fetch_parent : nil
73
+ end
74
+
75
+ def parent?
76
+ parent.present?
77
+ end
78
+
79
+ def root?
80
+ parent.blank?
81
+ end
82
+
83
+ def depth
84
+ @depth ||= parent ? parent.level + 1 : 0
85
+ end
86
+
87
+ # Locks current position. Technically, it means that pessimistic
88
+ # lock will be obtained on parent node (or all root nodes if position is root)
89
+ def lock!
90
+ if parent
91
+ parent.lock!
92
+ else
93
+ siblings.lock.reload
94
+ end
95
+ end
96
+
97
+ # predicate
98
+ def position?
99
+ position.present?
100
+ end
101
+
102
+ # Returns all nodes within given position
103
+ def siblings
104
+ node.scope.where(klass.ordered_tree.columns.parent => parent_id)
105
+ end
106
+
107
+ # Returns all nodes that are lower than current position
108
+ def lower
109
+ position? ?
110
+ siblings.where(klass.arel_table[klass.ordered_tree.columns.position].gteq(position)) :
111
+ siblings
112
+ end
113
+
114
+ def increment_counter
115
+ update_counter(:increment_counter)
116
+ end
117
+
118
+ def decrement_counter
119
+ update_counter(:decrement_counter)
120
+ end
121
+
122
+ # @param [ActsAsOrderedTree::Node::Position] other
123
+ def ==(other)
124
+ other.is_a?(self.class) &&
125
+ other.record == record &&
126
+ other.parent_id == parent_id &&
127
+ other.position == position
128
+ end
129
+
130
+ private
131
+ def fetch_parent
132
+ parent_id == record[klass.ordered_tree.columns.parent] ?
133
+ record.parent :
134
+ node.scope.find(parent_id)
135
+ end
136
+
137
+ def update_counter(method)
138
+ if (column = klass.ordered_tree.columns.counter_cache) && parent_id
139
+ klass.send(method, column, parent_id)
140
+ end
141
+ end
142
+ end # class Position
143
+ end