closure_tree 3.0.2 → 3.0.3
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 +20 -11
- data/lib/closure_tree/acts_as_tree.rb +4 -3
- data/lib/closure_tree/version.rb +1 -1
- data/spec/db/database.yml +2 -4
- data/spec/spec_helper.rb +2 -2
- data/spec/support/models.rb +6 -1
- data/spec/tag_spec.rb +286 -272
- metadata +119 -8
- data/lib/tasks/closure_tree_tasks.rake +0 -4
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# Closure Tree
|
2
2
|
|
3
|
+
[](http://travis-ci.org/mceachen/closure_tree)
|
4
|
+
|
3
5
|
Closure Tree is a mostly-API-compatible replacement for the
|
4
6
|
acts_as_tree and awesome_nested_set gems, but with much better
|
5
7
|
mutation performance thanks to the Closure Tree storage algorithm,
|
@@ -9,9 +11,9 @@ See [Bill Karwin](http://karwin.blogspot.com/)'s excellent
|
|
9
11
|
[Models for hierarchical data presentation](http://www.slideshare.net/billkarwin/models-for-hierarchical-data)
|
10
12
|
for a description of different tree storage algorithms.
|
11
13
|
|
12
|
-
##
|
14
|
+
## Installation
|
13
15
|
|
14
|
-
Note that closure_tree supports Rails 3.
|
16
|
+
Note that closure_tree only supports Rails 3.0 and later, and has test coverage for MySQL, PostgreSQL, and SQLite.
|
15
17
|
|
16
18
|
1. Add this to your Gemfile: ```gem 'closure_tree'```
|
17
19
|
|
@@ -21,8 +23,6 @@ Note that closure_tree supports Rails 3. Rails 2, not so much.
|
|
21
23
|
|
22
24
|
4. Add a migration to add a ```parent_id``` column to the model you want to act_as_tree.
|
23
25
|
|
24
|
-
Note that if the column is null, the tag will be considered a root node.
|
25
|
-
|
26
26
|
```ruby
|
27
27
|
class AddParentIdToTag < ActiveRecord::Migration
|
28
28
|
def change
|
@@ -31,9 +31,12 @@ Note that closure_tree supports Rails 3. Rails 2, not so much.
|
|
31
31
|
end
|
32
32
|
```
|
33
33
|
|
34
|
+
Note that if the column is null, the tag will be considered a root node.
|
35
|
+
|
34
36
|
5. Add a database migration to store the hierarchy for your model. By
|
35
|
-
|
36
|
-
"
|
37
|
+
default the table name will be the model's table name, followed by
|
38
|
+
"_hierarchies". Note that by calling ```acts_as_tree```, a "virtual model" (in this case, ```TagsHierarchy```)
|
39
|
+
will be added automatically, so you don't need to create it.
|
37
40
|
|
38
41
|
```ruby
|
39
42
|
class CreateTagHierarchies < ActiveRecord::Migration
|
@@ -44,10 +47,10 @@ Note that closure_tree supports Rails 3. Rails 2, not so much.
|
|
44
47
|
t.integer :generations, :null => false # Number of generations between the ancestor and the descendant. Parent/child = 1, for example.
|
45
48
|
end
|
46
49
|
|
47
|
-
# For "all progeny of
|
50
|
+
# For "all progeny of…" selects:
|
48
51
|
add_index :tag_hierarchies, [:ancestor_id, :descendant_id], :unique => true
|
49
52
|
|
50
|
-
# For "all ancestors of
|
53
|
+
# For "all ancestors of…" selects
|
51
54
|
add_index :tag_hierarchies, [:descendant_id]
|
52
55
|
end
|
53
56
|
end
|
@@ -55,9 +58,9 @@ Note that closure_tree supports Rails 3. Rails 2, not so much.
|
|
55
58
|
|
56
59
|
6. Run ```rake db:migrate```
|
57
60
|
|
58
|
-
7. If you're migrating
|
61
|
+
7. If you're migrating from another system where your model already has a
|
59
62
|
```parent_id``` column, run ```Tag.rebuild!``` and the
|
60
|
-
|
63
|
+
…_hierarchy table will be truncated and rebuilt.
|
61
64
|
|
62
65
|
If you're starting from scratch you don't need to call ```rebuild!```.
|
63
66
|
|
@@ -154,7 +157,7 @@ When you include ```acts_as_tree``` in your model, you can provide a hash to ove
|
|
154
157
|
* ```tag.level``` returns the level, or "generation", for this node in the tree. A root node == 0.
|
155
158
|
* ```tag.parent``` returns the node's immediate parent. Root nodes will return nil.
|
156
159
|
* ```tag.children``` returns an array of immediate children (just those nodes whose parent is the current node).
|
157
|
-
* ```tag.ancestors``` returns an array of [ parent, grandparent, great grandparent,
|
160
|
+
* ```tag.ancestors``` returns an array of [ parent, grandparent, great grandparent, … ]. Note that the size of this array will always equal ```tag.level```.
|
158
161
|
* ```tag.self_and_ancestors``` returns an array of self, parent, grandparent, great grandparent, etc.
|
159
162
|
* ```tag.siblings``` returns an array of brothers and sisters (all at that level), excluding self.
|
160
163
|
* ```tag.self_and_siblings``` returns an array of brothers and sisters (all at that level), including self.
|
@@ -201,6 +204,12 @@ class WhatTag < Tag ; end
|
|
201
204
|
|
202
205
|
* Fix for ancestry-loop detection (performed by a validation, not through raising an exception in before_save)
|
203
206
|
|
207
|
+
### 3.0.3
|
208
|
+
|
209
|
+
* Added support for ActiveRecord's whitelist_attributes
|
210
|
+
(Make sure you read [the Rails Security Guide](http://guides.rubyonrails.org/security.html), and
|
211
|
+
enable ```config.active_record.whitelist_attributes``` in your ```config/application.rb``` ASAP!)
|
212
|
+
|
204
213
|
## Thanks to
|
205
214
|
|
206
215
|
* https://github.com/collectiveidea/awesome_nested_set
|
@@ -23,6 +23,7 @@ module ClosureTree
|
|
23
23
|
self.hierarchy_class.class_eval <<-RUBY
|
24
24
|
belongs_to :ancestor, :class_name => "#{ct_class.to_s}"
|
25
25
|
belongs_to :descendant, :class_name => "#{ct_class.to_s}"
|
26
|
+
attr_accessible :ancestor, :descendant, :generations
|
26
27
|
RUBY
|
27
28
|
|
28
29
|
include ClosureTree::Model
|
@@ -137,7 +138,7 @@ module ClosureTree
|
|
137
138
|
def find_by_path(path)
|
138
139
|
path = [path] unless path.is_a? Enumerable
|
139
140
|
node = self
|
140
|
-
while
|
141
|
+
while !path.empty? && node
|
141
142
|
node = node.children.send("find_by_#{name_column}", path.shift)
|
142
143
|
end
|
143
144
|
node
|
@@ -153,7 +154,7 @@ module ClosureTree
|
|
153
154
|
attrs[name_sym] = name
|
154
155
|
child = node.children.where(attrs).first
|
155
156
|
unless child
|
156
|
-
child = self.class.new(attributes.merge
|
157
|
+
child = self.class.new(attributes.merge(attrs))
|
157
158
|
node.children << child
|
158
159
|
end
|
159
160
|
node = child
|
@@ -173,7 +174,7 @@ module ClosureTree
|
|
173
174
|
|
174
175
|
def acts_as_tree_before_save
|
175
176
|
@was_new_record = new_record?
|
176
|
-
|
177
|
+
true # don't cancel the save
|
177
178
|
end
|
178
179
|
|
179
180
|
def acts_as_tree_after_save
|
data/lib/closure_tree/version.rb
CHANGED
data/spec/db/database.yml
CHANGED
@@ -7,12 +7,10 @@ sqlite3mem:
|
|
7
7
|
postgresql:
|
8
8
|
adapter: postgresql
|
9
9
|
username: postgres
|
10
|
-
|
11
|
-
database: closure_tree_plugin_test
|
10
|
+
database: closure_tree_test
|
12
11
|
min_messages: ERROR
|
13
12
|
mysql:
|
14
13
|
adapter: mysql2
|
15
14
|
host: localhost
|
16
15
|
username: root
|
17
|
-
|
18
|
-
database: closure_tree_plugin_test
|
16
|
+
database: closure_tree_test
|
data/spec/spec_helper.rb
CHANGED
@@ -10,7 +10,7 @@ require 'logger'
|
|
10
10
|
require 'active_support'
|
11
11
|
require 'active_model'
|
12
12
|
require 'active_record'
|
13
|
-
require 'action_controller'
|
13
|
+
require 'action_controller' # rspec-rails needs this :(
|
14
14
|
|
15
15
|
require 'closure_tree'
|
16
16
|
|
@@ -24,8 +24,8 @@ ActiveRecord::Migration.verbose = false
|
|
24
24
|
load(File.join(plugin_test_dir, "db", "schema.rb"))
|
25
25
|
|
26
26
|
require 'support/models'
|
27
|
+
require 'rspec/rails' # TODO: clean this up-- I don't want to pull the elephant through the mouse hole just for fixture support
|
27
28
|
|
28
|
-
require 'rspec/rails'
|
29
29
|
RSpec.configure do |config|
|
30
30
|
config.fixture_path = "#{plugin_test_dir}/fixtures"
|
31
31
|
# true runs the tests 1 second faster, but then you can't
|
data/spec/support/models.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
class Tag < ActiveRecord::Base
|
2
2
|
acts_as_tree :dependent => :destroy
|
3
3
|
before_destroy :add_destroyed_tag
|
4
|
+
attr_accessible :name
|
4
5
|
|
5
6
|
def to_s
|
6
7
|
name
|
@@ -13,12 +14,14 @@ class Tag < ActiveRecord::Base
|
|
13
14
|
end
|
14
15
|
|
15
16
|
class DestroyedTag < ActiveRecord::Base
|
17
|
+
attr_accessible :name
|
16
18
|
end
|
17
19
|
|
18
20
|
class User < ActiveRecord::Base
|
19
21
|
acts_as_tree :parent_column_name => "referrer_id",
|
20
22
|
:name_column => 'email',
|
21
23
|
:hierarchy_table_name => 'referral_hierarchies'
|
24
|
+
attr_accessible :email, :referrer
|
22
25
|
|
23
26
|
def to_s
|
24
27
|
email
|
@@ -27,6 +30,8 @@ end
|
|
27
30
|
|
28
31
|
class Label < ActiveRecord::Base
|
29
32
|
acts_as_tree
|
33
|
+
attr_accessible :name
|
34
|
+
|
30
35
|
def to_s
|
31
36
|
"#{self.class}: #{name}"
|
32
37
|
end
|
@@ -39,4 +44,4 @@ class DateLabel < Label
|
|
39
44
|
end
|
40
45
|
|
41
46
|
class DirectoryLabel < Label
|
42
|
-
end
|
47
|
+
end
|
data/spec/tag_spec.rb
CHANGED
@@ -1,293 +1,307 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
|
3
|
+
shared_examples_for Tag do
|
4
|
+
describe "empty db" do
|
4
5
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
end
|
10
|
-
|
11
|
-
before :each do
|
12
|
-
nuke_db
|
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
|
6
|
+
def nuke_db
|
7
|
+
Tag.delete_all
|
8
|
+
TagHierarchy.delete_all
|
9
|
+
DestroyedTag.delete_all
|
19
10
|
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
11
|
|
39
|
-
context "3 tag collection.create db" do
|
40
12
|
before :each do
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
13
|
+
nuke_db
|
14
|
+
end
|
15
|
+
|
16
|
+
context "empty db" do
|
17
|
+
it "should return no entities" do
|
18
|
+
Tag.roots.should be_empty
|
19
|
+
Tag.leaves.should be_empty
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "1 tag db" do
|
24
|
+
it "should return the only entity as a root and leaf" do
|
25
|
+
a = Tag.create!(:name => "a")
|
26
|
+
Tag.roots.should == [a]
|
27
|
+
Tag.leaves.should == [a]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context "2 tag db" do
|
32
|
+
it "should return a simple root and leaf" do
|
33
|
+
root = Tag.create!(:name => "root")
|
34
|
+
leaf = root.add_child(Tag.create!(:name => "leaf"))
|
35
|
+
Tag.roots.should == [root]
|
36
|
+
Tag.leaves.should == [leaf]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "3 tag collection.create db" do
|
41
|
+
before :each do
|
42
|
+
@root = Tag.create! :name => "root"
|
43
|
+
@mid = @root.children.create! :name => "mid"
|
44
|
+
@leaf = @mid.children.create! :name => "leaf"
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should create all tags" do
|
48
|
+
Tag.all.should =~ [@root, @mid, @leaf]
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should return a root and leaf without middle tag" do
|
52
|
+
Tag.roots.should == [@root]
|
53
|
+
Tag.leaves.should == [@leaf]
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should delete leaves" do
|
57
|
+
Tag.leaves.destroy_all
|
58
|
+
Tag.roots.should == [@root] # untouched
|
59
|
+
Tag.leaves.should == [@mid]
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should delete everything if you delete the roots" do
|
63
|
+
Tag.roots.destroy_all
|
64
|
+
Tag.all.should be_empty
|
65
|
+
Tag.roots.should be_empty
|
66
|
+
Tag.leaves.should be_empty
|
67
|
+
DestroyedTag.all.collect { |t| t.name }.should =~ %w{root mid leaf}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "3 tag explicit_create db" do
|
72
|
+
before :each do
|
73
|
+
@root = Tag.create!(:name => "root")
|
74
|
+
@mid = @root.add_child(Tag.create!(:name => "mid"))
|
75
|
+
@leaf = @mid.add_child(Tag.create!(:name => "leaf"))
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should create all tags" do
|
79
|
+
Tag.all.should =~ [@root, @mid, @leaf]
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should return a root and leaf without middle tag" do
|
83
|
+
Tag.roots.should == [@root]
|
84
|
+
Tag.leaves.should == [@leaf]
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should prevent parental loops from torso" do
|
88
|
+
@mid.children << @root
|
89
|
+
@root.valid?.should be_false
|
90
|
+
@mid.reload.children.should == [@leaf]
|
91
|
+
end
|
92
|
+
|
93
|
+
it "should prevent parental loops from toes" do
|
94
|
+
@leaf.children << @root
|
95
|
+
@root.valid?.should be_false
|
96
|
+
@leaf.reload.children.should be_empty
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should support reparenting" do
|
100
|
+
@root.children << @leaf
|
101
|
+
Tag.leaves.should =~ [@leaf, @mid]
|
102
|
+
end
|
67
103
|
end
|
68
104
|
end
|
69
105
|
|
70
|
-
|
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
|
106
|
+
describe Tag do
|
80
107
|
|
81
|
-
|
82
|
-
Tag.roots.should == [@root]
|
83
|
-
Tag.leaves.should == [@leaf]
|
84
|
-
end
|
108
|
+
fixtures :tags
|
85
109
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
110
|
+
before :each do
|
111
|
+
Tag.rebuild!
|
112
|
+
end
|
113
|
+
|
114
|
+
context "class injection" do
|
115
|
+
it "should build hierarchy classname correctly" do
|
116
|
+
Tag.hierarchy_class.to_s.should == "TagHierarchy"
|
117
|
+
Tag.hierarchy_class_name.should == "TagHierarchy"
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should have a correct parent column name" do
|
121
|
+
Tag.parent_column_name.should == "parent_id"
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context "roots" do
|
126
|
+
it "should find global roots" do
|
127
|
+
roots = Tag.roots.to_a
|
128
|
+
roots.should be_member(tags(:people))
|
129
|
+
roots.should be_member(tags(:events))
|
130
|
+
roots.should_not be_member(tags(:child))
|
131
|
+
tags(:people).root?.should be_true
|
132
|
+
tags(:child).root?.should be_false
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should find an instance root" do
|
136
|
+
tags(:grandparent).root.should == tags(:grandparent)
|
137
|
+
tags(:parent).root.should == tags(:grandparent)
|
138
|
+
tags(:child).root.should == tags(:grandparent)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context "leaves" do
|
143
|
+
it "should assemble global leaves" do
|
144
|
+
Tag.leaves.size.should > 0
|
145
|
+
Tag.leaves.each { |t| t.children.should be_empty, "#{t.name} was returned by leaves but has children: #{t.children}" }
|
146
|
+
Tag.leaves.each { |t| t.should be_leaf, "{t.name} was returned by leaves but was not a leaf" }
|
147
|
+
end
|
148
|
+
|
149
|
+
it "should assemble instance leaves" do
|
150
|
+
tags(:grandparent).leaves.should == [tags(:child)]
|
151
|
+
tags(:parent).leaves.should == [tags(:child)]
|
152
|
+
tags(:child).leaves.should == [tags(:child)]
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context "adding children" do
|
157
|
+
it "should work explicitly" do
|
158
|
+
sb = Tag.create!(:name => "Santa Barbara")
|
159
|
+
sb.leaf?.should_not be_nil
|
160
|
+
tags(:california).add_child sb
|
161
|
+
sb.leaf?.should_not be_nil
|
162
|
+
validate_city_tag sb
|
163
|
+
end
|
164
|
+
|
165
|
+
it "should work implicitly through the collection" do
|
166
|
+
eg = Tag.create!(:name => "El Granada")
|
167
|
+
eg.leaf?.should_not be_nil
|
168
|
+
tags(:california).children << eg
|
169
|
+
eg.leaf?.should_not be_nil
|
170
|
+
validate_city_tag eg
|
171
|
+
end
|
172
|
+
|
173
|
+
it "should fail to create ancestor loops" do
|
174
|
+
child = tags(:child)
|
175
|
+
parent = child.parent
|
176
|
+
child.add_child(parent) # this should fail
|
177
|
+
parent.valid?.should be_false
|
178
|
+
child.reload.children.should be_empty
|
179
|
+
parent.reload.children.should =~ [child]
|
180
|
+
end
|
181
|
+
|
182
|
+
it "should move non-leaves" do
|
183
|
+
# This is what the fixture should encode:
|
184
|
+
tags(:d2).ancestry_path.should == %w{a1 b2 c2 d2}
|
185
|
+
tags(:b1).add_child(tags(:c2))
|
186
|
+
tags(:b2).leaf?.should_not be_nil
|
187
|
+
tags(:b1).children.include?(tags(:c2)).should_not be_nil
|
188
|
+
tags(:d2).reload.ancestry_path.should == %w{a1 b1 c2 d2}
|
189
|
+
end
|
190
|
+
|
191
|
+
it "should move leaves" do
|
192
|
+
l = Tag.find_or_create_by_path(%w{leaftest branch1 leaf})
|
193
|
+
b2 = Tag.find_or_create_by_path(%w{leaftest branch2})
|
194
|
+
b2.children << l
|
195
|
+
l.ancestry_path.should == %w{leaftest branch2 leaf}
|
196
|
+
end
|
197
|
+
|
198
|
+
it "should move roots" do
|
199
|
+
l1 = Tag.find_or_create_by_path(%w{roottest1 branch1 leaf1})
|
200
|
+
l2 = Tag.find_or_create_by_path(%w{roottest2 branch2 leaf2})
|
201
|
+
l1.children << l2.root
|
202
|
+
l1.ancestry_path.should == %w{roottest1 branch1 leaf1}
|
203
|
+
l2.ancestry_path.should == %w{roottest1 branch1 leaf1 roottest2 branch2 leaf2}
|
204
|
+
end
|
205
|
+
|
206
|
+
it "should cascade delete all children" do
|
207
|
+
b2 = tags(:b2)
|
208
|
+
entities = b2.self_and_descendants.to_a
|
209
|
+
names = b2.self_and_descendants.collect { |t| t.name }
|
210
|
+
b2.destroy
|
211
|
+
entities.each { |e| Tag.find_by_id(e.id).should be_nil }
|
212
|
+
DestroyedTag.all.collect { |t| t.name }.should =~ names
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
context "injected attributes" do
|
217
|
+
it "should compute level correctly" do
|
218
|
+
tags(:grandparent).level.should == 0
|
219
|
+
tags(:parent).level.should == 1
|
220
|
+
tags(:child).level.should == 2
|
221
|
+
end
|
222
|
+
|
223
|
+
it "should determine parent correctly" do
|
224
|
+
tags(:grandparent).parent.should == nil
|
225
|
+
tags(:parent).parent.should == tags(:grandparent)
|
226
|
+
tags(:child).parent.should == tags(:parent)
|
227
|
+
end
|
228
|
+
|
229
|
+
it "should have a sane children collection" do
|
230
|
+
tags(:grandparent).children.include? tags(:parent).should_not be_nil
|
231
|
+
tags(:parent).children.include? tags(:child).should_not be_nil
|
232
|
+
tags(:child).children.empty?.should_not be_nil
|
233
|
+
end
|
234
|
+
|
235
|
+
it "should assemble ancestors correctly" do
|
236
|
+
tags(:child).ancestors.should == [tags(:parent), tags(:grandparent)]
|
237
|
+
tags(:child).self_and_ancestors.should == [tags(:child), tags(:parent), tags(:grandparent)]
|
238
|
+
end
|
239
|
+
|
240
|
+
it "should assemble descendants correctly" do
|
241
|
+
tags(:parent).descendants.should == [tags(:child)]
|
242
|
+
tags(:parent).self_and_descendants.should == [tags(:parent), tags(:child)]
|
243
|
+
tags(:grandparent).descendants.should == [tags(:parent), tags(:child)]
|
244
|
+
tags(:grandparent).self_and_descendants.should == [tags(:grandparent), tags(:parent), tags(:child)]
|
245
|
+
tags(:grandparent).self_and_descendants.collect { |t| t.name }.join(" > ").should == "grandparent > parent > child"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
context "paths" do
|
250
|
+
|
251
|
+
it "should build ancestry path" do
|
252
|
+
tags(:child).ancestry_path.should == %w{grandparent parent child}
|
253
|
+
tags(:child).ancestry_path(:name).should == %w{grandparent parent child}
|
254
|
+
tags(:child).ancestry_path(:title).should == %w{Nonnie Mom Kid}
|
255
|
+
end
|
256
|
+
|
257
|
+
it "should find by path" do
|
258
|
+
# class method:
|
259
|
+
Tag.find_by_path(%w{grandparent parent child}).should == tags(:child)
|
260
|
+
# instance method:
|
261
|
+
tags(:parent).find_by_path(%w{child}).should == tags(:child)
|
262
|
+
tags(:grandparent).find_by_path(%w{parent child}).should == tags(:child)
|
263
|
+
tags(:parent).find_by_path(%w{child larvae}).should be_nil
|
264
|
+
end
|
265
|
+
|
266
|
+
it "should return nil for missing nodes" do
|
267
|
+
Tag.find_by_path(%w{missing}).should be_nil
|
268
|
+
Tag.find_by_path(%w{grandparent missing}).should be_nil
|
269
|
+
Tag.find_by_path(%w{grandparent parent missing}).should be_nil
|
270
|
+
Tag.find_by_path(%w{grandparent parent missing child}).should be_nil
|
271
|
+
end
|
272
|
+
|
273
|
+
it "should find or create by path" do
|
274
|
+
# class method:
|
275
|
+
grandparent = Tag.find_or_create_by_path(%w{grandparent})
|
276
|
+
grandparent.should == tags(:grandparent)
|
277
|
+
child = Tag.find_or_create_by_path(%w{grandparent parent child})
|
278
|
+
child.should == tags(:child)
|
279
|
+
Tag.find_or_create_by_path(%w{events anniversary}).ancestry_path.should == %w{events anniversary}
|
280
|
+
a = Tag.find_or_create_by_path(%w{a})
|
281
|
+
a.ancestry_path.should == %w{a}
|
282
|
+
# instance method:
|
283
|
+
a.find_or_create_by_path(%w{b c}).ancestry_path.should == %w{a b c}
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
def validate_city_tag city
|
288
|
+
tags(:california).children.include?(city).should_not be_nil
|
289
|
+
city.ancestors.should == [tags(:california), tags(:united_states), tags(:places)]
|
290
|
+
city.self_and_ancestors.should == [city, tags(:california), tags(:united_states), tags(:places)]
|
90
291
|
end
|
91
292
|
|
92
|
-
it "should prevent parental loops from toes" do
|
93
|
-
@leaf.children << @root
|
94
|
-
@root.valid?.should be_false
|
95
|
-
@leaf.reload.children.should be_empty
|
96
|
-
end
|
97
|
-
|
98
|
-
it "should support reparenting" do
|
99
|
-
@root.children << @leaf
|
100
|
-
Tag.leaves.should =~ [@leaf, @mid]
|
101
|
-
end
|
102
293
|
end
|
103
294
|
end
|
104
295
|
|
105
296
|
describe Tag do
|
297
|
+
it_behaves_like Tag
|
298
|
+
end
|
106
299
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
end
|
112
|
-
|
113
|
-
context "class injection" do
|
114
|
-
it "should build hierarchy classname correctly" do
|
115
|
-
Tag.hierarchy_class.to_s.should == "TagHierarchy"
|
116
|
-
Tag.hierarchy_class_name.should == "TagHierarchy"
|
117
|
-
end
|
118
|
-
|
119
|
-
it "should have a correct parent column name" do
|
120
|
-
Tag.parent_column_name.should == "parent_id"
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
context "roots" do
|
125
|
-
it "should find global roots" do
|
126
|
-
roots = Tag.roots.to_a
|
127
|
-
roots.should be_member(tags(:people))
|
128
|
-
roots.should be_member(tags(:events))
|
129
|
-
roots.should_not be_member(tags(:child))
|
130
|
-
tags(:people).root?.should be_true
|
131
|
-
tags(:child).root?.should be_false
|
132
|
-
end
|
133
|
-
|
134
|
-
it "should find an instance root" do
|
135
|
-
tags(:grandparent).root.should == tags(:grandparent)
|
136
|
-
tags(:parent).root.should == tags(:grandparent)
|
137
|
-
tags(:child).root.should == tags(:grandparent)
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
context "leaves" do
|
142
|
-
it "should assemble global leaves" do
|
143
|
-
Tag.leaves.size.should > 0
|
144
|
-
Tag.leaves.each { |t| t.children.should be_empty, "#{t.name} was returned by leaves but has children: #{t.children}" }
|
145
|
-
Tag.leaves.each { |t| t.should be_leaf, "{t.name} was returned by leaves but was not a leaf" }
|
146
|
-
end
|
147
|
-
|
148
|
-
it "should assemble instance leaves" do
|
149
|
-
tags(:grandparent).leaves.should == [tags(:child)]
|
150
|
-
tags(:parent).leaves.should == [tags(:child)]
|
151
|
-
tags(:child).leaves.should == [tags(:child)]
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
context "adding children" do
|
156
|
-
it "should work explicitly" do
|
157
|
-
sb = Tag.create!(:name => "Santa Barbara")
|
158
|
-
sb.leaf?.should_not be_nil
|
159
|
-
tags(:california).add_child sb
|
160
|
-
sb.leaf?.should_not be_nil
|
161
|
-
validate_city_tag sb
|
162
|
-
end
|
163
|
-
|
164
|
-
it "should work implicitly through the collection" do
|
165
|
-
eg = Tag.create!(:name => "El Granada")
|
166
|
-
eg.leaf?.should_not be_nil
|
167
|
-
tags(:california).children << eg
|
168
|
-
eg.leaf?.should_not be_nil
|
169
|
-
validate_city_tag eg
|
170
|
-
end
|
171
|
-
|
172
|
-
it "should fail to create ancestor loops" do
|
173
|
-
child = tags(:child)
|
174
|
-
parent = child.parent
|
175
|
-
child.add_child(parent) # this should fail
|
176
|
-
parent.valid?.should be_false
|
177
|
-
child.reload.children.should be_empty
|
178
|
-
parent.reload.children.should =~ [child]
|
179
|
-
end
|
180
|
-
|
181
|
-
it "should move non-leaves" do
|
182
|
-
# This is what the fixture should encode:
|
183
|
-
tags(:d2).ancestry_path.should == %w{a1 b2 c2 d2}
|
184
|
-
tags(:b1).add_child(tags(:c2))
|
185
|
-
tags(:b2).leaf?.should_not be_nil
|
186
|
-
tags(:b1).children.include?(tags(:c2)).should_not be_nil
|
187
|
-
tags(:d2).reload.ancestry_path.should == %w{a1 b1 c2 d2}
|
188
|
-
end
|
189
|
-
|
190
|
-
it "should move leaves" do
|
191
|
-
l = Tag.find_or_create_by_path(%w{leaftest branch1 leaf})
|
192
|
-
b2 = Tag.find_or_create_by_path(%w{leaftest branch2})
|
193
|
-
b2.children << l
|
194
|
-
l.ancestry_path.should == %w{leaftest branch2 leaf}
|
195
|
-
end
|
196
|
-
|
197
|
-
it "should move roots" do
|
198
|
-
l1 = Tag.find_or_create_by_path(%w{roottest1 branch1 leaf1})
|
199
|
-
l2 = Tag.find_or_create_by_path(%w{roottest2 branch2 leaf2})
|
200
|
-
l1.children << l2.root
|
201
|
-
l1.ancestry_path.should == %w{roottest1 branch1 leaf1}
|
202
|
-
l2.ancestry_path.should == %w{roottest1 branch1 leaf1 roottest2 branch2 leaf2}
|
203
|
-
end
|
204
|
-
|
205
|
-
it "should cascade delete all children" do
|
206
|
-
b2 = tags(:b2)
|
207
|
-
entities = b2.self_and_descendants.to_a
|
208
|
-
names = b2.self_and_descendants.collect { |t| t.name }
|
209
|
-
b2.destroy
|
210
|
-
entities.each { |e| Tag.find_by_id(e.id).should be_nil }
|
211
|
-
DestroyedTag.all.collect { |t| t.name }.should =~ names
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
context "injected attributes" do
|
216
|
-
it "should compute level correctly" do
|
217
|
-
tags(:grandparent).level.should == 0
|
218
|
-
tags(:parent).level.should == 1
|
219
|
-
tags(:child).level.should == 2
|
220
|
-
end
|
221
|
-
|
222
|
-
it "should determine parent correctly" do
|
223
|
-
tags(:grandparent).parent.should == nil
|
224
|
-
tags(:parent).parent.should == tags(:grandparent)
|
225
|
-
tags(:child).parent.should == tags(:parent)
|
226
|
-
end
|
227
|
-
|
228
|
-
it "should have a sane children collection" do
|
229
|
-
tags(:grandparent).children.include? tags(:parent).should_not be_nil
|
230
|
-
tags(:parent).children.include? tags(:child).should_not be_nil
|
231
|
-
tags(:child).children.empty?.should_not be_nil
|
232
|
-
end
|
233
|
-
|
234
|
-
it "should assemble ancestors correctly" do
|
235
|
-
tags(:child).ancestors.should == [tags(:parent), tags(:grandparent)]
|
236
|
-
tags(:child).self_and_ancestors.should == [tags(:child), tags(:parent), tags(:grandparent)]
|
237
|
-
end
|
238
|
-
|
239
|
-
it "should assemble descendants correctly" do
|
240
|
-
tags(:parent).descendants.should == [tags(:child)]
|
241
|
-
tags(:parent).self_and_descendants.should == [tags(:parent), tags(:child)]
|
242
|
-
tags(:grandparent).descendants.should == [tags(:parent), tags(:child)]
|
243
|
-
tags(:grandparent).self_and_descendants.should == [tags(:grandparent), tags(:parent), tags(:child)]
|
244
|
-
tags(:grandparent).self_and_descendants.collect { |t| t.name }.join(" > ").should == "grandparent > parent > child"
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
context "paths" do
|
249
|
-
|
250
|
-
it "should build ancestry path" do
|
251
|
-
tags(:child).ancestry_path.should == %w{grandparent parent child}
|
252
|
-
tags(:child).ancestry_path(:name).should == %w{grandparent parent child}
|
253
|
-
tags(:child).ancestry_path(:title).should == %w{Nonnie Mom Kid}
|
254
|
-
end
|
255
|
-
|
256
|
-
it "should find by path" do
|
257
|
-
# class method:
|
258
|
-
Tag.find_by_path(%w{grandparent parent child}).should == tags(:child)
|
259
|
-
# instance method:
|
260
|
-
tags(:parent).find_by_path(%w{child}).should == tags(:child)
|
261
|
-
tags(:grandparent).find_by_path(%w{parent child}).should == tags(:child)
|
262
|
-
tags(:parent).find_by_path(%w{child larvae}).should be_nil
|
263
|
-
end
|
264
|
-
|
265
|
-
it "should return nil for missing nodes" do
|
266
|
-
Tag.find_by_path(%w{missing}).should be_nil
|
267
|
-
Tag.find_by_path(%w{grandparent missing}).should be_nil
|
268
|
-
Tag.find_by_path(%w{grandparent parent missing}).should be_nil
|
269
|
-
Tag.find_by_path(%w{grandparent parent missing child}).should be_nil
|
270
|
-
end
|
271
|
-
|
272
|
-
it "should find or create by path" do
|
273
|
-
# class method:
|
274
|
-
grandparent = Tag.find_or_create_by_path(%w{grandparent})
|
275
|
-
grandparent.should == tags(:grandparent)
|
276
|
-
child = Tag.find_or_create_by_path(%w{grandparent parent child})
|
277
|
-
child.should == tags(:child)
|
278
|
-
Tag.find_or_create_by_path(%w{events anniversary}).ancestry_path.should == %w{events anniversary}
|
279
|
-
a = Tag.find_or_create_by_path(%w{a})
|
280
|
-
a.ancestry_path.should == %w{a}
|
281
|
-
# instance method:
|
282
|
-
a.find_or_create_by_path(%w{b c}).ancestry_path.should == %w{a b c}
|
283
|
-
end
|
284
|
-
end
|
285
|
-
|
286
|
-
def validate_city_tag city
|
287
|
-
tags(:california).children.include?(city).should_not be_nil
|
288
|
-
city.ancestors.should == [tags(:california), tags(:united_states), tags(:places)]
|
289
|
-
city.self_and_ancestors.should == [city, tags(:california), tags(:united_states), tags(:places)]
|
300
|
+
describe "Tag with AR whitelisted attributes enabled" do
|
301
|
+
before(:all) do
|
302
|
+
ActiveRecord::Base.attr_accessible(nil) # turn on whitelisted attributes
|
303
|
+
ActiveRecord::Base.subclasses.each{|ea|ea.reset_column_information}
|
290
304
|
end
|
291
|
-
|
305
|
+
it_behaves_like Tag
|
292
306
|
end
|
293
307
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: closure_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 1
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 3
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 3.0.
|
9
|
+
- 3
|
10
|
+
version: 3.0.3
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Matthew McEachen
|
@@ -15,11 +15,10 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-
|
18
|
+
date: 2012-03-11 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
prerelease: false
|
22
|
-
type: :runtime
|
23
22
|
requirement: &id001 !ruby/object:Gem::Requirement
|
24
23
|
none: false
|
25
24
|
requirements:
|
@@ -31,8 +30,121 @@ dependencies:
|
|
31
30
|
- 0
|
32
31
|
- 0
|
33
32
|
version: 3.0.0
|
34
|
-
name: activerecord
|
35
33
|
version_requirements: *id001
|
34
|
+
name: activerecord
|
35
|
+
type: :runtime
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
version_requirements: *id002
|
48
|
+
name: rake
|
49
|
+
type: :development
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
version_requirements: *id003
|
62
|
+
name: yard
|
63
|
+
type: :development
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
prerelease: false
|
66
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ">="
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
hash: 3
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
version: "0"
|
75
|
+
version_requirements: *id004
|
76
|
+
name: rspec
|
77
|
+
type: :development
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
prerelease: false
|
80
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
hash: 3
|
86
|
+
segments:
|
87
|
+
- 0
|
88
|
+
version: "0"
|
89
|
+
version_requirements: *id005
|
90
|
+
name: rails
|
91
|
+
type: :development
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
prerelease: false
|
94
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
95
|
+
none: false
|
96
|
+
requirements:
|
97
|
+
- - ">="
|
98
|
+
- !ruby/object:Gem::Version
|
99
|
+
hash: 3
|
100
|
+
segments:
|
101
|
+
- 0
|
102
|
+
version: "0"
|
103
|
+
version_requirements: *id006
|
104
|
+
name: rspec-rails
|
105
|
+
type: :development
|
106
|
+
- !ruby/object:Gem::Dependency
|
107
|
+
prerelease: false
|
108
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
hash: 3
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
version: "0"
|
117
|
+
version_requirements: *id007
|
118
|
+
name: mysql2
|
119
|
+
type: :development
|
120
|
+
- !ruby/object:Gem::Dependency
|
121
|
+
prerelease: false
|
122
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
123
|
+
none: false
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
hash: 3
|
128
|
+
segments:
|
129
|
+
- 0
|
130
|
+
version: "0"
|
131
|
+
version_requirements: *id008
|
132
|
+
name: pg
|
133
|
+
type: :development
|
134
|
+
- !ruby/object:Gem::Dependency
|
135
|
+
prerelease: false
|
136
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
hash: 3
|
142
|
+
segments:
|
143
|
+
- 0
|
144
|
+
version: "0"
|
145
|
+
version_requirements: *id009
|
146
|
+
name: sqlite3
|
147
|
+
type: :development
|
36
148
|
description: " A mostly-API-compatible replacement for the acts_as_tree and awesome_nested_set gems,\n but with much better mutation performance thanks to the Closure Tree storage algorithm\n"
|
37
149
|
email:
|
38
150
|
- matthew-github@mceachen.org
|
@@ -46,7 +158,6 @@ files:
|
|
46
158
|
- lib/closure_tree/acts_as_tree.rb
|
47
159
|
- lib/closure_tree/version.rb
|
48
160
|
- lib/closure_tree.rb
|
49
|
-
- lib/tasks/closure_tree_tasks.rake
|
50
161
|
- MIT-LICENSE
|
51
162
|
- Rakefile
|
52
163
|
- README.md
|
@@ -87,7 +198,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
198
|
requirements: []
|
88
199
|
|
89
200
|
rubyforge_project:
|
90
|
-
rubygems_version: 1.8.
|
201
|
+
rubygems_version: 1.8.17
|
91
202
|
signing_key:
|
92
203
|
specification_version: 3
|
93
204
|
summary: Hierarchies for ActiveRecord models using a Closure Tree storage algorithm
|