glebtv-mongoid_nested_set 0.3.0
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 +7 -0
- data/.document +5 -0
- data/.gitignore +44 -0
- data/.rspec +1 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +147 -0
- data/Rakefile +1 -0
- data/glebtv-mongoid_nested_set.gemspec +29 -0
- data/lib/glebtv-mongoid_nested_set.rb +1 -0
- data/lib/mongoid_nested_set/base.rb +90 -0
- data/lib/mongoid_nested_set/document.rb +238 -0
- data/lib/mongoid_nested_set/fields.rb +60 -0
- data/lib/mongoid_nested_set/outline_number.rb +122 -0
- data/lib/mongoid_nested_set/rebuild.rb +40 -0
- data/lib/mongoid_nested_set/relations.rb +100 -0
- data/lib/mongoid_nested_set/remove_order_by.rb +15 -0
- data/lib/mongoid_nested_set/update.rb +230 -0
- data/lib/mongoid_nested_set/validation.rb +59 -0
- data/lib/mongoid_nested_set/version.rb +3 -0
- data/lib/mongoid_nested_set.rb +40 -0
- data/spec/matchers/nestedset_pos.rb +46 -0
- data/spec/models/circle_node.rb +4 -0
- data/spec/models/node.rb +10 -0
- data/spec/models/node_without_nested_set.rb +6 -0
- data/spec/models/numbering_node.rb +10 -0
- data/spec/models/renamed_fields.rb +7 -0
- data/spec/models/shape_node.rb +18 -0
- data/spec/models/square_node.rb +4 -0
- data/spec/models/test_document.rb +36 -0
- data/spec/models/unscoped_node.rb +9 -0
- data/spec/mongoid_nested_set_spec.rb +786 -0
- data/spec/spec_helper.rb +43 -0
- metadata +147 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
|
3
|
+
module OutlineNumber
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
base.send(:include, InstanceMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# Iterates over tree elements and determines the current outline number in
|
13
|
+
# the tree.
|
14
|
+
# Only accepts default ordering, ordering by an other field than lft
|
15
|
+
# does not work.
|
16
|
+
# This method does not used the cached number field.
|
17
|
+
#
|
18
|
+
# Example:
|
19
|
+
# Category.each_with_outline_number(Category.root.self_and_descendants) do |o, level|
|
20
|
+
#
|
21
|
+
def each_with_outline_number(objects, parent_number=nil)
|
22
|
+
objects = Array(objects) unless objects.is_a? Array
|
23
|
+
|
24
|
+
stack = []
|
25
|
+
last_num = parent_number
|
26
|
+
objects.each_with_index do |o, i|
|
27
|
+
if i == 0 && last_num == nil && !o.root?
|
28
|
+
last_num = o.parent.outline_number
|
29
|
+
end
|
30
|
+
|
31
|
+
if stack.last.nil? || o.parent_id != stack.last[:parent_id]
|
32
|
+
# we are on a new level, did we descend or ascend?
|
33
|
+
if stack.any? { |h| h[:parent_id] == o.parent_id }
|
34
|
+
# ascend
|
35
|
+
stack.pop while stack.last[:parent_id] != o.parent_id
|
36
|
+
else
|
37
|
+
# descend
|
38
|
+
stack << {:parent_id => o.parent_id, :parent_number => last_num, :siblings => []}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
if o.root? && !roots_have_outline_numbers?
|
43
|
+
num = nil
|
44
|
+
else
|
45
|
+
num = o.send(:build_outline_number,
|
46
|
+
o.root? ? '' : stack.last[:parent_number],
|
47
|
+
o.send(:outline_number_sequence, stack.last[:siblings])
|
48
|
+
)
|
49
|
+
end
|
50
|
+
yield(o, num)
|
51
|
+
|
52
|
+
stack.last[:siblings] << o
|
53
|
+
last_num = num
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def update_outline_numbers(objects, parent_number=nil)
|
59
|
+
each_with_outline_number(objects, parent_number) do |o, num|
|
60
|
+
o.update_attributes(outline_number_field_name => num)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# Do root nodes have outline numbers
|
66
|
+
def roots_have_outline_numbers?
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
module InstanceMethods
|
73
|
+
|
74
|
+
def outline_number
|
75
|
+
self[outline_number_field_name]
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def update_outline_number
|
80
|
+
self.class.update_outline_numbers(self)
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def update_self_and_descendants_outline_number
|
85
|
+
self.class.update_outline_numbers(self_and_descendants)
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def update_descendants_outline_number
|
90
|
+
self.class.update_outline_numbers(self.descendants, self.outline_number)
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
protected
|
95
|
+
|
96
|
+
# Gets the outline sequence number for this node
|
97
|
+
#
|
98
|
+
# For example, if the parent's outline number is 1.2 and this is the
|
99
|
+
# 3rd sibling this will return 3.
|
100
|
+
#
|
101
|
+
def outline_number_sequence(prev_siblings)
|
102
|
+
prev_siblings.count + 1
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
# Constructs the full outline number
|
107
|
+
#
|
108
|
+
def build_outline_number(parent_number, sequence)
|
109
|
+
if parent_number && parent_number != ''
|
110
|
+
parent_number + outline_number_seperator + sequence.to_s
|
111
|
+
else
|
112
|
+
sequence.to_s
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def outline_number_seperator
|
117
|
+
'.'
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
|
3
|
+
module Rebuild
|
4
|
+
|
5
|
+
# Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
|
6
|
+
# Warning: Very expensive!
|
7
|
+
def rebuild!(options = {})
|
8
|
+
# Don't rebuild a valid tree.
|
9
|
+
return true if valid?
|
10
|
+
|
11
|
+
scope = lambda{ |node| {} }
|
12
|
+
if acts_as_nested_set_options[:scope]
|
13
|
+
scope = lambda { |node| node.nested_set_scope.options.merge(node.nested_set_scope.selector) }
|
14
|
+
end
|
15
|
+
indices = {}
|
16
|
+
|
17
|
+
set_left_and_rights = lambda do |node|
|
18
|
+
# set left
|
19
|
+
left = (indices[scope.call(node)] += 1)
|
20
|
+
# find
|
21
|
+
node.nested_set_scope.where(parent_field_name => node.id).asc(left_field_name).asc(right_field_name).each { |n| set_left_and_rights.call(n) }
|
22
|
+
# set right
|
23
|
+
right = (indices[scope.call(node)] += 1)
|
24
|
+
|
25
|
+
node.class.collection.find(:_id => node.id).update(
|
26
|
+
{"$set" => {left_field_name => left, right_field_name => right}},
|
27
|
+
{:safe => true}
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Find root node(s)
|
32
|
+
root_nodes = self.where(parent_field_name => nil).asc(left_field_name).asc(right_field_name).asc(:_id).each do |root_node|
|
33
|
+
# setup index for this scope
|
34
|
+
indices[scope.call(root_node)] ||= 0
|
35
|
+
set_left_and_rights.call(root_node)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
|
3
|
+
module Relations
|
4
|
+
|
5
|
+
# Returns root
|
6
|
+
def root
|
7
|
+
self_and_ancestors.first
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
# Returns the array of all parents and self
|
12
|
+
def self_and_ancestors
|
13
|
+
nested_set_scope.where(
|
14
|
+
left_field_name => {"$lte" => left},
|
15
|
+
right_field_name => {"$gte" => right}
|
16
|
+
)
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
# Returns an array of all parents
|
21
|
+
def ancestors
|
22
|
+
without_self self_and_ancestors
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# Returns the array of all children of the parent, including self
|
27
|
+
def self_and_siblings
|
28
|
+
nested_set_scope.where(parent_field_name => parent_id)
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
# Returns the array of all children of the parent, except self
|
33
|
+
def siblings
|
34
|
+
without_self self_and_siblings
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
# Returns a set of all of its nested children which do not have children
|
39
|
+
def leaves
|
40
|
+
descendants.where("this.#{right_field_name} - this.#{left_field_name} == 1")
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
# Returns the level of this object in the tree
|
45
|
+
# root level is 0
|
46
|
+
def level
|
47
|
+
parent_id.nil? ? 0 : ancestors.count
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# Returns a set of itself and all of its nested children
|
52
|
+
def self_and_descendants
|
53
|
+
nested_set_scope.where(
|
54
|
+
left_field_name => {"$gte" => left},
|
55
|
+
right_field_name => {"$lte" => right}
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# Returns a set of all of its children and nested children
|
61
|
+
def descendants
|
62
|
+
without_self self_and_descendants
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
def is_descendant_of?(other)
|
67
|
+
other.left < self.left && self.left < other.right && same_scope?(other)
|
68
|
+
end
|
69
|
+
alias :descendant_of? is_descendant_of?
|
70
|
+
|
71
|
+
|
72
|
+
def is_or_is_descendant_of?(other)
|
73
|
+
other.left <= self.left && self.left < other.right && same_scope?(other)
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def is_ancestor_of?(other)
|
78
|
+
self.left < other.left && other.left < self.right && same_scope?(other)
|
79
|
+
end
|
80
|
+
alias :ancestor_of? is_ancestor_of?
|
81
|
+
|
82
|
+
|
83
|
+
def is_or_is_ancestor_of?(other)
|
84
|
+
self.left <= other.left && other.left < self.right && same_scope?(other)
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
# Find the first sibling to the left
|
89
|
+
def left_sibling
|
90
|
+
siblings.where(left_field_name => {"$lt" => left}).remove_order_by.desc(left_field_name).first
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
# Find the first sibling to the right
|
95
|
+
def right_sibling
|
96
|
+
siblings.where(left_field_name => {"$gt" => left}).asc(left_field_name).first
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
|
3
|
+
module Update
|
4
|
+
|
5
|
+
# Shorthand method for finding the left sibling and moving to the left of it
|
6
|
+
def move_left
|
7
|
+
move_to_left_of left_sibling
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
# Shorthand method for finding the right sibling and moving to the right of it
|
12
|
+
def move_right
|
13
|
+
move_to_right_of right_sibling
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
# Move the node to the left of another node (you can pass id only)
|
18
|
+
def move_to_left_of(node)
|
19
|
+
move_to node, :left
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
# Move the node to the right of another node (you can pass id only)
|
24
|
+
def move_to_right_of(node)
|
25
|
+
move_to node, :right
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
# Move the node to the child of another node (you can pass id only)
|
30
|
+
def move_to_child_of(node)
|
31
|
+
move_to node, :child
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# Move the node to root nodes
|
36
|
+
def move_to_root
|
37
|
+
move_to nil, :root
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def move_possible?(target)
|
42
|
+
self != target && # Can't target self
|
43
|
+
same_scope?(target) && # can't be in different scopes
|
44
|
+
!((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def store_new_parent
|
52
|
+
@move_to_new_parent_id = ((self.new_record? && parent_id) || send("#{parent_field_name}_changed?")) ? parent_id : false
|
53
|
+
true # force callback to return true
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
def move_to_new_parent
|
58
|
+
if @move_to_new_parent_id.nil?
|
59
|
+
move_to_root
|
60
|
+
elsif @move_to_new_parent_id
|
61
|
+
move_to_child_of(@move_to_new_parent_id)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# on creation, set automatically lft and rgt to the end of the tree
|
67
|
+
def set_default_left_and_right
|
68
|
+
maxright = nested_set_scope.remove_order_by.max(right_field_name) || 0
|
69
|
+
self[left_field_name] = maxright + 1
|
70
|
+
self[right_field_name] = maxright + 2
|
71
|
+
self[:depth] = 0
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def move_to(target, position)
|
76
|
+
raise Mongoid::Errors::MongoidError, "You cannot move a new node" if self.new_record?
|
77
|
+
|
78
|
+
res = run_callbacks :move do
|
79
|
+
|
80
|
+
# No transaction support in MongoDB.
|
81
|
+
# ACID is not guaranteed
|
82
|
+
# TODO
|
83
|
+
|
84
|
+
if target.is_a? scope_class
|
85
|
+
target.reload_nested_set
|
86
|
+
elsif position != :root
|
87
|
+
# load object if node is not an object
|
88
|
+
target = nested_set_scope.where(:_id => target).first
|
89
|
+
end
|
90
|
+
self.reload_nested_set
|
91
|
+
|
92
|
+
unless position == :root || target
|
93
|
+
raise Mongoid::Errors::MongoidError, "Impossible move, target node cannot be found."
|
94
|
+
end
|
95
|
+
|
96
|
+
unless position == :root || move_possible?(target)
|
97
|
+
raise Mongoid::Errors::MongoidError, "Impossible move, target node cannot be inside moved tree."
|
98
|
+
end
|
99
|
+
|
100
|
+
bound = case position
|
101
|
+
when :child; target[right_field_name]
|
102
|
+
when :left; target[left_field_name]
|
103
|
+
when :right; target[right_field_name] + 1
|
104
|
+
when :root; 1
|
105
|
+
else raise Mongoid::Errors::MongoidError, "Position should be :child, :left, :right or :root ('#{position}' received)."
|
106
|
+
end
|
107
|
+
|
108
|
+
old_parent = self[parent_field_name]
|
109
|
+
new_parent = case position
|
110
|
+
when :child; target.id
|
111
|
+
when :root; nil
|
112
|
+
else target[parent_field_name]
|
113
|
+
end
|
114
|
+
|
115
|
+
left, right = [self[left_field_name], self[right_field_name]]
|
116
|
+
width, distance = [right - left + 1, bound - left]
|
117
|
+
edge = bound > right ? bound - 1 : bound
|
118
|
+
|
119
|
+
# there would be no change
|
120
|
+
return self if left == edge || right == edge
|
121
|
+
|
122
|
+
# moving backwards
|
123
|
+
if distance < 0
|
124
|
+
distance -= width
|
125
|
+
left += width
|
126
|
+
end
|
127
|
+
|
128
|
+
scope_class.mongo_session.with(:safe => true) do |session|
|
129
|
+
collection = session[scope_class.collection_name]
|
130
|
+
scope = nested_set_scope.remove_order_by
|
131
|
+
|
132
|
+
# allocate space for new move
|
133
|
+
collection.find(
|
134
|
+
scope.gte(left_field_name => bound).selector
|
135
|
+
).update_all("$inc" => { left_field_name => width })
|
136
|
+
|
137
|
+
collection.find(
|
138
|
+
scope.gte(right_field_name => bound).selector
|
139
|
+
).update_all("$inc" => { right_field_name => width })
|
140
|
+
|
141
|
+
# move the nodes
|
142
|
+
collection.find(
|
143
|
+
scope.and(left_field_name => {"$gte" => left}, right_field_name => {"$lt" => left + width}).selector
|
144
|
+
).update_all("$inc" => { left_field_name => distance, right_field_name => distance })
|
145
|
+
|
146
|
+
# remove the hole
|
147
|
+
collection.find(
|
148
|
+
scope.gt(left_field_name => right).selector
|
149
|
+
).update_all("$inc" => { left_field_name => -width })
|
150
|
+
|
151
|
+
collection.find(
|
152
|
+
scope.gt(right_field_name => right).selector
|
153
|
+
).update_all("$inc" => { right_field_name => -width })
|
154
|
+
end
|
155
|
+
|
156
|
+
self.set(parent_field_name, new_parent)
|
157
|
+
self.reload_nested_set
|
158
|
+
self.update_self_and_descendants_depth
|
159
|
+
|
160
|
+
if outline_numbering?
|
161
|
+
if old_parent && old_parent != new_parent
|
162
|
+
scope_class.where(:_id => old_parent).first.update_descendants_outline_number
|
163
|
+
end
|
164
|
+
if new_parent
|
165
|
+
scope_class.where(:_id => new_parent).first.update_descendants_outline_number
|
166
|
+
else
|
167
|
+
update_self_and_descendants_outline_number
|
168
|
+
end
|
169
|
+
self.reload_nested_set
|
170
|
+
end
|
171
|
+
|
172
|
+
target.reload_nested_set if target
|
173
|
+
end
|
174
|
+
self
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
# Update cached level attribute
|
179
|
+
def update_depth
|
180
|
+
if depth?
|
181
|
+
self.update_attribute(:depth, level)
|
182
|
+
end
|
183
|
+
self
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
# Update cached level attribute for self and descendants
|
188
|
+
def update_self_and_descendants_depth
|
189
|
+
if depth?
|
190
|
+
scope_class.each_with_level(self_and_descendants) do |node, level|
|
191
|
+
node.with(:safe => true).set(:depth, level) unless node.depth == level
|
192
|
+
end
|
193
|
+
self.reload
|
194
|
+
end
|
195
|
+
self
|
196
|
+
end
|
197
|
+
|
198
|
+
|
199
|
+
# Prunes a branch off of the tree, shifting all of the elements on the right
|
200
|
+
# back to the left so the counts still work
|
201
|
+
def destroy_descendants
|
202
|
+
return if right.nil? || left.nil? || skip_before_destroy
|
203
|
+
|
204
|
+
if acts_as_nested_set_options[:dependent] == :destroy
|
205
|
+
descendants.each do |model|
|
206
|
+
model.skip_before_destroy = true
|
207
|
+
model.destroy
|
208
|
+
end
|
209
|
+
else
|
210
|
+
c = nested_set_scope.where(left_field_name.to_sym.gt => left, right_field_name.to_sym.lt => right)
|
211
|
+
scope_class.where(c.selector).delete_all
|
212
|
+
end
|
213
|
+
|
214
|
+
# update lefts and rights for remaining nodes
|
215
|
+
diff = right - left + 1
|
216
|
+
|
217
|
+
scope_class.with(:safe => true).where(
|
218
|
+
nested_set_scope.where(left_field_name.to_sym.gt => right).selector
|
219
|
+
).inc(left_field_name, -diff)
|
220
|
+
|
221
|
+
scope_class.with(:safe => true).where(
|
222
|
+
nested_set_scope.where(right_field_name.to_sym.gt => right).selector
|
223
|
+
).inc(right_field_name, -diff)
|
224
|
+
|
225
|
+
# Don't allow multiple calls to destroy to corrupt the set
|
226
|
+
self.skip_before_destroy = true
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
|
3
|
+
module Validation
|
4
|
+
|
5
|
+
# Warning: Very expensive! Do not use unless you know what you are doing.
|
6
|
+
# This method is only useful for determining if the entire tree is valid
|
7
|
+
def valid?
|
8
|
+
left_and_rights_valid? && no_duplicates_for_fields? && all_roots_valid?
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
# Warning: Very expensive! Do not use unless you know what you are doing.
|
13
|
+
def left_and_rights_valid?
|
14
|
+
all.detect { |node|
|
15
|
+
node.send(left_field_name).nil? ||
|
16
|
+
node.send(right_field_name).nil? ||
|
17
|
+
node.send(left_field_name) >= node.send(right_field_name) ||
|
18
|
+
!node.parent.nil? && (
|
19
|
+
node.send(left_field_name) <= node.parent.send(left_field_name) ||
|
20
|
+
node.send(right_field_name) >= node.parent.send(right_field_name)
|
21
|
+
)
|
22
|
+
}.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
# Warning: Very expensive! Do not use unless you know what you are doing.
|
27
|
+
def no_duplicates_for_fields?
|
28
|
+
roots.group_by{|record| scope_field_names.collect{|field| record.send(field.to_sym)}}.all? do |scope, grouped_roots|
|
29
|
+
[left_field_name, right_field_name].all? do |field|
|
30
|
+
grouped_roots.first.nested_set_scope.only(field).group_by {|doc| doc.send(field)}.all? {|k, v| v.size == 1}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
# Wrapper for each_root_valid? that can deal with scope
|
37
|
+
# Warning: Very expensive! Do not use unless you know what you are doing.
|
38
|
+
def all_roots_valid?
|
39
|
+
if acts_as_nested_set_options[:scope]
|
40
|
+
roots.group_by{|record| scope_field_names.collect{|field| record.send(field.to_sym)}}.all? do |scope, grouped_roots|
|
41
|
+
each_root_valid?(grouped_roots)
|
42
|
+
end
|
43
|
+
else
|
44
|
+
each_root_valid?(roots)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def each_root_valid?(roots_to_validate)
|
50
|
+
right = 0
|
51
|
+
roots_to_validate.all? do |root|
|
52
|
+
(root.left > right && root.right > right).tap do
|
53
|
+
right = root.right
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
|
2
|
+
require 'mongoid_nested_set/remove_order_by'
|
3
|
+
|
4
|
+
# This acts provides Nested Set functionality. Nested Set is a smart way to implement
|
5
|
+
# an _ordered_ tree, with the added feature that you can select the children and all of
|
6
|
+
# their descendants with a single query. The drawback is that insertion or move need
|
7
|
+
# multiple queries. But everything is done here by this module!
|
8
|
+
#
|
9
|
+
# Nested sets are appropriate each time you want either an ordered tree (menus,
|
10
|
+
# commercial categories) or an efficient way of querying big trees (threaded posts).
|
11
|
+
#
|
12
|
+
# == API
|
13
|
+
#
|
14
|
+
# Method names are aligned with acts_as_tree as much as possible to make replacement
|
15
|
+
# from one by another easier.
|
16
|
+
#
|
17
|
+
# item.children.create(:name => 'child1')
|
18
|
+
#
|
19
|
+
module Mongoid
|
20
|
+
module Acts
|
21
|
+
module NestedSet
|
22
|
+
require 'mongoid_nested_set/base'
|
23
|
+
autoload :Document, 'mongoid_nested_set/document'
|
24
|
+
autoload :Fields, 'mongoid_nested_set/fields'
|
25
|
+
autoload :Rebuild, 'mongoid_nested_set/rebuild'
|
26
|
+
autoload :Relations, 'mongoid_nested_set/relations'
|
27
|
+
autoload :Update, 'mongoid_nested_set/update'
|
28
|
+
autoload :Validation, 'mongoid_nested_set/validation'
|
29
|
+
autoload :OutlineNumber, 'mongoid_nested_set/outline_number'
|
30
|
+
|
31
|
+
def self.included(base)
|
32
|
+
base.extend(Base)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
# Enable the acts_as_nested_set method
|
40
|
+
Mongoid::Document::ClassMethods.send(:include, Mongoid::Acts::NestedSet::Base)
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Mongoid::Acts::NestedSet
|
2
|
+
module Matchers
|
3
|
+
|
4
|
+
def have_nestedset_pos(lft, rgt, options = {})
|
5
|
+
NestedSetPosition.new(lft, rgt, options)
|
6
|
+
end
|
7
|
+
|
8
|
+
class NestedSetPosition
|
9
|
+
|
10
|
+
def initialize(lft, rgt, options)
|
11
|
+
@lft = lft
|
12
|
+
@rgt = rgt
|
13
|
+
@options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def matches?(node)
|
17
|
+
@node = node
|
18
|
+
!!(
|
19
|
+
node.respond_to?('left') && node.respond_to?('right') &&
|
20
|
+
node.left == @lft &&
|
21
|
+
node.right == @rgt
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def description
|
26
|
+
"have position {left: #{@lft}, right: #{@rgt}}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def failure_message_for_should
|
30
|
+
sprintf("expected nested set position: {left: %2s, right: %2s}\n" +
|
31
|
+
" got: {left: %2s, right: %2s}",
|
32
|
+
@lft,
|
33
|
+
@rgt,
|
34
|
+
@node.respond_to?('left') ? @node.left : '?',
|
35
|
+
@node.respond_to?('right') ? @node.right : '?'
|
36
|
+
)
|
37
|
+
end
|
38
|
+
|
39
|
+
def failure_message_for_should_not
|
40
|
+
sprintf("expected nested set to not have position: {left: %2s, right: %2s}", @lft, @rgt)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
data/spec/models/node.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/test_document"
|
2
|
+
|
3
|
+
class NumberingNode
|
4
|
+
include Mongoid::Document
|
5
|
+
include Mongoid::Acts::NestedSet::TestDocument
|
6
|
+
acts_as_nested_set :scope => :root_id, :outline_number_field => 'number'
|
7
|
+
|
8
|
+
field :name
|
9
|
+
field :root_id, :type => Integer
|
10
|
+
end
|