closure_tree 3.1.0 → 3.2.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 +125 -26
- data/lib/closure_tree/acts_as_tree.rb +130 -19
- data/lib/closure_tree/version.rb +1 -1
- data/spec/db/schema.rb +2 -0
- data/spec/fixtures/labels.yml +51 -0
- data/spec/fixtures/tags.yml +3 -1
- data/spec/label_spec.rb +79 -2
- data/spec/spec_helper.rb +6 -1
- data/spec/support/models.rb +2 -2
- data/spec/tag_spec.rb +22 -5
- metadata +6 -4
data/README.md
CHANGED
@@ -1,20 +1,22 @@
|
|
1
1
|
# Closure Tree [](http://travis-ci.org/mceachen/closure_tree)
|
2
2
|
|
3
3
|
Closure Tree is a mostly-API-compatible replacement for the
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
[ancestry](https://github.com/stefankroes/ancestry),
|
5
|
+
[acts_as_tree](https://github.com/amerine/acts_as_tree) and
|
6
|
+
[awesome_nested_set](https://github.com/collectiveidea/awesome_nested_set/) gems, giving you:
|
7
|
+
|
8
|
+
* Much better mutation performance thanks to the Closure Tree storage algorithm
|
9
|
+
* Very efficient select performance (again, thanks to Closure Tree)
|
10
|
+
* Efficient subtree selects
|
11
|
+
* Support for polymorphism [STI](#sti) within the hierarchy
|
12
|
+
* ```find_or_create_by_path``` for [building out hierarchies quickly and conveniently](#find_or_create_by_path)
|
13
|
+
* Support for [deterministic ordering](#deterministic-ordering) of children
|
14
|
+
* Excellent [test coverage](#testing) in a variety of environments
|
7
15
|
|
8
16
|
See [Bill Karwin](http://karwin.blogspot.com/)'s excellent
|
9
17
|
[Models for hierarchical data presentation](http://www.slideshare.net/billkarwin/models-for-hierarchical-data)
|
10
18
|
for a description of different tree storage algorithms.
|
11
19
|
|
12
|
-
Closure tree is [tested under every combination](https://secure.travis-ci.org/mceachen/closure_tree.png?branch=master) of
|
13
|
-
|
14
|
-
* Ruby 1.8.7 and Ruby 1.9.3
|
15
|
-
* The latest Rails 3.0, 3.1, and 3.2 branches, and
|
16
|
-
* Using MySQL, Postgresql, and SQLite.
|
17
|
-
|
18
20
|
## Installation
|
19
21
|
|
20
22
|
Note that closure_tree only supports Rails 3.0 and later, and has test coverage for MySQL, PostgreSQL, and SQLite.
|
@@ -26,6 +28,7 @@ Note that closure_tree only supports Rails 3.0 and later, and has test coverage
|
|
26
28
|
3. Add ```acts_as_tree``` to your hierarchical model(s) (see the <em>Available options</em> section below for details).
|
27
29
|
|
28
30
|
4. Add a migration to add a ```parent_id``` column to the model you want to act_as_tree.
|
31
|
+
You may want to also [add a column for deterministic ordering of children](#sort_order), but that's optional.
|
29
32
|
|
30
33
|
```ruby
|
31
34
|
class AddParentIdToTag < ActiveRecord::Migration
|
@@ -102,15 +105,15 @@ Then:
|
|
102
105
|
|
103
106
|
```ruby
|
104
107
|
puts grandparent.self_and_descendants.collect{ |t| t.name }.join(" > ")
|
105
|
-
"grandparent > parent > child"
|
108
|
+
=> "grandparent > parent > child"
|
106
109
|
|
107
110
|
child.ancestry_path
|
108
|
-
["grandparent", "parent", "child"]
|
111
|
+
=> ["grandparent", "parent", "child"]
|
109
112
|
```
|
110
113
|
|
111
114
|
### find_or_create_by_path
|
112
115
|
|
113
|
-
We can do all the node creation and add_child calls
|
116
|
+
We can do all the node creation and add_child calls with one method call:
|
114
117
|
|
115
118
|
```ruby
|
116
119
|
child = Tag.find_or_create_by_path(["grandparent", "parent", "child"])
|
@@ -130,8 +133,20 @@ This will pass the attribute hash of ```{:name => "home", :tag_type => "File"}``
|
|
130
133
|
```Tag.find_or_create_by_name``` if the root directory doesn't exist (and
|
131
134
|
```{:name => "chuck", :tag_type => "File"}``` if the second-level tag doesn't exist, and so on).
|
132
135
|
|
133
|
-
###
|
134
|
-
|
136
|
+
### Moving nodes around the tree
|
137
|
+
|
138
|
+
Nodes can be moved around to other parents, and closure_tree moves the node's descendancy to the new parent for you:
|
139
|
+
|
140
|
+
```ruby
|
141
|
+
d = Tag.find_or_create_by_path %w(a b c d)
|
142
|
+
h = Tag.find_or_create_by_path %w(e f g h)
|
143
|
+
e = h.root
|
144
|
+
d.add_child(e) # "d.children << e" would work too, of course
|
145
|
+
h.ancestry_path
|
146
|
+
=> ["a", "b", "c", "d", "e", "f", "g", "h"]
|
147
|
+
```
|
148
|
+
|
149
|
+
### <a id="options"></a>Available options
|
135
150
|
|
136
151
|
When you include ```acts_as_tree``` in your model, you can provide a hash to override the following defaults:
|
137
152
|
|
@@ -142,6 +157,7 @@ When you include ```acts_as_tree``` in your model, you can provide a hash to ove
|
|
142
157
|
* ```:delete_all``` will delete all descendant nodes (which circumvents the destroy hooks)
|
143
158
|
* ```:destroy``` will destroy all descendant nodes (which runs the destroy hooks on each child node)
|
144
159
|
* ```:name_column``` used by #```find_or_create_by_path```, #```find_by_path```, and ```ancestry_path``` instance methods. This is primarily useful if the model only has one required field (like a "tag").
|
160
|
+
* ```:order``` used to set up [deterministic ordering](#deterministic-ordering)
|
145
161
|
|
146
162
|
## Accessing Data
|
147
163
|
|
@@ -157,24 +173,24 @@ When you include ```acts_as_tree``` in your model, you can provide a hash to ove
|
|
157
173
|
* ```tag.root?``` returns true if this is a root node
|
158
174
|
* ```tag.child?``` returns true if this is a child node. It has a parent.
|
159
175
|
* ```tag.leaf?``` returns true if this is a leaf node. It has no children.
|
160
|
-
* ```tag.leaves```
|
161
|
-
* ```tag.
|
176
|
+
* ```tag.leaves``` is scoped to all leaf nodes in self_and_descendants.
|
177
|
+
* ```tag.depth``` returns the depth, or "generation", for this node in the tree. A root node will have a value of 0.
|
162
178
|
* ```tag.parent``` returns the node's immediate parent. Root nodes will return nil.
|
163
|
-
* ```tag.children```
|
164
|
-
* ```tag.ancestors```
|
165
|
-
* ```tag.self_and_ancestors``` returns
|
166
|
-
* ```tag.siblings``` returns
|
167
|
-
* ```tag.self_and_siblings``` returns
|
168
|
-
* ```tag.descendants``` returns
|
169
|
-
* ```tag.self_and_descendants``` returns
|
179
|
+
* ```tag.children``` is a ```has_many``` of immediate children (just those nodes whose parent is the current node).
|
180
|
+
* ```tag.ancestors``` is a ordered scope of [ parent, grandparent, great grandparent, … ]. Note that the size of this array will always equal ```tag.depth```.
|
181
|
+
* ```tag.self_and_ancestors``` returns a scope containing self, parent, grandparent, great grandparent, etc.
|
182
|
+
* ```tag.siblings``` returns a scope containing all nodes with the same parent as ```tag```, excluding self.
|
183
|
+
* ```tag.self_and_siblings``` returns a scope containing all nodes with the same parent as ```tag```, including self.
|
184
|
+
* ```tag.descendants``` returns a scope of all children, childrens' children, etc., excluding self ordered by depth.
|
185
|
+
* ```tag.self_and_descendants``` returns a scope of all children, childrens' children, etc., including self, ordered by depth.
|
170
186
|
* ```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```.
|
171
187
|
|
172
|
-
## Polymorphic hierarchies
|
188
|
+
## <a id="sti"></a>Polymorphic hierarchies with STI
|
173
189
|
|
174
|
-
Polymorphic models are supported:
|
190
|
+
Polymorphic models using single table inheritance (STI) are supported:
|
175
191
|
|
176
192
|
1. Create a db migration that adds a String ```type``` column to your model
|
177
|
-
2. Subclass the model class. You only need to add acts_as_tree to your base class
|
193
|
+
2. Subclass the model class. You only need to add ```acts_as_tree``` to your base class:
|
178
194
|
|
179
195
|
```ruby
|
180
196
|
class Tag < ActiveRecord::Base
|
@@ -185,8 +201,91 @@ class WhereTag < Tag ; end
|
|
185
201
|
class WhatTag < Tag ; end
|
186
202
|
```
|
187
203
|
|
204
|
+
## Deterministic ordering
|
205
|
+
|
206
|
+
By default, children will be ordered by your database engine, which may not be what you want.
|
207
|
+
|
208
|
+
If you want to order children alphabetically, and your model has a ```name``` column, you'd do this:
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
class Tag < ActiveRecord::Base
|
212
|
+
acts_as_tree :order => 'name'
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
If you want a specific order, add a new integer column to your model in a migration:
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
t.integer :sort_order
|
220
|
+
```
|
221
|
+
|
222
|
+
and in your model:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
class OrderedTag < ActiveRecord::Base
|
226
|
+
acts_as_tree :order => 'sort_order'
|
227
|
+
end
|
228
|
+
```
|
229
|
+
|
230
|
+
When you enable ```order```, you'll also have the following new methods injected into your model:
|
231
|
+
|
232
|
+
* ```tag.siblings_before``` is a scope containing all nodes with the same parent as ```tag```,
|
233
|
+
whose sort order column is less than ```self```. These will be ordered properly, so the ```last```
|
234
|
+
element in scope will be the sibling immediately before ```self```
|
235
|
+
* ```tag.siblings_after``` is a scope containing all nodes with the same parent as ```tag```,
|
236
|
+
whose sort order column is more than ```self```. These will be ordered properly, so the ```first```
|
237
|
+
element in scope will be the sibling immediately "after" ```self```
|
238
|
+
|
239
|
+
If your ```order``` column is an integer attribute, you'll also have these:
|
240
|
+
|
241
|
+
* ```tag.add_sibling_before(sibling_node)``` which will
|
242
|
+
1. move ```tag``` to the same parent as ```sibling_node```,
|
243
|
+
2. decrement the sort_order values of the nodes before the ```sibling_node``` by one, and
|
244
|
+
3. set ```tag```'s order column to 1 less than the ```sibling_node```'s value.
|
245
|
+
|
246
|
+
* ```tag.add_sibling_after(sibling_node)``` which will
|
247
|
+
1. move ```tag``` to the same parent as ```sibling_node```,
|
248
|
+
2. increment the sort_order values of the nodes after the ```sibling_node``` by one, and
|
249
|
+
3. set ```tag```'s order column to 1 more than the ```sibling_node```'s value.
|
250
|
+
|
251
|
+
```ruby
|
252
|
+
root = OrderedTag.create(:name => "root")
|
253
|
+
a = OrderedTag.create(:name => "a", :parent => "root")
|
254
|
+
b = OrderedTag.create(:name => "b")
|
255
|
+
c = OrderedTag.create(:name => "c")
|
256
|
+
|
257
|
+
a.append_sibling(b)
|
258
|
+
root.children.collect(&:name)
|
259
|
+
=> ["a", "b"]
|
260
|
+
|
261
|
+
a.prepend_sibling(b)
|
262
|
+
root.children.collect(&:name)
|
263
|
+
=> ["b", "a"]
|
264
|
+
|
265
|
+
a.append_sibling(c)
|
266
|
+
root.children.collect(&:name)
|
267
|
+
=> ["a", "c", "b"]
|
268
|
+
|
269
|
+
b.append_sibling(c)
|
270
|
+
root.children.collect(&:name)
|
271
|
+
=> ["a", "b", "c"]
|
272
|
+
```
|
273
|
+
|
274
|
+
## Testing
|
275
|
+
|
276
|
+
Closure tree is [tested under every combination](https://secure.travis-ci.org/mceachen/closure_tree.png?branch=master) of
|
277
|
+
|
278
|
+
* Ruby 1.8.7 and Ruby 1.9.3
|
279
|
+
* The latest Rails 3.0, 3.1, and 3.2 branches, and
|
280
|
+
* MySQL, PostgreSQL, and SQLite.
|
281
|
+
|
282
|
+
|
188
283
|
## Change log
|
189
284
|
|
285
|
+
### 3.2.0
|
286
|
+
|
287
|
+
* Added support for deterministic ordering of nodes.
|
288
|
+
|
190
289
|
### 3.1.0
|
191
290
|
|
192
291
|
* Switched to using ```has_many :though``` rather than ```has_and_belongs_to_many```
|
@@ -31,21 +31,27 @@ module ClosureTree
|
|
31
31
|
alias :eql? :==
|
32
32
|
RUBY
|
33
33
|
|
34
|
+
unless order_option.nil?
|
35
|
+
include ClosureTree::DeterministicOrdering
|
36
|
+
include ClosureTree::DeterministicNumericOrdering if order_is_numeric
|
37
|
+
end
|
38
|
+
|
34
39
|
include ClosureTree::Model
|
35
40
|
|
36
|
-
validate :
|
37
|
-
before_save :
|
38
|
-
after_save :
|
39
|
-
before_destroy :
|
41
|
+
validate :ct_validate
|
42
|
+
before_save :ct_before_save
|
43
|
+
after_save :ct_after_save
|
44
|
+
before_destroy :ct_before_destroy
|
40
45
|
|
41
46
|
belongs_to :parent,
|
42
47
|
:class_name => ct_class.to_s,
|
43
48
|
:foreign_key => parent_column_name
|
44
49
|
|
45
|
-
has_many :children,
|
50
|
+
has_many :children, with_order_option(
|
46
51
|
:class_name => ct_class.to_s,
|
47
|
-
|
48
|
-
|
52
|
+
:foreign_key => parent_column_name,
|
53
|
+
:dependent => closure_tree_options[:dependent]
|
54
|
+
)
|
49
55
|
|
50
56
|
has_many :ancestor_hierarchies,
|
51
57
|
:class_name => hierarchy_class_name,
|
@@ -63,19 +69,26 @@ module ClosureTree
|
|
63
69
|
:foreign_key => "ancestor_id",
|
64
70
|
:order => "generations asc",
|
65
71
|
:dependent => :destroy
|
72
|
+
# TODO: FIXME: this collection currently ignores sort_order
|
73
|
+
# (because the quoted_table_named would need to be joined in to get to the order column)
|
66
74
|
|
67
75
|
has_many :self_and_descendants,
|
68
76
|
:through => :descendant_hierarchies,
|
69
77
|
:source => :descendant,
|
70
|
-
:order => "generations asc"
|
78
|
+
:order => append_order("generations asc")
|
71
79
|
|
72
|
-
|
80
|
+
def self.roots
|
81
|
+
where(parent_column_name => nil)
|
82
|
+
end
|
73
83
|
|
74
|
-
|
84
|
+
def self.leaves
|
85
|
+
s = where("#{quoted_table_name}.#{primary_key} IN
|
75
86
|
(SELECT ancestor_id
|
76
87
|
FROM #{quoted_hierarchy_table_name}
|
77
88
|
GROUP BY 1
|
78
89
|
HAVING MAX(generations) = 0)")
|
90
|
+
order_option ? s.order(order_option) : s
|
91
|
+
end
|
79
92
|
end
|
80
93
|
end
|
81
94
|
|
@@ -84,7 +97,7 @@ module ClosureTree
|
|
84
97
|
|
85
98
|
# Returns true if this node has no parents.
|
86
99
|
def root?
|
87
|
-
|
100
|
+
ct_parent_id.nil?
|
88
101
|
end
|
89
102
|
|
90
103
|
# Returns true if this node has a parent, and is not a root.
|
@@ -113,10 +126,12 @@ module ClosureTree
|
|
113
126
|
)
|
114
127
|
end
|
115
128
|
|
116
|
-
def
|
129
|
+
def depth
|
117
130
|
ancestors.size
|
118
131
|
end
|
119
132
|
|
133
|
+
alias :level :depth
|
134
|
+
|
120
135
|
def ancestors
|
121
136
|
without_self(self_and_ancestors)
|
122
137
|
end
|
@@ -133,7 +148,8 @@ module ClosureTree
|
|
133
148
|
end
|
134
149
|
|
135
150
|
def self_and_siblings
|
136
|
-
self.class.scoped.where(:parent_id => parent)
|
151
|
+
s = self.class.scoped.where(:parent_id => parent)
|
152
|
+
quoted_order_column ? s.order(quoted_order_column) : s
|
137
153
|
end
|
138
154
|
|
139
155
|
def siblings
|
@@ -177,7 +193,7 @@ module ClosureTree
|
|
177
193
|
|
178
194
|
protected
|
179
195
|
|
180
|
-
def
|
196
|
+
def ct_validate
|
181
197
|
if changes[parent_column_name] &&
|
182
198
|
parent.present? &&
|
183
199
|
parent.self_and_ancestors.include?(self)
|
@@ -185,12 +201,12 @@ module ClosureTree
|
|
185
201
|
end
|
186
202
|
end
|
187
203
|
|
188
|
-
def
|
204
|
+
def ct_before_save
|
189
205
|
@was_new_record = new_record?
|
190
206
|
true # don't cancel the save
|
191
207
|
end
|
192
208
|
|
193
|
-
def
|
209
|
+
def ct_after_save
|
194
210
|
rebuild! if changes[parent_column_name] || @was_new_record
|
195
211
|
end
|
196
212
|
|
@@ -203,13 +219,13 @@ module ClosureTree
|
|
203
219
|
(ancestor_id, descendant_id, generations)
|
204
220
|
SELECT x.ancestor_id, #{id}, x.generations + 1
|
205
221
|
FROM #{quoted_hierarchy_table_name} x
|
206
|
-
WHERE x.descendant_id = #{self.
|
222
|
+
WHERE x.descendant_id = #{self.ct_parent_id}
|
207
223
|
SQL
|
208
224
|
end
|
209
225
|
children.each { |c| c.rebuild! }
|
210
226
|
end
|
211
227
|
|
212
|
-
def
|
228
|
+
def ct_before_destroy
|
213
229
|
delete_hierarchy_references
|
214
230
|
if closure_tree_options[:dependent] == :nullify
|
215
231
|
children.each { |c| c.rebuild! }
|
@@ -236,10 +252,13 @@ module ClosureTree
|
|
236
252
|
scope.where(["#{quoted_table_name}.#{self.class.primary_key} != ?", self])
|
237
253
|
end
|
238
254
|
|
239
|
-
def
|
255
|
+
def ct_parent_id
|
240
256
|
send(parent_column_name)
|
241
257
|
end
|
242
258
|
|
259
|
+
# TODO: _parent_id will be removed in the next major version
|
260
|
+
alias :_parent_id :ct_parent_id
|
261
|
+
|
243
262
|
module ClassMethods
|
244
263
|
|
245
264
|
# Returns an arbitrary node that has no parents.
|
@@ -315,6 +334,24 @@ module ClosureTree
|
|
315
334
|
connection.quote_column_name parent_column_name
|
316
335
|
end
|
317
336
|
|
337
|
+
def order_option
|
338
|
+
closure_tree_options[:order]
|
339
|
+
end
|
340
|
+
|
341
|
+
def with_order_option(options)
|
342
|
+
order_option ? options.merge(:order => order_option) : options
|
343
|
+
end
|
344
|
+
|
345
|
+
def append_order(order_by)
|
346
|
+
order_option ? "#{order_by}, #{order_option}" : order_by
|
347
|
+
end
|
348
|
+
|
349
|
+
def order_is_numeric
|
350
|
+
return false unless order_option
|
351
|
+
c = ct_class.columns_hash[order_option]
|
352
|
+
c && c.type == :integer
|
353
|
+
end
|
354
|
+
|
318
355
|
def ct_class
|
319
356
|
(self.is_a?(Class) ? self : self.class)
|
320
357
|
end
|
@@ -339,4 +376,78 @@ module ClosureTree
|
|
339
376
|
connection.quote_column_name ct_table_name
|
340
377
|
end
|
341
378
|
end
|
379
|
+
|
380
|
+
module DeterministicOrdering
|
381
|
+
def order_column
|
382
|
+
o = order_option
|
383
|
+
o.split(' ', 2).first if o
|
384
|
+
end
|
385
|
+
|
386
|
+
def require_order_column
|
387
|
+
raise ":order value, '#{order_option}', isn't a column" if order_column.nil?
|
388
|
+
end
|
389
|
+
|
390
|
+
def order_column_sym
|
391
|
+
require_order_column
|
392
|
+
order_column.to_sym
|
393
|
+
end
|
394
|
+
|
395
|
+
def order_value
|
396
|
+
send(order_column_sym)
|
397
|
+
end
|
398
|
+
|
399
|
+
def order_value=(new_order_value)
|
400
|
+
require_order_column
|
401
|
+
send("#{order_column}=".to_sym, new_order_value)
|
402
|
+
end
|
403
|
+
|
404
|
+
def quoted_order_column(include_table_name = true)
|
405
|
+
require_order_column
|
406
|
+
prefix = include_table_name ? "#{quoted_table_name}." : ""
|
407
|
+
"#{prefix}#{connection.quote_column_name(order_column)}"
|
408
|
+
end
|
409
|
+
|
410
|
+
def siblings_before
|
411
|
+
siblings.where(["#{quoted_order_column} < ?", order_value])
|
412
|
+
end
|
413
|
+
|
414
|
+
def siblings_after
|
415
|
+
siblings.where(["#{quoted_order_column} > ?", order_value])
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# This module is only included if the order column is an integer.
|
420
|
+
module DeterministicNumericOrdering
|
421
|
+
def append_sibling(sibling_node, use_update_all = true)
|
422
|
+
add_sibling(sibling_node, use_update_all, true)
|
423
|
+
end
|
424
|
+
|
425
|
+
def prepend_sibling(sibling_node, use_update_all = true)
|
426
|
+
add_sibling(sibling_node, use_update_all, false)
|
427
|
+
end
|
428
|
+
|
429
|
+
def add_sibling(sibling_node, use_update_all = true, add_after = true)
|
430
|
+
fail "can't add self as sibling" if self == sibling_node
|
431
|
+
sibling_node.order_value = self.order_value.to_i + (add_after ? 1 : -1)
|
432
|
+
# We need to incr the before_siblings to make room for sibling_node:
|
433
|
+
if use_update_all
|
434
|
+
col = quoted_order_column(false)
|
435
|
+
ct_class.update_all(
|
436
|
+
["#{col} = #{col} #{add_after ? '+' : '-'} 1", "updated_at = now()"],
|
437
|
+
["#{quoted_parent_column_name} = ? AND #{col} #{add_after ? '>=' : '<='} ?",
|
438
|
+
ct_parent_id,
|
439
|
+
sibling_node.order_value])
|
440
|
+
else
|
441
|
+
last_value = sibling_node.order_value.to_i
|
442
|
+
(add_after ? siblings_after : siblings_before.reverse).each do |ea|
|
443
|
+
last_value += (add_after ? 1 : -1)
|
444
|
+
ea.order_value = last_value
|
445
|
+
ea.save!
|
446
|
+
end
|
447
|
+
end
|
448
|
+
sibling_node.parent = self.parent
|
449
|
+
sibling_node.save!
|
450
|
+
sibling_node.reload
|
451
|
+
end
|
452
|
+
end
|
342
453
|
end
|
data/lib/closure_tree/version.rb
CHANGED
data/spec/db/schema.rb
CHANGED
@@ -5,6 +5,7 @@ ActiveRecord::Schema.define(:version => 0) do
|
|
5
5
|
t.string "name"
|
6
6
|
t.string "title"
|
7
7
|
t.integer "parent_id"
|
8
|
+
t.integer "sort_order"
|
8
9
|
t.datetime "created_at"
|
9
10
|
t.datetime "updated_at"
|
10
11
|
end
|
@@ -41,6 +42,7 @@ ActiveRecord::Schema.define(:version => 0) do
|
|
41
42
|
create_table "labels", :force => true do |t|
|
42
43
|
t.string "name"
|
43
44
|
t.string "type"
|
45
|
+
t.integer "sort_order"
|
44
46
|
t.integer "parent_id"
|
45
47
|
end
|
46
48
|
|
@@ -0,0 +1,51 @@
|
|
1
|
+
a1:
|
2
|
+
name: a1
|
3
|
+
sort_order: 1
|
4
|
+
|
5
|
+
b1:
|
6
|
+
name: b1
|
7
|
+
parent: a1
|
8
|
+
sort_order: 1
|
9
|
+
|
10
|
+
b2:
|
11
|
+
name: b2
|
12
|
+
parent: a1
|
13
|
+
sort_order: 2
|
14
|
+
|
15
|
+
# Note that the names are not alphabetically ordered:
|
16
|
+
c16:
|
17
|
+
name: c1-six
|
18
|
+
parent: b1
|
19
|
+
sort_order: 6
|
20
|
+
|
21
|
+
c17:
|
22
|
+
name: c1-seven
|
23
|
+
parent: b1
|
24
|
+
sort_order: 7
|
25
|
+
|
26
|
+
c18:
|
27
|
+
name: c1-eight
|
28
|
+
parent: b1
|
29
|
+
sort_order: 8
|
30
|
+
|
31
|
+
c19:
|
32
|
+
name: c1-nine
|
33
|
+
parent: b1
|
34
|
+
sort_order: 9
|
35
|
+
|
36
|
+
c2:
|
37
|
+
name: c2
|
38
|
+
parent: b2
|
39
|
+
sort_order: 1
|
40
|
+
|
41
|
+
d2:
|
42
|
+
name: d2
|
43
|
+
parent: c2
|
44
|
+
sort_order: 1
|
45
|
+
|
46
|
+
e2:
|
47
|
+
name: e2
|
48
|
+
parent: d2
|
49
|
+
sort_order: 1
|
50
|
+
|
51
|
+
|
data/spec/fixtures/tags.yml
CHANGED
data/spec/label_spec.rb
CHANGED
@@ -15,8 +15,8 @@ describe Label do
|
|
15
15
|
c.parent.name.should == "parent"
|
16
16
|
end
|
17
17
|
end
|
18
|
-
context "DateLabel" do
|
19
18
|
|
19
|
+
context "DateLabel" do
|
20
20
|
it "should find or create by path" do
|
21
21
|
date = DateLabel.find_or_create_by_path(%w{2011 November 23})
|
22
22
|
date.ancestry_path.should == %w{2011 November 23}
|
@@ -44,7 +44,7 @@ describe Label do
|
|
44
44
|
it "should support mixed type ancestors" do
|
45
45
|
[Label, DateLabel, DirectoryLabel, EventLabel].permutation do |classes|
|
46
46
|
nuke_db
|
47
|
-
classes.each{|c|c.all.should(be_empty, "class #{c} wasn't cleaned out") }
|
47
|
+
classes.each { |c| c.all.should(be_empty, "class #{c} wasn't cleaned out") }
|
48
48
|
names = ('A'..'Z').to_a.first(classes.size)
|
49
49
|
instances = classes.collect { |clazz| clazz.new(:name => names.shift) }
|
50
50
|
a = instances.first
|
@@ -64,4 +64,81 @@ describe Label do
|
|
64
64
|
end
|
65
65
|
end
|
66
66
|
end
|
67
|
+
|
68
|
+
context "Deterministic siblings sort with custom integer column" do
|
69
|
+
nuke_db
|
70
|
+
fixtures :labels
|
71
|
+
|
72
|
+
before :each do
|
73
|
+
Label.rebuild!
|
74
|
+
end
|
75
|
+
|
76
|
+
it "orders siblings_before and siblings_after correctly" do
|
77
|
+
labels(:c16).self_and_siblings.to_a.should == [labels(:c16), labels(:c17), labels(:c18), labels(:c19)]
|
78
|
+
labels(:c16).siblings_before.to_a.should == []
|
79
|
+
labels(:c16).siblings_after.to_a.should == [labels(:c17), labels(:c18), labels(:c19)]
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should prepend a node as a sibling of another node" do
|
83
|
+
labels(:c16).prepend_sibling(labels(:c17))
|
84
|
+
labels(:c16).self_and_siblings.to_a.should == [labels(:c17), labels(:c16), labels(:c18), labels(:c19)]
|
85
|
+
labels(:c19).prepend_sibling(labels(:c16))
|
86
|
+
labels(:c16).self_and_siblings.to_a.should == [labels(:c17), labels(:c18), labels(:c16), labels(:c19)]
|
87
|
+
labels(:c16).siblings_before.to_a.should == [labels(:c17), labels(:c18)]
|
88
|
+
labels(:c16).siblings_after.to_a.should == [labels(:c19)]
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should prepend a node as a sibling of another node (!update_all)" do
|
92
|
+
labels(:c16).prepend_sibling(labels(:c17), false)
|
93
|
+
labels(:c16).self_and_siblings.to_a.should == [labels(:c17), labels(:c16), labels(:c18), labels(:c19)]
|
94
|
+
labels(:c19).reload.prepend_sibling(labels(:c16).reload, false)
|
95
|
+
labels(:c16).self_and_siblings.to_a.should == [labels(:c17), labels(:c18), labels(:c16), labels(:c19)]
|
96
|
+
labels(:c16).siblings_before.to_a.should == [labels(:c17), labels(:c18)]
|
97
|
+
labels(:c16).siblings_after.to_a.should == [labels(:c19)]
|
98
|
+
end
|
99
|
+
|
100
|
+
it "appends a node as a sibling of another node" do
|
101
|
+
labels(:c19).append_sibling(labels(:c17))
|
102
|
+
labels(:c16).self_and_siblings.to_a.should == [labels(:c16), labels(:c18), labels(:c19), labels(:c17)]
|
103
|
+
labels(:c16).append_sibling(labels(:c19))
|
104
|
+
labels(:c16).self_and_siblings.to_a.should == [labels(:c16), labels(:c19), labels(:c18), labels(:c17)]
|
105
|
+
labels(:c16).siblings_before.to_a.should == []
|
106
|
+
labels(:c16).siblings_after.to_a.should == labels(:c16).siblings.to_a
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should move a node before another node (update_all)" do
|
110
|
+
labels(:c2).ancestry_path.should == %w{a1 b2 c2}
|
111
|
+
labels(:b2).prepend_sibling(labels(:c2))
|
112
|
+
labels(:c2).ancestry_path.should == %w{a1 c2}
|
113
|
+
labels(:c2).self_and_siblings.to_a.should == [labels(:b1), labels(:c2), labels(:b2)]
|
114
|
+
labels(:c2).siblings_before.to_a.should == [labels(:b1)]
|
115
|
+
labels(:c2).siblings_after.to_a.should == [labels(:b2)]
|
116
|
+
labels(:b1).siblings_after.to_a.should == [labels(:c2), labels(:b2)]
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should move a node after another node (update_all)" do
|
120
|
+
labels(:c2).ancestry_path.should == %w{a1 b2 c2}
|
121
|
+
labels(:b2).append_sibling(labels(:c2))
|
122
|
+
labels(:c2).ancestry_path.should == %w{a1 c2}
|
123
|
+
labels(:c2).self_and_siblings.to_a.should == [labels(:b1), labels(:b2), labels(:c2)]
|
124
|
+
end
|
125
|
+
|
126
|
+
it "should move a node before another node" do
|
127
|
+
labels(:c2).ancestry_path.should == %w{a1 b2 c2}
|
128
|
+
labels(:b2).prepend_sibling(labels(:c2), false)
|
129
|
+
labels(:c2).ancestry_path.should == %w{a1 c2}
|
130
|
+
labels(:c2).self_and_siblings.to_a.should == [labels(:b1), labels(:c2), labels(:b2)]
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should move a node after another node" do
|
134
|
+
labels(:c2).ancestry_path.should == %w{a1 b2 c2}
|
135
|
+
labels(:b2).append_sibling(labels(:c2), false)
|
136
|
+
labels(:c2).ancestry_path.should == %w{a1 c2}
|
137
|
+
labels(:c2).self_and_siblings.to_a.should == [labels(:b1), labels(:b2), labels(:c2)]
|
138
|
+
labels(:c2).append_sibling(labels(:e2), false)
|
139
|
+
labels(:e2).self_and_siblings.to_a.should == [labels(:b1), labels(:b2), labels(:c2), labels(:e2)]
|
140
|
+
labels(:a1).self_and_descendants.collect(&:name).should == %w(a1 b1 b2 c2 e2 d2 c1-six c1-seven c1-eight c1-nine)
|
141
|
+
labels(:a1).leaves.collect(&:name).should == %w(d2 b2 e2 c1-six c1-seven c1-eight c1-nine)
|
142
|
+
end
|
143
|
+
end
|
67
144
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -14,7 +14,12 @@ require 'action_controller' # rspec-rails needs this :(
|
|
14
14
|
|
15
15
|
require 'closure_tree'
|
16
16
|
|
17
|
-
|
17
|
+
log = Logger.new(plugin_test_dir + "/debug.log")
|
18
|
+
log.sev_threshold = Logger::DEBUG
|
19
|
+
log.datetime_format = "%Y-%m-%d %H:%M:%S"
|
20
|
+
log.formatter = Logger::Formatter.new
|
21
|
+
|
22
|
+
ActiveRecord::Base.logger = log
|
18
23
|
|
19
24
|
require 'yaml'
|
20
25
|
require 'erb'
|
data/spec/support/models.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
class Tag < ActiveRecord::Base
|
2
|
-
acts_as_tree :dependent => :destroy
|
2
|
+
acts_as_tree :dependent => :destroy, :order => "name"
|
3
3
|
before_destroy :add_destroyed_tag
|
4
4
|
attr_accessible :name
|
5
5
|
|
@@ -29,7 +29,7 @@ class User < ActiveRecord::Base
|
|
29
29
|
end
|
30
30
|
|
31
31
|
class Label < ActiveRecord::Base
|
32
|
-
acts_as_tree
|
32
|
+
acts_as_tree :order => "sort_order"
|
33
33
|
attr_accessible :name
|
34
34
|
|
35
35
|
def to_s
|
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
|
@@ -96,11 +96,11 @@ shared_examples_for Tag do
|
|
96
96
|
@leaf.reload.children.should be_empty
|
97
97
|
end
|
98
98
|
|
99
|
-
it "should support
|
99
|
+
it "should support re-parenting" do
|
100
100
|
@root.children << @leaf
|
101
|
-
Tag.leaves.should
|
101
|
+
Tag.leaves.should == [@leaf, @mid]
|
102
102
|
end
|
103
|
-
|
103
|
+
|
104
104
|
it "cleans up hierarchy references for leaves" do
|
105
105
|
@leaf.destroy
|
106
106
|
TagHierarchy.find_all_by_ancestor_id(@leaf.id).should be_empty
|
@@ -194,7 +194,7 @@ shared_examples_for Tag do
|
|
194
194
|
child.add_child(parent) # this should fail
|
195
195
|
parent.valid?.should be_false
|
196
196
|
child.reload.children.should be_empty
|
197
|
-
parent.reload.children.should
|
197
|
+
parent.reload.children.should == [child]
|
198
198
|
end
|
199
199
|
|
200
200
|
it "should move non-leaves" do
|
@@ -254,6 +254,23 @@ shared_examples_for Tag do
|
|
254
254
|
tags(:b1).siblings.to_a.should =~ [tags(:b2)]
|
255
255
|
tags(:a1).siblings.to_a.should =~ (Tag.roots.to_a - [tags(:a1)])
|
256
256
|
tags(:a1).self_and_siblings.to_a.should =~ Tag.roots.to_a
|
257
|
+
|
258
|
+
# must be ordered
|
259
|
+
tags(:indoor).siblings.to_a.should == [tags(:home), tags(:museum), tags(:outdoor), tags(:united_states)]
|
260
|
+
tags(:indoor).self_and_siblings.to_a.should == [tags(:home), tags(:indoor), tags(:museum), tags(:outdoor), tags(:united_states)]
|
261
|
+
end
|
262
|
+
|
263
|
+
it "assembles siblings before correctly" do
|
264
|
+
tags(:home).siblings_before.to_a.should == []
|
265
|
+
tags(:indoor).siblings_before.to_a.should == [tags(:home)]
|
266
|
+
tags(:outdoor).siblings_before.to_a.should == [tags(:home), tags(:indoor), tags(:museum)]
|
267
|
+
tags(:united_states).siblings_before.to_a.should == [tags(:home), tags(:indoor), tags(:museum), tags(:outdoor)]
|
268
|
+
end
|
269
|
+
|
270
|
+
it "assembles siblings after correctly" do
|
271
|
+
tags(:indoor).siblings_after.to_a.should == [tags(:museum), tags(:outdoor), tags(:united_states)]
|
272
|
+
tags(:outdoor).siblings_after.to_a.should == [tags(:united_states)]
|
273
|
+
tags(:united_states).siblings_after.to_a.should == []
|
257
274
|
end
|
258
275
|
|
259
276
|
it "assembles ancestors" do
|
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.2.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-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -172,6 +172,7 @@ files:
|
|
172
172
|
- README.md
|
173
173
|
- spec/db/database.yml
|
174
174
|
- spec/db/schema.rb
|
175
|
+
- spec/fixtures/labels.yml
|
175
176
|
- spec/fixtures/tags.yml
|
176
177
|
- spec/label_spec.rb
|
177
178
|
- spec/spec_helper.rb
|
@@ -192,7 +193,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
192
193
|
version: '0'
|
193
194
|
segments:
|
194
195
|
- 0
|
195
|
-
hash:
|
196
|
+
hash: -1361799512496949037
|
196
197
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
197
198
|
none: false
|
198
199
|
requirements:
|
@@ -201,7 +202,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
201
202
|
version: '0'
|
202
203
|
segments:
|
203
204
|
- 0
|
204
|
-
hash:
|
205
|
+
hash: -1361799512496949037
|
205
206
|
requirements: []
|
206
207
|
rubyforge_project:
|
207
208
|
rubygems_version: 1.8.21
|
@@ -211,6 +212,7 @@ summary: Hierarchies for ActiveRecord models using a Closure Tree storage algori
|
|
211
212
|
test_files:
|
212
213
|
- spec/db/database.yml
|
213
214
|
- spec/db/schema.rb
|
215
|
+
- spec/fixtures/labels.yml
|
214
216
|
- spec/fixtures/tags.yml
|
215
217
|
- spec/label_spec.rb
|
216
218
|
- spec/spec_helper.rb
|