jnicklas-even_better_nested_set 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Jonas Nicklas
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ EvenBetterNestedSet
2
+ ===================
3
+
4
+
5
+ this is an alternative to ActsAsNestedSet, and BetterNestedSet which is just a little bit less stupid.
6
+
7
+ class Directory < ActiveRecord::Base
8
+
9
+ acts_as_nested_set
10
+
11
+ end
12
+
13
+ d = Directory.new
14
+
15
+ d.children.create!(:name => 'blah')
16
+ d.children.create!(:name => 'gurr')
17
+ d.children.create!(:name => 'doh')
18
+
19
+ d.bounds #=> 1..8
20
+ d.children[1].bounds #=> 4..5
21
+ d.children[1].name #=> 'gurr'
22
+ d.children[1].parent #=> d
23
+
24
+ c = Directory.create!(:name => 'test', :parent => d.directory[1]
25
+
data/Rakefile ADDED
@@ -0,0 +1,87 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rubygems/specification'
4
+ require 'date'
5
+ require 'spec/rake/spectask'
6
+
7
+ GEM = "eb_nested_set"
8
+ GEM_VERSION = "0.3.2"
9
+ AUTHOR = "Jonas Nicklas"
10
+ EMAIL = "jonas.nicklas@gmail.com"
11
+ HOMEPAGE = "http://github.com/jnicklas/even_better_nested_set/tree/master"
12
+ SUMMARY = "A cool acts_as_nested_set alternative"
13
+
14
+ spec = Gem::Specification.new do |s|
15
+ s.name = GEM
16
+ s.version = GEM_VERSION
17
+ s.platform = Gem::Platform::RUBY
18
+ s.has_rdoc = true
19
+ s.extra_rdoc_files = ['README.md', 'LICENSE']
20
+ s.summary = SUMMARY
21
+ s.description = s.summary
22
+ s.author = AUTHOR
23
+ s.email = EMAIL
24
+ s.homepage = HOMEPAGE
25
+ s.require_path = 'lib'
26
+ s.autorequire = GEM
27
+ s.files = %w(LICENSE README.md Rakefile init.rb) + Dir.glob("{lib,spec}/**/*")
28
+ end
29
+
30
+ Rake::GemPackageTask.new(spec) do |pkg|
31
+ pkg.gem_spec = spec
32
+ end
33
+
34
+ desc "install the plugin locally"
35
+ task :install => [:package] do
36
+ sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION} --no-update-sources}
37
+ end
38
+
39
+ desc "create a gemspec file"
40
+ task :make_spec do
41
+ File.open("#{GEM}.gemspec", "w") do |file|
42
+ file.puts spec.to_ruby
43
+ end
44
+ end
45
+
46
+ namespace :jruby do
47
+
48
+ desc "Run :package and install the resulting .gem with jruby"
49
+ task :install => :package do
50
+ sh %{#{SUDO} jruby -S gem install pkg/#{GEM}-#{GEM_VERSION}.gem --no-rdoc --no-ri}
51
+ end
52
+
53
+ end
54
+
55
+ spec_files = FileList['spec/*_spec.rb']
56
+
57
+ desc 'Default: run unit tests.'
58
+ task :default => 'spec'
59
+
60
+ task :specs => :spec
61
+ desc "Run all examples"
62
+ Spec::Rake::SpecTask.new('spec') do |t|
63
+ t.spec_opts = ['--color']
64
+ t.spec_files = spec_files
65
+ end
66
+
67
+ namespace :spec do
68
+ desc "Run all examples with RCov"
69
+ Spec::Rake::SpecTask.new('rcov') do |t|
70
+ t.spec_files = spec_files
71
+ t.rcov = true
72
+ t.rcov_dir = "doc/coverage"
73
+ t.rcov_opts = ['--exclude', 'spec,rspec-*,rcov-*,gems']
74
+ t.spec_opts = ['--color']
75
+ end
76
+
77
+ desc "Generate an html report"
78
+ Spec::Rake::SpecTask.new('report') do |t|
79
+ t.spec_files = spec_files
80
+ t.rcov = true
81
+ t.rcov_dir = "doc/coverage"
82
+ t.rcov_opts = ['--exclude', 'spec']
83
+ t.spec_opts = ['--color', "--format", "html:doc/reports/specs.html"]
84
+ t.fail_on_error = false
85
+ end
86
+
87
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.join(File.dirname(__FILE__), 'lib', 'even_better_nested_set')
@@ -0,0 +1,27 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require File.dirname(__FILE__) + '/nested_set_behavior'
3
+
4
+ class Directory < ActiveRecord::Base
5
+ acts_as_nested_set :left => :lft, :right => :rgt
6
+
7
+ validates_presence_of :name
8
+ end
9
+
10
+ describe Directory do
11
+
12
+ def invalid_attributes(options = {})
13
+ return { }.merge(options)
14
+ end
15
+
16
+ def valid_attributes(options = {})
17
+ $directory_no = $directory_no ? $directory_no + 1 : 0
18
+ return { :name => "directory#{$directory_no}" }.merge(options)
19
+ end
20
+
21
+ before do
22
+ @model = Directory
23
+ end
24
+
25
+ it_should_behave_like "all nested set models"
26
+
27
+ end
@@ -0,0 +1,74 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require File.dirname(__FILE__) + '/nested_set_behavior'
3
+
4
+ class Employee < ActiveRecord::Base
5
+ acts_as_nested_set :scope => :company
6
+
7
+ validates_presence_of :name
8
+ end
9
+
10
+ describe Employee, "with nested sets for two different companies" do
11
+ before do
12
+ # Company 1...
13
+ Employee.with_options :company_id => 1 do |c1|
14
+ @c1_1 = c1.create!(:name => "Company 1 - 1")
15
+ @c1_2 = c1.create!(:name => "Company 1 - 2")
16
+
17
+ @c1_11 = c1.create!(:name => "Company 1 - 11", :parent => @c1_1)
18
+ @c1_12 = c1.create!(:name => "Company 1 - 12", :parent => @c1_1)
19
+
20
+ @c1_111 = c1.create!(:name => "Company 1 - 111", :parent => @c1_11)
21
+ end
22
+
23
+ # Company 2...
24
+ Employee.with_options :company_id => 2 do |c2|
25
+ @c2_1 = c2.create!(:name => "Company 2 - 1")
26
+ @c2_11 = c2.create!(:name => "Company 2 - 11", :parent => @c2_1)
27
+ end
28
+ end
29
+
30
+ after do
31
+ Employee.delete_all
32
+ end
33
+
34
+ it "should not allow a new employee in one company to be a child of an employee in the other company, when parent is assigned to" do
35
+ @employee = Employee.create(:company_id => 1, :parent => @c2_11)
36
+ @employee.errors[:parent_id].should_not be_nil
37
+ end
38
+
39
+ it "should not allow a new employee in one company to be a child of an employee in the other company, when parent_id is assigned to" do
40
+ @employee = Employee.create(:company_id => 1, :parent_id => @c2_11.id)
41
+ @employee.errors[:parent_id].should_not be_nil
42
+ end
43
+
44
+ it "should not allow an existing employee in one company to become a child of an employee in the other company, when parent is assigned to" do
45
+ @c1_11.parent = @c2_11
46
+ @c1_11.save
47
+ @c1_11.errors[:parent_id].should_not be_nil
48
+ end
49
+
50
+ it "should not allow an existing employee in one company to become a child of an employee in the other company, when parent_id is assigned to" do
51
+ @c1_11.parent_id = @c2_11.id
52
+ @c1_11.save
53
+ @c1_11.errors[:parent_id].should_not be_nil
54
+ end
55
+
56
+ it "should keep the tree for company 1 and for company 2 entirely disjoint" do
57
+ c1_tree = (@c1_1.family + @c1_2.family).flatten
58
+ c2_tree = @c2_1.family
59
+
60
+ (c1_tree & c2_tree).should be_empty
61
+ end
62
+
63
+ it "should return the correct descendants when retrieving via a database query" do
64
+ @c1_1.descendants.should == [@c1_11, @c1_111, @c1_12]
65
+ @c1_2.descendants.should == []
66
+ @c2_1.descendants.should == [@c2_11]
67
+ end
68
+
69
+ it "should return the correct levels when retrieving via a database query" do
70
+ @c1_1.family.map { |d| d.level }.should == [0, 1, 2, 1]
71
+ @c1_2.family.map { |d| d.level }.should == [0]
72
+ @c2_1.family.map { |d| d.level }.should == [0, 1]
73
+ end
74
+ end
@@ -0,0 +1,571 @@
1
+ describe "all nested set models", :shared => true do
2
+
3
+ describe @model, 'model with acts_as_nested_set' do
4
+
5
+ before do
6
+ @instance = @model.new(valid_attributes)
7
+ end
8
+
9
+ it "should throw an error when attempting to assign left directly" do
10
+ lambda {
11
+ @instance.left = 42
12
+ }.should raise_error(EvenBetterNestedSet::IllegalAssignmentError)
13
+ @instance.left.should_not == 42
14
+ end
15
+
16
+ it "should throw an error when attempting to assign right directly" do
17
+ lambda {
18
+ @instance.right = 42
19
+ }.should raise_error(EvenBetterNestedSet::IllegalAssignmentError)
20
+ @instance.right.should_not == 42
21
+ end
22
+
23
+ it "should throw an error when mass assigning to left" do
24
+ lambda {
25
+ @model.new(valid_attributes(:left => 1))
26
+ }.should raise_error(EvenBetterNestedSet::IllegalAssignmentError)
27
+ end
28
+
29
+ it "should throw an error when mass assigning to right" do
30
+ lambda {
31
+ @model.new(valid_attributes(:right => 1))
32
+ }.should raise_error(EvenBetterNestedSet::IllegalAssignmentError)
33
+ end
34
+
35
+ it "should change the parent_id in the database when a parent is assigned" do
36
+ without_changing_the_database do
37
+ @parent = @model.create!(valid_attributes)
38
+
39
+ @instance.parent = @parent
40
+ @instance.save!
41
+ @instance = @model.find(@instance.id)
42
+
43
+ @instance.parent_id.should == @parent.id
44
+ end
45
+ end
46
+
47
+ it "should change the parent_id in the database when a parent_id is assigned" do
48
+ without_changing_the_database do
49
+ @parent = @model.create!(valid_attributes)
50
+
51
+ @instance.parent_id = @parent.id
52
+ @instance.save!
53
+ @instance = @model.find(@instance.id)
54
+
55
+ @instance.parent_id.should == @parent.id
56
+ end
57
+ end
58
+
59
+ describe '#bounds' do
60
+
61
+ it "should return a range, from left to right" do
62
+ without_changing_the_database do
63
+ @instance.save!
64
+ @instance.left.should == 1
65
+ @instance.right.should == 2
66
+ @instance.bounds.should == (1..2)
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ end
73
+
74
+ describe @model, "with many descendants" do
75
+ before do
76
+ @r1 = @model.create!(valid_attributes)
77
+ @r2 = @model.create!(valid_attributes)
78
+ @r3 = @model.create!(valid_attributes)
79
+
80
+ @r1c1 = @model.create!(valid_attributes(:parent => @r1))
81
+ @r1c2 = @model.create!(valid_attributes(:parent => @r1))
82
+ @r1c3 = @model.create!(valid_attributes(:parent => @r1))
83
+ @r2c1 = @model.create!(valid_attributes(:parent => @r2))
84
+
85
+ @r1c1s1 = @model.create!(valid_attributes(:parent => @r1c1))
86
+ @r1c2s1 = @model.create!(valid_attributes(:parent => @r1c2))
87
+ @r1c2s2 = @model.create!(valid_attributes(:parent => @r1c2))
88
+ @r1c2s3 = @model.create!(valid_attributes(:parent => @r1c2))
89
+
90
+ @r1c2s2m1 = @model.create!(valid_attributes(:parent => @r1c2s2))
91
+ end
92
+
93
+ after do
94
+ @model.delete_all
95
+ end
96
+
97
+ it "should find all root nodes" do
98
+ @model.roots.all.should == [@r1, @r2, @r3]
99
+ end
100
+
101
+ it "should find a root nodes" do
102
+ @model.roots.first.should == @r1
103
+ end
104
+
105
+ it "should maintain the integrity of the tree if a node is deleted" do
106
+ @r1c2.destroy
107
+
108
+ @r1.reload
109
+ @r1c3.reload
110
+
111
+ @r1.bounds.should == (1..8)
112
+ @r1c3.bounds.should == (6..7)
113
+ end
114
+
115
+ it "should maintain the integrity of the tree if a node is moved" do
116
+ @r1c2.parent_id = @r2.id
117
+ @r1c2.save!
118
+
119
+ reload_models(@r1, @r1c3, @r2, @r1c2, @r1c2s1)
120
+
121
+ @r1.bounds.should == (1..8)
122
+ @r1c3.bounds.should == (6..7)
123
+ @r2.bounds.should == (9..22)
124
+ @r1c2.bounds.should == (12..21)
125
+ @r1c2s1.bounds.should == (13..14)
126
+ end
127
+
128
+ it "should change the parent, left and right in the database when a node is moved" do
129
+
130
+ @r1c2.parent_id = @r2.id
131
+
132
+ @r1c2.save!
133
+ @r1c2 = @model.find(@r1c2.id)
134
+
135
+ @r1c2.bounds.should == (12..21)
136
+ @r1c2.parent_id.should == @r2.id
137
+ end
138
+
139
+ it "should maintain the integrity of the tree if a node is moved to a root position" do
140
+ @r1c2.parent_id = nil
141
+ @r1c2.save!
142
+
143
+ reload_models(@r1, @r1c3, @r2, @r1c2, @r1c2s1)
144
+
145
+ @r1.bounds.should == (1..8)
146
+ @r1c3.bounds.should == (6..7)
147
+ @r1c2.bounds.should == (15..24)
148
+ @r1c2s1.bounds.should == (16..17)
149
+ end
150
+
151
+ it "should maintain the integrity of the tree if a node is moved to a root position by assigning a blank string (mass assignment)" do
152
+ @r1c2.parent_id = ""
153
+ @r1c2.save!
154
+
155
+ reload_models(@r1, @r1c3, @r2, @r1c2, @r1c2s1)
156
+
157
+ @r1.bounds.should == (1..8)
158
+ @r1c3.bounds.should == (6..7)
159
+ @r1c2.bounds.should == (15..24)
160
+ @r1c2s1.bounds.should == (16..17)
161
+ end
162
+
163
+ it "should maintain the integrity of the tree if a root is to a non-root position" do
164
+ @r1c2.reload
165
+ @r2.parent_id = @r1c2.id
166
+ @r2.save!
167
+
168
+ reload_models(@r1, @r2, @r2c1, @r1c3, @r3, @r1c2)
169
+
170
+ @r1.bounds.should == (1..22)
171
+ @r1c2.bounds.should == (6..19)
172
+ @r1c3.bounds.should == (20..21)
173
+ @r3.bounds.should == (23..24)
174
+ @r2.bounds.should == (15..18)
175
+ @r2c1.bounds.should == (16..17)
176
+ end
177
+
178
+ it "should maintain the integrity of the tree if a node is moved through the parent association" do
179
+ @r1c2.parent = @r2
180
+ @r1c2.save!
181
+
182
+ reload_models(@r1, @r1c3, @r2, @r1c2, @r1c2s1)
183
+
184
+ @r1.bounds.should == (1..8)
185
+ @r1c3.bounds.should == (6..7)
186
+ @r2.bounds.should == (9..22)
187
+ @r1c2.bounds.should == (12..21)
188
+ @r1c2s1.bounds.should == (13..14)
189
+ end
190
+
191
+ it "should maintain the integrity of the tree if a node is moved to a root position through the parent association" do
192
+ @r1c2.parent = nil
193
+ @r1c2.save!
194
+
195
+ reload_models(@r1, @r1c3, @r2, @r1c2, @r1c2s1)
196
+
197
+ @r1.bounds.should == (1..8)
198
+ @r1c3.bounds.should == (6..7)
199
+ @r1c2.bounds.should == (15..24)
200
+ @r1c2s1.bounds.should == (16..17)
201
+ end
202
+
203
+ it "should maintain the integrity of the tree if a root is to a non-root position through the parent association" do
204
+ @r1c2.reload
205
+ @r2.parent = @r1c2
206
+ @r2.save!
207
+
208
+ reload_models(@r1, @r2, @r2c1, @r1c3, @r3, @r1c2)
209
+
210
+ @r1.bounds.should == (1..22)
211
+ @r1c2.bounds.should == (6..19)
212
+ @r1c3.bounds.should == (20..21)
213
+ @r3.bounds.should == (23..24)
214
+ @r2.bounds.should == (15..18)
215
+ @r2c1.bounds.should == (16..17)
216
+ end
217
+
218
+ it "should be invalid if parent is a descendant" do
219
+ @r2.parent = @r2c1
220
+ @r2.should_not be_valid
221
+ end
222
+
223
+ it "should be invalid if parent is self" do
224
+ @r2.parent = @r2
225
+ @r2.should_not be_valid
226
+ end
227
+
228
+ describe ".nested_set" do
229
+ it "should find all nodes as a nested set and cache that data" do
230
+ roots = @model.nested_set
231
+
232
+ @model.delete_all
233
+
234
+ roots[0].should == @r1
235
+ roots[0].children[0].should == @r1c1
236
+ roots[0].children[0].children[0].should == @r1c1s1
237
+ roots[0].children[1].should == @r1c2
238
+ roots[0].children[1].children[0].should == @r1c2s1
239
+ roots[0].children[1].children[1].should == @r1c2s2
240
+ roots[0].children[1].children[1].children[0].should == @r1c2s2m1
241
+ roots[0].children[1].children[2].should == @r1c2s3
242
+ roots[0].children[2].should == @r1c3
243
+ roots[1].should == @r2
244
+ roots[1].children[0].should == @r2c1
245
+ roots[2].should == @r3
246
+
247
+ roots[1].children[0].parent.should == @r2
248
+ end
249
+ end
250
+
251
+ describe ".sort_nodes_to_nested_set" do
252
+ it "should accept a list of nodes and sort them to a nested set" do
253
+ roots = @model.sort_nodes_to_nested_set(@model.find(:all))
254
+ roots[0].should == @r1
255
+ roots[0].children[0].should == @r1c1
256
+ roots[0].children[0].children[0].should == @r1c1s1
257
+ roots[0].children[1].should == @r1c2
258
+ roots[0].children[1].children[0].should == @r1c2s1
259
+ roots[0].children[1].children[1].should == @r1c2s2
260
+ roots[0].children[1].children[1].children[0].should == @r1c2s2m1
261
+ roots[0].children[1].children[2].should == @r1c2s3
262
+ roots[0].children[2].should == @r1c3
263
+ roots[1].should == @r2
264
+ roots[1].children[0].should == @r2c1
265
+ roots[2].should == @r3
266
+ end
267
+ end
268
+
269
+ describe ".recalculate_nested_set" do
270
+ def values
271
+ @model.find(:all, :order => :id).map { |node| [node.left, node.right] }
272
+ end
273
+
274
+ before do
275
+ @model.find(:all, :order => :id).each do |i|
276
+ i.send(:set_boundaries, rand(1000), rand(1000))
277
+ i.save_without_validation!
278
+ end
279
+ end
280
+
281
+ it "should correctly restore the left and right values for a messed up nested set" do
282
+ @model.recalculate_nested_set
283
+ [@r1, @r2, @r3].each(&:reload)
284
+
285
+ expected = [
286
+ [@r1c1, @r1c2, @r1c3, @r1c1s1, @r1c2s1, @r1c2s2, @r1c2s3, @r1c2s2m1],
287
+ [@r2c1],
288
+ [],
289
+ [@r1c1s1],
290
+ [@r1c2s1, @r1c2s2, @r1c2s3, @r1c2s2m1],
291
+ [],
292
+ [],
293
+ [],
294
+ [],
295
+ [@r1c2s2m1],
296
+ [],
297
+ []
298
+ ]
299
+
300
+ [@r1, @r2, @r3, @r1c1, @r1c2, @r1c3, @r2c1, @r1c1s1, @r1c2s1, @r1c2s2, @r1c2s3, @r1c2s2m1].each_with_index do |node, i|
301
+ node.descendants.find(:all, :order => :id).should == expected[i]
302
+ end
303
+ end
304
+
305
+ it "should leave all records valid after running" do
306
+ @model.recalculate_nested_set
307
+ @model.find(:all).each do |node|
308
+ node.should be_valid
309
+ end
310
+ end
311
+ end
312
+
313
+ describe "#cache_nested_set" do
314
+ it "should cache all descendant nodes so that calls to #children or #parent don't hit the database" do
315
+ @r1c2.cache_nested_set
316
+
317
+ @model.delete_all
318
+
319
+ @r1c2.children[0].should == @r1c2s1
320
+ @r1c2.children[1].should == @r1c2s2
321
+ @r1c2.children[1].children[0].should == @r1c2s2m1
322
+ @r1c2.children[2].should == @r1c2s3
323
+
324
+ @r1c2.children[1].children[0].parent.should == @r1c2s2
325
+ end
326
+ end
327
+
328
+ describe "#parent" do
329
+ it "should find the parent node" do
330
+ @r1c1.parent.should == @r1
331
+ @r1c2s2.parent.should == @r1c2
332
+ @r1c2s2m1.parent.should == @r1c2s2
333
+ end
334
+ end
335
+
336
+ describe "#children" do
337
+ it "should find all nodes that are direct descendants of this one" do
338
+ @r1.children.should == [@r1c1, @r1c2, @r1c3]
339
+ @r1c2s2.children.should == [@r1c2s2m1]
340
+ end
341
+
342
+ it "should allow creation of children" do
343
+ child = @r1c2.children.create!(valid_attributes)
344
+
345
+ child.parent_id.should == @r1c2.id
346
+ child.bounds.should == (15..16)
347
+ end
348
+
349
+ it "should allow addition of children" do
350
+ @r2.children << @r1c2
351
+
352
+ reload_models(@r1, @r1c3, @r2, @r1c2, @r1c2s1)
353
+
354
+ @r1.bounds.should == (1..8)
355
+ @r1c3.bounds.should == (6..7)
356
+ @r2.bounds.should == (9..22)
357
+ @r1c2.bounds.should == (12..21)
358
+ @r1c2s1.bounds.should == (13..14)
359
+ end
360
+ end
361
+
362
+ describe "#patriarch" do
363
+ it "should find the root node that this node descended from" do
364
+ @r1c1.patriarch.should == @r1
365
+ @r1c2s2.patriarch.should == @r1
366
+ @r1c2s2m1.patriarch.should == @r1
367
+ @r2c1.patriarch.should == @r2
368
+ @r1.patriarch.should == @r1
369
+ end
370
+ end
371
+
372
+ describe "#root" do
373
+ it "should find the root node that this node descended from" do
374
+ @r1c1.root.should == @r1
375
+ @r1c2s2.root.should == @r1
376
+ @r1c2s2m1.root.should == @r1
377
+ @r2c1.root.should == @r2
378
+ @r1.root.should == @r1
379
+ end
380
+ end
381
+
382
+ describe "#root?" do
383
+ it "should be true if node doesn't have a parent" do
384
+ @r1.should be_root
385
+ @model.roots.should include(@r1)
386
+ @r1.parent.should be_nil
387
+ end
388
+ end
389
+
390
+ describe "#descendant_of(other_node)" do
391
+ it "should be true if other_node is an ancestor of node" do
392
+ reload_models @r1, @r1c2s2
393
+
394
+ @r1c2s2.should be_descendant_of(@r1)
395
+ @r1c2s2.ancestors.should include(@r1)
396
+ @r1.descendants.should include(@r1c2s2)
397
+ end
398
+ end
399
+
400
+ describe "#generation" do
401
+ it "should find all nodes in the same generation as this one for a root node" do
402
+ @r1.generation.should == [@r1, @r2, @r3]
403
+ end
404
+
405
+ it "should find all nodes in the same generation as this one" do
406
+ @r1c1.generation.should == [@r1c1, @r1c2, @r1c3]
407
+ end
408
+ end
409
+
410
+ describe "#siblings" do
411
+ it "should find all sibling nodes for a root node" do
412
+ @r1.siblings.should == [@r2, @r3]
413
+ end
414
+
415
+ it "should find all sibling nodes for a child node" do
416
+ @r1c1.siblings.should == [@r1c2, @r1c3]
417
+ end
418
+ end
419
+
420
+ describe "#descendants" do
421
+ it "should find all descendants of this node" do
422
+ @r1.descendants.should == [@r1c1, @r1c1s1, @r1c2, @r1c2s1, @r1c2s2, @r1c2s2m1, @r1c2s3, @r1c3]
423
+ end
424
+ end
425
+
426
+ describe "#family" do
427
+ it "should combine self and descendants" do
428
+ @r1.family.should == [@r1, @r1c1, @r1c1s1, @r1c2, @r1c2s1, @r1c2s2, @r1c2s2m1, @r1c2s3, @r1c3]
429
+ end
430
+ end
431
+
432
+ describe "#family_ids" do
433
+ it "should find all ids of the node's nested set" do
434
+ @r1c1.family_ids.should == [@r1c1.id, @r1c1s1.id]
435
+ @r1c2.family_ids.should == [@r1c2.id, @r1c2s1.id, @r1c2s2.id, @r1c2s2m1.id, @r1c2s3.id]
436
+ end
437
+ end
438
+
439
+ describe "#ancestors" do
440
+ it "should return a node's parent and its parent's parents" do
441
+ @r1c2s2m1.ancestors.should == [@r1c2s2, @r1c2, @r1]
442
+ end
443
+ end
444
+
445
+ describe "#lineage" do
446
+ it "should return a node, it's parent and its parent's parents" do
447
+ @r1c2s2m1.lineage.should == [@r1c2s2m1, @r1c2s2, @r1c2, @r1]
448
+ end
449
+ end
450
+
451
+ describe "#level" do
452
+ it "should give the depth from the node to its root" do
453
+ @r1.level.should == 0
454
+ @r1c2.level.should == 1
455
+ @r1c2s2.level.should == 2
456
+ @r1c2s2m1.level.should == 3
457
+ end
458
+ end
459
+
460
+ describe "#kin" do
461
+ it "should find the patriarch and all its descendants" do
462
+ @r1c2s2.kin.should == [@r1, @r1c1, @r1c1s1, @r1c2, @r1c2s1, @r1c2s2, @r1c2s2m1, @r1c2s3, @r1c3]
463
+ end
464
+ end
465
+
466
+ end
467
+
468
+ describe @model, "with acts_as_nested_set" do
469
+
470
+ it "should add a new root node if the parent is not set" do
471
+ without_changing_the_database do
472
+ @instance = @model.create!(valid_attributes)
473
+ @instance.parent_id.should be_nil
474
+
475
+ @instance.bounds.should == (1..2)
476
+ end
477
+ end
478
+
479
+ it "should add a new root node if the parent is not set and there already are some root nodes" do
480
+ without_changing_the_database do
481
+ @model.create!(valid_attributes)
482
+ @model.create!(valid_attributes)
483
+ @instance = @model.create!(valid_attributes)
484
+ @instance.reload
485
+
486
+ @instance.parent_id.should be_nil
487
+ @instance.bounds.should == (5..6)
488
+ end
489
+ end
490
+
491
+ it "should append a child node to a parent" do
492
+ without_changing_the_database do
493
+ @parent = @model.create!(valid_attributes)
494
+ @parent.bounds.should == (1..2)
495
+
496
+ @instance = @model.create!(valid_attributes(:parent => @parent))
497
+
498
+ @parent.reload
499
+
500
+ @instance.parent.should == @parent
501
+
502
+ @instance.bounds.should == (2..3)
503
+ @parent.bounds.should == (1..4)
504
+ end
505
+ end
506
+
507
+ it "should rollback changes if the save is not successfull for some reason" do
508
+ without_changing_the_database do
509
+ @parent = @model.create!(valid_attributes)
510
+ @parent.bounds.should == (1..2)
511
+
512
+ @instance = @model.create(invalid_attributes(:parent => @parent))
513
+ @instance.should be_a_new_record
514
+
515
+ @parent.reload
516
+
517
+ @parent.bounds.should == (1..2)
518
+ end
519
+ end
520
+
521
+ it "should append a child node to a parent and shift other nodes out of the way" do
522
+ without_changing_the_database do
523
+ @root1 = @model.create!(valid_attributes)
524
+ @root2 = @model.create!(valid_attributes)
525
+
526
+ @root1.bounds.should == (1..2)
527
+ @root2.bounds.should == (3..4)
528
+
529
+ @child1 = @model.create!(valid_attributes(:parent => @root1))
530
+ reload_models(@root1, @root2)
531
+
532
+ @root1.bounds.should == (1..4)
533
+ @root2.bounds.should == (5..6)
534
+ @child1.bounds.should == (2..3)
535
+
536
+ @child2 = @model.create!(valid_attributes(:parent => @root1))
537
+ reload_models(@root1, @root2, @child1)
538
+
539
+ @root1.bounds.should == (1..6)
540
+ @root2.bounds.should == (7..8)
541
+ @child1.bounds.should == (2..3)
542
+ @child2.bounds.should == (4..5)
543
+
544
+ @subchild1 = @model.create!(valid_attributes(:parent => @child2))
545
+ reload_models(@root1, @root2, @child1, @child2)
546
+
547
+ @root1.bounds.should == (1..8)
548
+ @root2.bounds.should == (9..10)
549
+ @child1.bounds.should == (2..3)
550
+ @child2.bounds.should == (4..7)
551
+ @subchild1.bounds.should == (5..6)
552
+
553
+ @subchild2 = @model.create!(valid_attributes(:parent => @child1))
554
+ reload_models(@root1, @root2, @child1, @child2, @subchild1)
555
+
556
+ @root1.bounds.should == (1..10)
557
+ @root2.bounds.should == (11..12)
558
+ @child1.bounds.should == (2..5)
559
+ @child2.bounds.should == (6..9)
560
+ @subchild1.bounds.should == (7..8)
561
+ @subchild2.bounds.should == (3..4)
562
+ end
563
+ end
564
+
565
+ end
566
+
567
+ def reload_models(*attrs)
568
+ attrs.each {|m| m.reload }
569
+ end
570
+
571
+ end
@@ -0,0 +1,83 @@
1
+ $TESTING=true
2
+ $:.push File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'rubygems'
5
+ require 'active_record'
6
+ # require 'ruby-debug' # Slows down the tests massively
7
+
8
+ require 'eb_nested_set'
9
+
10
+ require 'spec'
11
+
12
+ # change this if sqlite is unavailable
13
+ dbconfig = case ENV["DB"]
14
+ when "postgresql"
15
+ {
16
+ :adapter => 'postgresql',
17
+ :database => 'even_better_nested_set_test',
18
+ :host => '127.0.0.1'
19
+ }
20
+ when "mysql"
21
+ {
22
+ :adapter => 'mysql',
23
+ :database => 'even_better_nested_set_test',
24
+ :host => '127.0.0.1'
25
+ }
26
+ else
27
+ {
28
+ :adapter => 'sqlite3',
29
+ :database => File.join(File.dirname(__FILE__), 'db', 'test.sqlite3')
30
+ }
31
+ end
32
+
33
+ ActiveRecord::Base.establish_connection(dbconfig)
34
+ ActiveRecord::Migration.verbose = false
35
+
36
+ def show_model_variables_for(context, model)
37
+ context.instance_variables.sort.each do |i|
38
+ m = eval(i)
39
+ if m.is_a?(model)
40
+ m.reload
41
+ puts "#{i.ljust(8)}\t#{m.left}\t#{m.right}\t#{m.name}"
42
+ end
43
+ end
44
+ end
45
+
46
+ #ActiveRecord::Base.logger = Logger.new(STDOUT)
47
+
48
+
49
+ class TestMigration < ActiveRecord::Migration
50
+ def self.up
51
+ create_table :directories, :force => true do |t|
52
+ t.column :lft, :integer
53
+ t.column :rgt, :integer
54
+ t.column :parent_id, :integer
55
+ t.column :name, :string
56
+ end
57
+
58
+ create_table :employees, :force => true do |t|
59
+ t.column :left, :integer
60
+ t.column :right, :integer
61
+ t.column :parent_id, :integer
62
+ t.column :name, :string
63
+ t.column :company_id, :integer
64
+ end
65
+ end
66
+
67
+ def self.down
68
+ drop_table :directories
69
+ drop_table :employees
70
+ rescue
71
+ nil
72
+ end
73
+ end
74
+
75
+ def without_changing_the_database
76
+ ActiveRecord::Base.transaction do
77
+ yield
78
+ raise ActiveRecord::Rollback
79
+ end
80
+ end
81
+
82
+ TestMigration.down
83
+ TestMigration.up
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jnicklas-even_better_nested_set
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.2
5
+ platform: ruby
6
+ authors:
7
+ - Jonas Nicklas
8
+ autorequire: even_better_nested_set
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-12 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: A cool acts_as_nested_set alternative
17
+ email: jonas.nicklas@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README.md
24
+ - LICENSE
25
+ files:
26
+ - LICENSE
27
+ - README.md
28
+ - Rakefile
29
+ - init.rb
30
+ - lib/even_better_nested_set.rb
31
+ - spec/directory_spec.rb
32
+ - spec/employee_spec.rb
33
+ - spec/nested_set_behavior.rb
34
+ - spec/spec_helper.rb
35
+ has_rdoc: true
36
+ homepage: http://github.com/jnicklas/even_better_nested_set/tree/master
37
+ post_install_message:
38
+ rdoc_options: []
39
+
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ version:
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ requirements: []
55
+
56
+ rubyforge_project:
57
+ rubygems_version: 1.2.0
58
+ signing_key:
59
+ specification_version: 2
60
+ summary: A cool acts_as_nested_set alternative
61
+ test_files: []
62
+