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.
- checksums.yaml +4 -4
- data/lib/acts_as_ordered_tree.rb +22 -100
- data/lib/acts_as_ordered_tree/adapters.rb +17 -0
- data/lib/acts_as_ordered_tree/adapters/abstract.rb +23 -0
- data/lib/acts_as_ordered_tree/adapters/postgresql.rb +150 -0
- data/lib/acts_as_ordered_tree/adapters/recursive.rb +157 -0
- data/lib/acts_as_ordered_tree/compatibility.rb +22 -0
- data/lib/acts_as_ordered_tree/compatibility/active_record/association_scope.rb +9 -0
- data/lib/acts_as_ordered_tree/compatibility/active_record/default_scoped.rb +19 -0
- data/lib/acts_as_ordered_tree/compatibility/active_record/null_relation.rb +71 -0
- data/lib/acts_as_ordered_tree/compatibility/features.rb +153 -0
- data/lib/acts_as_ordered_tree/deprecate.rb +24 -0
- data/lib/acts_as_ordered_tree/hooks.rb +38 -0
- data/lib/acts_as_ordered_tree/hooks/update.rb +86 -0
- data/lib/acts_as_ordered_tree/instance_methods.rb +92 -453
- data/lib/acts_as_ordered_tree/iterators/arranger.rb +35 -0
- data/lib/acts_as_ordered_tree/iterators/level_calculator.rb +52 -0
- data/lib/acts_as_ordered_tree/iterators/orphans_pruner.rb +58 -0
- data/lib/acts_as_ordered_tree/node.rb +78 -0
- data/lib/acts_as_ordered_tree/node/attributes.rb +48 -0
- data/lib/acts_as_ordered_tree/node/movement.rb +62 -0
- data/lib/acts_as_ordered_tree/node/movements.rb +111 -0
- data/lib/acts_as_ordered_tree/node/predicates.rb +98 -0
- data/lib/acts_as_ordered_tree/node/reloading.rb +49 -0
- data/lib/acts_as_ordered_tree/node/siblings.rb +139 -0
- data/lib/acts_as_ordered_tree/node/traversals.rb +53 -0
- data/lib/acts_as_ordered_tree/persevering_transaction.rb +93 -0
- data/lib/acts_as_ordered_tree/position.rb +143 -0
- data/lib/acts_as_ordered_tree/relation/arrangeable.rb +33 -0
- data/lib/acts_as_ordered_tree/relation/iterable.rb +41 -0
- data/lib/acts_as_ordered_tree/relation/preloaded.rb +46 -11
- data/lib/acts_as_ordered_tree/transaction/base.rb +57 -0
- data/lib/acts_as_ordered_tree/transaction/callbacks.rb +67 -0
- data/lib/acts_as_ordered_tree/transaction/create.rb +68 -0
- data/lib/acts_as_ordered_tree/transaction/destroy.rb +34 -0
- data/lib/acts_as_ordered_tree/transaction/dsl.rb +214 -0
- data/lib/acts_as_ordered_tree/transaction/factory.rb +67 -0
- data/lib/acts_as_ordered_tree/transaction/move.rb +70 -0
- data/lib/acts_as_ordered_tree/transaction/passthrough.rb +12 -0
- data/lib/acts_as_ordered_tree/transaction/reorder.rb +42 -0
- data/lib/acts_as_ordered_tree/transaction/save.rb +64 -0
- data/lib/acts_as_ordered_tree/transaction/update.rb +78 -0
- data/lib/acts_as_ordered_tree/tree.rb +148 -0
- data/lib/acts_as_ordered_tree/tree/association.rb +20 -0
- data/lib/acts_as_ordered_tree/tree/callbacks.rb +57 -0
- data/lib/acts_as_ordered_tree/tree/children_association.rb +120 -0
- data/lib/acts_as_ordered_tree/tree/columns.rb +102 -0
- data/lib/acts_as_ordered_tree/tree/deprecated_columns_accessors.rb +24 -0
- data/lib/acts_as_ordered_tree/tree/parent_association.rb +31 -0
- data/lib/acts_as_ordered_tree/tree/perseverance.rb +19 -0
- data/lib/acts_as_ordered_tree/tree/scopes.rb +56 -0
- data/lib/acts_as_ordered_tree/validators.rb +1 -1
- data/lib/acts_as_ordered_tree/version.rb +1 -1
- data/spec/acts_as_ordered_tree_spec.rb +80 -909
- data/spec/adapters/postgresql_spec.rb +14 -0
- data/spec/adapters/recursive_spec.rb +12 -0
- data/spec/adapters/shared.rb +272 -0
- data/spec/callbacks_spec.rb +177 -0
- data/spec/counter_cache_spec.rb +31 -0
- data/spec/create_spec.rb +110 -0
- data/spec/destroy_spec.rb +57 -0
- data/spec/inheritance_spec.rb +176 -0
- data/spec/move_spec.rb +94 -0
- data/spec/node/movements/concurrent_movements_spec.rb +354 -0
- data/spec/node/movements/move_higher_spec.rb +46 -0
- data/spec/node/movements/move_lower_spec.rb +46 -0
- data/spec/node/movements/move_to_child_of_spec.rb +147 -0
- data/spec/node/movements/move_to_child_with_index_spec.rb +124 -0
- data/spec/node/movements/move_to_child_with_position_spec.rb +85 -0
- data/spec/node/movements/move_to_left_of_spec.rb +120 -0
- data/spec/node/movements/move_to_right_of_spec.rb +120 -0
- data/spec/node/movements/move_to_root_spec.rb +67 -0
- data/spec/node/predicates_spec.rb +211 -0
- data/spec/node/reloading_spec.rb +42 -0
- data/spec/node/siblings_spec.rb +193 -0
- data/spec/node/traversals_spec.rb +71 -0
- data/spec/persevering_transaction_spec.rb +98 -0
- data/spec/relation/arrangeable_spec.rb +88 -0
- data/spec/relation/iterable_spec.rb +104 -0
- data/spec/relation/preloaded_spec.rb +57 -0
- data/spec/reorder_spec.rb +83 -0
- data/spec/spec_helper.rb +30 -38
- data/spec/support/db/boot.rb +22 -0
- data/spec/{db → support/db}/config.travis.yml +2 -0
- data/spec/{db → support/db}/config.yml +1 -0
- data/spec/{db → support/db}/schema.rb +9 -0
- data/spec/support/factories.rb +2 -2
- data/spec/support/matchers.rb +67 -58
- data/spec/support/models.rb +6 -14
- data/spec/support/tree_factory.rb +315 -0
- data/spec/tree/children_association_spec.rb +72 -0
- data/spec/tree/columns_spec.rb +65 -0
- data/spec/tree/scopes_spec.rb +39 -0
- metadata +161 -43
- data/lib/acts_as_ordered_tree/adapters/postgresql_adapter.rb +0 -104
- data/lib/acts_as_ordered_tree/arrangeable.rb +0 -80
- data/lib/acts_as_ordered_tree/class_methods.rb +0 -72
- data/lib/acts_as_ordered_tree/relation/base.rb +0 -26
- data/lib/acts_as_ordered_tree/tenacious_transaction.rb +0 -30
- 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
|