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 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
- acts_as_tree and awesome_nested_set gems, but with much better
5
- mutation performance thanks to the Closure Tree storage algorithm,
6
- as well as support for polymorphism within the hierarchy.
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 from the prior section with one method call:
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
- ### Available options
134
- <a id="options" />
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``` returns an array of all the nodes in self_and_descendants that are leaves.
161
- * ```tag.level``` returns the level, or "generation", for this node in the tree. A root node == 0.
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``` returns an array of immediate children (just those nodes whose parent is the current node).
164
- * ```tag.ancestors``` returns an array of [ parent, grandparent, great grandparent, … ]. Note that the size of this array will always equal ```tag.level```.
165
- * ```tag.self_and_ancestors``` returns an array of self, parent, grandparent, great grandparent, etc.
166
- * ```tag.siblings``` returns an array of brothers and sisters (all at that level), excluding self.
167
- * ```tag.self_and_siblings``` returns an array of brothers and sisters (all at that level), including self.
168
- * ```tag.descendants``` returns an array of all children, childrens' children, etc., excluding self.
169
- * ```tag.self_and_descendants``` returns an array of all children, childrens' children, etc., including self.
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 :acts_as_tree_validate
37
- before_save :acts_as_tree_before_save
38
- after_save :acts_as_tree_after_save
39
- before_destroy :acts_as_tree_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
- :foreign_key => parent_column_name,
48
- :dependent => closure_tree_options[:dependent]
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
- scope :roots, where(parent_column_name => nil)
80
+ def self.roots
81
+ where(parent_column_name => nil)
82
+ end
73
83
 
74
- scope :leaves, where(" #{quoted_table_name}.#{primary_key} IN
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
- _parent_id.nil?
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 level
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 acts_as_tree_validate
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 acts_as_tree_before_save
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 acts_as_tree_after_save
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._parent_id}
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 acts_as_tree_before_destroy
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 _parent_id
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
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = "3.1.0" unless defined?(::ClosureTree::VERSION)
2
+ VERSION = "3.2.0" unless defined?(::ClosureTree::VERSION)
3
3
  end
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
+
@@ -78,10 +78,12 @@ b2:
78
78
  c1a:
79
79
  name: c1a
80
80
  parent: b1
81
+ sort_order: 2
81
82
 
82
83
  c1b:
83
84
  name: c1b
84
85
  parent: b1
86
+ sort_order: 1
85
87
 
86
88
  c2:
87
89
  name: c2
@@ -93,4 +95,4 @@ d2:
93
95
 
94
96
  e2:
95
97
  name: e2
96
- parent: d2
98
+ parent: d2
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
- ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log")
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'
@@ -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 =~ [@root, @mid, @leaf]
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 reparenting" do
99
+ it "should support re-parenting" do
100
100
  @root.children << @leaf
101
- Tag.leaves.should =~ [@leaf, @mid]
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 =~ [child]
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.1.0
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-09 00:00:00.000000000 Z
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: 1583374080415694998
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: 1583374080415694998
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