jnicklas-even_better_nested_set 0.3.2
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.
- data/LICENSE +20 -0
- data/README.md +25 -0
- data/Rakefile +87 -0
- data/init.rb +1 -0
- data/spec/directory_spec.rb +27 -0
- data/spec/employee_spec.rb +74 -0
- data/spec/nested_set_behavior.rb +571 -0
- data/spec/spec_helper.rb +83 -0
- metadata +62 -0
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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|