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 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