glebtv-mongoid_nested_set 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|