closure_tree 3.0.2 → 3.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://secure.travis-ci.org/mceachen/closure_tree.png?branch=master)](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
|