closure_tree 3.7.2 → 3.7.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +6 -0
- data/lib/closure_tree.rb +2 -1
- data/lib/closure_tree/acts_as_tree.rb +57 -45
- data/lib/closure_tree/version.rb +1 -1
- data/spec/db/database.yml +2 -2
- data/spec/db/schema.rb +13 -13
- data/spec/parallel_prepend_sibling_spec.rb +45 -0
- data/spec/parallel_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -5
- data/spec/support/models.rb +0 -10
- data/spec/tag_spec.rb +32 -5
- metadata +8 -8
- data/spec/node_spec.rb +0 -148
data/README.md
CHANGED
@@ -396,6 +396,12 @@ Parallelism is not tested with Rails 3.0.x nor 3.1.x due to this
|
|
396
396
|
|
397
397
|
## Change log
|
398
398
|
|
399
|
+
### 3.7.3
|
400
|
+
|
401
|
+
Due to MySQL's inability to lock rows properly, I've switched to advisory_locks for
|
402
|
+
all write paths. This will prevent deadlocks, addressing
|
403
|
+
[issue 41](https://github.com/mceachen/closure_tree/issues/41).
|
404
|
+
|
399
405
|
### 3.7.2
|
400
406
|
|
401
407
|
* Support for UUID primary keys. Addresses
|
data/lib/closure_tree.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
require 'active_support'
|
2
|
+
require 'active_record'
|
2
3
|
|
3
4
|
ActiveSupport.on_load :active_record do
|
4
|
-
require 'closure_tree/acts_as_tree'
|
5
5
|
require 'with_advisory_lock'
|
6
|
+
require 'closure_tree/acts_as_tree'
|
6
7
|
|
7
8
|
ActiveRecord::Base.send :extend, ClosureTree::ActsAsTree
|
8
9
|
end
|
@@ -216,20 +216,23 @@ module ClosureTree
|
|
216
216
|
|
217
217
|
# Find a child node whose +ancestry_path+ minus self.ancestry_path is +path+
|
218
218
|
def find_or_create_by_path(path, attributes = {})
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
219
|
+
with_advisory_lock("closure_tree") do
|
220
|
+
transaction do
|
221
|
+
subpath = path.is_a?(Enumerable) ? path.dup : [path]
|
222
|
+
child_name = subpath.shift
|
223
|
+
return self unless child_name
|
224
|
+
child = transaction do
|
225
|
+
attrs = {name_sym => child_name}
|
226
|
+
attrs[:type] = self.type if ct_subclass? && ct_has_type?
|
227
|
+
self.children.where(attrs).first || begin
|
228
|
+
child = self.class.new(attributes.merge(attrs))
|
229
|
+
self.children << child
|
230
|
+
child
|
231
|
+
end
|
232
|
+
end
|
233
|
+
child.find_or_create_by_path(subpath, attributes)
|
230
234
|
end
|
231
235
|
end
|
232
|
-
child.find_or_create_by_path(subpath, attributes)
|
233
236
|
end
|
234
237
|
|
235
238
|
def find_all_by_generation(generation_level)
|
@@ -284,18 +287,22 @@ module ClosureTree
|
|
284
287
|
end
|
285
288
|
|
286
289
|
def rebuild!
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
290
|
+
with_advisory_lock("closure_tree") do
|
291
|
+
transaction do
|
292
|
+
delete_hierarchy_references unless @was_new_record
|
293
|
+
hierarchy_class.create!(:ancestor => self, :descendant => self, :generations => 0)
|
294
|
+
unless root?
|
295
|
+
connection.execute <<-SQL
|
291
296
|
INSERT INTO #{quoted_hierarchy_table_name}
|
292
297
|
(ancestor_id, descendant_id, generations)
|
293
298
|
SELECT x.ancestor_id, #{ct_quote(id)}, x.generations + 1
|
294
299
|
FROM #{quoted_hierarchy_table_name} x
|
295
300
|
WHERE x.descendant_id = #{ct_quote(self.ct_parent_id)}
|
296
|
-
|
301
|
+
SQL
|
302
|
+
end
|
303
|
+
children.each { |c| c.rebuild! }
|
304
|
+
end
|
297
305
|
end
|
298
|
-
children.each { |c| c.rebuild! }
|
299
306
|
end
|
300
307
|
|
301
308
|
def ct_before_destroy
|
@@ -351,7 +358,7 @@ module ClosureTree
|
|
351
358
|
# Rebuilds the hierarchy table based on the parent_id column in the database.
|
352
359
|
# Note that the hierarchy table will be truncated.
|
353
360
|
def rebuild!
|
354
|
-
with_advisory_lock("closure_tree
|
361
|
+
with_advisory_lock("closure_tree") do
|
355
362
|
transaction do
|
356
363
|
hierarchy_class.delete_all # not destroy_all -- we just want a simple truncate.
|
357
364
|
roots.each { |n| n.send(:rebuild!) } # roots just uses the parent_id column, so this is safe.
|
@@ -371,15 +378,15 @@ module ClosureTree
|
|
371
378
|
def find_or_create_by_path(path, attributes = {})
|
372
379
|
subpath = path.dup
|
373
380
|
root_name = subpath.shift
|
374
|
-
|
381
|
+
with_advisory_lock("closure_tree") do
|
375
382
|
transaction do
|
376
383
|
# shenanigans because find_or_create can't infer we want the same class as this:
|
377
384
|
# Note that roots will already be constrained to this subclass (in the case of polymorphism):
|
378
|
-
roots.where(name_sym => root_name).first
|
379
|
-
|
385
|
+
root = roots.where(name_sym => root_name).first
|
386
|
+
root ||= create!(attributes.merge(name_sym => root_name))
|
387
|
+
root.find_or_create_by_path(subpath, attributes)
|
380
388
|
end
|
381
389
|
end
|
382
|
-
root.find_or_create_by_path(subpath, attributes)
|
383
390
|
end
|
384
391
|
|
385
392
|
def hash_tree_scope(limit_depth = nil)
|
@@ -566,29 +573,34 @@ module ClosureTree
|
|
566
573
|
|
567
574
|
def add_sibling(sibling_node, use_update_all = true, add_after = true)
|
568
575
|
fail "can't add self as sibling" if self == sibling_node
|
569
|
-
# issue
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
576
|
+
# issue 40: we need to lock the parent to prevent deadlocks on parallel sibling additions
|
577
|
+
with_advisory_lock("closure_tree") do
|
578
|
+
transaction do
|
579
|
+
# issue 18: we need to set the order_value explicitly so subsequent orders will work.
|
580
|
+
update_attribute(:order_value, 0) if self.order_value.nil?
|
581
|
+
sibling_node.order_value = self.order_value.to_i + (add_after ? 1 : -1)
|
582
|
+
# We need to incr the before_siblings to make room for sibling_node:
|
583
|
+
if use_update_all
|
584
|
+
col = quoted_order_column(false)
|
585
|
+
# issue 21: we have to use the base class, so STI doesn't get in the way of only updating the child class instances:
|
586
|
+
ct_base_class.update_all(
|
587
|
+
["#{col} = #{col} #{add_after ? '+' : '-'} 1", "updated_at = now()"],
|
588
|
+
["#{quoted_parent_column_name} = ? AND #{col} #{add_after ? '>=' : '<='} ?",
|
589
|
+
ct_parent_id,
|
590
|
+
sibling_node.order_value])
|
591
|
+
else
|
592
|
+
last_value = sibling_node.order_value.to_i
|
593
|
+
(add_after ? siblings_after : siblings_before.reverse).each do |ea|
|
594
|
+
last_value += (add_after ? 1 : -1)
|
595
|
+
ea.order_value = last_value
|
596
|
+
ea.save!
|
597
|
+
end
|
598
|
+
end
|
599
|
+
sibling_node.parent = self.parent
|
600
|
+
sibling_node.save!
|
601
|
+
sibling_node.reload
|
587
602
|
end
|
588
603
|
end
|
589
|
-
sibling_node.parent = self.parent
|
590
|
-
sibling_node.save!
|
591
|
-
sibling_node.reload # <- because siblings_before and siblings_after will have changed.
|
592
604
|
end
|
593
605
|
end
|
594
|
-
end
|
606
|
+
end
|
data/lib/closure_tree/version.rb
CHANGED
data/spec/db/database.yml
CHANGED
data/spec/db/schema.rb
CHANGED
@@ -11,34 +11,34 @@ end
|
|
11
11
|
|
12
12
|
ActiveRecord::Schema.define(:version => 0) do
|
13
13
|
|
14
|
-
create_table "
|
15
|
-
t.string "id"
|
14
|
+
create_table "tags", :force => true do |t|
|
16
15
|
t.string "name"
|
17
|
-
t.string "
|
16
|
+
t.string "title"
|
17
|
+
t.integer "parent_id"
|
18
|
+
t.integer "sort_order"
|
18
19
|
t.datetime "created_at"
|
19
20
|
t.datetime "updated_at"
|
20
21
|
end
|
21
22
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
t.string "ancestor_id", :null => false
|
26
|
-
t.string "descendant_id", :null => false
|
23
|
+
create_table "tag_hierarchies", :id => false, :force => true do |t|
|
24
|
+
t.integer "ancestor_id", :null => false
|
25
|
+
t.integer "descendant_id", :null => false
|
27
26
|
t.integer "generations", :null => false
|
28
27
|
end
|
29
28
|
|
30
|
-
create_table "
|
29
|
+
create_table "tags_uuid", :id => false, :force => true do |t|
|
30
|
+
t.string "id", :unique => true
|
31
31
|
t.string "name"
|
32
32
|
t.string "title"
|
33
|
-
t.
|
33
|
+
t.string "parent_id"
|
34
34
|
t.integer "sort_order"
|
35
35
|
t.datetime "created_at"
|
36
36
|
t.datetime "updated_at"
|
37
37
|
end
|
38
38
|
|
39
|
-
create_table "
|
40
|
-
t.
|
41
|
-
t.
|
39
|
+
create_table "tag_hierarchies_uuid", :id => false, :force => true do |t|
|
40
|
+
t.string "ancestor_id", :null => false
|
41
|
+
t.string "descendant_id", :null => false
|
42
42
|
t.integer "generations", :null => false
|
43
43
|
end
|
44
44
|
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
describe "threadhot" do
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
LabelHierarchy.delete_all
|
8
|
+
Label.delete_all
|
9
|
+
@iterations = 5
|
10
|
+
@workers = 8
|
11
|
+
end
|
12
|
+
|
13
|
+
def prepend_sibling_at_even_second(run_at)
|
14
|
+
ActiveRecord::Base.connection.reconnect!
|
15
|
+
sibling = Label.new(:name => SecureRandom.hex(10))
|
16
|
+
target = Label.find(@target.id)
|
17
|
+
sleep(run_at - Time.now.to_f)
|
18
|
+
target.prepend_sibling sibling
|
19
|
+
end
|
20
|
+
|
21
|
+
def run_workers
|
22
|
+
start_time = Time.now.to_i + 2
|
23
|
+
@times = @iterations.times.collect { |ea| start_time + (ea * 2) }
|
24
|
+
@names = @times.collect { |ea| ea.to_s }
|
25
|
+
@threads = @workers.times.collect do
|
26
|
+
Thread.new do
|
27
|
+
@times.each { |ea| prepend_sibling_at_even_second(ea) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@threads.each { |ea| ea.join }
|
31
|
+
end
|
32
|
+
|
33
|
+
it "prepend_sibling on a non-root node doesn't cause deadlocks" do
|
34
|
+
@target = Label.find_or_create_by_path %w(root parent)
|
35
|
+
run_workers
|
36
|
+
children = Label.roots
|
37
|
+
uniq_sort_orders = children.collect { |ea| ea.sort_order }.uniq
|
38
|
+
children.size.should == uniq_sort_orders.size
|
39
|
+
|
40
|
+
# The only non-root node should be "root":
|
41
|
+
Label.all.select { |ea| ea.root? }.should == [@target.parent]
|
42
|
+
end
|
43
|
+
|
44
|
+
# SQLite doesn't like parallelism, and Rails 3.0 and 3.1 have known threading issues. SKIP.
|
45
|
+
end if ((ENV["DB"] != "sqlite3") && (ActiveRecord::VERSION::STRING =~ /^3.2/))
|
data/spec/parallel_spec.rb
CHANGED
@@ -56,4 +56,4 @@ describe "threadhot" do
|
|
56
56
|
end
|
57
57
|
|
58
58
|
# SQLite doesn't like parallelism, and Rails 3.0 and 3.1 have known threading issues. SKIP.
|
59
|
-
end if ((ENV["DB"] != "
|
59
|
+
end if ((ENV["DB"] != "sqlite") && (ActiveRecord::VERSION::STRING =~ /^3.2/))
|
data/spec/spec_helper.rb
CHANGED
@@ -7,11 +7,7 @@ require 'bundler/setup'
|
|
7
7
|
require 'rspec'
|
8
8
|
require 'logger'
|
9
9
|
|
10
|
-
require 'active_support'
|
11
|
-
require 'active_model'
|
12
|
-
require 'active_record'
|
13
10
|
require 'action_controller' # rspec-rails needs this :(
|
14
|
-
require 'with_advisory_lock'
|
15
11
|
require 'closure_tree'
|
16
12
|
require 'tmpdir'
|
17
13
|
|
@@ -58,4 +54,4 @@ RSpec.configure do |config|
|
|
58
54
|
config.after(:all) do
|
59
55
|
FileUtils.remove_entry_secure ENV['FLOCK_DIR']
|
60
56
|
end
|
61
|
-
end
|
57
|
+
end
|
data/spec/support/models.rb
CHANGED
@@ -1,15 +1,5 @@
|
|
1
1
|
require 'uuidtools'
|
2
2
|
|
3
|
-
class Node < ActiveRecord::Base
|
4
|
-
acts_as_tree :dependent => :destroy
|
5
|
-
before_create :generate_uuid
|
6
|
-
attr_accessible :name
|
7
|
-
|
8
|
-
def generate_uuid
|
9
|
-
self.id = UUIDTools::UUID.random_create.to_s
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
3
|
class Tag < ActiveRecord::Base
|
14
4
|
acts_as_tree :dependent => :destroy, :order => "name"
|
15
5
|
before_destroy :add_destroyed_tag
|
data/spec/tag_spec.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
shared_examples_for Tag do
|
3
|
+
shared_examples_for "Tag (1)" do
|
4
4
|
|
5
5
|
it "has correct accessible_attributes" do
|
6
6
|
Tag.accessible_attributes.to_a.should =~ %w(parent name)
|
@@ -146,8 +146,10 @@ shared_examples_for Tag do
|
|
146
146
|
end
|
147
147
|
|
148
148
|
end
|
149
|
+
end
|
149
150
|
|
150
|
-
|
151
|
+
shared_examples_for "Tag (2)" do
|
152
|
+
describe "Tag (2)" do
|
151
153
|
|
152
154
|
fixtures :tags
|
153
155
|
|
@@ -367,7 +369,8 @@ describe Tag do
|
|
367
369
|
Tag.ancestors.should_not include(ActiveModel::ForbiddenAttributesProtection)
|
368
370
|
end
|
369
371
|
end
|
370
|
-
it_behaves_like Tag
|
372
|
+
it_behaves_like "Tag (1)"
|
373
|
+
it_behaves_like "Tag (2)"
|
371
374
|
end
|
372
375
|
|
373
376
|
describe "Tag with AR whitelisted attributes enabled" do
|
@@ -380,7 +383,8 @@ describe "Tag with AR whitelisted attributes enabled" do
|
|
380
383
|
Tag.ancestors.should_not include(ActiveModel::ForbiddenAttributesProtection)
|
381
384
|
end
|
382
385
|
end
|
383
|
-
it_behaves_like Tag
|
386
|
+
it_behaves_like "Tag (1)"
|
387
|
+
it_behaves_like "Tag (2)"
|
384
388
|
end
|
385
389
|
|
386
390
|
# This has to be the last one, because we include strong parameters into Tag
|
@@ -391,6 +395,29 @@ describe "Tag with strong parameters" do
|
|
391
395
|
include ActiveModel::ForbiddenAttributesProtection
|
392
396
|
end
|
393
397
|
end
|
394
|
-
it_behaves_like Tag
|
398
|
+
it_behaves_like "Tag (1)"
|
399
|
+
it_behaves_like "Tag (2)"
|
395
400
|
end
|
396
401
|
|
402
|
+
describe "Tag with UUID" do
|
403
|
+
before(:all) do
|
404
|
+
# Change tables
|
405
|
+
Tag.table_name = Tag.table_name.gsub('tags', 'tags_uuid')
|
406
|
+
Tag.reset_column_information
|
407
|
+
TagHierarchy.table_name = TagHierarchy.table_name.gsub('tag_hierarchies', 'tag_hierarchies_uuid')
|
408
|
+
TagHierarchy.reset_column_information
|
409
|
+
|
410
|
+
# We have to reset a few other caches
|
411
|
+
Tag.closure_tree_options[:hierarchy_table_name] = 'tag_hierarchies_uuid'
|
412
|
+
Tag.reflections.each do |key, ref|
|
413
|
+
ref.instance_variable_set('@table_name', nil)
|
414
|
+
ref.instance_variable_set('@quoted_table_name', nil)
|
415
|
+
ref.options[:order].sub! 'tag_hierarchies', 'tag_hierarchies_uuid' if ref.options[:order]
|
416
|
+
end
|
417
|
+
|
418
|
+
# Add ID
|
419
|
+
Tag.before_create { self.id = UUIDTools::UUID.random_create.to_s }
|
420
|
+
end
|
421
|
+
|
422
|
+
it_behaves_like "Tag (1)"
|
423
|
+
end
|
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.7.
|
4
|
+
version: 3.7.3
|
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: 2013-02-
|
12
|
+
date: 2013-02-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -34,7 +34,7 @@ dependencies:
|
|
34
34
|
requirements:
|
35
35
|
- - ! '>='
|
36
36
|
- !ruby/object:Gem::Version
|
37
|
-
version:
|
37
|
+
version: 0.0.6
|
38
38
|
type: :runtime
|
39
39
|
prerelease: false
|
40
40
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -42,7 +42,7 @@ dependencies:
|
|
42
42
|
requirements:
|
43
43
|
- - ! '>='
|
44
44
|
- !ruby/object:Gem::Version
|
45
|
-
version:
|
45
|
+
version: 0.0.6
|
46
46
|
- !ruby/object:Gem::Dependency
|
47
47
|
name: rake
|
48
48
|
requirement: !ruby/object:Gem::Requirement
|
@@ -223,7 +223,7 @@ files:
|
|
223
223
|
- spec/fixtures/tags.yml
|
224
224
|
- spec/hash_tree_spec.rb
|
225
225
|
- spec/label_spec.rb
|
226
|
-
- spec/
|
226
|
+
- spec/parallel_prepend_sibling_spec.rb
|
227
227
|
- spec/parallel_spec.rb
|
228
228
|
- spec/spec_helper.rb
|
229
229
|
- spec/support/models.rb
|
@@ -243,7 +243,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
243
243
|
version: '0'
|
244
244
|
segments:
|
245
245
|
- 0
|
246
|
-
hash: -
|
246
|
+
hash: -1398803141248485966
|
247
247
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
248
248
|
none: false
|
249
249
|
requirements:
|
@@ -252,7 +252,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
252
252
|
version: '0'
|
253
253
|
segments:
|
254
254
|
- 0
|
255
|
-
hash: -
|
255
|
+
hash: -1398803141248485966
|
256
256
|
requirements: []
|
257
257
|
rubyforge_project:
|
258
258
|
rubygems_version: 1.8.23
|
@@ -267,7 +267,7 @@ test_files:
|
|
267
267
|
- spec/fixtures/tags.yml
|
268
268
|
- spec/hash_tree_spec.rb
|
269
269
|
- spec/label_spec.rb
|
270
|
-
- spec/
|
270
|
+
- spec/parallel_prepend_sibling_spec.rb
|
271
271
|
- spec/parallel_spec.rb
|
272
272
|
- spec/spec_helper.rb
|
273
273
|
- spec/support/models.rb
|
data/spec/node_spec.rb
DELETED
@@ -1,148 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
shared_examples_for Node do
|
4
|
-
|
5
|
-
it "has correct accessible_attributes" do
|
6
|
-
Node.accessible_attributes.to_a.should =~ %w(parent name)
|
7
|
-
end
|
8
|
-
|
9
|
-
describe "empty db" do
|
10
|
-
|
11
|
-
def nuke_db
|
12
|
-
NodeHierarchy.delete_all
|
13
|
-
Node.delete_all
|
14
|
-
end
|
15
|
-
|
16
|
-
before :each do
|
17
|
-
nuke_db
|
18
|
-
end
|
19
|
-
|
20
|
-
context "empty db" do
|
21
|
-
it "should return no entities" do
|
22
|
-
Node.roots.should be_empty
|
23
|
-
Node.leaves.should be_empty
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
context "1 node db" do
|
28
|
-
it "should return the only entity as a root and leaf" do
|
29
|
-
a = Node.create!(:name => "a")
|
30
|
-
Node.roots.should == [a]
|
31
|
-
Node.leaves.should == [a]
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
context "2 node db" do
|
36
|
-
it "should return a simple root and leaf" do
|
37
|
-
root = Node.create!(:name => "root")
|
38
|
-
leaf = root.add_child(Node.create!(:name => "leaf"))
|
39
|
-
Node.roots.should == [root]
|
40
|
-
Node.leaves.should == [leaf]
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
context "3 node collection.create db" do
|
45
|
-
before :each do
|
46
|
-
@root = Node.create! :name => "root"
|
47
|
-
@mid = @root.children.create! :name => "mid"
|
48
|
-
@leaf = @mid.children.create! :name => "leaf"
|
49
|
-
end
|
50
|
-
|
51
|
-
it "should create all nodes" do
|
52
|
-
Node.all.should =~ [@root, @mid, @leaf]
|
53
|
-
end
|
54
|
-
|
55
|
-
it "should return a root and leaf without middle node" do
|
56
|
-
Node.roots.should == [@root]
|
57
|
-
Node.leaves.should == [@leaf]
|
58
|
-
end
|
59
|
-
|
60
|
-
it "should delete leaves" do
|
61
|
-
Node.leaves.destroy_all
|
62
|
-
Node.roots.should == [@root] # untouched
|
63
|
-
Node.leaves.should == [@mid]
|
64
|
-
end
|
65
|
-
|
66
|
-
it "should delete everything if you delete the roots" do
|
67
|
-
Node.roots.destroy_all
|
68
|
-
Node.all.should be_empty
|
69
|
-
Node.roots.should be_empty
|
70
|
-
Node.leaves.should be_empty
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
context "3 node explicit_create db" do
|
75
|
-
before :each do
|
76
|
-
@root = Node.create!(:name => "root")
|
77
|
-
@mid = @root.add_child(Node.create!(:name => "mid"))
|
78
|
-
@leaf = @mid.add_child(Node.create!(:name => "leaf"))
|
79
|
-
end
|
80
|
-
|
81
|
-
it "should create all nodes" do
|
82
|
-
Node.all.should =~ [@root, @mid, @leaf]
|
83
|
-
end
|
84
|
-
|
85
|
-
it "should return a root and leaf without middle node" do
|
86
|
-
Node.roots.should == [@root]
|
87
|
-
Node.leaves.should == [@leaf]
|
88
|
-
end
|
89
|
-
|
90
|
-
it "should prevent parental loops from torso" do
|
91
|
-
@mid.children << @root
|
92
|
-
@root.valid?.should be_false
|
93
|
-
@mid.reload.children.should == [@leaf]
|
94
|
-
end
|
95
|
-
|
96
|
-
it "should prevent parental loops from toes" do
|
97
|
-
@leaf.children << @root
|
98
|
-
@root.valid?.should be_false
|
99
|
-
@leaf.reload.children.should be_empty
|
100
|
-
end
|
101
|
-
|
102
|
-
it "should support re-parenting" do
|
103
|
-
@root.children << @leaf
|
104
|
-
Node.leaves.should =~ [@leaf, @mid]
|
105
|
-
end
|
106
|
-
|
107
|
-
it "cleans up hierarchy references for leaves" do
|
108
|
-
@leaf.destroy
|
109
|
-
NodeHierarchy.find_all_by_ancestor_id(@leaf.id).should be_empty
|
110
|
-
NodeHierarchy.find_all_by_descendant_id(@leaf.id).should be_empty
|
111
|
-
end
|
112
|
-
|
113
|
-
it "cleans up hierarchy references" do
|
114
|
-
@mid.destroy
|
115
|
-
NodeHierarchy.find_all_by_ancestor_id(@mid.id).should be_empty
|
116
|
-
NodeHierarchy.find_all_by_descendant_id(@mid.id).should be_empty
|
117
|
-
@root.reload.should be_root
|
118
|
-
root_hiers = @root.ancestor_hierarchies.to_a
|
119
|
-
root_hiers.size.should == 1
|
120
|
-
NodeHierarchy.find_all_by_ancestor_id(@root.id).should == root_hiers
|
121
|
-
NodeHierarchy.find_all_by_descendant_id(@root.id).should == root_hiers
|
122
|
-
end
|
123
|
-
end
|
124
|
-
|
125
|
-
it "performs as the readme says it does" do
|
126
|
-
grandparent = Node.create(:name => 'Grandparent')
|
127
|
-
parent = grandparent.children.create(:name => 'Parent')
|
128
|
-
child1 = Node.create(:name => 'First Child', :parent => parent)
|
129
|
-
child2 = Node.new(:name => 'Second Child')
|
130
|
-
parent.children << child2
|
131
|
-
child3 = Node.new(:name => 'Third Child')
|
132
|
-
parent.add_child child3
|
133
|
-
grandparent.self_and_descendants.collect(&:name).should ==
|
134
|
-
["Grandparent", "Parent", "First Child", "Second Child", "Third Child"]
|
135
|
-
child1.ancestry_path.should ==
|
136
|
-
["Grandparent", "Parent", "First Child"]
|
137
|
-
child3.ancestry_path.should ==
|
138
|
-
["Grandparent", "Parent", "Third Child"]
|
139
|
-
d = Node.find_or_create_by_path %w(a b c d)
|
140
|
-
h = Node.find_or_create_by_path %w(e f g h)
|
141
|
-
e = h.root
|
142
|
-
d.add_child(e) # "d.children << e" would work too, of course
|
143
|
-
h.ancestry_path.should == %w(a b c d e f g h)
|
144
|
-
end
|
145
|
-
|
146
|
-
end
|
147
|
-
|
148
|
-
end
|