closure_tree 1.0.0 → 2.0.0.beta1
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/README.md +34 -10
- data/Rakefile +5 -13
- data/lib/closure_tree/acts_as_tree.rb +155 -107
- data/lib/closure_tree/version.rb +1 -1
- data/spec/db/database.yml +18 -0
- data/spec/db/schema.rb +40 -0
- data/{test/dummy/test → spec}/fixtures/tags.yml +34 -1
- data/spec/spec_helper.rb +34 -0
- data/spec/support/models.rb +26 -0
- data/spec/tag_spec.rb +278 -0
- data/spec/user_spec.rb +75 -0
- metadata +73 -75
- data/test/dummy/Rakefile +0 -7
- data/test/dummy/app/models/.gitkeep +0 -0
- data/test/dummy/app/models/tag.rb +0 -3
- data/test/dummy/config.ru +0 -4
- data/test/dummy/config/application.rb +0 -53
- data/test/dummy/config/boot.rb +0 -10
- data/test/dummy/config/database.yml +0 -45
- data/test/dummy/config/environment.rb +0 -5
- data/test/dummy/config/environments/development.rb +0 -25
- data/test/dummy/config/environments/production.rb +0 -52
- data/test/dummy/config/environments/test.rb +0 -39
- data/test/dummy/config/routes.rb +0 -3
- data/test/dummy/db/migrate/20110522004834_create_tags.rb +0 -24
- data/test/dummy/db/schema.rb +0 -32
- data/test/dummy/log/.gitkeep +0 -0
- data/test/dummy/script/rails +0 -6
- data/test/dummy/test/unit/tag_test.rb +0 -118
- data/test/test_helper.rb +0 -10
data/lib/closure_tree/version.rb
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
sqlite3:
|
2
|
+
adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
|
3
|
+
database: closure_tree.sqlite3.db
|
4
|
+
sqlite3mem:
|
5
|
+
adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3
|
6
|
+
database: ":memory:"
|
7
|
+
postgresql:
|
8
|
+
adapter: postgresql
|
9
|
+
username: postgres
|
10
|
+
password: postgres
|
11
|
+
database: closure_tree_plugin_test
|
12
|
+
min_messages: ERROR
|
13
|
+
mysql:
|
14
|
+
adapter: mysql2
|
15
|
+
host: localhost
|
16
|
+
username: root
|
17
|
+
password:
|
18
|
+
database: closure_tree_plugin_test
|
data/spec/db/schema.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
ActiveRecord::Schema.define(:version => 0) do
|
3
|
+
|
4
|
+
create_table "tags", :force => true do |t|
|
5
|
+
t.string "name"
|
6
|
+
t.string "title"
|
7
|
+
t.integer "parent_id"
|
8
|
+
t.datetime "created_at"
|
9
|
+
t.datetime "updated_at"
|
10
|
+
end
|
11
|
+
|
12
|
+
create_table "tag_hierarchies", :id => false, :force => true do |t|
|
13
|
+
t.integer "ancestor_id", :null => false
|
14
|
+
t.integer "descendant_id", :null => false
|
15
|
+
t.integer "generations", :null => false
|
16
|
+
end
|
17
|
+
|
18
|
+
create_table "destroyed_tags", :force => true do |t|
|
19
|
+
t.string "name"
|
20
|
+
end
|
21
|
+
|
22
|
+
add_index :tag_hierarchies, [:ancestor_id, :descendant_id], :unique => true
|
23
|
+
add_index :tag_hierarchies, [:descendant_id]
|
24
|
+
|
25
|
+
create_table "users", :force => true do |t|
|
26
|
+
t.string "email"
|
27
|
+
t.integer "referrer_id"
|
28
|
+
t.datetime "created_at"
|
29
|
+
t.datetime "updated_at"
|
30
|
+
end
|
31
|
+
|
32
|
+
create_table "referral_hierarchies", :id => false, :force => true do |t|
|
33
|
+
t.integer "ancestor_id", :null => false
|
34
|
+
t.integer "descendant_id", :null => false
|
35
|
+
t.integer "generations", :null => false
|
36
|
+
end
|
37
|
+
|
38
|
+
add_index :referral_hierarchies, [:ancestor_id, :descendant_id], :unique => true
|
39
|
+
add_index :referral_hierarchies, [:descendant_id]
|
40
|
+
end
|
@@ -12,7 +12,7 @@ parent:
|
|
12
12
|
child:
|
13
13
|
name: child
|
14
14
|
parent: parent
|
15
|
-
title: Kid
|
15
|
+
title: Kid
|
16
16
|
|
17
17
|
people:
|
18
18
|
name: people
|
@@ -61,3 +61,36 @@ san_francisco:
|
|
61
61
|
name: san_francisco
|
62
62
|
parent: california
|
63
63
|
|
64
|
+
|
65
|
+
# Move and deletion test tree
|
66
|
+
|
67
|
+
a1:
|
68
|
+
name: a1
|
69
|
+
|
70
|
+
b1:
|
71
|
+
name: b1
|
72
|
+
parent: a1
|
73
|
+
|
74
|
+
b2:
|
75
|
+
name: b2
|
76
|
+
parent: a1
|
77
|
+
|
78
|
+
c1a:
|
79
|
+
name: c1a
|
80
|
+
parent: b1
|
81
|
+
|
82
|
+
c1b:
|
83
|
+
name: c1b
|
84
|
+
parent: b1
|
85
|
+
|
86
|
+
c2:
|
87
|
+
name: c2
|
88
|
+
parent: b2
|
89
|
+
|
90
|
+
d2:
|
91
|
+
name: d2
|
92
|
+
parent: c2
|
93
|
+
|
94
|
+
e2:
|
95
|
+
name: e2
|
96
|
+
parent: d2
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
2
|
+
plugin_test_dir = File.dirname(__FILE__)
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'bundler/setup'
|
6
|
+
|
7
|
+
require 'rspec'
|
8
|
+
require 'logger'
|
9
|
+
|
10
|
+
require 'active_support'
|
11
|
+
require 'active_model'
|
12
|
+
require 'active_record'
|
13
|
+
require 'action_controller'
|
14
|
+
|
15
|
+
require 'closure_tree'
|
16
|
+
|
17
|
+
ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log")
|
18
|
+
|
19
|
+
require 'yaml'
|
20
|
+
require 'erb'
|
21
|
+
ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(plugin_test_dir + "/db/database.yml")).result)
|
22
|
+
ActiveRecord::Base.establish_connection(ENV["DB"] || "mysql")
|
23
|
+
ActiveRecord::Migration.verbose = false
|
24
|
+
load(File.join(plugin_test_dir, "db", "schema.rb"))
|
25
|
+
|
26
|
+
require 'support/models'
|
27
|
+
|
28
|
+
require 'rspec/rails'
|
29
|
+
RSpec.configure do |config|
|
30
|
+
config.fixture_path = "#{plugin_test_dir}/fixtures"
|
31
|
+
# true runs the tests 1 second faster, but then you can't
|
32
|
+
# see what's going on while debuggering with different db connections.
|
33
|
+
config.use_transactional_fixtures = false
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class Tag < ActiveRecord::Base
|
2
|
+
acts_as_tree :dependent => :destroy
|
3
|
+
before_destroy :create_tag
|
4
|
+
|
5
|
+
def to_s
|
6
|
+
name
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_tag
|
10
|
+
# Proof for the tests that the destroy rather than the delete method was called:
|
11
|
+
DestroyedTag.create(:name => name)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class DestroyedTag < ActiveRecord::Base
|
16
|
+
end
|
17
|
+
|
18
|
+
class User < ActiveRecord::Base
|
19
|
+
acts_as_tree :parent_column_name => "referrer_id",
|
20
|
+
:name_column => 'email',
|
21
|
+
:hierarchy_table_name => 'referral_hierarchies'
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
email
|
25
|
+
end
|
26
|
+
end
|
data/spec/tag_spec.rb
ADDED
@@ -0,0 +1,278 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "empty db" do
|
4
|
+
|
5
|
+
def nuke
|
6
|
+
Tag.delete_all
|
7
|
+
TagHierarchy.delete_all
|
8
|
+
DestroyedTag.delete_all
|
9
|
+
end
|
10
|
+
|
11
|
+
before :each do
|
12
|
+
nuke
|
13
|
+
end
|
14
|
+
|
15
|
+
context "empty db" do
|
16
|
+
it "should return no entities" do
|
17
|
+
Tag.roots.should be_empty
|
18
|
+
Tag.leaves.should be_empty
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
context "1 tag db" do
|
23
|
+
it "should return the only entity as a root and leaf" do
|
24
|
+
a = Tag.create!(:name => "a")
|
25
|
+
Tag.roots.should == [a]
|
26
|
+
Tag.leaves.should == [a]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context "2 tag db" do
|
31
|
+
it "should return a simple root and leaf" do
|
32
|
+
root = Tag.create!(:name => "root")
|
33
|
+
leaf = root.add_child(Tag.create!(:name => "leaf"))
|
34
|
+
Tag.roots.should == [root]
|
35
|
+
Tag.leaves.should == [leaf]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "3 tag collection.create db" do
|
40
|
+
before :each do
|
41
|
+
@root = Tag.create! :name => "root"
|
42
|
+
@mid = @root.children.create! :name => "mid"
|
43
|
+
@leaf = @mid.children.create! :name => "leaf"
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should create all tags" do
|
47
|
+
Tag.all.should =~ [@root, @mid, @leaf]
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should return a root and leaf without middle tag" do
|
51
|
+
Tag.roots.should == [@root]
|
52
|
+
Tag.leaves.should == [@leaf]
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should delete leaves" do
|
56
|
+
Tag.leaves.destroy_all
|
57
|
+
Tag.roots.should == [@root] # untouched
|
58
|
+
Tag.leaves.should == [@mid]
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should delete everything if you delete the roots" do
|
62
|
+
Tag.roots.destroy_all
|
63
|
+
Tag.all.should be_empty
|
64
|
+
Tag.roots.should be_empty
|
65
|
+
Tag.leaves.should be_empty
|
66
|
+
DestroyedTag.all.collect{|t|t.name}.should =~ %w{root mid leaf}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context "3 tag explicit_create db" do
|
71
|
+
before :each do
|
72
|
+
@root = Tag.create!(:name => "root")
|
73
|
+
@mid = @root.add_child(Tag.create!(:name => "mid"))
|
74
|
+
@leaf = @mid.add_child(Tag.create!(:name => "leaf"))
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should create all tags" do
|
78
|
+
Tag.all.should =~ [@root, @mid, @leaf]
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should return a root and leaf without middle tag" do
|
82
|
+
Tag.roots.should == [@root]
|
83
|
+
Tag.leaves.should == [@leaf]
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should prevent parental loops" do
|
87
|
+
lambda do
|
88
|
+
@mid.children << @root
|
89
|
+
end.should raise_error
|
90
|
+
|
91
|
+
lambda do
|
92
|
+
@leaf.children << @root
|
93
|
+
end.should raise_error
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should support reparenting" do
|
97
|
+
@root.children << @leaf
|
98
|
+
Tag.leaves.should =~ [@leaf, @mid]
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe Tag do
|
104
|
+
|
105
|
+
fixtures :tags
|
106
|
+
|
107
|
+
before :each do
|
108
|
+
Tag.rebuild!
|
109
|
+
end
|
110
|
+
|
111
|
+
context "class injection" do
|
112
|
+
it "should build hierarchy classname correctly" do
|
113
|
+
Tag.hierarchy_class.to_s.should == "TagHierarchy"
|
114
|
+
Tag.hierarchy_class_name.should == "TagHierarchy"
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should have a correct parent column name" do
|
118
|
+
Tag.parent_column_name.should == "parent_id"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "roots" do
|
123
|
+
it "should find global roots" do
|
124
|
+
roots = Tag.roots.to_a
|
125
|
+
roots.should be_member(tags(:people))
|
126
|
+
roots.should be_member(tags(:events))
|
127
|
+
roots.should_not be_member(tags(:child))
|
128
|
+
tags(:people).root?.should be_true
|
129
|
+
tags(:child).root?.should be_false
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should find an instance root" do
|
133
|
+
tags(:grandparent).root.should == tags(:grandparent)
|
134
|
+
tags(:parent).root.should == tags(:grandparent)
|
135
|
+
tags(:child).root.should == tags(:grandparent)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context "leaves" do
|
140
|
+
it "should assemble global leaves" do
|
141
|
+
Tag.leaves.size.should > 0
|
142
|
+
Tag.leaves.each { |t| t.children.should be_empty, "#{t.name} was returned by leaves but has children: #{t.children}" }
|
143
|
+
Tag.leaves.each { |t| t.should be_leaf, "{t.name} was returned by leaves but was not a leaf" }
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should assemble instance leaves" do
|
147
|
+
tags(:grandparent).leaves.should == [tags(:child)]
|
148
|
+
tags(:parent).leaves.should == [tags(:child)]
|
149
|
+
tags(:child).leaves.should == [tags(:child)]
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context "adding children" do
|
154
|
+
it "should work explicitly" do
|
155
|
+
sb = Tag.create!(:name => "Santa Barbara")
|
156
|
+
sb.leaf?.should_not be_nil
|
157
|
+
tags(:california).add_child sb
|
158
|
+
sb.leaf?.should_not be_nil
|
159
|
+
validate_city_tag sb
|
160
|
+
end
|
161
|
+
|
162
|
+
it "should work implicitly through the collection" do
|
163
|
+
eg = Tag.create!(:name => "El Granada")
|
164
|
+
eg.leaf?.should_not be_nil
|
165
|
+
tags(:california).children << eg
|
166
|
+
eg.leaf?.should_not be_nil
|
167
|
+
validate_city_tag eg
|
168
|
+
end
|
169
|
+
|
170
|
+
it "should fail to create ancestor loops" do
|
171
|
+
lambda do
|
172
|
+
tags(:child).add_child(tags(:grandparent))
|
173
|
+
end.should raise_error
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should move non-leaves" do
|
177
|
+
# This is what the fixture should encode:
|
178
|
+
tags(:d2).ancestry_path.should == %w{a1 b2 c2 d2}
|
179
|
+
tags(:b1).add_child(tags(:c2))
|
180
|
+
tags(:b2).leaf?.should_not be_nil
|
181
|
+
tags(:b1).children.include?(tags(:c2)).should_not be_nil
|
182
|
+
tags(:d2).reload.ancestry_path.should == %w{a1 b1 c2 d2}
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should move leaves" do
|
186
|
+
l = Tag.find_or_create_by_path(%w{leaftest branch1 leaf})
|
187
|
+
b2 = Tag.find_or_create_by_path(%w{leaftest branch2})
|
188
|
+
b2.children << l
|
189
|
+
l.ancestry_path.should == %w{leaftest branch2 leaf}
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should move roots" do
|
193
|
+
l1 = Tag.find_or_create_by_path(%w{roottest1 branch1 leaf1})
|
194
|
+
l2 = Tag.find_or_create_by_path(%w{roottest2 branch2 leaf2})
|
195
|
+
l1.children << l2.root
|
196
|
+
l1.ancestry_path.should == %w{roottest1 branch1 leaf1}
|
197
|
+
l2.ancestry_path.should == %w{roottest1 branch1 leaf1 roottest2 branch2 leaf2}
|
198
|
+
end
|
199
|
+
|
200
|
+
it "should cascade delete all children" do
|
201
|
+
b2 = tags(:b2)
|
202
|
+
entities = b2.self_and_descendants.to_a
|
203
|
+
names = b2.self_and_descendants.collect{|t|t.name}
|
204
|
+
b2.destroy
|
205
|
+
entities.each{|e| Tag.find_by_id(e.id).should be_nil }
|
206
|
+
DestroyedTag.all.collect{|t|t.name}.should =~ names
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
context "injected attributes" do
|
211
|
+
it "should compute level correctly" do
|
212
|
+
tags(:grandparent).level.should == 0
|
213
|
+
tags(:parent).level.should == 1
|
214
|
+
tags(:child).level.should == 2
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should determine parent correctly" do
|
218
|
+
tags(:grandparent).parent.should == nil
|
219
|
+
tags(:parent).parent.should == tags(:grandparent)
|
220
|
+
tags(:child).parent.should == tags(:parent)
|
221
|
+
end
|
222
|
+
|
223
|
+
it "should have a sane children collection" do
|
224
|
+
tags(:grandparent).children.include? tags(:parent).should_not be_nil
|
225
|
+
tags(:parent).children.include? tags(:child).should_not be_nil
|
226
|
+
tags(:child).children.empty?.should_not be_nil
|
227
|
+
end
|
228
|
+
|
229
|
+
it "should assemble ancestors correctly" do
|
230
|
+
tags(:child).ancestors.should == [tags(:parent), tags(:grandparent)]
|
231
|
+
tags(:child).self_and_ancestors.should == [tags(:child), tags(:parent), tags(:grandparent)]
|
232
|
+
end
|
233
|
+
|
234
|
+
it "should assemble descendants correctly" do
|
235
|
+
tags(:parent).descendants.should == [tags(:child)]
|
236
|
+
tags(:parent).self_and_descendants.should == [tags(:parent), tags(:child)]
|
237
|
+
tags(:grandparent).descendants.should == [tags(:parent), tags(:child)]
|
238
|
+
tags(:grandparent).self_and_descendants.should == [tags(:grandparent), tags(:parent), tags(:child)]
|
239
|
+
tags(:grandparent).self_and_descendants.collect { |t| t.name }.join(" > ").should == "grandparent > parent > child"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
context "paths" do
|
244
|
+
|
245
|
+
it "should build ancestry path" do
|
246
|
+
tags(:child).ancestry_path.should == %w{grandparent parent child}
|
247
|
+
tags(:child).ancestry_path(:name).should == %w{grandparent parent child}
|
248
|
+
tags(:child).ancestry_path(:title).should == %w{Nonnie Mom Kid}
|
249
|
+
end
|
250
|
+
|
251
|
+
it "should find by path" do
|
252
|
+
# class method:
|
253
|
+
Tag.find_by_path(%w{grandparent parent child}).should == tags(:child)
|
254
|
+
# instance method:
|
255
|
+
tags(:parent).find_by_path(%w{child}).should == tags(:child)
|
256
|
+
tags(:grandparent).find_by_path(%w{parent child}).should == tags(:child)
|
257
|
+
tags(:parent).find_by_path(%w{child larvae}).should be_nil
|
258
|
+
end
|
259
|
+
|
260
|
+
it "should find or create by path" do
|
261
|
+
# class method:
|
262
|
+
Tag.find_or_create_by_path(%w{grandparent parent child}).should == tags(:child)
|
263
|
+
Tag.find_or_create_by_path(%w{events anniversary}).ancestry_path.should == %w{events anniversary}
|
264
|
+
a = Tag.find_or_create_by_path(%w{a})
|
265
|
+
a.ancestry_path.should == %w{a}
|
266
|
+
# instance method:
|
267
|
+
a.find_or_create_by_path(%w{b c}).ancestry_path.should == %w{a b c}
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
def validate_city_tag city
|
272
|
+
tags(:california).children.include?(city).should_not be_nil
|
273
|
+
city.ancestors.should == [tags(:california), tags(:united_states), tags(:places)]
|
274
|
+
city.self_and_ancestors.should == [city, tags(:california), tags(:united_states), tags(:places)]
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|
278
|
+
|