closure_tree 3.2.1 → 3.3.0
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 +47 -11
- data/lib/closure_tree/acts_as_tree.rb +25 -5
- data/lib/closure_tree/version.rb +1 -1
- data/spec/tag_spec.rb +23 -1
- data/spec/user_spec.rb +18 -0
- metadata +4 -4
data/README.md
CHANGED
@@ -11,6 +11,7 @@ Closure Tree is a mostly-API-compatible replacement for the
|
|
11
11
|
* Support for polymorphism [STI](#sti) within the hierarchy
|
12
12
|
* ```find_or_create_by_path``` for [building out hierarchies quickly and conveniently](#find_or_create_by_path)
|
13
13
|
* Support for [deterministic ordering](#deterministic-ordering) of children
|
14
|
+
* Support for single-select depth-limited [nested hashes](#nested-hashes)
|
14
15
|
* Excellent [test coverage](#testing) in a variety of environments
|
15
16
|
|
16
17
|
See [Bill Karwin](http://karwin.blogspot.com/)'s excellent
|
@@ -25,7 +26,7 @@ Note that closure_tree only supports Rails 3.0 and later, and has test coverage
|
|
25
26
|
|
26
27
|
2. Run ```bundle install```
|
27
28
|
|
28
|
-
3. Add ```acts_as_tree``` to your hierarchical model(s)
|
29
|
+
3. Add ```acts_as_tree``` to your hierarchical model(s). There are a number of [options](#available-options) you can pass in, too.
|
29
30
|
|
30
31
|
4. Add a migration to add a ```parent_id``` column to the model you want to act_as_tree.
|
31
32
|
You may want to also [add a column for deterministic ordering of children](#sort_order), but that's optional.
|
@@ -84,31 +85,31 @@ grandparent = Tag.create(:name => 'Grandparent')
|
|
84
85
|
Child nodes are created by appending to the children collection:
|
85
86
|
|
86
87
|
```ruby
|
87
|
-
|
88
|
+
parent = grandparent.children.create(:name => 'Parent')
|
88
89
|
```
|
89
90
|
|
90
91
|
You can also append to the children collection:
|
91
92
|
|
92
93
|
```ruby
|
93
|
-
|
94
|
-
parent.children <<
|
94
|
+
child1 = Tag.create(:name => 'First Child')
|
95
|
+
parent.children << child1
|
95
96
|
```
|
96
97
|
|
97
98
|
Or call the "add_child" method:
|
98
99
|
|
99
100
|
```ruby
|
100
|
-
|
101
|
-
|
101
|
+
child2 = Tag.create(:name => 'Second Child')
|
102
|
+
parent.add_child child2
|
102
103
|
```
|
103
104
|
|
104
105
|
Then:
|
105
106
|
|
106
107
|
```ruby
|
107
|
-
|
108
|
-
=> "
|
108
|
+
grandparent.self_and_descendants.collect(&:name)
|
109
|
+
=> ["Grandparent", "Parent", "First Child", "Second Child"]
|
109
110
|
|
110
|
-
|
111
|
-
=> ["
|
111
|
+
child1.ancestry_path
|
112
|
+
=> ["Grandparent", "Parent", "First Child"]
|
112
113
|
```
|
113
114
|
|
114
115
|
### find_or_create_by_path
|
@@ -146,13 +147,42 @@ h.ancestry_path
|
|
146
147
|
=> ["a", "b", "c", "d", "e", "f", "g", "h"]
|
147
148
|
```
|
148
149
|
|
150
|
+
### Nested hashes
|
151
|
+
|
152
|
+
```hash_tree``` provides a method for rendering a subtree as an
|
153
|
+
ordered nested hash:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
b = Tag.find_or_create_by_path %w(a b)
|
157
|
+
a = b.parent
|
158
|
+
b2 = Tag.find_or_create_by_path %w(a b2)
|
159
|
+
d1 = b.find_or_create_by_path %w(c1 d1)
|
160
|
+
c1 = d1.parent
|
161
|
+
d2 = b.find_or_create_by_path %w(c2 d2)
|
162
|
+
c2 = d2.parent
|
163
|
+
|
164
|
+
Tag.hash_tree
|
165
|
+
=> {a => {b => {c1 => {d1 => {}}, c2 => {d2 => {}}}, b2 => {}}}
|
166
|
+
|
167
|
+
Tag.hash_tree(:limit_depth => 2)
|
168
|
+
=> {a => {b => {}, b2 => {}}}
|
169
|
+
|
170
|
+
b.hash_tree
|
171
|
+
=> {b => {c1 => {d1 => {}}, c2 => {d2 => {}}}}
|
172
|
+
|
173
|
+
b.hash_tree(:limit_depth => 2)
|
174
|
+
=> {b => {c1 => {}, c2 => {}}}
|
175
|
+
```
|
176
|
+
|
177
|
+
HT: [ancestry](https://github.com/stefankroes/ancestry#arrangement) and [elhoyos](https://github.com/mceachen/closure_tree/issues/11)
|
178
|
+
|
149
179
|
### <a id="options"></a>Available options
|
150
180
|
|
151
181
|
When you include ```acts_as_tree``` in your model, you can provide a hash to override the following defaults:
|
152
182
|
|
153
183
|
* ```:parent_column_name``` to override the column name of the parent foreign key in the model's table. This defaults to "parent_id".
|
154
184
|
* ```:hierarchy_table_name``` to override the hierarchy table name. This defaults to the singular name of the model + "_hierarchies".
|
155
|
-
* ```:dependent``` determines what happens when a node is destroyed. Defaults to ```
|
185
|
+
* ```:dependent``` determines what happens when a node is destroyed. Defaults to ```nullify```.
|
156
186
|
* ```:nullify``` will simply set the parent column to null. Each child node will be considered a "root" node. This is the default.
|
157
187
|
* ```:delete_all``` will delete all descendant nodes (which circumvents the destroy hooks)
|
158
188
|
* ```:destroy``` will destroy all descendant nodes (which runs the destroy hooks on each child node)
|
@@ -166,6 +196,7 @@ When you include ```acts_as_tree``` in your model, you can provide a hash to ove
|
|
166
196
|
* ```Tag.root``` returns an arbitrary root node
|
167
197
|
* ```Tag.roots``` returns all root nodes
|
168
198
|
* ```Tag.leaves``` returns all leaf nodes
|
199
|
+
* ```Tag.hash_tree``` returns an [ordered, nested hash](#nested-hashes) that can be depth-limited.
|
169
200
|
|
170
201
|
### Instance methods
|
171
202
|
|
@@ -186,6 +217,7 @@ When you include ```acts_as_tree``` in your model, you can provide a hash to ove
|
|
186
217
|
* ```tag.descendants``` returns a scope of all children, childrens' children, etc., excluding self ordered by depth.
|
187
218
|
* ```tag.descendant_ids``` returns an array of the IDs of the descendants.
|
188
219
|
* ```tag.self_and_descendants``` returns a scope of all children, childrens' children, etc., including self, ordered by depth.
|
220
|
+
* ```tag.hash_tree``` returns an [ordered, nested hash](#nested-hashes) that can be depth-limited.
|
189
221
|
* ```tag.destroy``` will destroy a node and do <em>something</em> to its children, which is determined by the ```:dependent``` option passed to ```acts_as_tree```.
|
190
222
|
|
191
223
|
## <a id="sti"></a>Polymorphic hierarchies with STI
|
@@ -285,6 +317,10 @@ Closure tree is [tested under every combination](https://secure.travis-ci.org/mc
|
|
285
317
|
|
286
318
|
## Change log
|
287
319
|
|
320
|
+
### 3.3.0
|
321
|
+
|
322
|
+
* Added [```hash_tree```](#nested-hashes).
|
323
|
+
|
288
324
|
### 3.2.1
|
289
325
|
|
290
326
|
* Added ```ancestor_ids```, ```descendant_ids```, and ```sibling_ids```
|
@@ -56,18 +56,18 @@ module ClosureTree
|
|
56
56
|
has_many :ancestor_hierarchies,
|
57
57
|
:class_name => hierarchy_class_name,
|
58
58
|
:foreign_key => "descendant_id",
|
59
|
-
:order => "generations asc",
|
59
|
+
:order => "#{quoted_hierarchy_table_name}.generations asc",
|
60
60
|
:dependent => :destroy
|
61
61
|
|
62
62
|
has_many :self_and_ancestors,
|
63
63
|
:through => :ancestor_hierarchies,
|
64
64
|
:source => :ancestor,
|
65
|
-
:order => "generations asc"
|
65
|
+
:order => "#{quoted_hierarchy_table_name}.generations asc"
|
66
66
|
|
67
67
|
has_many :descendant_hierarchies,
|
68
68
|
:class_name => hierarchy_class_name,
|
69
69
|
:foreign_key => "ancestor_id",
|
70
|
-
:order => "generations asc",
|
70
|
+
:order => "#{quoted_hierarchy_table_name}.generations asc",
|
71
71
|
:dependent => :destroy
|
72
72
|
# TODO: FIXME: this collection currently ignores sort_order
|
73
73
|
# (because the quoted_table_named would need to be joined in to get to the order column)
|
@@ -75,18 +75,22 @@ module ClosureTree
|
|
75
75
|
has_many :self_and_descendants,
|
76
76
|
:through => :descendant_hierarchies,
|
77
77
|
:source => :descendant,
|
78
|
-
:order => append_order("generations asc")
|
78
|
+
:order => append_order("#{quoted_hierarchy_table_name}.generations asc")
|
79
79
|
|
80
80
|
def self.roots
|
81
81
|
where(parent_column_name => nil)
|
82
82
|
end
|
83
83
|
|
84
|
+
def self.hash_tree(options = {})
|
85
|
+
roots.inject(ActiveSupport::OrderedHash.new) { |h, ea| h.merge(ea.hash_tree(options)) }
|
86
|
+
end
|
87
|
+
|
84
88
|
def self.leaves
|
85
89
|
s = where("#{quoted_table_name}.#{primary_key} IN
|
86
90
|
(SELECT ancestor_id
|
87
91
|
FROM #{quoted_hierarchy_table_name}
|
88
92
|
GROUP BY 1
|
89
|
-
HAVING MAX(generations) = 0)")
|
93
|
+
HAVING MAX(#{quoted_hierarchy_table_name}.generations) = 0)")
|
90
94
|
order_option ? s.order(order_option) : s
|
91
95
|
end
|
92
96
|
end
|
@@ -203,6 +207,22 @@ module ClosureTree
|
|
203
207
|
node
|
204
208
|
end
|
205
209
|
|
210
|
+
def hash_tree(options = {})
|
211
|
+
tree = ActiveSupport::OrderedHash.new
|
212
|
+
tree[self] = ActiveSupport::OrderedHash.new
|
213
|
+
id_to_hash = {self.id => tree[self]}
|
214
|
+
scope = descendants
|
215
|
+
if options[:limit_depth]
|
216
|
+
limit_depth = options[:limit_depth]
|
217
|
+
return {} if limit_depth <= 0
|
218
|
+
scope = scope.where("generations <= #{limit_depth - 1}")
|
219
|
+
end
|
220
|
+
scope.each do |ea|
|
221
|
+
id_to_hash[ea.ct_parent_id][ea] = (id_to_hash[ea.id] = ActiveSupport::OrderedHash.new)
|
222
|
+
end
|
223
|
+
tree
|
224
|
+
end
|
225
|
+
|
206
226
|
protected
|
207
227
|
|
208
228
|
def ct_validate
|
data/lib/closure_tree/version.rb
CHANGED
data/spec/tag_spec.rb
CHANGED
@@ -76,7 +76,7 @@ shared_examples_for Tag do
|
|
76
76
|
end
|
77
77
|
|
78
78
|
it "should create all tags" do
|
79
|
-
Tag.all.should
|
79
|
+
Tag.all.should =~ [@root, @mid, @leaf]
|
80
80
|
end
|
81
81
|
|
82
82
|
it "should return a root and leaf without middle tag" do
|
@@ -118,6 +118,28 @@ shared_examples_for Tag do
|
|
118
118
|
TagHierarchy.find_all_by_descendant_id(@root.id).should == root_hiers
|
119
119
|
end
|
120
120
|
end
|
121
|
+
|
122
|
+
it "builds hash_trees properly" do
|
123
|
+
b = Tag.find_or_create_by_path %w(a b)
|
124
|
+
a = b.parent
|
125
|
+
b2 = Tag.find_or_create_by_path %w(a b2)
|
126
|
+
d1 = b.find_or_create_by_path %w(c1 d1)
|
127
|
+
c1 = d1.parent
|
128
|
+
d2 = b.find_or_create_by_path %w(c2 d2)
|
129
|
+
c2 = d2.parent
|
130
|
+
Tag.hash_tree(:limit_depth => 0).should == {}
|
131
|
+
Tag.hash_tree(:limit_depth => 1).should == {a => {}}
|
132
|
+
Tag.hash_tree(:limit_depth => 2).should == {a => {b => {}, b2 => {}}}
|
133
|
+
tree = {a => {b => {c1 => {d1 => {}}, c2 => {d2 => {}}}, b2 => {}}}
|
134
|
+
Tag.hash_tree(:limit_depth => 4).should == tree
|
135
|
+
Tag.hash_tree.should == tree
|
136
|
+
b.hash_tree(:limit_depth => 0).should == {}
|
137
|
+
b.hash_tree(:limit_depth => 1).should == {b => {}}
|
138
|
+
b.hash_tree(:limit_depth => 2).should == {b => {c1 => {}, c2 => {}}}
|
139
|
+
b_tree = {b => {c1 => {d1 => {}}, c2 => {d2 => {}}}}
|
140
|
+
b.hash_tree(:limit_depth => 3).should == b_tree
|
141
|
+
b.hash_tree.should == b_tree
|
142
|
+
end
|
121
143
|
end
|
122
144
|
|
123
145
|
describe Tag do
|
data/spec/user_spec.rb
CHANGED
@@ -86,4 +86,22 @@ describe "empty db" do
|
|
86
86
|
c2 = u.parent.contracts.create!
|
87
87
|
u.root.indirect_contracts.to_a.should =~ [c1, c2]
|
88
88
|
end
|
89
|
+
|
90
|
+
it "performs as the readme says it does" do
|
91
|
+
grandparent = Tag.create(:name => 'Grandparent')
|
92
|
+
parent = grandparent.children.create(:name => 'Parent')
|
93
|
+
child1 = Tag.create(:name => 'First Child')
|
94
|
+
parent.children << child1
|
95
|
+
child2 = Tag.create(:name => 'Second Child')
|
96
|
+
parent.add_child child2
|
97
|
+
grandparent.self_and_descendants.collect(&:name).should ==
|
98
|
+
["Grandparent", "Parent", "First Child", "Second Child"]
|
99
|
+
child1.ancestry_path.should ==
|
100
|
+
["Grandparent", "Parent", "First Child"]
|
101
|
+
d = Tag.find_or_create_by_path %w(a b c d)
|
102
|
+
h = Tag.find_or_create_by_path %w(e f g h)
|
103
|
+
e = h.root
|
104
|
+
d.add_child(e) # "d.children << e" would work too, of course
|
105
|
+
h.ancestry_path.should == %w(a b c d e f g h)
|
106
|
+
end
|
89
107
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: closure_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-07-
|
12
|
+
date: 2012-07-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -193,7 +193,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
193
193
|
version: '0'
|
194
194
|
segments:
|
195
195
|
- 0
|
196
|
-
hash:
|
196
|
+
hash: 1888023415669580942
|
197
197
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
198
198
|
none: false
|
199
199
|
requirements:
|
@@ -202,7 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
202
202
|
version: '0'
|
203
203
|
segments:
|
204
204
|
- 0
|
205
|
-
hash:
|
205
|
+
hash: 1888023415669580942
|
206
206
|
requirements: []
|
207
207
|
rubyforge_project:
|
208
208
|
rubygems_version: 1.8.21
|