closure_tree 4.2.0 → 4.2.1

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.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- MWJmNzNmNzk4OGM2YmJlYWIzYjdkNTE2Yzk2YjZlNWM1ZDgwMTlhYg==
5
- data.tar.gz: !binary |-
6
- YjcwMmY4MTcwMTRlZmJmODAwOGViNGMyNzU4NGQ5MGY4ZDU1MjQ5Mg==
7
- !binary "U0hBNTEy":
8
- metadata.gz: !binary |-
9
- NGRlYjk1MDU4NzdiYzQzNzIzZGQxZTZjMjc1NDA4NzQ0YTAwYTExYTZlM2E4
10
- YWM0OTY4NzQ2ZmI4NjA2NDc4MWNjN2RlODIwNTY0ZWMwZGJmNzUwNjk0ZmE3
11
- M2NlYmU1MTk5MzViN2ExZmIwYjBmYTU5N2JhMGVmOGE5N2U3NmQ=
12
- data.tar.gz: !binary |-
13
- NmE0NGFjNmZiMDAwM2U5MTc3Y2E3OWIyMGFiNTEzYWI3MTA1MmRlYWI5MzQw
14
- OGMxNGQwODZmY2ZiNzQwZDhmNGUzYTEwNjNkMTZlOTNjZTE5ZTA2NGNhNTg5
15
- MTJjMjgxODk1NzdmOWQ1NzhhODA1MWZhYjNlYzUyOGUxNmVlYTk=
2
+ SHA1:
3
+ metadata.gz: 78c02789c9a49942f41eea1ce0ce205d38f1df9a
4
+ data.tar.gz: dcad9031af038ee30c9ec61a0f60941c028536f2
5
+ SHA512:
6
+ metadata.gz: 31034ae207d81917f3e60d69312c80561d21382123be62fb0b753db933f420be6007b5d69438c37b2ecd9800a38310ee184c3f9b570312d7d27cc0adc03971ab
7
+ data.tar.gz: 2068b5ecf26b41e96cb339a0e14427e6e6baa0527180dce781bd5f10d179662ac8b45b5f9232d5a2413fbf43c72f3a7ee42a0f2f745f95f2851711827b8a1bb3
data/README.md CHANGED
@@ -8,6 +8,7 @@ and tracking user referrals.
8
8
  [![Build Status](https://secure.travis-ci.org/mceachen/closure_tree.png?branch=master)](http://travis-ci.org/mceachen/closure_tree)
9
9
  [![Gem Version](https://badge.fury.io/rb/closure_tree.png)](http://rubygems.org/gems/closure_tree)
10
10
  [![Code Climate](https://codeclimate.com/github/mceachen/closure_tree.png)](https://codeclimate.com/github/mceachen/closure_tree)
11
+ [![Coverage Status](https://coveralls.io/repos/mceachen/closure_tree/badge.png?branch=master)](https://coveralls.io/r/mceachen/closure_tree?branch=master)
11
12
 
12
13
  Substantially more efficient than
13
14
  [ancestry](https://github.com/stefankroes/ancestry) and
@@ -471,6 +472,10 @@ Parallelism is not tested with Rails 3.0.x nor 3.1.x due to this
471
472
 
472
473
  ## Change log
473
474
 
475
+ ### 4.2.1
476
+
477
+ * Deleting from NumericDeterministicOrdering doesn't create sort order gaps anymore.
478
+
474
479
  ### 4.2.0
475
480
 
476
481
  * Added ```with_ancestor(*ancestors)```. Thanks for the idea, [Matt](https://github.com/mgornick)!
@@ -479,6 +484,7 @@ Parallelism is not tested with Rails 3.0.x nor 3.1.x due to this
479
484
  Thanks for the help, [Judd Blair](https://github.com/juddblair)!
480
485
  **Please note that this changes prior behavior—test your code with this new version!**
481
486
  * ```ct_advisory_lock``` was moved into the ```_ct``` support class, to reduce model method pollution
487
+ * Moved a bunch of code into more focused piles of module mixins
482
488
 
483
489
  ### 4.1.0
484
490
 
@@ -52,6 +52,7 @@ module ClosureTree
52
52
  SQL
53
53
  end
54
54
  children.each { |c| c.rebuild! }
55
+ _ct_reorder_children if _ct.order_is_numeric?
55
56
  end
56
57
  end
57
58
 
@@ -33,9 +33,6 @@ module ClosureTree
33
33
  :foreign_key => "ancestor_id",
34
34
  :order => order_by_generations)
35
35
 
36
- # TODO: FIXME: this collection currently ignores sort_order
37
- # (because the quoted_table_named would need to be joined in to get to the order column)
38
-
39
36
  has_many :self_and_descendants, *_ct.has_many_with_order_option(
40
37
  :through => :descendant_hierarchies,
41
38
  :source => :descendant,
@@ -127,8 +124,25 @@ module ClosureTree
127
124
  read_attribute(_ct.parent_column_sym)
128
125
  end
129
126
 
127
+ def _ct_quoted_parent_id
128
+ _ct.quoted_value(_ct_parent_id)
129
+ end
130
+
130
131
  def _ct_id
131
132
  read_attribute(_ct.model_class.primary_key)
132
133
  end
134
+
135
+ def _ct_quoted_id
136
+ _ct.quoted_value(_ct_id)
137
+ end
138
+
139
+ def _ct_update_column(column, value)
140
+ if respond_to?(:update_column)
141
+ update_column(column, value)
142
+ else
143
+ # This will run callbacks, but it's better than failing outright:
144
+ update_attribute(column, value)
145
+ end
146
+ end
133
147
  end
134
148
  end
@@ -1,8 +1,26 @@
1
+ require 'active_support/concern'
2
+
1
3
  # This module is only included if the order column is an integer.
2
4
  module ClosureTree
3
5
  module NumericDeterministicOrdering
4
6
  extend ActiveSupport::Concern
5
7
 
8
+ included do
9
+ after_destroy :_ct_reorder_after_destroy
10
+ end
11
+
12
+ def _ct_reorder_after_destroy
13
+ _ct_reorder_siblings
14
+ end
15
+
16
+ def _ct_reorder_siblings(minimum_sort_order_value = nil, delta = 0)
17
+ _ct.reorder_with_parent_id(_ct_parent_id, minimum_sort_order_value, delta)
18
+ end
19
+
20
+ def _ct_reorder_children(minimum_sort_order_value = nil, delta = 0)
21
+ _ct.reorder_with_parent_id(_ct_id, minimum_sort_order_value, delta)
22
+ end
23
+
6
24
  def self_and_descendants_preordered
7
25
  # TODO: raise NotImplementedError if sort_order is not numeric and not null?
8
26
  h = _ct.connection.select_one(<<-SQL)
@@ -60,27 +78,41 @@ module ClosureTree
60
78
  add_sibling(sibling_node, false)
61
79
  end
62
80
 
63
- def add_sibling(sibling_node, add_after = true)
64
- fail "can't add self as sibling" if self == sibling_node
81
+ def add_sibling(sibling, add_after = true)
82
+ fail "can't add self as sibling" if self == sibling
65
83
  _ct.with_advisory_lock do
66
- if self.order_value.nil? || siblings_before.without(sibling_node).empty?
67
- update_attribute(:order_value, 0)
84
+ if self.order_value.nil?
85
+ # ergh, we don't know where we stand within the siblings, so establish that first:
86
+ _ct_reorder_siblings
87
+ reload # < because self.order_value changed
68
88
  end
69
- sibling_node.parent = self.parent
70
- starting_order_value = self.order_value.to_i
71
- to_reorder = siblings_after.without(sibling_node).to_a
72
- if add_after
73
- to_reorder.unshift(sibling_node)
74
- else
75
- to_reorder.unshift(self)
76
- sibling_node.update_attribute(:order_value, starting_order_value)
89
+ prior_sibling_parent = sibling.parent
90
+ if prior_sibling_parent == self.parent
91
+ # We have to adjust the prior siblings by moving sibling out of the way:
92
+ sibling._ct_update_column(_ct.parent_column_sym, nil)
93
+ if sibling.order_value && sibling.order_value < self.order_value
94
+ _ct_reorder_siblings(sibling.order_value, 0)
95
+ reload # < because self.order_value changed
96
+ end
77
97
  end
78
-
79
- to_reorder.each_with_index do |ea, idx|
80
- ea.update_attribute(:order_value, starting_order_value + idx + 1)
98
+ _ct_move_new_sibling(sibling, add_after)
99
+ if prior_sibling_parent && prior_sibling_parent != self.parent
100
+ prior_sibling_parent._ct_reorder_children
81
101
  end
82
- sibling_node.reload # because the parent may have changed.
102
+ sibling
103
+ end
104
+ end
105
+
106
+ def _ct_move_new_sibling(sibling, add_after)
107
+ _ct_reorder_siblings(self.order_value + 1, 1)
108
+ if add_after
109
+ sibling.order_value = self.order_value + 1
110
+ else
111
+ sibling.order_value = self.order_value
112
+ self.order_value += 1
113
+ self.save!
83
114
  end
115
+ parent.add_child(sibling) # <- this causes sibling to be saved.
84
116
  end
85
117
  end
86
118
  end
@@ -0,0 +1,63 @@
1
+ module ClosureTree
2
+ module NumericOrderSupport
3
+
4
+ def self.adapter_for_connection(connection)
5
+ case connection.adapter_name.downcase.to_sym
6
+ when :postgresql
7
+ ::ClosureTree::NumericOrderSupport::PostgreSQLAdapter
8
+ when :mysql, :mysql2
9
+ ::ClosureTree::NumericOrderSupport::MysqlAdapter
10
+ else
11
+ ::ClosureTree::NumericOrderSupport::GenericAdapter
12
+ end
13
+ end
14
+
15
+ module MysqlAdapter
16
+ def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil, delta = 0)
17
+ min_where = if minimum_sort_order_value
18
+ "AND #{quoted_order_column} >= #{minimum_sort_order_value}"
19
+ else
20
+ ""
21
+ end
22
+ connection.execute "SET @i = 0"
23
+ connection.execute <<-SQL
24
+ UPDATE #{quoted_table_name}
25
+ SET #{quoted_order_column} = (@i := @i + 1) + #{minimum_sort_order_value.to_i + delta - 1}
26
+ WHERE #{quoted_parent_column_name} = #{quoted_value(parent_id)} #{min_where}
27
+ ORDER BY #{order_by}
28
+ SQL
29
+ end
30
+ end
31
+
32
+ module PostgreSQLAdapter
33
+ def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil, delta = 0)
34
+ min_where = if minimum_sort_order_value
35
+ "AND #{quoted_order_column} >= #{minimum_sort_order_value}"
36
+ else
37
+ ""
38
+ end
39
+ connection.execute <<-SQL
40
+ UPDATE #{quoted_table_name}
41
+ SET #{quoted_order_column(false)} = t.seq + #{minimum_sort_order_value.to_i + delta - 1}
42
+ FROM (
43
+ SELECT #{quoted_id_column_name} AS id, row_number() OVER(ORDER BY #{order_by}) AS seq
44
+ FROM #{quoted_table_name}
45
+ WHERE #{quoted_parent_column_name} = #{quoted_value(parent_id)} #{min_where}) AS t
46
+ WHERE #{quoted_table_name}.#{quoted_id_column_name} = t.id
47
+ SQL
48
+ end
49
+ end
50
+
51
+ module GenericAdapter
52
+ def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil, delta = 0)
53
+ scope = model_class.where(parent_column_sym => parent_id)
54
+ if minimum_sort_order_value
55
+ scope = scope.where("#{quoted_order_column} >= #{minimum_sort_order_value}")
56
+ end
57
+ scope.each_with_index do |ea, idx|
58
+ ea.update_attribute(order_column_sym, idx + minimum_sort_order_value.to_i + delta)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -1,5 +1,6 @@
1
1
  require 'closure_tree/support_flags'
2
2
  require 'closure_tree/support_attributes'
3
+ require 'closure_tree/numeric_order_support'
3
4
 
4
5
  module ClosureTree
5
6
  class Support
@@ -19,6 +20,9 @@ module ClosureTree
19
20
  :with_advisory_lock => true
20
21
  }.merge(options)
21
22
  raise IllegalArgumentException, "name_column can't be 'path'" if options[:name_column] == 'path'
23
+ if order_is_numeric?
24
+ extend NumericOrderSupport.adapter_for_connection(connection)
25
+ end
22
26
  end
23
27
 
24
28
  def hierarchy_class_for_model
@@ -58,13 +62,13 @@ module ClosureTree
58
62
 
59
63
  def with_order_option(opts)
60
64
  if order_option?
61
- opts[:order] = [opts[:order], options[:order]].compact.join(",")
65
+ opts[:order] = [opts[:order], order_by].compact.join(",")
62
66
  end
63
67
  opts
64
68
  end
65
69
 
66
70
  def scope_with_order(scope, additional_order_by = nil)
67
- order_option? ? scope.order(*([additional_order_by, options[:order]].compact)) : scope
71
+ order_option? ? scope.order(*([additional_order_by, order_by].compact)) : scope
68
72
  end
69
73
 
70
74
  # lambda-ize the order, but don't apply the default order_option
@@ -78,7 +82,7 @@ module ClosureTree
78
82
 
79
83
  def has_many_with_order_option(opts)
80
84
  if ActiveRecord::VERSION::MAJOR > 3
81
- order_options = [opts[:order], options[:order]].compact
85
+ order_options = [opts[:order], order_by].compact
82
86
  [lambda { order(order_options) }, opts.except(:order)]
83
87
  else
84
88
  [with_order_option(opts)]
@@ -1,6 +1,10 @@
1
+ require 'forwardable'
1
2
  module ClosureTree
2
3
  module SupportAttributes
3
4
 
5
+ extend Forwardable
6
+ def_delegators :model_class, :connection, :transaction, :table_name
7
+
4
8
  # This is the "topmost" class. This will only potentially not be ct_class if you are using STI.
5
9
  def base_class
6
10
  options[:base_class]
@@ -10,18 +14,14 @@ module ClosureTree
10
14
  @attribute_names ||= model_class.new.attributes.keys - model_class.protected_attributes.to_a
11
15
  end
12
16
 
13
- def connection
14
- model_class.connection
15
- end
16
-
17
- def table_name
18
- model_class.table_name
19
- end
20
-
21
17
  def quoted_table_name
22
18
  connection.quote_table_name(table_name)
23
19
  end
24
20
 
21
+ def quoted_value(value)
22
+ value.is_a?(Numeric) ? value : quote(value)
23
+ end
24
+
25
25
  def hierarchy_class_name
26
26
  options[:hierarchy_class_name] || model_class.to_s + "Hierarchy"
27
27
  end
@@ -69,8 +69,12 @@ module ClosureTree
69
69
  connection.quote_column_name name_column
70
70
  end
71
71
 
72
+ def order_by
73
+ options[:order]
74
+ end
75
+
72
76
  def order_column
73
- o = options[:order]
77
+ o = order_by
74
78
  if o.nil?
75
79
  nil
76
80
  elsif o.is_a?(String)
@@ -94,6 +98,5 @@ module ClosureTree
94
98
  prefix = include_table_name ? "#{quoted_table_name}." : ""
95
99
  "#{prefix}#{connection.quote_column_name(order_column)}"
96
100
  end
97
-
98
101
  end
99
102
  end
@@ -15,7 +15,7 @@ module ClosureTree
15
15
  end
16
16
 
17
17
  def order_option?
18
- !options[:order].nil?
18
+ order_by.present?
19
19
  end
20
20
 
21
21
  def order_is_numeric?
@@ -1,3 +1,3 @@
1
1
  module ClosureTree
2
- VERSION = Gem::Version.new("4.2.0") unless defined?(::ClosureTree::VERSION)
2
+ VERSION = Gem::Version.new("4.2.1") unless defined?(::ClosureTree::VERSION)
3
3
  end
data/spec/label_spec.rb CHANGED
@@ -216,23 +216,29 @@ describe Label do
216
216
  @a = EventLabel.new(:name => "a")
217
217
  @b = DirectoryLabel.new(:name => "b")
218
218
  @c = DateLabel.new(:name => "c")
219
+ @d = Label.new(:name => "d")
219
220
  @parent.children << @a
220
221
  @a.append_sibling(@b)
221
222
  @b.append_sibling(@c)
223
+ @c.append_sibling(@d)
222
224
  end
223
225
 
224
- it "when inserted before" do
225
- @b.append_sibling(@a)
226
- # Have to reload because the sort_order will have changed out from under the references:
227
- @b.reload.sort_order.should be < @a.reload.sort_order
228
- @a.reload.sort_order.should be < @c.reload.sort_order
226
+ def children_name_and_order
227
+ @parent.reload.children.map { |ea| [ea.name, ea.sort_order] }
228
+ end
229
+
230
+ it "sort_orders properly" do
231
+ children_name_and_order.should == [['a', 0], ['b', 1], ['c', 2], ['d', 3]]
229
232
  end
230
233
 
231
234
  it "when inserted before" do
232
235
  @b.append_sibling(@a)
233
- # Have to reload because the sort_order will have changed out from under the references:
234
- @b.reload.sort_order.should be < @a.reload.sort_order
235
- @a.reload.sort_order.should be < @c.reload.sort_order
236
+ children_name_and_order.should == [['b', 0], ['a', 1], ['c', 2], ['d', 3]]
237
+ end
238
+
239
+ it "when inserted after" do
240
+ @a.append_sibling(@c)
241
+ children_name_and_order.should == [['a', 0], ['c', 1], ['b', 2], ['d', 3]]
236
242
  end
237
243
  end
238
244
 
@@ -243,116 +249,100 @@ describe Label do
243
249
  c = Label.create(:name => "c")
244
250
 
245
251
  a.append_sibling(b)
252
+ a.self_and_siblings.collect(&:name).should == %w(a b)
246
253
  root.reload.children.collect(&:name).should == %w(a b)
247
- root.reload.children.collect(&:sort_order).should == [0, 1]
254
+ root.children.collect(&:sort_order).should == [0, 1]
248
255
 
249
256
  a.prepend_sibling(b)
257
+ a.self_and_siblings.collect(&:name).should == %w(b a)
250
258
  root.reload.children.collect(&:name).should == %w(b a)
251
- root.reload.children.collect(&:sort_order).should == [0, 1]
259
+ root.children.collect(&:sort_order).should == [0, 1]
252
260
 
253
261
  a.append_sibling(c)
262
+ a.self_and_siblings.collect(&:name).should == %w(b a c)
254
263
  root.reload.children.collect(&:name).should == %w(b a c)
255
- root.reload.children.collect(&:sort_order).should == [0, 1, 2]
264
+ root.children.collect(&:sort_order).should == [0, 1, 2]
256
265
 
257
- b.append_sibling(c)
266
+ # We need to reload b because it was updated by a.append_sibling(c)
267
+ b.reload.append_sibling(c)
258
268
  root.reload.children.collect(&:name).should == %w(b c a)
259
- root.reload.children.collect(&:sort_order).should == [0, 1, 2]
260
- end
269
+ root.children.collect(&:sort_order).should == [0, 1, 2]
261
270
 
262
- context "Deterministic siblings sort with custom integer column" do
263
- delete_all_labels
264
- fixtures :labels
271
+ # We need to reload a because it was updated by b.append_sibling(c)
272
+ d = a.reload.append_sibling(Label.new(:name => "d"))
273
+ d.self_and_siblings.collect(&:name).should == %w(b c a d)
274
+ d.self_and_siblings.collect(&:sort_order).should == [0, 1, 2, 3]
275
+ end
265
276
 
277
+ context "#add_sibling" do
266
278
  before :each do
267
- Label.rebuild!
268
- end
269
-
270
- it "orders siblings_before and siblings_after correctly" do
271
- labels(:c16).self_and_siblings.to_a.should == [labels(:c16), labels(:c17), labels(:c18), labels(:c19)]
272
- labels(:c16).siblings_before.to_a.should == []
273
- labels(:c16).siblings_after.to_a.should == [labels(:c17), labels(:c18), labels(:c19)]
274
- end
275
-
276
- it "should prepend a node as a sibling of another node" do
277
- labels(:c16).prepend_sibling(labels(:c17))
278
- labels(:c16).self_and_siblings.to_a.should == [labels(:c17), labels(:c16), labels(:c18), labels(:c19)]
279
- labels(:c19).prepend_sibling(labels(:c16))
280
- labels(:c16).self_and_siblings.to_a.should == [labels(:c17), labels(:c18), labels(:c16), labels(:c19)]
281
- labels(:c16).siblings_before.to_a.should == [labels(:c17), labels(:c18)]
282
- labels(:c16).siblings_after.to_a.should == [labels(:c19)]
283
- end
284
-
285
- it "should prepend a node as a sibling of another node (!update_all)" do
286
- labels(:c16).prepend_sibling(labels(:c17))
287
- labels(:c16).self_and_siblings.to_a.should == [labels(:c17), labels(:c16), labels(:c18), labels(:c19)]
288
- labels(:c19).reload.prepend_sibling(labels(:c16).reload)
289
- labels(:c16).self_and_siblings.to_a.should == [labels(:c17), labels(:c18), labels(:c16), labels(:c19)]
290
- labels(:c16).siblings_before.to_a.should == [labels(:c17), labels(:c18)]
291
- labels(:c16).siblings_after.to_a.should == [labels(:c19)]
279
+ Label.delete_all
292
280
  end
293
281
 
294
- it "appends a node as a sibling of another node" do
295
- labels(:c19).append_sibling(labels(:c17))
296
- labels(:c16).self_and_siblings.to_a.should == [labels(:c16), labels(:c18), labels(:c19), labels(:c17)]
297
- labels(:c16).append_sibling(labels(:c19))
298
- labels(:c16).self_and_siblings.to_a.should == [labels(:c16), labels(:c19), labels(:c18), labels(:c17)]
299
- labels(:c16).siblings_before.to_a.should == []
300
- labels(:c16).siblings_after.to_a.should == labels(:c16).siblings.to_a
301
- end
302
-
303
- it "should move a node before another node (update_all)" do
304
- labels(:c2).ancestry_path.should == %w{a1 b2 c2}
305
- labels(:b2).prepend_sibling(labels(:c2))
306
- labels(:c2).ancestry_path.should == %w{a1 c2}
307
- labels(:c2).self_and_siblings.to_a.should == [labels(:b1), labels(:c2), labels(:b2)]
308
- labels(:c2).siblings_before.to_a.should == [labels(:b1)]
309
- labels(:c2).siblings_after.to_a.should == [labels(:b2)]
310
- labels(:b1).siblings_after.to_a.should == [labels(:c2), labels(:b2)]
311
- end
312
-
313
- it "should move a node after another node (update_all)" do
314
- labels(:c2).ancestry_path.should == %w{a1 b2 c2}
315
- labels(:b2).append_sibling(labels(:c2))
316
- labels(:c2).ancestry_path.should == %w{a1 c2}
317
- labels(:c2).self_and_siblings.to_a.should == [labels(:b1), labels(:b2), labels(:c2)]
282
+ it "should move a node before another node which has an uninitialized sort_order" do
283
+ f = Label.find_or_create_by_path %w(a b c d e fa)
284
+ f0 = f.prepend_sibling(Label.new(:name => "fb")) # < not alpha sort, so name shouldn't matter
285
+ f0.ancestry_path.should == %w(a b c d e fb)
286
+ f.siblings_before.to_a.should == [f0]
287
+ f0.siblings_before.should be_empty
288
+ f0.siblings_after.should == [f]
289
+ f.siblings_after.should be_empty
290
+ f0.self_and_siblings.should == [f0, f]
291
+ f.self_and_siblings.should == [f0, f]
292
+ end
293
+
294
+ it "should move a node to another tree" do
295
+ f1 = Label.find_or_create_by_path %w(a1 b1 c1 d1 e1 f1)
296
+ f2 = Label.find_or_create_by_path %w(a2 b2 c2 d2 e2 f2)
297
+ f1.add_sibling(f2)
298
+ f2.ancestry_path.should == %w(a1 b1 c1 d1 e1 f2)
299
+ f1.parent.children.should == [f1, f2]
300
+ end
301
+
302
+ it "should reorder old-parent siblings when a node moves to another tree" do
303
+ f1 = Label.find_or_create_by_path %w(a1 b1 c1 d1 e1 f1)
304
+ f2 = Label.find_or_create_by_path %w(a2 b2 c2 d2 e2 f2)
305
+ f3 = f2.prepend_sibling(Label.new(:name => "f3"))
306
+ f4 = f2.append_sibling(Label.new(:name => "f4"))
307
+ f1.add_sibling(f2)
308
+ f1.self_and_siblings.collect(&:sort_order).should == [0, 1]
309
+ f3.self_and_siblings.collect(&:sort_order).should == [0, 1]
310
+ f1.self_and_siblings.collect(&:name).should == %w(f1 f2)
311
+ f3.self_and_siblings.collect(&:name).should == %w(f3 f4)
318
312
  end
313
+ end
319
314
 
320
- it "should move a node before another node" do
321
- labels(:c2).ancestry_path.should == %w{a1 b2 c2}
322
- labels(:b2).prepend_sibling(labels(:c2))
323
- labels(:c2).ancestry_path.should == %w{a1 c2}
324
- labels(:c2).self_and_siblings.to_a.should == [labels(:b1), labels(:c2), labels(:b2)]
315
+ context "destructive reordering" do
316
+ before :each do
317
+ Label.delete_all
318
+ # to make sure sort_order isn't affected by additional nodes:
319
+ create_preorder_tree
320
+ @root = Label.create(:name => "root")
321
+ @a = @root.children.create!(:name => "a")
322
+ @b = @a.append_sibling(Label.new(:name => "b"))
323
+ @c = @b.append_sibling(Label.new(:name => "c"))
324
+ end
325
+ context "doesn't create sort order gaps from" do
326
+ it "from head" do
327
+ @a.destroy
328
+ @root.reload.children.should == [@b, @c]
329
+ @root.children.map { |ea| ea.sort_order }.should == [0, 1]
330
+ end
331
+ it "from mid" do
332
+ @b.destroy
333
+ @root.reload.children.should == [@a, @c]
334
+ @root.children.map { |ea| ea.sort_order }.should == [0, 1]
335
+ end
336
+ it "from tail" do
337
+ @c.destroy
338
+ @root.reload.children.should == [@a, @b]
339
+ @root.children.map { |ea| ea.sort_order }.should == [0, 1]
340
+ end
325
341
  end
326
-
327
- it "should move a node before another node which has an uninitialized sort_order" do
328
- labels(:f3).ancestry_path.should == %w{f3}
329
- labels(:e2).children << labels(:f3)
330
- labels(:f3).reload.ancestry_path.should == %w{a1 b2 c2 d2 e2 f3}
331
- labels(:f3).self_and_siblings.to_a.should == [labels(:f3)]
332
- labels(:f3).prepend_sibling labels(:f4)
333
- labels(:f3).siblings_before.to_a.should == [labels(:f4)]
334
- labels(:f3).self_and_siblings.to_a.should == [labels(:f4), labels(:f3)]
335
- end
336
-
337
- it "should move a node after another node which has an uninitialized sort_order" do
338
- labels(:f3).ancestry_path.should == %w{f3}
339
- labels(:e2).children << labels(:f3)
340
- labels(:f3).reload.ancestry_path.should == %w{a1 b2 c2 d2 e2 f3}
341
- labels(:f3).self_and_siblings.to_a.should == [labels(:f3)]
342
- labels(:f3).append_sibling labels(:f4)
343
- labels(:f3).siblings_after.to_a.should == [labels(:f4)]
344
- labels(:f3).self_and_siblings.to_a.should == [labels(:f3), labels(:f4)]
345
- end
346
-
347
- it "should move a node after another node" do
348
- labels(:c2).ancestry_path.should == %w{a1 b2 c2}
349
- labels(:b2).append_sibling(labels(:c2))
350
- labels(:c2).ancestry_path.should == %w{a1 c2}
351
- labels(:c2).self_and_siblings.to_a.should == [labels(:b1), labels(:b2), labels(:c2)]
352
- labels(:c2).append_sibling(labels(:e2))
353
- labels(:e2).self_and_siblings.to_a.should == [labels(:b1), labels(:b2), labels(:c2), labels(:e2)]
354
- labels(:a1).self_and_descendants.collect(&:name).should == %w(a1 b1 b2 c2 e2 d2 c1-six c1-seven c1-eight c1-nine)
355
- labels(:a1).leaves.collect(&:name).should =~ %w(b2 e2 d2 c1-six c1-seven c1-eight c1-nine)
342
+ it "shouldn't fail if all children are destroyed" do
343
+ roots = Label.roots.to_a
344
+ roots.each { |ea| ea.children.destroy_all }
345
+ Label.all.to_a.should =~ roots
356
346
  end
357
347
  end
358
348
 
data/spec/spec_helper.rb CHANGED
@@ -12,6 +12,8 @@ require 'rspec/rails/adapters'
12
12
  require 'rspec/rails/fixture_support'
13
13
  require 'closure_tree'
14
14
  require 'tmpdir'
15
+ require 'coveralls'
16
+ Coveralls.wear!
15
17
 
16
18
  #log = Logger.new(STDOUT)
17
19
  #log.sev_threshold = Logger::DEBUG
metadata CHANGED
@@ -1,167 +1,181 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: closure_tree
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.0
4
+ version: 4.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew McEachen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-06-10 00:00:00.000000000 Z
11
+ date: 2013-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ! '>='
17
+ - - '>='
18
18
  - !ruby/object:Gem::Version
19
19
  version: 3.0.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ! '>='
24
+ - - '>='
25
25
  - !ruby/object:Gem::Version
26
26
  version: 3.0.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: with_advisory_lock
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ! '>='
31
+ - - '>='
32
32
  - !ruby/object:Gem::Version
33
33
  version: 0.0.6
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ! '>='
38
+ - - '>='
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.0.6
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ! '>='
45
+ - - '>='
46
46
  - !ruby/object:Gem::Version
47
47
  version: '0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ! '>='
52
+ - - '>='
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: yard
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ! '>='
59
+ - - '>='
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ! '>='
66
+ - - '>='
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rspec
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ! '>='
73
+ - - '>='
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ! '>='
80
+ - - '>='
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: fuubar
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ! '>='
87
+ - - '>='
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ! '>='
94
+ - - '>='
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: rspec-rails
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ! '>='
101
+ - - '>='
102
102
  - !ruby/object:Gem::Version
103
103
  version: '0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ! '>='
108
+ - - '>='
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: mysql2
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
- - - ! '>='
115
+ - - '>='
116
116
  - !ruby/object:Gem::Version
117
117
  version: '0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
- - - ! '>='
122
+ - - '>='
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: pg
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
- - - ! '>='
129
+ - - '>='
130
130
  - !ruby/object:Gem::Version
131
131
  version: '0'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
- - - ! '>='
136
+ - - '>='
137
137
  - !ruby/object:Gem::Version
138
138
  version: '0'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: sqlite3
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
- - - ! '>='
143
+ - - '>='
144
144
  - !ruby/object:Gem::Version
145
145
  version: '0'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
- - - ! '>='
150
+ - - '>='
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: uuidtools
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
- - - ! '>='
157
+ - - '>='
158
158
  - !ruby/object:Gem::Version
159
159
  version: '0'
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
- - - ! '>='
164
+ - - '>='
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: coveralls
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - '>='
165
179
  - !ruby/object:Gem::Version
166
180
  version: '0'
167
181
  description: Easily and efficiently make your ActiveRecord model support hierarchies
@@ -179,6 +193,7 @@ files:
179
193
  - lib/closure_tree/hierarchy_maintenance.rb
180
194
  - lib/closure_tree/model.rb
181
195
  - lib/closure_tree/numeric_deterministic_ordering.rb
196
+ - lib/closure_tree/numeric_order_support.rb
182
197
  - lib/closure_tree/support.rb
183
198
  - lib/closure_tree/support_attributes.rb
184
199
  - lib/closure_tree/support_flags.rb
@@ -190,7 +205,6 @@ files:
190
205
  - spec/cuisine_type_spec.rb
191
206
  - spec/db/database.yml
192
207
  - spec/db/schema.rb
193
- - spec/fixtures/labels.yml
194
208
  - spec/fixtures/tags.yml
195
209
  - spec/label_spec.rb
196
210
  - spec/namespace_type_spec.rb
@@ -212,17 +226,17 @@ require_paths:
212
226
  - lib
213
227
  required_ruby_version: !ruby/object:Gem::Requirement
214
228
  requirements:
215
- - - ! '>='
229
+ - - '>='
216
230
  - !ruby/object:Gem::Version
217
231
  version: '0'
218
232
  required_rubygems_version: !ruby/object:Gem::Requirement
219
233
  requirements:
220
- - - ! '>='
234
+ - - '>='
221
235
  - !ruby/object:Gem::Version
222
236
  version: '0'
223
237
  requirements: []
224
238
  rubyforge_project:
225
- rubygems_version: 2.0.3
239
+ rubygems_version: 2.0.2
226
240
  signing_key:
227
241
  specification_version: 4
228
242
  summary: Easily and efficiently make your ActiveRecord model support hierarchies
@@ -230,7 +244,6 @@ test_files:
230
244
  - spec/cuisine_type_spec.rb
231
245
  - spec/db/database.yml
232
246
  - spec/db/schema.rb
233
- - spec/fixtures/labels.yml
234
247
  - spec/fixtures/tags.yml
235
248
  - spec/label_spec.rb
236
249
  - spec/namespace_type_spec.rb
@@ -1,55 +0,0 @@
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
- f3:
52
- name: f3
53
-
54
- f4:
55
- name: f4