closure_tree 4.5.0 → 4.6.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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.travis.yml +11 -3
- data/CHANGELOG.md +13 -0
- data/Gemfile +13 -0
- data/README.md +61 -12
- data/closure_tree.gemspec +4 -5
- data/gemfiles/activerecord_3.2.gemfile +12 -0
- data/gemfiles/activerecord_4.0.gemfile +12 -0
- data/gemfiles/activerecord_4.1.gemfile +12 -0
- data/gemfiles/activerecord_edge.gemfile +12 -0
- data/lib/closure_tree/acts_as_tree.rb +2 -9
- data/lib/closure_tree/deterministic_ordering.rb +4 -0
- data/lib/closure_tree/finders.rb +4 -4
- data/lib/closure_tree/hash_tree.rb +1 -1
- data/lib/closure_tree/hierarchy_maintenance.rb +19 -6
- data/lib/closure_tree/model.rb +2 -10
- data/lib/closure_tree/numeric_deterministic_ordering.rb +53 -36
- data/lib/closure_tree/numeric_order_support.rb +20 -13
- data/lib/closure_tree/support.rb +8 -0
- data/lib/closure_tree/support_attributes.rb +10 -0
- data/lib/closure_tree/test/matcher.rb +85 -0
- data/lib/closure_tree/version.rb +1 -1
- data/lib/closure_tree.rb +14 -1
- data/spec/cache_invalidation_spec.rb +39 -0
- data/spec/{support → db}/models.rb +6 -0
- data/spec/db/schema.rb +18 -0
- data/spec/label_spec.rb +171 -46
- data/spec/matcher_spec.rb +32 -0
- data/spec/parallel_spec.rb +85 -49
- data/spec/spec_helper.rb +6 -96
- data/spec/support/database.rb +49 -0
- data/spec/support/database_cleaner.rb +14 -0
- data/spec/support/deprecated/attr_accessible.rb +5 -0
- data/spec/support/hash_monkey_patch.rb +13 -0
- data/spec/support/helpers.rb +8 -0
- data/spec/support/sqlite3_with_advisory_lock.rb +10 -0
- data/tests.sh +7 -2
- metadata +31 -43
- data/spec/parallel_prepend_sibling_spec.rb +0 -42
@@ -13,49 +13,56 @@ module ClosureTree
|
|
13
13
|
end
|
14
14
|
|
15
15
|
module MysqlAdapter
|
16
|
-
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil
|
16
|
+
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
|
17
17
|
min_where = if minimum_sort_order_value
|
18
18
|
"AND #{quoted_order_column} >= #{minimum_sort_order_value}"
|
19
19
|
else
|
20
20
|
""
|
21
21
|
end
|
22
|
-
connection.execute
|
23
|
-
connection.execute <<-SQL
|
22
|
+
connection.execute 'SET @i = 0'
|
23
|
+
connection.execute <<-SQL.strip_heredoc
|
24
24
|
UPDATE #{quoted_table_name}
|
25
|
-
SET #{quoted_order_column} = (@i := @i + 1) + #{minimum_sort_order_value.to_i
|
26
|
-
WHERE #{
|
27
|
-
ORDER BY #{
|
25
|
+
SET #{quoted_order_column} = (@i := @i + 1) + #{minimum_sort_order_value.to_i - 1}
|
26
|
+
WHERE #{where_eq(parent_column_name, parent_id)} #{min_where}
|
27
|
+
ORDER BY #{nulls_last_order_by}
|
28
28
|
SQL
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
module PostgreSQLAdapter
|
33
|
-
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil
|
33
|
+
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
|
34
34
|
min_where = if minimum_sort_order_value
|
35
35
|
"AND #{quoted_order_column} >= #{minimum_sort_order_value}"
|
36
36
|
else
|
37
37
|
""
|
38
38
|
end
|
39
|
-
connection.execute <<-SQL
|
39
|
+
connection.execute <<-SQL.strip_heredoc
|
40
40
|
UPDATE #{quoted_table_name}
|
41
|
-
SET #{quoted_order_column(false)} = t.seq + #{minimum_sort_order_value.to_i
|
41
|
+
SET #{quoted_order_column(false)} = t.seq + #{minimum_sort_order_value.to_i - 1}
|
42
42
|
FROM (
|
43
43
|
SELECT #{quoted_id_column_name} AS id, row_number() OVER(ORDER BY #{order_by}) AS seq
|
44
44
|
FROM #{quoted_table_name}
|
45
|
-
WHERE #{
|
45
|
+
WHERE #{where_eq(parent_column_name, parent_id)} #{min_where}
|
46
|
+
) AS t
|
46
47
|
WHERE #{quoted_table_name}.#{quoted_id_column_name} = t.id
|
47
48
|
SQL
|
48
49
|
end
|
50
|
+
|
51
|
+
def rows_updated(result)
|
52
|
+
result.cmd_status.sub(/\AUPDATE /, '').to_i
|
53
|
+
end
|
49
54
|
end
|
50
55
|
|
51
56
|
module GenericAdapter
|
52
|
-
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil
|
53
|
-
scope = model_class.
|
57
|
+
def reorder_with_parent_id(parent_id, minimum_sort_order_value = nil)
|
58
|
+
scope = model_class.
|
59
|
+
where(parent_column_sym => parent_id).
|
60
|
+
order(nulls_last_order_by)
|
54
61
|
if minimum_sort_order_value
|
55
62
|
scope = scope.where("#{quoted_order_column} >= #{minimum_sort_order_value}")
|
56
63
|
end
|
57
64
|
scope.each_with_index do |ea, idx|
|
58
|
-
ea.
|
65
|
+
ea.update_order_value(idx + minimum_sort_order_value.to_i)
|
59
66
|
end
|
60
67
|
end
|
61
68
|
end
|
data/lib/closure_tree/support.rb
CHANGED
@@ -106,6 +106,14 @@ module ClosureTree
|
|
106
106
|
scope.pluck(model_class.primary_key)
|
107
107
|
end
|
108
108
|
|
109
|
+
def where_eq(column_name, value)
|
110
|
+
if value.nil?
|
111
|
+
"#{connection.quote_column_name(column_name)} IS NULL"
|
112
|
+
else
|
113
|
+
"#{connection.quote_column_name(column_name)} = #{quoted_value(value)}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
109
117
|
def with_advisory_lock(&block)
|
110
118
|
if options[:with_advisory_lock]
|
111
119
|
model_class.with_advisory_lock("closure_tree") do
|
@@ -73,6 +73,16 @@ module ClosureTree
|
|
73
73
|
options[:order]
|
74
74
|
end
|
75
75
|
|
76
|
+
def nulls_last_order_by
|
77
|
+
"-#{quoted_order_column} #{order_by_order(reverse = true)}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def order_by_order(reverse = false)
|
81
|
+
desc = !!(order_by.to_s =~ /DESC\z/)
|
82
|
+
desc = !desc if reverse
|
83
|
+
desc ? 'DESC' : 'ASC'
|
84
|
+
end
|
85
|
+
|
76
86
|
def order_column
|
77
87
|
o = order_by
|
78
88
|
if o.nil?
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module ClosureTree
|
2
|
+
module Test
|
3
|
+
module Matcher
|
4
|
+
def be_a_closure_tree
|
5
|
+
ClosureTree.new
|
6
|
+
end
|
7
|
+
|
8
|
+
class ClosureTree
|
9
|
+
def matches?(subject)
|
10
|
+
@subject = subject
|
11
|
+
# OPTIMIZE
|
12
|
+
if @subject.respond_to?(:_ct)
|
13
|
+
|
14
|
+
unless @subject.column_names.include?(@subject._ct.parent_column_name)
|
15
|
+
@message = "expected #{@subject.class.name} to respond to #{@subject._ct.parent_column_name}"
|
16
|
+
return false
|
17
|
+
end
|
18
|
+
|
19
|
+
# Checking if hierarchy table exists (common error)
|
20
|
+
unless @subject.hierarchy_class.table_exists?
|
21
|
+
@message = "expected #{@subject.class.name}'s hierarchy table '#{@subject.hierarchy_class.table_name}' to exist"
|
22
|
+
return false
|
23
|
+
end
|
24
|
+
|
25
|
+
if @ordered
|
26
|
+
unless @subject._ct.options.include?(:order)
|
27
|
+
@message = "expected #{@subject.class.name} to be an ordered closure tree"
|
28
|
+
return false
|
29
|
+
end
|
30
|
+
unless @subject.column_names.include?(@subject._ct.options[:order].to_s)
|
31
|
+
@message = "expected #{@subject.class.name} to have #{@subject._ct.options[:order]} as column"
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if @with_advisory_lock && !@subject._ct.options[:with_advisory_lock]
|
37
|
+
@message = "expected #{@subject.class.name} to have advisory lock"
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
|
41
|
+
if @without_advisory_lock && @subject._ct.options[:with_advisory_lock]
|
42
|
+
@message = "expected #{@subject.class.name} to not have advisory lock"
|
43
|
+
return false
|
44
|
+
end
|
45
|
+
|
46
|
+
return true
|
47
|
+
end
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
def ordered(column = nil)
|
52
|
+
@ordered = 'n ordered'
|
53
|
+
@order_colum = column
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def with_advisory_lock
|
58
|
+
@with_advisory_lock = ' with advisory lock'
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
def without_advisory_lock
|
63
|
+
@without_advisory_lock = ' without advisory lock'
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def failure_message
|
68
|
+
@message || "expected #{@subject.class.name} to #{description}"
|
69
|
+
end
|
70
|
+
|
71
|
+
alias_method :failure_message_for_should, :failure_message
|
72
|
+
|
73
|
+
def failure_message_when_negated
|
74
|
+
"expected #{@subject.class.name} not be a closure tree, but it is."
|
75
|
+
end
|
76
|
+
|
77
|
+
alias_method :failure_message_for_should_not, :failure_message_when_negated
|
78
|
+
|
79
|
+
def description
|
80
|
+
"be a#{@ordered} closure tree#{@with_advisory_lock}"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/closure_tree/version.rb
CHANGED
data/lib/closure_tree.rb
CHANGED
@@ -1,6 +1,19 @@
|
|
1
1
|
require 'active_support'
|
2
2
|
|
3
|
+
module ClosureTree
|
4
|
+
extend ActiveSupport::Autoload
|
5
|
+
|
6
|
+
autoload :ActsAsTree
|
7
|
+
autoload :Support
|
8
|
+
autoload :HierarchyMaintenance
|
9
|
+
autoload :Model
|
10
|
+
autoload :Finders
|
11
|
+
autoload :HashTree
|
12
|
+
autoload :Digraphs
|
13
|
+
autoload :DeterministicOrdering
|
14
|
+
autoload :NumericDeterministicOrdering
|
15
|
+
end
|
16
|
+
|
3
17
|
ActiveSupport.on_load :active_record do
|
4
|
-
require 'closure_tree/acts_as_tree'
|
5
18
|
ActiveRecord::Base.send :extend, ClosureTree::ActsAsTree
|
6
19
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
|
4
|
+
describe 'cache invalidation', cache: true do
|
5
|
+
before do
|
6
|
+
Timecop.travel(10.seconds.ago) do
|
7
|
+
#create a long tree with 2 branch
|
8
|
+
@root = MenuItem.create(
|
9
|
+
name: SecureRandom.hex(10)
|
10
|
+
)
|
11
|
+
2.times do
|
12
|
+
parent = @root
|
13
|
+
10.times do
|
14
|
+
parent = parent.children.create(
|
15
|
+
name: SecureRandom.hex(10)
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
@first_leaf = MenuItem.leaves.first
|
20
|
+
@second_leaf = MenuItem.leaves.last
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'touch option' do
|
25
|
+
it 'should invalidate cache for all it ancestors' do
|
26
|
+
old_time_stamp = @first_leaf.ancestors.pluck(:updated_at)
|
27
|
+
@first_leaf.touch
|
28
|
+
new_time_stamp = @first_leaf.ancestors.pluck(:updated_at)
|
29
|
+
expect(old_time_stamp).to_not eq(new_time_stamp)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should not invalidate cache for another branch' do
|
33
|
+
old_time_stamp = @second_leaf.updated_at
|
34
|
+
@first_leaf.touch
|
35
|
+
new_time_stamp = @second_leaf.updated_at
|
36
|
+
expect(old_time_stamp).to eq(new_time_stamp)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -4,9 +4,11 @@ class Tag < ActiveRecord::Base
|
|
4
4
|
acts_as_tree :dependent => :destroy, :order => :name
|
5
5
|
before_destroy :add_destroyed_tag
|
6
6
|
attr_accessible :name, :title if _ct.use_attr_accessible?
|
7
|
+
|
7
8
|
def to_s
|
8
9
|
name
|
9
10
|
end
|
11
|
+
|
10
12
|
def add_destroyed_tag
|
11
13
|
# Proof for the tests that the destroy rather than the delete method was called:
|
12
14
|
DestroyedTag.create(:name => name)
|
@@ -99,3 +101,7 @@ class Metal < ActiveRecord::Base
|
|
99
101
|
acts_as_tree :order => 'sort_order'
|
100
102
|
self.inheritance_column = 'metal_type'
|
101
103
|
end
|
104
|
+
|
105
|
+
class MenuItem < ActiveRecord::Base
|
106
|
+
acts_as_tree(touch: true, with_advisory_lock: false)
|
107
|
+
end
|
data/spec/db/schema.rb
CHANGED
@@ -124,4 +124,22 @@ ActiveRecord::Schema.define(:version => 0) do
|
|
124
124
|
|
125
125
|
add_foreign_key(:metal_hierarchies, :metal, :column => 'ancestor_id')
|
126
126
|
add_foreign_key(:metal_hierarchies, :metal, :column => 'descendant_id')
|
127
|
+
|
128
|
+
create_table 'menu_items' do |t|
|
129
|
+
t.string 'name'
|
130
|
+
t.integer 'parent_id'
|
131
|
+
t.timestamps
|
132
|
+
end
|
133
|
+
|
134
|
+
add_foreign_key(:menu_items, :menu_items, :column => 'parent_id')
|
135
|
+
|
136
|
+
create_table 'menu_item_hierarchies', :id => false do |t|
|
137
|
+
t.integer 'ancestor_id', :null => false
|
138
|
+
t.integer 'descendant_id', :null => false
|
139
|
+
t.integer 'generations', :null => false
|
140
|
+
end
|
141
|
+
|
142
|
+
add_foreign_key(:menu_item_hierarchies, :menu_items, :column => 'ancestor_id')
|
143
|
+
add_foreign_key(:menu_item_hierarchies, :menu_items, :column => 'descendant_id')
|
144
|
+
|
127
145
|
end
|
data/spec/label_spec.rb
CHANGED
@@ -44,12 +44,36 @@ end
|
|
44
44
|
describe Label do
|
45
45
|
|
46
46
|
context "destruction" do
|
47
|
-
it "properly destroys descendents" do
|
47
|
+
it "properly destroys descendents created with find_or_create_by_path" do
|
48
48
|
c = Label.find_or_create_by_path %w(a b c)
|
49
49
|
b = c.parent
|
50
50
|
a = c.root
|
51
51
|
a.destroy
|
52
|
-
Label.exists?(id: [a.id,b.id,c.id]).should be_false
|
52
|
+
Label.exists?(id: [a.id, b.id, c.id]).should be_false
|
53
|
+
end
|
54
|
+
|
55
|
+
it "properly destroys descendents created with add_child" do
|
56
|
+
a = Label.create(name: 'a')
|
57
|
+
b = Label.new(name: 'b')
|
58
|
+
a.add_child b
|
59
|
+
c = Label.new(name: 'c')
|
60
|
+
b.add_child c
|
61
|
+
a.destroy
|
62
|
+
Label.exists?(a).should be_false
|
63
|
+
Label.exists?(b).should be_false
|
64
|
+
Label.exists?(c).should be_false
|
65
|
+
end
|
66
|
+
|
67
|
+
it "properly destroys descendents created with <<" do
|
68
|
+
a = Label.create(name: 'a')
|
69
|
+
b = Label.new(name: 'b')
|
70
|
+
a.children << b
|
71
|
+
c = Label.new(name: 'c')
|
72
|
+
b.children << c
|
73
|
+
a.destroy
|
74
|
+
Label.exists?(a).should be_false
|
75
|
+
Label.exists?(b).should be_false
|
76
|
+
Label.exists?(c).should be_false
|
53
77
|
end
|
54
78
|
end
|
55
79
|
|
@@ -131,7 +155,7 @@ describe Label do
|
|
131
155
|
end
|
132
156
|
|
133
157
|
it "returns descendents regardless of subclass" do
|
134
|
-
Label.root.descendants.map{|ea|ea.class.to_s}.uniq.should =~
|
158
|
+
Label.root.descendants.map { |ea| ea.class.to_s }.uniq.should =~
|
135
159
|
%w(Label DateLabel DirectoryLabel EventLabel)
|
136
160
|
end
|
137
161
|
end
|
@@ -199,57 +223,79 @@ describe Label do
|
|
199
223
|
end
|
200
224
|
|
201
225
|
it "self_and_descendants should result in one select" do
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
226
|
+
count_queries do
|
227
|
+
a1_array = @a1.self_and_descendants
|
228
|
+
a1_array.collect { |ea| ea.name }.should == %w(a1 b1 c1 c2 d1 d2)
|
229
|
+
end.should == 1
|
206
230
|
end
|
207
231
|
|
208
232
|
it "self_and_ancestors should result in one select" do
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
233
|
+
count_queries do
|
234
|
+
d1_array = @d1.self_and_ancestors
|
235
|
+
d1_array.collect { |ea| ea.name }.should == %w(d1 c1 b1 a1)
|
236
|
+
end.should == 1
|
213
237
|
end
|
214
238
|
end
|
215
239
|
|
216
240
|
context "deterministically orders with polymorphic siblings" do
|
217
241
|
before :each do
|
218
|
-
@parent = Label.create!(:name =>
|
219
|
-
@a = EventLabel.new(:name =>
|
220
|
-
@b = DirectoryLabel.new(:name => "b")
|
221
|
-
@c = DateLabel.new(:name => "c")
|
222
|
-
@d = Label.new(:name => "d")
|
242
|
+
@parent = Label.create!(:name => 'parent')
|
243
|
+
@a, @b, @c, @d, @e, @f = ('a'..'f').map { |ea| EventLabel.new(:name => ea) }
|
223
244
|
@parent.children << @a
|
224
245
|
@a.append_sibling(@b)
|
225
246
|
@b.append_sibling(@c)
|
226
247
|
@c.append_sibling(@d)
|
248
|
+
@parent.append_sibling(@e)
|
249
|
+
@e.append_sibling(@f)
|
250
|
+
end
|
251
|
+
|
252
|
+
def name_and_order(enum)
|
253
|
+
enum.map { |ea| [ea.name, ea.sort_order] }
|
227
254
|
end
|
228
255
|
|
229
256
|
def children_name_and_order
|
230
|
-
@parent.reload
|
257
|
+
name_and_order(@parent.children(reload = true))
|
258
|
+
end
|
259
|
+
|
260
|
+
def roots_name_and_order
|
261
|
+
name_and_order(Label.roots)
|
231
262
|
end
|
232
263
|
|
233
|
-
it
|
264
|
+
it 'sort_orders properly' do
|
234
265
|
children_name_and_order.should == [['a', 0], ['b', 1], ['c', 2], ['d', 3]]
|
235
266
|
end
|
236
267
|
|
237
|
-
it
|
268
|
+
it 'when inserted before' do
|
238
269
|
@b.append_sibling(@a)
|
239
270
|
children_name_and_order.should == [['b', 0], ['a', 1], ['c', 2], ['d', 3]]
|
240
271
|
end
|
241
272
|
|
242
|
-
it
|
273
|
+
it 'when inserted after' do
|
243
274
|
@a.append_sibling(@c)
|
244
275
|
children_name_and_order.should == [['a', 0], ['c', 1], ['b', 2], ['d', 3]]
|
245
276
|
end
|
277
|
+
|
278
|
+
it 'when inserted before the first' do
|
279
|
+
@a.prepend_sibling(@d)
|
280
|
+
children_name_and_order.should == [['d', 0], ['a', 1], ['b', 2], ['c', 3]]
|
281
|
+
end
|
282
|
+
|
283
|
+
it 'when inserted after the last' do
|
284
|
+
@d.append_sibling(@b)
|
285
|
+
children_name_and_order.should == [['a', 0], ['c', 1], ['d', 2], ['b', 3]]
|
286
|
+
end
|
287
|
+
|
288
|
+
it 'prepends to root nodes' do
|
289
|
+
@parent.prepend_sibling(@f)
|
290
|
+
roots_name_and_order.should == [['f', 0], ['parent', 1], ['e', 2]]
|
291
|
+
end
|
246
292
|
end
|
247
293
|
|
248
|
-
it
|
249
|
-
root = Label.create(:
|
250
|
-
a = Label.
|
251
|
-
b = Label.create(:
|
252
|
-
c = Label.create(:
|
294
|
+
it 'behaves like the readme' do
|
295
|
+
root = Label.create(name: 'root')
|
296
|
+
a = root.append_child(Label.new(name: 'a'))
|
297
|
+
b = Label.create(name: 'b')
|
298
|
+
c = Label.create(name: 'c')
|
253
299
|
|
254
300
|
a.append_sibling(b)
|
255
301
|
a.self_and_siblings.collect(&:name).should == %w(a b)
|
@@ -277,6 +323,30 @@ describe Label do
|
|
277
323
|
d.self_and_siblings.collect(&:sort_order).should == [0, 1, 2, 3]
|
278
324
|
end
|
279
325
|
|
326
|
+
# https://github.com/mceachen/closure_tree/issues/84
|
327
|
+
it "properly appends children with <<" do
|
328
|
+
root = Label.create(:name => "root")
|
329
|
+
a = Label.create(:name => "a", :parent => root)
|
330
|
+
b = Label.create(:name => "b", :parent => root)
|
331
|
+
a.sort_order.should == 0
|
332
|
+
b.sort_order.should == 1
|
333
|
+
#c = Label.create(:name => "c")
|
334
|
+
|
335
|
+
# should the sort_order for roots be set?
|
336
|
+
root.sort_order.should_not be_nil
|
337
|
+
root.sort_order.should == 0
|
338
|
+
|
339
|
+
# sort_order should never be nil on a child.
|
340
|
+
a.sort_order.should_not be_nil
|
341
|
+
a.sort_order.should == 0
|
342
|
+
# Add a child to root at end of children.
|
343
|
+
root.children << b
|
344
|
+
b.parent.should == root
|
345
|
+
a.self_and_siblings.collect(&:name).should == %w(a b)
|
346
|
+
root.reload.children.collect(&:name).should == %w(a b)
|
347
|
+
root.children.collect(&:sort_order).should == [0, 1]
|
348
|
+
end
|
349
|
+
|
280
350
|
context "#add_sibling" do
|
281
351
|
it "should move a node before another node which has an uninitialized sort_order" do
|
282
352
|
f = Label.find_or_create_by_path %w(a b c d e fa)
|
@@ -290,16 +360,16 @@ describe Label do
|
|
290
360
|
f.self_and_siblings.should == [f0, f]
|
291
361
|
end
|
292
362
|
|
363
|
+
let(:f1) { Label.find_or_create_by_path %w(a1 b1 c1 d1 e1 f1) }
|
364
|
+
|
293
365
|
it "should move a node to another tree" do
|
294
|
-
f1 = Label.find_or_create_by_path %w(a1 b1 c1 d1 e1 f1)
|
295
366
|
f2 = Label.find_or_create_by_path %w(a2 b2 c2 d2 e2 f2)
|
296
367
|
f1.add_sibling(f2)
|
297
368
|
f2.ancestry_path.should == %w(a1 b1 c1 d1 e1 f2)
|
298
|
-
f1.parent.children.should == [f1, f2]
|
369
|
+
f1.parent.reload.children.should == [f1, f2]
|
299
370
|
end
|
300
371
|
|
301
372
|
it "should reorder old-parent siblings when a node moves to another tree" do
|
302
|
-
f1 = Label.find_or_create_by_path %w(a1 b1 c1 d1 e1 f1)
|
303
373
|
f2 = Label.find_or_create_by_path %w(a2 b2 c2 d2 e2 f2)
|
304
374
|
f3 = f2.prepend_sibling(Label.new(:name => "f3"))
|
305
375
|
f4 = f2.append_sibling(Label.new(:name => "f4"))
|
@@ -311,32 +381,87 @@ describe Label do
|
|
311
381
|
end
|
312
382
|
end
|
313
383
|
|
384
|
+
context "sort_order must be set" do
|
385
|
+
|
386
|
+
before do
|
387
|
+
@root = Label.create(name: 'root')
|
388
|
+
@a, @b, @c = %w(a b c).map { |n| @root.children.create(name: n) }
|
389
|
+
end
|
390
|
+
|
391
|
+
it 'should set sort_order on roots' do
|
392
|
+
@root.sort_order.should == 0
|
393
|
+
end
|
394
|
+
|
395
|
+
it 'should set sort_order with siblings' do
|
396
|
+
@a.sort_order.should == 0
|
397
|
+
@b.sort_order.should == 1
|
398
|
+
@c.sort_order.should == 2
|
399
|
+
end
|
400
|
+
|
401
|
+
it 'should reset sort_order when a node is moved to another location' do
|
402
|
+
root2 = Label.create(name: 'root2')
|
403
|
+
root2.add_child @b
|
404
|
+
@a.sort_order.should == 0
|
405
|
+
@b.sort_order.should == 0
|
406
|
+
@c.reload.sort_order.should == 1
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
314
410
|
context "destructive reordering" do
|
315
411
|
before :each do
|
316
412
|
# to make sure sort_order isn't affected by additional nodes:
|
317
413
|
create_preorder_tree
|
318
|
-
@root = Label.create(:name =>
|
319
|
-
@a = @root.children.create!(:name =>
|
320
|
-
@b = @a.append_sibling(Label.new(:name =>
|
321
|
-
@c = @b.append_sibling(Label.new(:name =>
|
414
|
+
@root = Label.create(:name => 'root')
|
415
|
+
@a = @root.children.create!(:name => 'a')
|
416
|
+
@b = @a.append_sibling(Label.new(:name => 'b'))
|
417
|
+
@c = @b.append_sibling(Label.new(:name => 'c'))
|
322
418
|
end
|
323
|
-
context "doesn't create sort order gaps
|
324
|
-
it
|
419
|
+
context "doesn't create sort order gaps" do
|
420
|
+
it 'from head' do
|
325
421
|
@a.destroy
|
326
422
|
@root.reload.children.should == [@b, @c]
|
327
423
|
@root.children.map { |ea| ea.sort_order }.should == [0, 1]
|
328
424
|
end
|
329
|
-
it
|
425
|
+
it 'from mid' do
|
330
426
|
@b.destroy
|
331
427
|
@root.reload.children.should == [@a, @c]
|
332
428
|
@root.children.map { |ea| ea.sort_order }.should == [0, 1]
|
333
429
|
end
|
334
|
-
it
|
430
|
+
it 'from tail' do
|
335
431
|
@c.destroy
|
336
432
|
@root.reload.children.should == [@a, @b]
|
337
433
|
@root.children.map { |ea| ea.sort_order }.should == [0, 1]
|
338
434
|
end
|
339
435
|
end
|
436
|
+
|
437
|
+
context 'add_sibling moves descendant nodes' do
|
438
|
+
let(:roots) { (0..10).map { |ea| Label.create(name: ea) } }
|
439
|
+
let(:first_root) { roots.first }
|
440
|
+
let(:last_root) { roots.last }
|
441
|
+
it 'should retain sort orders of descendants when moving to a new parent' do
|
442
|
+
expected_order = ('a'..'z').to_a.shuffle
|
443
|
+
expected_order.map { |ea| first_root.add_child(Label.new(name: ea)) }
|
444
|
+
actual_order = first_root.children(reload = true).pluck(:name)
|
445
|
+
actual_order.should == expected_order
|
446
|
+
last_root.append_child(first_root)
|
447
|
+
last_root.self_and_descendants.pluck(:name).should == %w(10 0) + expected_order
|
448
|
+
end
|
449
|
+
|
450
|
+
it 'should retain sort orders of descendants when moving within the same new parent' do
|
451
|
+
path = ('a'..'z').to_a
|
452
|
+
z = first_root.find_or_create_by_path(path)
|
453
|
+
z_children_names = (100..150).to_a.shuffle.map { |ea| ea.to_s }
|
454
|
+
z_children_names.reverse.each { |ea| z.prepend_child(Label.new(name: ea)) }
|
455
|
+
z.children(reload = true).pluck(:name).should == z_children_names
|
456
|
+
a = first_root.find_by_path(['a'])
|
457
|
+
# move b up to a's level:
|
458
|
+
b = a.children.first
|
459
|
+
a.add_sibling(b)
|
460
|
+
b.parent.should == first_root
|
461
|
+
z.children(reload = true).pluck(:name).should == z_children_names
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
340
465
|
it "shouldn't fail if all children are destroyed" do
|
341
466
|
roots = Label.roots.to_a
|
342
467
|
roots.each { |ea| ea.children.destroy_all }
|
@@ -344,33 +469,33 @@ describe Label do
|
|
344
469
|
end
|
345
470
|
end
|
346
471
|
|
347
|
-
context
|
348
|
-
it
|
472
|
+
context 'descendent destruction' do
|
473
|
+
it 'properly destroys descendents created with add_child' do
|
349
474
|
a = Label.create(name: 'a')
|
350
475
|
b = Label.new(name: 'b')
|
351
476
|
a.add_child b
|
352
477
|
c = Label.new(name: 'c')
|
353
478
|
b.add_child c
|
354
479
|
a.destroy
|
355
|
-
Label.exists?(id: [a.id,b.id,c.id]).should be_false
|
480
|
+
Label.exists?(id: [a.id, b.id, c.id]).should be_false
|
356
481
|
end
|
357
482
|
|
358
|
-
it
|
483
|
+
it 'properly destroys descendents created with <<' do
|
359
484
|
a = Label.create(name: 'a')
|
360
485
|
b = Label.new(name: 'b')
|
361
486
|
a.children << b
|
362
487
|
c = Label.new(name: 'c')
|
363
488
|
b.children << c
|
364
489
|
a.destroy
|
365
|
-
Label.exists?(id: [a.id,b.id,c.id]).should be_false
|
490
|
+
Label.exists?(id: [a.id, b.id, c.id]).should be_false
|
366
491
|
end
|
367
492
|
end
|
368
493
|
|
369
|
-
context
|
370
|
-
it
|
494
|
+
context 'preorder' do
|
495
|
+
it 'returns descendants in proper order' do
|
371
496
|
create_preorder_tree
|
372
497
|
a = Label.root
|
373
|
-
a.name.should ==
|
498
|
+
a.name.should == 'a'
|
374
499
|
expected = ('a'..'r').to_a
|
375
500
|
a.self_and_descendants_preordered.collect { |ea| ea.name }.should == expected
|
376
501
|
Label.roots_and_descendants_preordered.collect { |ea| ea.name }.should == expected
|
@@ -379,11 +504,11 @@ describe Label do
|
|
379
504
|
l.name = "a1"
|
380
505
|
l.sort_order = a.sort_order + 1
|
381
506
|
end
|
382
|
-
create_preorder_tree(
|
507
|
+
create_preorder_tree('1')
|
383
508
|
# Should be no change:
|
384
509
|
a.reload.self_and_descendants_preordered.collect { |ea| ea.name }.should == expected
|
385
510
|
expected += ('a'..'r').collect { |ea| "#{ea}1" }
|
386
511
|
Label.roots_and_descendants_preordered.collect { |ea| ea.name }.should == expected
|
387
512
|
end
|
388
|
-
end unless ENV[
|
513
|
+
end unless ENV['DB'] == 'sqlite' # sqlite doesn't have a power function.
|
389
514
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'ClosureTree::Test::Matcher' do
|
4
|
+
|
5
|
+
describe 'be_a_closure_tree' do
|
6
|
+
it { UUIDTag.should be_a_closure_tree }
|
7
|
+
it { User.should be_a_closure_tree }
|
8
|
+
it { Label.should be_a_closure_tree.ordered }
|
9
|
+
it { Metal.should be_a_closure_tree.ordered(:sort_order) }
|
10
|
+
it { MenuItem.should be_a_closure_tree }
|
11
|
+
it { Contract.should_not be_a_closure_tree }
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'ordered' do
|
15
|
+
it { Label.should be_a_closure_tree.ordered }
|
16
|
+
it { UUIDTag.should be_a_closure_tree.ordered }
|
17
|
+
it { Metal.should be_a_closure_tree.ordered(:sort_order) }
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'advisory_lock' do
|
21
|
+
it 'should use advisory lock' do
|
22
|
+
User.should be_a_closure_tree.with_advisory_lock
|
23
|
+
Label.should be_a_closure_tree.ordered.with_advisory_lock
|
24
|
+
Metal.should be_a_closure_tree.ordered(:sort_order).with_advisory_lock
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should not use advisory lock' do
|
28
|
+
MenuItem.should be_a_closure_tree.without_advisory_lock
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|