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