closure_tree 3.7.2 → 3.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +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
|