mongoid-ancestry-fixes 0.0.1
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 +15 -0
- data/.gitignore +42 -0
- data/.travis.yml +3 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +47 -0
- data/Guardfile +10 -0
- data/MIT-LICENSE +20 -0
- data/README.md +255 -0
- data/Rakefile +21 -0
- data/init.rb +1 -0
- data/install.rb +1 -0
- data/lib/mongoid-ancestry/class_methods.rb +212 -0
- data/lib/mongoid-ancestry/exceptions.rb +6 -0
- data/lib/mongoid-ancestry/instance_methods.rb +248 -0
- data/lib/mongoid-ancestry/version.rb +5 -0
- data/lib/mongoid-ancestry.rb +15 -0
- data/log/.gitignore +4 -0
- data/mongoid-ancestry.gemspec +29 -0
- data/spec/lib/ancestry_spec.rb +110 -0
- data/spec/lib/mongoid-ancestry/class_methods_spec.rb +300 -0
- data/spec/lib/mongoid-ancestry/instance_methods_spec.rb +241 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/support/models.rb +40 -0
- metadata +106 -0
@@ -0,0 +1,300 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MongoidAncestry do
|
4
|
+
|
5
|
+
subject { MongoidAncestry }
|
6
|
+
|
7
|
+
it "should have scopes" do
|
8
|
+
subject.with_model :depth => 3, :width => 3 do |model, roots|
|
9
|
+
# Roots assertion
|
10
|
+
model.roots.all.to_a.should eql(roots.map(&:first))
|
11
|
+
|
12
|
+
model.all.each do |test_node|
|
13
|
+
# Assertions for ancestors_of named scope
|
14
|
+
model.ancestors_of(test_node).all.should == test_node.ancestors.all
|
15
|
+
model.ancestors_of(test_node.id).all.to_a.should eql(test_node.ancestors.all.to_a)
|
16
|
+
# Assertions for children_of named scope
|
17
|
+
model.children_of(test_node).all.to_a.should eql(test_node.children.all.to_a)
|
18
|
+
model.children_of(test_node.id).all.to_a.should eql(test_node.children.all.to_a)
|
19
|
+
# Assertions for descendants_of named scope
|
20
|
+
model.descendants_of(test_node).all.should == (test_node.descendants.all)
|
21
|
+
model.descendants_of(test_node.id).all.to_a.should eql(test_node.descendants.all.to_a)
|
22
|
+
# Assertions for subtree_of named scope
|
23
|
+
model.subtree_of(test_node).all.to_a.should eql(test_node.subtree.all.to_a)
|
24
|
+
model.subtree_of(test_node.id).all.to_a.should eql(test_node.subtree.all.to_a)
|
25
|
+
# Assertions for siblings_of named scope
|
26
|
+
model.siblings_of(test_node).all.to_a.should eql(test_node.siblings.all.to_a)
|
27
|
+
model.siblings_of(test_node.id).all.to_a.should eql(test_node.siblings.all.to_a)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should be arranged" do
|
33
|
+
subject.with_model :depth => 3, :width => 3 do |model, roots|
|
34
|
+
id_sorter = Proc.new {|a, b|; a.to_param <=> b.to_param }
|
35
|
+
arranged_nodes = model.arrange
|
36
|
+
arranged_nodes.size.should eql(3)
|
37
|
+
arranged_nodes.each do |node, children|
|
38
|
+
children.keys.sort(&id_sorter).should eql(node.children.sort(&id_sorter))
|
39
|
+
children.each do |node, children|
|
40
|
+
children.keys.sort(&id_sorter).should eql(node.children.sort(&id_sorter))
|
41
|
+
children.each do |node, children|
|
42
|
+
children.size.should eql(0)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should have arrange order option" do
|
50
|
+
subject.with_model :width => 3, :depth => 3 do |model, roots|
|
51
|
+
descending_nodes_lvl0 = model.arrange :order => [:_id, :desc]
|
52
|
+
ascending_nodes_lvl0 = model.arrange :order => [:_id, :asc]
|
53
|
+
|
54
|
+
descending_nodes_lvl0.keys.zip(ascending_nodes_lvl0.keys.reverse).each do |descending_node, ascending_node|
|
55
|
+
ascending_node.should eql(descending_node)
|
56
|
+
descending_nodes_lvl1 = descending_nodes_lvl0[descending_node]
|
57
|
+
ascending_nodes_lvl1 = ascending_nodes_lvl0[ascending_node]
|
58
|
+
descending_nodes_lvl1.keys.zip(ascending_nodes_lvl1.keys.reverse).each do |descending_node, ascending_node|
|
59
|
+
ascending_node.should eql(descending_node)
|
60
|
+
descending_nodes_lvl2 = descending_nodes_lvl1[descending_node]
|
61
|
+
ascending_nodes_lvl2 = ascending_nodes_lvl1[ascending_node]
|
62
|
+
descending_nodes_lvl2.keys.zip(ascending_nodes_lvl2.keys.reverse).each do |descending_node, ascending_node|
|
63
|
+
ascending_node.should eql(descending_node)
|
64
|
+
descending_nodes_lvl3 = descending_nodes_lvl2[descending_node]
|
65
|
+
ascending_nodes_lvl3 = ascending_nodes_lvl2[ascending_node]
|
66
|
+
descending_nodes_lvl3.keys.zip(ascending_nodes_lvl3.keys.reverse).each do |descending_node, ascending_node|
|
67
|
+
ascending_node.should eql(descending_node)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should have valid orphan rootify strategy" do
|
76
|
+
subject.with_model :depth => 3, :width => 3 do |model, roots|
|
77
|
+
model.orphan_strategy = :rootify
|
78
|
+
root = roots.first.first
|
79
|
+
children = root.children.all
|
80
|
+
root.destroy
|
81
|
+
children.each do |child|
|
82
|
+
child.reload
|
83
|
+
child.is_root?.should be_true
|
84
|
+
child.children.size.should eql(3)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should have valid orphan destroy strategy" do
|
90
|
+
subject.with_model :depth => 3, :width => 3 do |model, roots|
|
91
|
+
model.orphan_strategy = :destroy
|
92
|
+
root = roots.first.first
|
93
|
+
expect { root.destroy }.to change(model, :count).by(-root.subtree.size)
|
94
|
+
node = model.roots.first.children.first
|
95
|
+
expect { node.destroy }.to change(model, :count).by(-node.subtree.size)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should have valid orphan restrict strategy" do
|
100
|
+
subject.with_model :depth => 3, :width => 3 do |model, roots|
|
101
|
+
model.orphan_strategy = :restrict
|
102
|
+
root = roots.first.first
|
103
|
+
expect { root.destroy }.to raise_error Mongoid::Ancestry::Error
|
104
|
+
expect { root.children.first.children.first.destroy }.to_not raise_error Mongoid::Ancestry::Error
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should check that there are no errors on a valid tree" do
|
109
|
+
subject.with_model :width => 3, :depth => 3 do |model, roots|
|
110
|
+
expect { model.check_ancestry_integrity! }.to_not raise_error(Mongoid::Ancestry::Error)
|
111
|
+
model.check_ancestry_integrity!(:report => :list).size.should eql(0)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should check detection of invalid format for ancestry field" do
|
116
|
+
subject.with_model :width => 3, :depth => 3 do |model, roots|
|
117
|
+
roots.first.first.update_attribute model.ancestry_field, 'invalid_ancestry'
|
118
|
+
expect { model.check_ancestry_integrity! }.to raise_error(Mongoid::Ancestry::IntegrityError)
|
119
|
+
model.check_ancestry_integrity!(:report => :list).size.should eql(1)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should check detection of non-existent ancestor" do
|
124
|
+
subject.with_model :width => 3, :depth => 3 do |model, roots|
|
125
|
+
node = roots.first.first
|
126
|
+
node.without_ancestry_callbacks do
|
127
|
+
node.update_attribute model.ancestry_field, 35
|
128
|
+
end
|
129
|
+
expect { model.check_ancestry_integrity! }.to raise_error(Mongoid::Ancestry::IntegrityError)
|
130
|
+
model.check_ancestry_integrity!(:report => :list).size.should eql(1)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should check detection of cyclic ancestry" do
|
135
|
+
subject.with_model :width => 3, :depth => 3 do |model, roots|
|
136
|
+
node = roots.first.first
|
137
|
+
node.update_attribute model.ancestry_field, node.id
|
138
|
+
expect { model.check_ancestry_integrity! }.to raise_error(Mongoid::Ancestry::IntegrityError)
|
139
|
+
model.check_ancestry_integrity!(:report => :list).size.should eql(1)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should check detection of conflicting parent id" do
|
144
|
+
subject.with_model do |model|
|
145
|
+
model.destroy_all
|
146
|
+
model.create!(model.ancestry_field => model.create!(model.ancestry_field => model.create!(model.ancestry_field => nil).id).id)
|
147
|
+
expect { model.check_ancestry_integrity! }.to raise_error(Mongoid::Ancestry::IntegrityError)
|
148
|
+
model.check_ancestry_integrity!(:report => :list).size.should eql(1)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def assert_integrity_restoration model
|
153
|
+
expect { model.check_ancestry_integrity! }.to raise_error(Mongoid::Ancestry::IntegrityError)
|
154
|
+
model.restore_ancestry_integrity!
|
155
|
+
expect { model.check_ancestry_integrity! }.to_not raise_error(Mongoid::Ancestry::IntegrityError)
|
156
|
+
end
|
157
|
+
|
158
|
+
it "should check that integrity is restored for invalid format for ancestry field" do
|
159
|
+
subject.with_model :width => 3, :depth => 3 do |model, roots|
|
160
|
+
roots.first.first.update_attribute model.ancestry_field, 'invalid_ancestry'
|
161
|
+
assert_integrity_restoration model
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
it "should check that integrity is restored for non-existent ancestor" do
|
166
|
+
subject.with_model :width => 3, :depth => 3 do |model, roots|
|
167
|
+
roots.first.first.update_attribute model.ancestry_field, 35
|
168
|
+
assert_integrity_restoration model
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
it "should check that integrity is restored for cyclic ancestry" do
|
173
|
+
subject.with_model :width => 3, :depth => 3 do |model, roots|
|
174
|
+
node = roots.first.first
|
175
|
+
node.update_attribute model.ancestry_field, node.id
|
176
|
+
assert_integrity_restoration model
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
it "should check that integrity is restored for conflicting parent id" do
|
181
|
+
subject.with_model do |model|
|
182
|
+
model.destroy_all
|
183
|
+
model.create!(model.ancestry_field => model.create!(model.ancestry_field => model.create!(model.ancestry_field => nil).id).id)
|
184
|
+
assert_integrity_restoration model
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
it "should create node through scope" do
|
189
|
+
subject.with_model do |model|
|
190
|
+
node = model.create!
|
191
|
+
child = node.children.create # doesn't pass with .create!
|
192
|
+
child.parent.should eql(node)
|
193
|
+
|
194
|
+
other_child = child.siblings.create # doesn't pass with .create!
|
195
|
+
other_child.parent.should eql(node)
|
196
|
+
|
197
|
+
grandchild = model.children_of(child).build # doesn't pass with .new
|
198
|
+
grandchild.save
|
199
|
+
grandchild.parent.should eql(child)
|
200
|
+
|
201
|
+
other_grandchild = model.siblings_of(grandchild).build # doesn't pass with .new
|
202
|
+
other_grandchild.save!
|
203
|
+
other_grandchild.parent.should eql(child)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
it "should have depth scopes" do
|
208
|
+
subject.with_model :depth => 4, :width => 2, :cache_depth => true do |model, roots|
|
209
|
+
model.before_depth(2).all? { |node| node.depth < 2 }.should be_true
|
210
|
+
model.to_depth(2).all? { |node| node.depth <= 2 }.should be_true
|
211
|
+
model.at_depth(2).all? { |node| node.depth == 2 }.should be_true
|
212
|
+
model.from_depth(2).all? { |node| node.depth >= 2 }.should be_true
|
213
|
+
model.after_depth(2).all? { |node| node.depth > 2 }.should be_true
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should raise error on invalid scopes" do
|
218
|
+
subject.with_model do |model|
|
219
|
+
expect { model.before_depth(1) } .to raise_error(Mongoid::Ancestry::Error)
|
220
|
+
expect { model.to_depth(1) } .to raise_error(Mongoid::Ancestry::Error)
|
221
|
+
expect { model.at_depth(1) } .to raise_error(Mongoid::Ancestry::Error)
|
222
|
+
expect { model.from_depth(1) } .to raise_error(Mongoid::Ancestry::Error)
|
223
|
+
expect { model.after_depth(1) } .to raise_error(Mongoid::Ancestry::Error)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
it "should raise error on invalid has_ancestry options" do
|
228
|
+
subject.with_model do |model|
|
229
|
+
expect { model.has_ancestry :this_option_doesnt_exist => 42 }.to raise_error(Mongoid::Ancestry::Error)
|
230
|
+
expect { model.has_ancestry :not_a_hash }.to raise_error(Mongoid::Ancestry::Error)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
it "should build ancestry from parent ids" do
|
235
|
+
subject.with_model :skip_ancestry => true, :extra_columns => {:parent_id => :integer} do |model|
|
236
|
+
[model.create!].each do |parent1|
|
237
|
+
(Array.new(5) { model.create :parent_id => parent1.id }).each do |parent2|
|
238
|
+
(Array.new(5) { model.create :parent_id => parent2.id }).each do |parent3|
|
239
|
+
(Array.new(5) { model.create :parent_id => parent3.id })
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Assert all nodes where created
|
245
|
+
model.count.should eql((0..3).map { |n| 5 ** n }.sum)
|
246
|
+
end
|
247
|
+
|
248
|
+
subject.with_model do |model|
|
249
|
+
|
250
|
+
model.build_ancestry_from_parent_ids!
|
251
|
+
|
252
|
+
# Assert ancestry integrity
|
253
|
+
model.check_ancestry_integrity!
|
254
|
+
|
255
|
+
roots = model.roots.all
|
256
|
+
## Assert single root node
|
257
|
+
roots.size.should eql(1)
|
258
|
+
|
259
|
+
## Assert it has 5 children
|
260
|
+
roots.each do |parent|
|
261
|
+
parent.children.count.should eql(5)
|
262
|
+
parent.children.each do |parent|
|
263
|
+
parent.children.count.should eql(5)
|
264
|
+
parent.children.each do |parent|
|
265
|
+
parent.children.count.should eql(5)
|
266
|
+
parent.children.each do |parent|
|
267
|
+
parent.children.count.should eql(0)
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
it "should rebuild depth cache" do
|
276
|
+
subject.with_model :depth => 3, :width => 3, :cache_depth => true, :depth_cache_field => :depth_cache do |model, roots|
|
277
|
+
model.update_all(:depth_cache => nil)
|
278
|
+
|
279
|
+
# Assert cache was emptied correctly
|
280
|
+
model.all.each do |test_node|
|
281
|
+
test_node.depth_cache.should eql(nil)
|
282
|
+
end
|
283
|
+
|
284
|
+
# Rebuild cache
|
285
|
+
model.rebuild_depth_cache!
|
286
|
+
|
287
|
+
# Assert cache was rebuild correctly
|
288
|
+
model.all.each do |test_node|
|
289
|
+
test_node.depth_cache.should eql(test_node.depth)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
it "should raise exception when rebuilding depth cache for model without depth caching" do
|
295
|
+
subject.with_model do |model|
|
296
|
+
expect { model.rebuild_depth_cache! }.to raise_error(Mongoid::Ancestry::Error)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
end
|
@@ -0,0 +1,241 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MongoidAncestry do
|
4
|
+
|
5
|
+
subject { MongoidAncestry }
|
6
|
+
|
7
|
+
it "should have tree navigation" do
|
8
|
+
subject.with_model :depth => 3, :width => 3 do |model, roots|
|
9
|
+
roots.each do |lvl0_node, lvl0_children|
|
10
|
+
# Ancestors assertions
|
11
|
+
lvl0_node.ancestor_ids.should eql([])
|
12
|
+
lvl0_node.ancestors.to_a.should eql([])
|
13
|
+
lvl0_node.path_ids.should eql([lvl0_node.id])
|
14
|
+
lvl0_node.path.to_a.should eql([lvl0_node])
|
15
|
+
lvl0_node.depth.should eql(0)
|
16
|
+
# Parent assertions
|
17
|
+
lvl0_node.parent_id.should be_nil
|
18
|
+
lvl0_node.parent.should be_nil
|
19
|
+
# Root assertions
|
20
|
+
lvl0_node.root_id.should eql(lvl0_node.id)
|
21
|
+
lvl0_node.root.should eql(lvl0_node)
|
22
|
+
lvl0_node.is_root?.should be_true
|
23
|
+
# Children assertions
|
24
|
+
lvl0_node.child_ids.should eql(lvl0_children.map(&:first).map(&:id))
|
25
|
+
lvl0_node.children.to_a.should eql(lvl0_children.map(&:first))
|
26
|
+
lvl0_node.has_children?.should be_true
|
27
|
+
lvl0_node.is_childless?.should be_false
|
28
|
+
# Siblings assertions
|
29
|
+
lvl0_node.sibling_ids.should eql(roots.map(&:first).map(&:id))
|
30
|
+
lvl0_node.siblings.to_a.should eql(roots.map(&:first))
|
31
|
+
lvl0_node.has_siblings?.should be_true
|
32
|
+
lvl0_node.is_only_child?.should be_false
|
33
|
+
# Descendants assertions
|
34
|
+
descendants = model.all.find_all do |node|
|
35
|
+
node.ancestor_ids.include?(lvl0_node.id)
|
36
|
+
end
|
37
|
+
lvl0_node.descendant_ids.should eql(descendants.map(&:id))
|
38
|
+
lvl0_node.descendants.to_a.should eql(descendants)
|
39
|
+
lvl0_node.subtree.to_a.should eql([lvl0_node] + descendants)
|
40
|
+
|
41
|
+
lvl0_children.each do |lvl1_node, lvl1_children|
|
42
|
+
# Ancestors assertions
|
43
|
+
lvl1_node.ancestor_ids.should eql([lvl0_node.id])
|
44
|
+
lvl1_node.ancestors.to_a.should eql([lvl0_node])
|
45
|
+
lvl1_node.path_ids.should eql([lvl0_node.id, lvl1_node.id])
|
46
|
+
lvl1_node.path.to_a.should eql([lvl0_node, lvl1_node])
|
47
|
+
lvl1_node.depth.should eql(1)
|
48
|
+
# Parent assertions
|
49
|
+
lvl1_node.parent_id.should eql(lvl0_node.id)
|
50
|
+
lvl1_node.parent.should eql(lvl0_node)
|
51
|
+
# Root assertions
|
52
|
+
lvl1_node.root_id.should eql(lvl0_node.id)
|
53
|
+
lvl1_node.root.should eql(lvl0_node)
|
54
|
+
lvl1_node.is_root?.should be_false
|
55
|
+
# Children assertions
|
56
|
+
lvl1_node.child_ids.should eql(lvl1_children.map(&:first).map(&:id))
|
57
|
+
lvl1_node.children.to_a.should eql(lvl1_children.map(&:first))
|
58
|
+
lvl1_node.has_children?.should be_true
|
59
|
+
lvl1_node.is_childless?.should be_false
|
60
|
+
# Siblings assertions
|
61
|
+
lvl1_node.sibling_ids.should eql(lvl0_children.map(&:first).map(&:id))
|
62
|
+
lvl1_node.siblings.to_a.should eql(lvl0_children.map(&:first))
|
63
|
+
lvl1_node.has_siblings?.should be_true
|
64
|
+
lvl1_node.is_only_child?.should be_false
|
65
|
+
# Descendants assertions
|
66
|
+
descendants = model.all.find_all do |node|
|
67
|
+
node.ancestor_ids.include? lvl1_node.id
|
68
|
+
end
|
69
|
+
|
70
|
+
lvl1_node.descendant_ids.should eql(descendants.map(&:id))
|
71
|
+
lvl1_node.descendants.to_a.should eql(descendants)
|
72
|
+
lvl1_node.subtree.to_a.should eql([lvl1_node] + descendants)
|
73
|
+
|
74
|
+
lvl1_children.each do |lvl2_node, lvl2_children|
|
75
|
+
# Ancestors assertions
|
76
|
+
lvl2_node.ancestor_ids.should eql([lvl0_node.id, lvl1_node.id])
|
77
|
+
lvl2_node.ancestors.to_a.should eql([lvl0_node, lvl1_node])
|
78
|
+
lvl2_node.path_ids.should eql([lvl0_node.id, lvl1_node.id, lvl2_node.id])
|
79
|
+
lvl2_node.path.to_a.should eql([lvl0_node, lvl1_node, lvl2_node])
|
80
|
+
lvl2_node.depth.should eql(2)
|
81
|
+
# Parent assertions
|
82
|
+
lvl2_node.parent_id.should eql(lvl1_node.id)
|
83
|
+
lvl2_node.parent.should eql(lvl1_node)
|
84
|
+
# Root assertions
|
85
|
+
lvl2_node.root_id.should eql(lvl0_node.id)
|
86
|
+
lvl2_node.root.should eql(lvl0_node)
|
87
|
+
lvl2_node.is_root?.should be_false
|
88
|
+
# Children assertions
|
89
|
+
lvl2_node.child_ids.should eql([])
|
90
|
+
lvl2_node.children.to_a.should eql([])
|
91
|
+
lvl2_node.has_children?.should be_false
|
92
|
+
lvl2_node.is_childless?.should be_true
|
93
|
+
# Siblings assertions
|
94
|
+
lvl2_node.sibling_ids.should eql(lvl1_children.map(&:first).map(&:id))
|
95
|
+
lvl2_node.siblings.to_a.should eql(lvl1_children.map(&:first))
|
96
|
+
lvl2_node.has_siblings?.should be_true
|
97
|
+
lvl2_node.is_only_child?.should be_false
|
98
|
+
# Descendants assertions
|
99
|
+
descendants = model.all.find_all do |node|
|
100
|
+
node.ancestor_ids.include? lvl2_node.id
|
101
|
+
end
|
102
|
+
lvl2_node.descendant_ids.should eql(descendants.map(&:id))
|
103
|
+
lvl2_node.descendants.to_a.should eql(descendants)
|
104
|
+
lvl2_node.subtree.to_a.should eql([lvl2_node] + descendants)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
it "should validate ancestry field" do
|
112
|
+
subject.with_model do |model|
|
113
|
+
node = model.create
|
114
|
+
['3', '10/2', '1/4/30', nil].each do |value|
|
115
|
+
node.send :write_attribute, model.ancestry_field, value
|
116
|
+
node.should be_valid
|
117
|
+
node.errors[model.ancestry_field].blank?.should be_true
|
118
|
+
end
|
119
|
+
['1/3/', '/2/3', 'A/b', '-34', '/54'].each do |value|
|
120
|
+
node.send :write_attribute, model.ancestry_field, value
|
121
|
+
node.should_not be_valid
|
122
|
+
node.errors[model.ancestry_field].blank?.should be_false
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should move descendants with node" do
|
128
|
+
subject.with_model :depth => 3, :width => 3 do |model, roots|
|
129
|
+
root1, root2, root3 = roots.map(&:first)
|
130
|
+
|
131
|
+
descendants = root1.descendants.asc(:_id).map(&:to_param)
|
132
|
+
expect {
|
133
|
+
root1.parent = root2
|
134
|
+
root1.save!
|
135
|
+
root1.descendants.asc(:_id).map(&:to_param).should eql(descendants)
|
136
|
+
}.to change(root2.descendants, 'size').by(root1.subtree.size)
|
137
|
+
|
138
|
+
descendants = root2.descendants.asc(:_id).map(&:to_param)
|
139
|
+
expect {
|
140
|
+
root2.parent = root3
|
141
|
+
root2.save!
|
142
|
+
root2.descendants.asc(:_id).map(&:to_param).should eql(descendants)
|
143
|
+
}.to change(root3.descendants, 'size').by(root2.subtree.size)
|
144
|
+
|
145
|
+
descendants = root1.descendants.asc(:_id).map(&:to_param)
|
146
|
+
expect {
|
147
|
+
expect {
|
148
|
+
root1.parent = nil
|
149
|
+
root1.save!
|
150
|
+
root1.descendants.asc(:_id).map(&:to_param).should eql(descendants)
|
151
|
+
}.to change(root3.descendants, 'size').by(-root1.subtree.size)
|
152
|
+
}.to change(root2.descendants, 'size').by(-root1.subtree.size)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
it "should validate ancestry exclude self" do
|
157
|
+
subject.with_model do |model|
|
158
|
+
parent = model.create!
|
159
|
+
child = parent.children.create
|
160
|
+
expect { parent.update_attributes! :parent => child }.to raise_error(Mongoid::Errors::Validations)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should have depth caching" do
|
165
|
+
subject.with_model :depth => 3, :width => 3, :cache_depth => true, :depth_cache_field => :depth_cache do |model, roots|
|
166
|
+
roots.each do |lvl0_node, lvl0_children|
|
167
|
+
lvl0_node.depth_cache.should eql(0)
|
168
|
+
lvl0_children.each do |lvl1_node, lvl1_children|
|
169
|
+
lvl1_node.depth_cache.should eql(1)
|
170
|
+
lvl1_children.each do |lvl2_node, lvl2_children|
|
171
|
+
lvl2_node.depth_cache.should eql(2)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
it "should have descendants with depth constraints" do
|
179
|
+
subject.with_model :depth => 4, :width => 4, :cache_depth => true do |model, roots|
|
180
|
+
model.roots.first.descendants(:before_depth => 2).count.should eql(4)
|
181
|
+
model.roots.first.descendants(:to_depth => 2).count.should eql(20)
|
182
|
+
model.roots.first.descendants(:at_depth => 2).count.should eql(16)
|
183
|
+
model.roots.first.descendants(:from_depth => 2).count.should eql(80)
|
184
|
+
model.roots.first.descendants(:after_depth => 2).count.should eql(64)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
it "should have subtree with depth constraints" do
|
189
|
+
subject.with_model :depth => 4, :width => 4, :cache_depth => true do |model, roots|
|
190
|
+
model.roots.first.subtree(:before_depth => 2).count.should eql(5)
|
191
|
+
model.roots.first.subtree(:to_depth => 2).count.should eql(21)
|
192
|
+
model.roots.first.subtree(:at_depth => 2).count.should eql(16)
|
193
|
+
model.roots.first.subtree(:from_depth => 2).count.should eql(80)
|
194
|
+
model.roots.first.subtree(:after_depth => 2).count.should eql(64)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
it "should have ancestors with depth constraints" do
|
199
|
+
subject.with_model :cache_depth => true do |model|
|
200
|
+
node1 = model.create!
|
201
|
+
node2 = node1.children.create
|
202
|
+
node3 = node2.children.create
|
203
|
+
node4 = node3.children.create
|
204
|
+
node5 = node4.children.create
|
205
|
+
leaf = node5.children.create
|
206
|
+
|
207
|
+
leaf.ancestors(:before_depth => -2).to_a.should eql([node1, node2, node3])
|
208
|
+
leaf.ancestors(:to_depth => -2).to_a.should eql([node1, node2, node3, node4])
|
209
|
+
leaf.ancestors(:at_depth => -2).to_a.should eql([node4])
|
210
|
+
leaf.ancestors(:from_depth => -2).to_a.should eql([node4, node5])
|
211
|
+
leaf.ancestors(:after_depth => -2).to_a.should eql([node5])
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
it "should have path with depth constraints" do
|
216
|
+
subject.with_model :cache_depth => true do |model|
|
217
|
+
node1 = model.create!
|
218
|
+
node2 = node1.children.create
|
219
|
+
node3 = node2.children.create
|
220
|
+
node4 = node3.children.create
|
221
|
+
node5 = node4.children.create
|
222
|
+
leaf = node5.children.create
|
223
|
+
|
224
|
+
leaf.path(:before_depth => -2).to_a.should eql([node1, node2, node3])
|
225
|
+
leaf.path(:to_depth => -2).to_a.should eql([node1, node2, node3, node4])
|
226
|
+
leaf.path(:at_depth => -2).to_a.should eql([node4])
|
227
|
+
leaf.path(:from_depth => -2).to_a.should eql([node4, node5, leaf])
|
228
|
+
leaf.path(:after_depth => -2).to_a.should eql([node5, leaf])
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
it "should raise exception on unknown depth field" do
|
233
|
+
subject.with_model :cache_depth => true do |model|
|
234
|
+
expect {
|
235
|
+
model.create!.subtree(:this_is_not_a_valid_depth_option => 42)
|
236
|
+
}.to raise_error(Mongoid::Ancestry::Error)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
|
241
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'mongoid'
|
5
|
+
require 'rspec'
|
6
|
+
|
7
|
+
require 'mongoid-ancestry'
|
8
|
+
|
9
|
+
Mongoid.configure do |config|
|
10
|
+
logger = Logger.new('log/test.log')
|
11
|
+
config.master = Mongo::Connection.new('localhost', 27017,
|
12
|
+
:logger => logger).db('ancestry_test')
|
13
|
+
config.logger = logger
|
14
|
+
end
|
15
|
+
|
16
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
17
|
+
|
18
|
+
RSpec.configure do |config|
|
19
|
+
config.after :each do
|
20
|
+
Mongoid.master.collections.reject { |c| c.name =~ /^system\./ }.each(&:drop)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class MongoidAncestry
|
2
|
+
|
3
|
+
def self.with_model options = {}
|
4
|
+
depth = options.delete(:depth) || 0
|
5
|
+
width = options.delete(:width) || 0
|
6
|
+
extra_columns = options.delete(:extra_columns)
|
7
|
+
skip_ancestry = options.delete(:skip_ancestry)
|
8
|
+
|
9
|
+
begin
|
10
|
+
model = Class.new
|
11
|
+
(class << model; self; end).send :define_method, :model_name do; Struct.new(:human, :underscore, :i18n_key).new 'TestNode', 'test_node', 'key'; end
|
12
|
+
const_set 'TestNode', model
|
13
|
+
TestNode.send(:include, Mongoid::Document)
|
14
|
+
TestNode.send(:include, Mongoid::Ancestry) unless skip_ancestry
|
15
|
+
|
16
|
+
extra_columns.each do |name, type|
|
17
|
+
TestNode.send :field, name, :type => type.to_s.capitalize.constantize
|
18
|
+
end unless extra_columns.nil?
|
19
|
+
|
20
|
+
TestNode.has_ancestry options unless skip_ancestry
|
21
|
+
|
22
|
+
if depth > 0
|
23
|
+
yield TestNode, create_test_nodes(TestNode, depth, width)
|
24
|
+
else
|
25
|
+
yield TestNode
|
26
|
+
end
|
27
|
+
ensure
|
28
|
+
remove_const "TestNode"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.create_test_nodes model, depth, width, parent = nil
|
33
|
+
unless depth == 0
|
34
|
+
Array.new width do
|
35
|
+
node = model.create!(:parent => parent)
|
36
|
+
[node, create_test_nodes(model, depth - 1, width, node)]
|
37
|
+
end
|
38
|
+
else; []; end
|
39
|
+
end
|
40
|
+
end
|