closure_tree 6.4.0 → 7.2.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 +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +19 -12
- data/Appraisals +75 -7
- data/CHANGELOG.md +92 -39
- data/Gemfile +0 -12
- data/README.md +67 -24
- data/_config.yml +1 -0
- data/closure_tree.gemspec +10 -7
- data/lib/closure_tree/finders.rb +32 -9
- data/lib/closure_tree/has_closure_tree.rb +4 -0
- data/lib/closure_tree/has_closure_tree_root.rb +4 -6
- data/lib/closure_tree/hash_tree_support.rb +4 -4
- data/lib/closure_tree/hierarchy_maintenance.rb +31 -11
- data/lib/closure_tree/model.rb +42 -16
- data/lib/closure_tree/numeric_deterministic_ordering.rb +20 -6
- data/lib/closure_tree/numeric_order_support.rb +7 -3
- data/lib/closure_tree/support.rb +18 -12
- data/lib/closure_tree/support_attributes.rb +10 -1
- data/lib/closure_tree/support_flags.rb +1 -4
- data/lib/closure_tree/version.rb +1 -1
- data/lib/generators/closure_tree/migration_generator.rb +8 -0
- data/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb +1 -1
- metadata +29 -75
- data/gemfiles/activerecord_4.2.gemfile +0 -19
- data/gemfiles/activerecord_5.0.gemfile +0 -19
- data/gemfiles/activerecord_5.0_foreigner.gemfile +0 -20
- data/gemfiles/activerecord_edge.gemfile +0 -20
- data/img/example.png +0 -0
- data/img/preorder.png +0 -0
- data/spec/cache_invalidation_spec.rb +0 -39
- data/spec/cuisine_type_spec.rb +0 -38
- data/spec/db/database.yml +0 -21
- data/spec/db/models.rb +0 -128
- data/spec/db/schema.rb +0 -166
- data/spec/fixtures/tags.yml +0 -98
- data/spec/generators/migration_generator_spec.rb +0 -48
- data/spec/has_closure_tree_root_spec.rb +0 -154
- data/spec/hierarchy_maintenance_spec.rb +0 -16
- data/spec/label_spec.rb +0 -554
- data/spec/matcher_spec.rb +0 -34
- data/spec/metal_spec.rb +0 -55
- data/spec/model_spec.rb +0 -9
- data/spec/namespace_type_spec.rb +0 -13
- data/spec/parallel_spec.rb +0 -159
- data/spec/spec_helper.rb +0 -24
- data/spec/support/database.rb +0 -52
- data/spec/support/database_cleaner.rb +0 -14
- data/spec/support/exceed_query_limit.rb +0 -18
- data/spec/support/hash_monkey_patch.rb +0 -13
- data/spec/support/query_counter.rb +0 -18
- data/spec/support/sqlite3_with_advisory_lock.rb +0 -10
- data/spec/support_spec.rb +0 -14
- data/spec/tag_examples.rb +0 -665
- data/spec/tag_spec.rb +0 -6
- data/spec/user_spec.rb +0 -174
- data/spec/uuid_tag_spec.rb +0 -6
data/spec/fixtures/tags.yml
DELETED
@@ -1,98 +0,0 @@
|
|
1
|
-
# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html
|
2
|
-
|
3
|
-
grandparent:
|
4
|
-
name: grandparent
|
5
|
-
title: Nonnie
|
6
|
-
|
7
|
-
parent:
|
8
|
-
name: parent
|
9
|
-
parent: grandparent
|
10
|
-
title: Mom
|
11
|
-
|
12
|
-
child:
|
13
|
-
name: child
|
14
|
-
parent: parent
|
15
|
-
title: Kid
|
16
|
-
|
17
|
-
people:
|
18
|
-
name: people
|
19
|
-
|
20
|
-
# people has no children
|
21
|
-
|
22
|
-
events:
|
23
|
-
name: events
|
24
|
-
|
25
|
-
# events has only one child
|
26
|
-
|
27
|
-
birthday:
|
28
|
-
name: birthday
|
29
|
-
parent: events
|
30
|
-
|
31
|
-
places:
|
32
|
-
name: places
|
33
|
-
|
34
|
-
# places has many children, with many depths
|
35
|
-
|
36
|
-
home:
|
37
|
-
name: home
|
38
|
-
parent: places
|
39
|
-
|
40
|
-
indoor:
|
41
|
-
name: indoor
|
42
|
-
parent: places
|
43
|
-
|
44
|
-
outdoor:
|
45
|
-
name: outdoor
|
46
|
-
parent: places
|
47
|
-
|
48
|
-
museum:
|
49
|
-
name: museum
|
50
|
-
parent: places
|
51
|
-
|
52
|
-
united_states:
|
53
|
-
name: united_states
|
54
|
-
parent: places
|
55
|
-
|
56
|
-
california:
|
57
|
-
name: california
|
58
|
-
parent: united_states
|
59
|
-
|
60
|
-
san_francisco:
|
61
|
-
name: san_francisco
|
62
|
-
parent: california
|
63
|
-
|
64
|
-
|
65
|
-
# Move and deletion test tree
|
66
|
-
|
67
|
-
a1:
|
68
|
-
name: a1
|
69
|
-
|
70
|
-
b1:
|
71
|
-
name: b1
|
72
|
-
parent: a1
|
73
|
-
|
74
|
-
b2:
|
75
|
-
name: b2
|
76
|
-
parent: a1
|
77
|
-
|
78
|
-
c1a:
|
79
|
-
name: c1a
|
80
|
-
parent: b1
|
81
|
-
sort_order: 2
|
82
|
-
|
83
|
-
c1b:
|
84
|
-
name: c1b
|
85
|
-
parent: b1
|
86
|
-
sort_order: 1
|
87
|
-
|
88
|
-
c2:
|
89
|
-
name: c2
|
90
|
-
parent: b2
|
91
|
-
|
92
|
-
d2:
|
93
|
-
name: d2
|
94
|
-
parent: c2
|
95
|
-
|
96
|
-
e2:
|
97
|
-
name: e2
|
98
|
-
parent: d2
|
@@ -1,48 +0,0 @@
|
|
1
|
-
# require 'spec_helper'
|
2
|
-
# require 'ammeter/init'
|
3
|
-
#
|
4
|
-
# # Generators are not automatically loaded by Rails
|
5
|
-
# require 'generators/closure_tree/migration_generator'
|
6
|
-
#
|
7
|
-
# RSpec.describe ClosureTree::Generators::MigrationGenerator, type: :generator do
|
8
|
-
# TMPDIR = Dir.mktmpdir
|
9
|
-
# # Tell generator where to put its output
|
10
|
-
# destination TMPDIR
|
11
|
-
# before { prepare_destination }
|
12
|
-
#
|
13
|
-
# describe 'generator output' do
|
14
|
-
# before { run_generator %w(tag) }
|
15
|
-
# subject { migration_file('db/migrate/create_tag_hierarchies.rb') }
|
16
|
-
# it { is_expected.to be_a_migration }
|
17
|
-
# it { is_expected.to contain(/t.integer :ancestor_id, null: false/) }
|
18
|
-
# it { is_expected.to contain(/t.integer :descendant_id, null: false/) }
|
19
|
-
# it { is_expected.to contain(/t.integer :generations, null: false/) }
|
20
|
-
# it { is_expected.to contain(/add_index :tag_hierarchies/) }
|
21
|
-
# end
|
22
|
-
#
|
23
|
-
# describe 'generator output with namespaced model' do
|
24
|
-
# before { run_generator %w(Namespace::Type) }
|
25
|
-
# subject { migration_file('db/migrate/create_namespace_type_hierarchies.rb') }
|
26
|
-
# it { is_expected.to be_a_migration }
|
27
|
-
# it { is_expected.to contain(/t.integer :ancestor_id, null: false/) }
|
28
|
-
# it { is_expected.to contain(/t.integer :descendant_id, null: false/) }
|
29
|
-
# it { is_expected.to contain(/t.integer :generations, null: false/) }
|
30
|
-
# it { is_expected.to contain(/add_index :namespace_type_hierarchies/) }
|
31
|
-
# end
|
32
|
-
#
|
33
|
-
# describe 'generator output with namespaced model with /' do
|
34
|
-
# before { run_generator %w(namespace/type) }
|
35
|
-
# subject { migration_file('db/migrate/create_namespace_type_hierarchies.rb') }
|
36
|
-
# it { is_expected.to be_a_migration }
|
37
|
-
# it { is_expected.to contain(/t.integer :ancestor_id, null: false/) }
|
38
|
-
# it { is_expected.to contain(/t.integer :descendant_id, null: false/) }
|
39
|
-
# it { is_expected.to contain(/t.integer :generations, null: false/) }
|
40
|
-
# it { is_expected.to contain(/add_index :namespace_type_hierarchies/) }
|
41
|
-
# end
|
42
|
-
#
|
43
|
-
# it 'should run all tasks in generator without errors' do
|
44
|
-
# gen = generator %w(tag)
|
45
|
-
# expect(gen).to receive :create_migration_file
|
46
|
-
# capture(:stdout) { gen.invoke_all }
|
47
|
-
# end
|
48
|
-
# end
|
@@ -1,154 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
describe "has_closure_tree_root" do
|
4
|
-
let!(:ct1) { ContractType.create!(name: "Type1") }
|
5
|
-
let!(:ct2) { ContractType.create!(name: "Type2") }
|
6
|
-
let!(:user1) { User.create!(email: "1@example.com", group_id: group.id) }
|
7
|
-
let!(:user2) { User.create!(email: "2@example.com", group_id: group.id) }
|
8
|
-
let!(:user3) { User.create!(email: "3@example.com", group_id: group.id) }
|
9
|
-
let!(:user4) { User.create!(email: "4@example.com", group_id: group.id) }
|
10
|
-
let!(:user5) { User.create!(email: "5@example.com", group_id: group.id) }
|
11
|
-
let!(:user6) { User.create!(email: "6@example.com", group_id: group.id) }
|
12
|
-
let!(:group_reloaded) { group.class.find(group.id) } # Ensures were starting fresh.
|
13
|
-
|
14
|
-
before do
|
15
|
-
# The tree (contract types in parens)
|
16
|
-
#
|
17
|
-
# U1(1)
|
18
|
-
# / \
|
19
|
-
# U2(1) U3(1&2)
|
20
|
-
# / / \
|
21
|
-
# U4(2) U5(1) U6(2)
|
22
|
-
|
23
|
-
user1.children << user2
|
24
|
-
user1.children << user3
|
25
|
-
user2.children << user4
|
26
|
-
user3.children << user5
|
27
|
-
user3.children << user6
|
28
|
-
|
29
|
-
user1.contracts.create!(title: "Contract 1", contract_type: ct1)
|
30
|
-
user2.contracts.create!(title: "Contract 2", contract_type: ct1)
|
31
|
-
user3.contracts.create!(title: "Contract 3", contract_type: ct1)
|
32
|
-
user3.contracts.create!(title: "Contract 4", contract_type: ct2)
|
33
|
-
user4.contracts.create!(title: "Contract 5", contract_type: ct2)
|
34
|
-
user5.contracts.create!(title: "Contract 6", contract_type: ct1)
|
35
|
-
user6.contracts.create!(title: "Contract 7", contract_type: ct2)
|
36
|
-
end
|
37
|
-
|
38
|
-
context "with basic config" do
|
39
|
-
let!(:group) { Group.create!(name: "TheGroup") }
|
40
|
-
|
41
|
-
it "loads all nodes in a constant number of queries" do
|
42
|
-
expect do
|
43
|
-
root = group_reloaded.root_user_including_tree
|
44
|
-
expect(root.children[0].email).to eq "2@example.com"
|
45
|
-
expect(root.children[0].parent.children[1].email).to eq "3@example.com"
|
46
|
-
end.to_not exceed_query_limit(2)
|
47
|
-
end
|
48
|
-
|
49
|
-
it "loads all nodes plus single association in a constant number of queries" do
|
50
|
-
expect do
|
51
|
-
root = group_reloaded.root_user_including_tree(:contracts)
|
52
|
-
expect(root.children[0].email).to eq "2@example.com"
|
53
|
-
expect(root.children[0].parent.children[1].email).to eq "3@example.com"
|
54
|
-
expect(root.children[0].children[0].contracts[0].user.
|
55
|
-
parent.parent.children[1].children[1].contracts[0].title).to eq "Contract 7"
|
56
|
-
end.to_not exceed_query_limit(3)
|
57
|
-
end
|
58
|
-
|
59
|
-
it "loads all nodes and associations in a constant number of queries" do
|
60
|
-
expect do
|
61
|
-
root = group_reloaded.root_user_including_tree(contracts: :contract_type)
|
62
|
-
expect(root.children[0].email).to eq "2@example.com"
|
63
|
-
expect(root.children[0].parent.children[1].email).to eq "3@example.com"
|
64
|
-
expect(root.children[1].contracts.map(&:contract_type).map(&:name)).to eq %w(Type1 Type2)
|
65
|
-
expect(root.children[1].children[0].contracts[0].contract_type.name).to eq "Type1"
|
66
|
-
expect(root.children[0].children[0].contracts[0].user.
|
67
|
-
parent.parent.children[1].children[1].contracts[0].contract_type.name).to eq "Type2"
|
68
|
-
end.to_not exceed_query_limit(4) # Without this feature, this is 15, and scales with number of nodes.
|
69
|
-
end
|
70
|
-
|
71
|
-
it "memoizes by assoc_map" do
|
72
|
-
group_reloaded.root_user_including_tree.email = "x"
|
73
|
-
expect(group_reloaded.root_user_including_tree.email).to eq "x"
|
74
|
-
group_reloaded.root_user_including_tree(contracts: :contract_type).email = "y"
|
75
|
-
expect(group_reloaded.root_user_including_tree(contracts: :contract_type).email).to eq "y"
|
76
|
-
expect(group_reloaded.root_user_including_tree.email).to eq "x"
|
77
|
-
end
|
78
|
-
|
79
|
-
it "doesn't memoize if true argument passed" do
|
80
|
-
group_reloaded.root_user_including_tree.email = "x"
|
81
|
-
expect(group_reloaded.root_user_including_tree(true).email).to eq "1@example.com"
|
82
|
-
group_reloaded.root_user_including_tree(contracts: :contract_type).email = "y"
|
83
|
-
expect(group_reloaded.root_user_including_tree(true, contracts: :contract_type).email).
|
84
|
-
to eq "1@example.com"
|
85
|
-
end
|
86
|
-
|
87
|
-
it "works if true passed on first call" do
|
88
|
-
expect(group_reloaded.root_user_including_tree(true).email).to eq "1@example.com"
|
89
|
-
end
|
90
|
-
|
91
|
-
it "eager loads inverse association to group" do
|
92
|
-
expect do
|
93
|
-
root = group_reloaded.root_user_including_tree
|
94
|
-
expect(root.group).to eq group
|
95
|
-
expect(root.children[0].group).to eq group
|
96
|
-
end.to_not exceed_query_limit(2)
|
97
|
-
end
|
98
|
-
|
99
|
-
it "works if eager load association map is not given" do
|
100
|
-
expect do
|
101
|
-
root = group_reloaded.root_user_including_tree
|
102
|
-
expect(root.children[0].email).to eq "2@example.com"
|
103
|
-
expect(root.children[0].parent.children[1].children[0].email).to eq "5@example.com"
|
104
|
-
end.to_not exceed_query_limit(2)
|
105
|
-
end
|
106
|
-
|
107
|
-
context "with no tree root" do
|
108
|
-
let(:group2) { Group.create!(name: "OtherGroup") }
|
109
|
-
|
110
|
-
it "should return nil" do
|
111
|
-
expect(group2.root_user_including_tree(contracts: :contract_type)).to be_nil
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
context "with multiple tree roots" do
|
116
|
-
let!(:other_root) { User.create!(email: "10@example.com", group_id: group.id) }
|
117
|
-
|
118
|
-
it "should error" do
|
119
|
-
expect do
|
120
|
-
root = group_reloaded.root_user_including_tree(contracts: :contract_type)
|
121
|
-
end.to raise_error(ClosureTree::MultipleRootError)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
context "with explicit class_name and foreign_key" do
|
127
|
-
let(:group) { Grouping.create!(name: "TheGrouping") }
|
128
|
-
|
129
|
-
it "should still work" do
|
130
|
-
root = group_reloaded.root_person_including_tree(contracts: :contract_type)
|
131
|
-
expect(root.children[0].email).to eq "2@example.com"
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
context "with bad class_name" do
|
136
|
-
let(:group) { UserSet.create!(name: "TheUserSet") }
|
137
|
-
|
138
|
-
it "should error" do
|
139
|
-
expect do
|
140
|
-
root = group_reloaded.root_user_including_tree(contracts: :contract_type)
|
141
|
-
end.to raise_error(NameError)
|
142
|
-
end
|
143
|
-
end
|
144
|
-
|
145
|
-
context "with bad foreign_key" do
|
146
|
-
let(:group) { Team.create!(name: "TheTeam") }
|
147
|
-
|
148
|
-
it "should error" do
|
149
|
-
expect do
|
150
|
-
root = group_reloaded.root_user_including_tree(contracts: :contract_type)
|
151
|
-
end.to raise_error(ActiveRecord::StatementInvalid)
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
@@ -1,16 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe ClosureTree::HierarchyMaintenance do
|
4
|
-
describe '.rebuild!' do
|
5
|
-
it 'rebuild tree' do
|
6
|
-
20.times do |counter|
|
7
|
-
Metal.create(:value => "Nitro-#{counter}", parent: Metal.all.sample)
|
8
|
-
end
|
9
|
-
hierarchy_count = MetalHierarchy.count
|
10
|
-
expect(hierarchy_count).to be > (20*2)-1 # shallowest-possible case, where all children use the first root
|
11
|
-
MetalHierarchy.delete_all
|
12
|
-
Metal.rebuild!
|
13
|
-
expect(MetalHierarchy.count).to eq(hierarchy_count)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
data/spec/label_spec.rb
DELETED
@@ -1,554 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
def create_label_tree
|
4
|
-
@d1 = Label.find_or_create_by_path %w(a1 b1 c1 d1)
|
5
|
-
@c1 = @d1.parent
|
6
|
-
@b1 = @c1.parent
|
7
|
-
@a1 = @b1.parent
|
8
|
-
@d2 = Label.find_or_create_by_path %w(a1 b1 c2 d2)
|
9
|
-
@c2 = @d2.parent
|
10
|
-
@d3 = Label.find_or_create_by_path %w(a2 b2 c3 d3)
|
11
|
-
@c3 = @d3.parent
|
12
|
-
@b2 = @c3.parent
|
13
|
-
@a2 = @b2.parent
|
14
|
-
Label.update_all("#{Label._ct.order_column} = id")
|
15
|
-
end
|
16
|
-
|
17
|
-
def create_preorder_tree(suffix = "", &block)
|
18
|
-
%w(
|
19
|
-
a/l/n/r
|
20
|
-
a/l/n/q
|
21
|
-
a/l/n/p
|
22
|
-
a/l/n/o
|
23
|
-
a/l/m
|
24
|
-
a/b/h/i/j/k
|
25
|
-
a/b/c/d/g
|
26
|
-
a/b/c/d/f
|
27
|
-
a/b/c/d/e
|
28
|
-
).shuffle.each { |ea| Label.find_or_create_by_path(ea.split('/').collect { |ea| "#{ea}#{suffix}" }) }
|
29
|
-
|
30
|
-
Label.roots.each_with_index do |root, root_idx|
|
31
|
-
root.order_value = root_idx
|
32
|
-
yield(root) if block_given?
|
33
|
-
root.save!
|
34
|
-
root.self_and_descendants.each do |ea|
|
35
|
-
ea.children.to_a.sort_by(&:name).each_with_index do |ea, idx|
|
36
|
-
ea.order_value = idx
|
37
|
-
yield(ea) if block_given?
|
38
|
-
ea.save!
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
describe Label do
|
45
|
-
context "destruction" do
|
46
|
-
it "properly destroys descendents created with find_or_create_by_path" do
|
47
|
-
c = Label.find_or_create_by_path %w(a b c)
|
48
|
-
b = c.parent
|
49
|
-
a = c.root
|
50
|
-
a.destroy
|
51
|
-
expect(Label.exists?(id: [a.id, b.id, c.id])).to be_falsey
|
52
|
-
end
|
53
|
-
|
54
|
-
it "properly destroys descendents created with add_child" do
|
55
|
-
a = Label.create(name: 'a')
|
56
|
-
b = a.add_child Label.new(name: 'b')
|
57
|
-
c = b.add_child Label.new(name: 'c')
|
58
|
-
a.destroy
|
59
|
-
expect(Label.exists?(a.id)).to be_falsey
|
60
|
-
expect(Label.exists?(b.id)).to be_falsey
|
61
|
-
expect(Label.exists?(c.id)).to be_falsey
|
62
|
-
end
|
63
|
-
|
64
|
-
it "properly destroys descendents created with <<" do
|
65
|
-
a = Label.create(name: 'a')
|
66
|
-
b = Label.new(name: 'b')
|
67
|
-
a.children << b
|
68
|
-
c = Label.new(name: 'c')
|
69
|
-
b.children << c
|
70
|
-
a.destroy
|
71
|
-
expect(Label.exists?(a.id)).to be_falsey
|
72
|
-
expect(Label.exists?(b.id)).to be_falsey
|
73
|
-
expect(Label.exists?(c.id)).to be_falsey
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
context "roots" do
|
78
|
-
it "sorts alphabetically" do
|
79
|
-
expected = (0..10).to_a
|
80
|
-
expected.shuffle.each do |ea|
|
81
|
-
Label.create! do |l|
|
82
|
-
l.name = "root #{ea}"
|
83
|
-
l.order_value = ea
|
84
|
-
end
|
85
|
-
end
|
86
|
-
expect(Label.roots.collect { |ea| ea.order_value }).to eq(expected)
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
context "Base Label class" do
|
91
|
-
it "should find or create by path" do
|
92
|
-
# class method:
|
93
|
-
c = Label.find_or_create_by_path(%w{grandparent parent child})
|
94
|
-
expect(c.ancestry_path).to eq(%w{grandparent parent child})
|
95
|
-
expect(c.name).to eq("child")
|
96
|
-
expect(c.parent.name).to eq("parent")
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
context "Parent/child inverse relationships" do
|
101
|
-
it "should associate both sides of the parent and child relationships" do
|
102
|
-
parent = Label.new(:name => 'parent')
|
103
|
-
child = parent.children.build(:name => 'child')
|
104
|
-
expect(parent).to be_root
|
105
|
-
expect(parent).not_to be_leaf
|
106
|
-
expect(child).not_to be_root
|
107
|
-
expect(child).to be_leaf
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
context "DateLabel" do
|
112
|
-
it "should find or create by path" do
|
113
|
-
date = DateLabel.find_or_create_by_path(%w{2011 November 23})
|
114
|
-
expect(date.ancestry_path).to eq(%w{2011 November 23})
|
115
|
-
date.self_and_ancestors.each { |ea| expect(ea.class).to eq(DateLabel) }
|
116
|
-
expect(date.name).to eq("23")
|
117
|
-
expect(date.parent.name).to eq("November")
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
context "DirectoryLabel" do
|
122
|
-
it "should find or create by path" do
|
123
|
-
dir = DirectoryLabel.find_or_create_by_path(%w{grandparent parent child})
|
124
|
-
expect(dir.ancestry_path).to eq(%w{grandparent parent child})
|
125
|
-
expect(dir.name).to eq("child")
|
126
|
-
expect(dir.parent.name).to eq("parent")
|
127
|
-
expect(dir.parent.parent.name).to eq("grandparent")
|
128
|
-
expect(dir.root.name).to eq("grandparent")
|
129
|
-
expect(dir.id).not_to eq(Label.find_or_create_by_path(%w{grandparent parent child}))
|
130
|
-
dir.self_and_ancestors.each { |ea| expect(ea.class).to eq(DirectoryLabel) }
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
context "Mixed class tree" do
|
135
|
-
context "preorder tree" do
|
136
|
-
before do
|
137
|
-
classes = [Label, DateLabel, DirectoryLabel, EventLabel]
|
138
|
-
create_preorder_tree do |ea|
|
139
|
-
ea.type = classes[ea.order_value % 4].to_s
|
140
|
-
end
|
141
|
-
end
|
142
|
-
it "finds roots with specific classes" do
|
143
|
-
expect(Label.roots).to eq(Label.where(:name => 'a').to_a)
|
144
|
-
expect(DirectoryLabel.roots).to be_empty
|
145
|
-
expect(EventLabel.roots).to be_empty
|
146
|
-
end
|
147
|
-
|
148
|
-
it "all is limited to subclasses" do
|
149
|
-
expect(DateLabel.all.map(&:name)).to match_array(%w(f h l n p))
|
150
|
-
expect(DirectoryLabel.all.map(&:name)).to match_array(%w(g q))
|
151
|
-
expect(EventLabel.all.map(&:name)).to eq(%w(r))
|
152
|
-
end
|
153
|
-
|
154
|
-
it "returns descendents regardless of subclass" do
|
155
|
-
expect(Label.root.descendants.map { |ea| ea.class.to_s }.uniq).to match_array(
|
156
|
-
%w(Label DateLabel DirectoryLabel EventLabel)
|
157
|
-
)
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
it "supports children << and add_child" do
|
162
|
-
a = EventLabel.create!(:name => "a")
|
163
|
-
b = DateLabel.new(:name => "b")
|
164
|
-
a.children << b
|
165
|
-
c = Label.new(:name => "c")
|
166
|
-
b.add_child(c)
|
167
|
-
|
168
|
-
expect(a.self_and_descendants.collect do |ea|
|
169
|
-
ea.class
|
170
|
-
end).to eq([EventLabel, DateLabel, Label])
|
171
|
-
|
172
|
-
expect(a.self_and_descendants.collect do |ea|
|
173
|
-
ea.name
|
174
|
-
end).to eq(%w(a b c))
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
context "find_all_by_generation" do
|
179
|
-
before :each do
|
180
|
-
create_label_tree
|
181
|
-
end
|
182
|
-
|
183
|
-
it "finds roots from the class method" do
|
184
|
-
expect(Label.find_all_by_generation(0).to_a).to eq([@a1, @a2])
|
185
|
-
end
|
186
|
-
|
187
|
-
it "finds roots from themselves" do
|
188
|
-
expect(@a1.find_all_by_generation(0).to_a).to eq([@a1])
|
189
|
-
end
|
190
|
-
|
191
|
-
it "finds itself for non-roots" do
|
192
|
-
expect(@b1.find_all_by_generation(0).to_a).to eq([@b1])
|
193
|
-
end
|
194
|
-
|
195
|
-
it "finds children for roots" do
|
196
|
-
expect(Label.find_all_by_generation(1).to_a).to eq([@b1, @b2])
|
197
|
-
end
|
198
|
-
|
199
|
-
it "finds children" do
|
200
|
-
expect(@a1.find_all_by_generation(1).to_a).to eq([@b1])
|
201
|
-
expect(@b1.find_all_by_generation(1).to_a).to eq([@c1, @c2])
|
202
|
-
end
|
203
|
-
|
204
|
-
it "finds grandchildren for roots" do
|
205
|
-
expect(Label.find_all_by_generation(2).to_a).to eq([@c1, @c2, @c3])
|
206
|
-
end
|
207
|
-
|
208
|
-
it "finds grandchildren" do
|
209
|
-
expect(@a1.find_all_by_generation(2).to_a).to eq([@c1, @c2])
|
210
|
-
expect(@b1.find_all_by_generation(2).to_a).to eq([@d1, @d2])
|
211
|
-
end
|
212
|
-
|
213
|
-
it "finds great-grandchildren for roots" do
|
214
|
-
expect(Label.find_all_by_generation(3).to_a).to eq([@d1, @d2, @d3])
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
context "loading through self_and_ scopes" do
|
219
|
-
before :each do
|
220
|
-
create_label_tree
|
221
|
-
end
|
222
|
-
|
223
|
-
it "self_and_descendants should result in one select" do
|
224
|
-
expect(count_queries do
|
225
|
-
a1_array = @a1.self_and_descendants
|
226
|
-
expect(a1_array.collect { |ea| ea.name }).to eq(%w(a1 b1 c1 c2 d1 d2))
|
227
|
-
end).to eq(1)
|
228
|
-
end
|
229
|
-
|
230
|
-
it "self_and_ancestors should result in one select" do
|
231
|
-
expect(count_queries do
|
232
|
-
d1_array = @d1.self_and_ancestors
|
233
|
-
expect(d1_array.collect { |ea| ea.name }).to eq(%w(d1 c1 b1 a1))
|
234
|
-
end).to eq(1)
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
context "deterministically orders with polymorphic siblings" do
|
239
|
-
before :each do
|
240
|
-
@parent = Label.create!(:name => 'parent')
|
241
|
-
@a, @b, @c, @d, @e, @f = ('a'..'f').map { |ea| EventLabel.new(:name => ea) }
|
242
|
-
@parent.children << @a
|
243
|
-
@a.append_sibling(@b)
|
244
|
-
@b.append_sibling(@c)
|
245
|
-
@c.append_sibling(@d)
|
246
|
-
@parent.append_sibling(@e)
|
247
|
-
@e.append_sibling(@f)
|
248
|
-
end
|
249
|
-
|
250
|
-
def name_and_order(enum)
|
251
|
-
enum.map { |ea| [ea.name, ea.order_value] }
|
252
|
-
end
|
253
|
-
|
254
|
-
def children_name_and_order
|
255
|
-
name_and_order(@parent.children.reload)
|
256
|
-
end
|
257
|
-
|
258
|
-
def roots_name_and_order
|
259
|
-
name_and_order(Label.roots)
|
260
|
-
end
|
261
|
-
|
262
|
-
it 'order_values properly' do
|
263
|
-
expect(children_name_and_order).to eq([['a', 0], ['b', 1], ['c', 2], ['d', 3]])
|
264
|
-
end
|
265
|
-
|
266
|
-
it 'when inserted before' do
|
267
|
-
@b.append_sibling(@a)
|
268
|
-
expect(children_name_and_order).to eq([['b', 0], ['a', 1], ['c', 2], ['d', 3]])
|
269
|
-
end
|
270
|
-
|
271
|
-
it 'when inserted after' do
|
272
|
-
@a.append_sibling(@c)
|
273
|
-
expect(children_name_and_order).to eq([['a', 0], ['c', 1], ['b', 2], ['d', 3]])
|
274
|
-
end
|
275
|
-
|
276
|
-
it 'when inserted before the first' do
|
277
|
-
@a.prepend_sibling(@d)
|
278
|
-
expect(children_name_and_order).to eq([['d', 0], ['a', 1], ['b', 2], ['c', 3]])
|
279
|
-
end
|
280
|
-
|
281
|
-
it 'when inserted after the last' do
|
282
|
-
@d.append_sibling(@b)
|
283
|
-
expect(children_name_and_order).to eq([['a', 0], ['c', 1], ['d', 2], ['b', 3]])
|
284
|
-
end
|
285
|
-
|
286
|
-
it 'prepends to root nodes' do
|
287
|
-
@parent.prepend_sibling(@f)
|
288
|
-
expect(roots_name_and_order).to eq([['f', 0], ['parent', 1], ['e', 2]])
|
289
|
-
end
|
290
|
-
end
|
291
|
-
|
292
|
-
describe 'code in the readme' do
|
293
|
-
it 'creates STI label hierarchies' do
|
294
|
-
child = Label.find_or_create_by_path([
|
295
|
-
{type: 'DateLabel', name: '2014'},
|
296
|
-
{type: 'DateLabel', name: 'August'},
|
297
|
-
{type: 'DateLabel', name: '5'},
|
298
|
-
{type: 'EventLabel', name: 'Visit the Getty Center'}
|
299
|
-
])
|
300
|
-
expect(child).to be_a(EventLabel)
|
301
|
-
expect(child.name).to eq('Visit the Getty Center')
|
302
|
-
expect(child.ancestors.map(&:name)).to eq(%w(5 August 2014))
|
303
|
-
expect(child.ancestors.map(&:class)).to eq([DateLabel, DateLabel, DateLabel])
|
304
|
-
end
|
305
|
-
|
306
|
-
it 'appends and prepends siblings' do
|
307
|
-
root = Label.create(name: 'root')
|
308
|
-
a = root.append_child(Label.new(name: 'a'))
|
309
|
-
b = Label.create(name: 'b')
|
310
|
-
c = Label.create(name: 'c')
|
311
|
-
|
312
|
-
a.append_sibling(b)
|
313
|
-
expect(a.self_and_siblings.collect(&:name)).to eq(%w(a b))
|
314
|
-
expect(root.reload.children.collect(&:name)).to eq(%w(a b))
|
315
|
-
expect(root.children.collect(&:order_value)).to eq([0, 1])
|
316
|
-
|
317
|
-
a.prepend_sibling(b)
|
318
|
-
expect(a.self_and_siblings.collect(&:name)).to eq(%w(b a))
|
319
|
-
expect(root.reload.children.collect(&:name)).to eq(%w(b a))
|
320
|
-
expect(root.children.collect(&:order_value)).to eq([0, 1])
|
321
|
-
|
322
|
-
a.append_sibling(c)
|
323
|
-
expect(a.self_and_siblings.collect(&:name)).to eq(%w(b a c))
|
324
|
-
expect(root.reload.children.collect(&:name)).to eq(%w(b a c))
|
325
|
-
expect(root.children.collect(&:order_value)).to eq([0, 1, 2])
|
326
|
-
|
327
|
-
# We need to reload b because it was updated by a.append_sibling(c)
|
328
|
-
b.reload.append_sibling(c)
|
329
|
-
expect(root.reload.children.collect(&:name)).to eq(%w(b c a))
|
330
|
-
expect(root.children.collect(&:order_value)).to eq([0, 1, 2])
|
331
|
-
|
332
|
-
# We need to reload a because it was updated by b.append_sibling(c)
|
333
|
-
d = a.reload.append_sibling(Label.new(:name => "d"))
|
334
|
-
expect(d.self_and_siblings.collect(&:name)).to eq(%w(b c a d))
|
335
|
-
expect(d.self_and_siblings.collect(&:order_value)).to eq([0, 1, 2, 3])
|
336
|
-
end
|
337
|
-
end
|
338
|
-
|
339
|
-
# https://github.com/mceachen/closure_tree/issues/84
|
340
|
-
it "properly appends children with <<" do
|
341
|
-
root = Label.create(:name => "root")
|
342
|
-
a = Label.create(:name => "a", :parent => root)
|
343
|
-
b = Label.create(:name => "b", :parent => root)
|
344
|
-
expect(a.order_value).to eq(0)
|
345
|
-
expect(b.order_value).to eq(1)
|
346
|
-
#c = Label.create(:name => "c")
|
347
|
-
|
348
|
-
# should the order_value for roots be set?
|
349
|
-
expect(root.order_value).not_to be_nil
|
350
|
-
expect(root.order_value).to eq(0)
|
351
|
-
|
352
|
-
# order_value should never be nil on a child.
|
353
|
-
expect(a.order_value).not_to be_nil
|
354
|
-
expect(a.order_value).to eq(0)
|
355
|
-
# Add a child to root at end of children.
|
356
|
-
root.children << b
|
357
|
-
expect(b.parent).to eq(root)
|
358
|
-
expect(a.self_and_siblings.collect(&:name)).to eq(%w(a b))
|
359
|
-
expect(root.reload.children.collect(&:name)).to eq(%w(a b))
|
360
|
-
expect(root.children.collect(&:order_value)).to eq([0, 1])
|
361
|
-
end
|
362
|
-
|
363
|
-
context "#add_sibling" do
|
364
|
-
it "should move a node before another node which has an uninitialized order_value" do
|
365
|
-
f = Label.find_or_create_by_path %w(a b c d e fa)
|
366
|
-
f0 = f.prepend_sibling(Label.new(:name => "fb")) # < not alpha sort, so name shouldn't matter
|
367
|
-
expect(f0.ancestry_path).to eq(%w(a b c d e fb))
|
368
|
-
expect(f.siblings_before.to_a).to eq([f0])
|
369
|
-
expect(f0.siblings_before).to be_empty
|
370
|
-
expect(f0.siblings_after).to eq([f])
|
371
|
-
expect(f.siblings_after).to be_empty
|
372
|
-
expect(f0.self_and_siblings).to eq([f0, f])
|
373
|
-
expect(f.self_and_siblings).to eq([f0, f])
|
374
|
-
end
|
375
|
-
|
376
|
-
let(:f1) { Label.find_or_create_by_path %w(a1 b1 c1 d1 e1 f1) }
|
377
|
-
|
378
|
-
it "should move a node to another tree" do
|
379
|
-
f2 = Label.find_or_create_by_path %w(a2 b2 c2 d2 e2 f2)
|
380
|
-
f1.add_sibling(f2)
|
381
|
-
expect(f2.ancestry_path).to eq(%w(a1 b1 c1 d1 e1 f2))
|
382
|
-
expect(f1.parent.reload.children).to eq([f1, f2])
|
383
|
-
end
|
384
|
-
|
385
|
-
it "should reorder old-parent siblings when a node moves to another tree" do
|
386
|
-
f2 = Label.find_or_create_by_path %w(a2 b2 c2 d2 e2 f2)
|
387
|
-
f3 = f2.prepend_sibling(Label.new(:name => "f3"))
|
388
|
-
f4 = f2.append_sibling(Label.new(:name => "f4"))
|
389
|
-
f1.add_sibling(f2)
|
390
|
-
expect(f1.self_and_siblings.collect(&:order_value)).to eq([0, 1])
|
391
|
-
expect(f3.self_and_siblings.collect(&:order_value)).to eq([0, 1])
|
392
|
-
expect(f1.self_and_siblings.collect(&:name)).to eq(%w(f1 f2))
|
393
|
-
expect(f3.self_and_siblings.collect(&:name)).to eq(%w(f3 f4))
|
394
|
-
end
|
395
|
-
end
|
396
|
-
|
397
|
-
context "order_value must be set" do
|
398
|
-
|
399
|
-
before do
|
400
|
-
@root = Label.create(name: 'root')
|
401
|
-
@a, @b, @c = %w(a b c).map { |n| @root.children.create(name: n) }
|
402
|
-
end
|
403
|
-
|
404
|
-
it 'should set order_value on roots' do
|
405
|
-
expect(@root.order_value).to eq(0)
|
406
|
-
end
|
407
|
-
|
408
|
-
it 'should set order_value with siblings' do
|
409
|
-
expect(@a.order_value).to eq(0)
|
410
|
-
expect(@b.order_value).to eq(1)
|
411
|
-
expect(@c.order_value).to eq(2)
|
412
|
-
end
|
413
|
-
|
414
|
-
it 'should reset order_value when a node is moved to another location' do
|
415
|
-
root2 = Label.create(name: 'root2')
|
416
|
-
root2.add_child @b
|
417
|
-
expect(@a.order_value).to eq(0)
|
418
|
-
expect(@b.order_value).to eq(0)
|
419
|
-
expect(@c.reload.order_value).to eq(1)
|
420
|
-
end
|
421
|
-
end
|
422
|
-
|
423
|
-
context "destructive reordering" do
|
424
|
-
before :each do
|
425
|
-
# to make sure order_value isn't affected by additional nodes:
|
426
|
-
create_preorder_tree
|
427
|
-
@root = Label.create(:name => 'root')
|
428
|
-
@a = @root.children.create!(:name => 'a')
|
429
|
-
@b = @a.append_sibling(Label.new(:name => 'b'))
|
430
|
-
@c = @b.append_sibling(Label.new(:name => 'c'))
|
431
|
-
end
|
432
|
-
context "doesn't create sort order gaps" do
|
433
|
-
it 'from head' do
|
434
|
-
@a.destroy
|
435
|
-
expect(@root.reload.children).to eq([@b, @c])
|
436
|
-
expect(@root.children.map { |ea| ea.order_value }).to eq([0, 1])
|
437
|
-
end
|
438
|
-
it 'from mid' do
|
439
|
-
@b.destroy
|
440
|
-
expect(@root.reload.children).to eq([@a, @c])
|
441
|
-
expect(@root.children.map { |ea| ea.order_value }).to eq([0, 1])
|
442
|
-
end
|
443
|
-
it 'from tail' do
|
444
|
-
@c.destroy
|
445
|
-
expect(@root.reload.children).to eq([@a, @b])
|
446
|
-
expect(@root.children.map { |ea| ea.order_value }).to eq([0, 1])
|
447
|
-
end
|
448
|
-
end
|
449
|
-
|
450
|
-
context 'add_sibling moves descendant nodes' do
|
451
|
-
let(:roots) { (0..10).map { |ea| Label.create(name: ea) } }
|
452
|
-
let(:first_root) { roots.first }
|
453
|
-
let(:last_root) { roots.last }
|
454
|
-
it 'should retain sort orders of descendants when moving to a new parent' do
|
455
|
-
expected_order = ('a'..'z').to_a.shuffle
|
456
|
-
expected_order.map { |ea| first_root.add_child(Label.new(name: ea)) }
|
457
|
-
actual_order = first_root.children.reload.pluck(:name)
|
458
|
-
expect(actual_order).to eq(expected_order)
|
459
|
-
last_root.append_child(first_root)
|
460
|
-
expect(last_root.self_and_descendants.pluck(:name)).to eq(%w(10 0) + expected_order)
|
461
|
-
end
|
462
|
-
|
463
|
-
it 'should retain sort orders of descendants when moving within the same new parent' do
|
464
|
-
path = ('a'..'z').to_a
|
465
|
-
z = first_root.find_or_create_by_path(path)
|
466
|
-
z_children_names = (100..150).to_a.shuffle.map { |ea| ea.to_s }
|
467
|
-
z_children_names.reverse.each { |ea| z.prepend_child(Label.new(name: ea)) }
|
468
|
-
expect(z.children.reload.pluck(:name)).to eq(z_children_names)
|
469
|
-
a = first_root.find_by_path(['a'])
|
470
|
-
# move b up to a's level:
|
471
|
-
b = a.children.first
|
472
|
-
a.add_sibling(b)
|
473
|
-
expect(b.parent).to eq(first_root)
|
474
|
-
expect(z.children.reload.pluck(:name)).to eq(z_children_names)
|
475
|
-
end
|
476
|
-
end
|
477
|
-
|
478
|
-
it "shouldn't fail if all children are destroyed" do
|
479
|
-
roots = Label.roots.to_a
|
480
|
-
roots.each { |ea| ea.children.destroy_all }
|
481
|
-
expect(Label.all.to_a).to match_array(roots)
|
482
|
-
end
|
483
|
-
end
|
484
|
-
|
485
|
-
context 'descendent destruction' do
|
486
|
-
it 'properly destroys descendents created with add_child' do
|
487
|
-
a = Label.create(name: 'a')
|
488
|
-
b = Label.new(name: 'b')
|
489
|
-
a.add_child b
|
490
|
-
c = Label.new(name: 'c')
|
491
|
-
b.add_child c
|
492
|
-
a.destroy
|
493
|
-
expect(Label.exists?(id: [a.id, b.id, c.id])).to be_falsey
|
494
|
-
end
|
495
|
-
|
496
|
-
it 'properly destroys descendents created with <<' do
|
497
|
-
a = Label.create(name: 'a')
|
498
|
-
b = Label.new(name: 'b')
|
499
|
-
a.children << b
|
500
|
-
c = Label.new(name: 'c')
|
501
|
-
b.children << c
|
502
|
-
a.destroy
|
503
|
-
expect(Label.exists?(id: [a.id, b.id, c.id])).to be_falsey
|
504
|
-
end
|
505
|
-
end
|
506
|
-
|
507
|
-
context 'preorder' do
|
508
|
-
it 'returns descendants in proper order' do
|
509
|
-
create_preorder_tree
|
510
|
-
a = Label.root
|
511
|
-
expect(a.name).to eq('a')
|
512
|
-
expected = ('a'..'r').to_a
|
513
|
-
expect(a.self_and_descendants_preordered.collect { |ea| ea.name }).to eq(expected)
|
514
|
-
expect(Label.roots_and_descendants_preordered.collect { |ea| ea.name }).to eq(expected)
|
515
|
-
# Let's create the second root by hand so we can explicitly set the sort order
|
516
|
-
Label.create! do |l|
|
517
|
-
l.name = "a1"
|
518
|
-
l.order_value = a.order_value + 1
|
519
|
-
end
|
520
|
-
create_preorder_tree('1')
|
521
|
-
# Should be no change:
|
522
|
-
expect(a.reload.self_and_descendants_preordered.collect { |ea| ea.name }).to eq(expected)
|
523
|
-
expected += ('a'..'r').collect { |ea| "#{ea}1" }
|
524
|
-
expect(Label.roots_and_descendants_preordered.collect { |ea| ea.name }).to eq(expected)
|
525
|
-
end
|
526
|
-
end unless sqlite? # sqlite doesn't have a power function.
|
527
|
-
|
528
|
-
context 'hash_tree' do
|
529
|
-
before do
|
530
|
-
@a = EventLabel.create(name: 'a')
|
531
|
-
@b = DateLabel.create(name: 'b')
|
532
|
-
@c = DirectoryLabel.create(name: 'c')
|
533
|
-
(1..3).each { |i| DirectoryLabel.create!(name: "c#{ i }", mother_id: @c.id) }
|
534
|
-
end
|
535
|
-
it 'should return tree with correct scope when called on class' do
|
536
|
-
tree = DirectoryLabel.hash_tree
|
537
|
-
expect(tree.keys.size).to eq(1)
|
538
|
-
expect(tree.keys.first).to eq(@c)
|
539
|
-
expect(tree[@c].keys.size).to eq(3)
|
540
|
-
end
|
541
|
-
it 'should return tree with correct scope when called on all' do
|
542
|
-
tree = DirectoryLabel.all.hash_tree
|
543
|
-
expect(tree.keys.size).to eq(1)
|
544
|
-
expect(tree.keys.first).to eq(@c)
|
545
|
-
expect(tree[@c].keys.size).to eq(3)
|
546
|
-
end
|
547
|
-
it 'should return tree with correct scope when called on scope chain' do
|
548
|
-
tree = Label.where(name: 'b').hash_tree
|
549
|
-
expect(tree.keys.size).to eq(1)
|
550
|
-
expect(tree.keys.first).to eq(@b)
|
551
|
-
expect(tree[@b]).to eq({})
|
552
|
-
end
|
553
|
-
end
|
554
|
-
end
|