closure_tree 6.0.0.alpha → 6.0.0.gamma
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -1
- data/CHANGELOG.md +5 -1
- data/README.md +2 -2
- data/lib/closure_tree/active_record_support.rb +5 -0
- data/lib/closure_tree/finders.rb +1 -6
- data/lib/closure_tree/hash_tree.rb +2 -43
- data/lib/closure_tree/hash_tree_support.rb +40 -0
- data/lib/closure_tree/model.rb +8 -1
- data/lib/closure_tree/numeric_deterministic_ordering.rb +24 -17
- data/lib/closure_tree/support.rb +5 -15
- data/lib/closure_tree/version.rb +1 -1
- data/spec/support/database.rb +1 -1
- data/spec/tag_examples.rb +157 -121
- data/tests.sh +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d32d23158e9b974aeffc5fe307d6dc59aa37907b
|
4
|
+
data.tar.gz: 190e8a6f9a7e8678657f82de4335aa14f9239a0c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a7cbc97513019349f2c193dcf3976bb402406b68e3c285a19d07717a0e3ff24f5fa7860aef0589ba634a3d0f57046eb8580bcb20587d14ee2d786277ba5d1401
|
7
|
+
data.tar.gz: e9efa70b8a6c7c5e048296ceef1d285d210db2e89497fd2ea8179ebdef60b85b53b174a9d8935a7aa69530e100a274eadc635ec89e98d4e194e4cfadc5136250
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
3
|
### 6.0.0.alpha
|
4
|
-
* Drop support for unsupported
|
4
|
+
* Drop support for unsupported versions of Rails, 3.2 and 4.0.
|
5
|
+
* Drop support for Ruby 1.9 and JRuby 1.9
|
6
|
+
* Added support for `.hash_tree` from `.parent` and `.children`.
|
7
|
+
Addresses [PR146](https://github.com/mceachen/closure_tree/pull/146).
|
8
|
+
Thanks for reporting this and the breaking test, [Mike](https://github.com/mkralla11)!
|
5
9
|
|
6
10
|
### 5.2.0
|
7
11
|
|
data/README.md
CHANGED
@@ -480,7 +480,7 @@ the spec ```tag_spec.rb```:
|
|
480
480
|
Tag.rebuild! # <- required if you use fixtures
|
481
481
|
end
|
482
482
|
```
|
483
|
-
|
483
|
+
|
484
484
|
**However, if you're just starting with Rails, may I humbly suggest you adopt a factory library**,
|
485
485
|
rather than using fixtures? [Lots of people have written about this already](https://www.google.com/search?q=fixtures+versus+factories).
|
486
486
|
|
@@ -552,7 +552,7 @@ See the [change log](https://github.com/mceachen/closure_tree/blob/master/CHANGE
|
|
552
552
|
|
553
553
|
## Thanks to
|
554
554
|
|
555
|
-
* The more than
|
555
|
+
* The more than 30 engineers around the world that have contributed their time and code to this gem
|
556
556
|
(see the [changelog](https://github.com/mceachen/closure_tree/blob/master/CHANGELOG.md)!)
|
557
557
|
* https://github.com/collectiveidea/awesome_nested_set
|
558
558
|
* https://github.com/patshaughnessy/class_factory
|
data/lib/closure_tree/finders.rb
CHANGED
@@ -52,11 +52,6 @@ module ClosureTree
|
|
52
52
|
|
53
53
|
module ClassMethods
|
54
54
|
|
55
|
-
# Fix deprecation warning:
|
56
|
-
def _ct_all
|
57
|
-
(ActiveRecord::VERSION::MAJOR >= 4) ? all : scoped
|
58
|
-
end
|
59
|
-
|
60
55
|
def without(instance)
|
61
56
|
if instance.new_record?
|
62
57
|
all
|
@@ -88,7 +83,7 @@ module ClosureTree
|
|
88
83
|
|
89
84
|
def with_ancestor(*ancestors)
|
90
85
|
ancestor_ids = ancestors.map { |ea| ea.is_a?(ActiveRecord::Base) ? ea._ct_id : ea }
|
91
|
-
scope = ancestor_ids.blank? ?
|
86
|
+
scope = ancestor_ids.blank? ? all : joins(:ancestor_hierarchies).
|
92
87
|
where("#{_ct.hierarchy_table_name}.ancestor_id" => ancestor_ids).
|
93
88
|
where("#{_ct.hierarchy_table_name}.generations > 0").
|
94
89
|
readonly(false)
|
@@ -2,17 +2,8 @@ module ClosureTree
|
|
2
2
|
module HashTree
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
|
-
def hash_tree_scope(limit_depth = nil)
|
6
|
-
scope = self_and_descendants
|
7
|
-
if limit_depth
|
8
|
-
scope.where("#{_ct.quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}")
|
9
|
-
else
|
10
|
-
scope
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
5
|
def hash_tree(options = {})
|
15
|
-
|
6
|
+
_ct.hash_tree(self_and_descendants, options[:limit_depth])
|
16
7
|
end
|
17
8
|
|
18
9
|
module ClassMethods
|
@@ -20,39 +11,7 @@ module ClosureTree
|
|
20
11
|
# There is no default depth limit. This might be crazy-big, depending
|
21
12
|
# on your tree shape. Hash huge trees at your own peril!
|
22
13
|
def hash_tree(options = {})
|
23
|
-
|
24
|
-
end
|
25
|
-
|
26
|
-
def hash_tree_scope(limit_depth = nil)
|
27
|
-
# Deepest generation, within limit, for each descendant
|
28
|
-
# NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
|
29
|
-
having_clause = limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ''
|
30
|
-
generation_depth = <<-SQL.strip_heredoc
|
31
|
-
INNER JOIN (
|
32
|
-
SELECT descendant_id, MAX(generations) as depth
|
33
|
-
FROM #{_ct.quoted_hierarchy_table_name}
|
34
|
-
GROUP BY descendant_id
|
35
|
-
#{having_clause}
|
36
|
-
) AS generation_depth
|
37
|
-
ON #{_ct.quoted_table_name}.#{primary_key} = generation_depth.descendant_id
|
38
|
-
SQL
|
39
|
-
_ct.scope_with_order(joins(generation_depth), "generation_depth.depth")
|
40
|
-
end
|
41
|
-
|
42
|
-
# Builds nested hash structure using the scope returned from the passed in scope
|
43
|
-
def build_hash_tree(tree_scope)
|
44
|
-
tree = ActiveSupport::OrderedHash.new
|
45
|
-
id_to_hash = {}
|
46
|
-
|
47
|
-
tree_scope.each do |ea|
|
48
|
-
h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
|
49
|
-
if ea.root? || tree.empty? # We're at the top of the tree.
|
50
|
-
tree[ea] = h
|
51
|
-
else
|
52
|
-
id_to_hash[ea._ct_parent_id][ea] = h
|
53
|
-
end
|
54
|
-
end
|
55
|
-
tree
|
14
|
+
_ct.hash_tree(nil, options[:limit_depth])
|
56
15
|
end
|
57
16
|
end
|
58
17
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ClosureTree
|
2
|
+
module HashTreeSupport
|
3
|
+
def default_tree_scope(limit_depth = nil)
|
4
|
+
# Deepest generation, within limit, for each descendant
|
5
|
+
# NOTE: Postgres requires HAVING clauses to always contains aggregate functions (!!)
|
6
|
+
having_clause = limit_depth ? "HAVING MAX(generations) <= #{limit_depth - 1}" : ''
|
7
|
+
generation_depth = <<-SQL.strip_heredoc
|
8
|
+
INNER JOIN (
|
9
|
+
SELECT descendant_id, MAX(generations) as depth
|
10
|
+
FROM #{quoted_hierarchy_table_name}
|
11
|
+
GROUP BY descendant_id
|
12
|
+
#{having_clause}
|
13
|
+
) AS generation_depth
|
14
|
+
ON #{quoted_table_name}.#{model_class.primary_key} = generation_depth.descendant_id
|
15
|
+
SQL
|
16
|
+
scope_with_order(model_class.joins(generation_depth), 'generation_depth.depth')
|
17
|
+
end
|
18
|
+
|
19
|
+
def hash_tree(tree_scope, limit_depth = nil)
|
20
|
+
limited_scope = if tree_scope
|
21
|
+
limit_depth ? tree_scope.where("#{quoted_hierarchy_table_name}.generations <= #{limit_depth - 1}") : tree_scope
|
22
|
+
else
|
23
|
+
default_tree_scope(limit_depth)
|
24
|
+
end
|
25
|
+
build_hash_tree(limited_scope)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Builds nested hash structure using the scope returned from the passed in scope
|
29
|
+
def build_hash_tree(tree_scope)
|
30
|
+
tree = ActiveSupport::OrderedHash.new
|
31
|
+
id_to_hash = {}
|
32
|
+
|
33
|
+
tree_scope.each do |ea|
|
34
|
+
h = id_to_hash[ea.id] = ActiveSupport::OrderedHash.new
|
35
|
+
(id_to_hash[ea._ct_parent_id] || tree)[ea] = h
|
36
|
+
end
|
37
|
+
tree
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/closure_tree/model.rb
CHANGED
@@ -17,7 +17,14 @@ module ClosureTree
|
|
17
17
|
class_name: _ct.model_class.to_s,
|
18
18
|
foreign_key: _ct.parent_column_name,
|
19
19
|
dependent: _ct.options[:dependent],
|
20
|
-
inverse_of: :parent)
|
20
|
+
inverse_of: :parent) do
|
21
|
+
# We have to redefine hash_tree because the activerecord relation is already scoped to parent_id.
|
22
|
+
def hash_tree(options = {})
|
23
|
+
# we want limit_depth + 1 because we don't do self_and_descendants.
|
24
|
+
limit_depth = options[:limit_depth]
|
25
|
+
_ct.hash_tree(@association.owner.descendants, limit_depth ? limit_depth + 1 : nil)
|
26
|
+
end
|
27
|
+
end
|
21
28
|
|
22
29
|
has_many :ancestor_hierarchies, *_ct.has_many_without_order_option(
|
23
30
|
class_name: _ct.hierarchy_class_name,
|
@@ -27,13 +27,6 @@ module ClosureTree
|
|
27
27
|
|
28
28
|
def self_and_descendants_preordered
|
29
29
|
# TODO: raise NotImplementedError if sort_order is not numeric and not null?
|
30
|
-
h = _ct.connection.select_one(<<-SQL)
|
31
|
-
SELECT
|
32
|
-
count(*) as total_descendants,
|
33
|
-
max(generations) as max_depth
|
34
|
-
FROM #{_ct.quoted_hierarchy_table_name}
|
35
|
-
WHERE ancestor_id = #{_ct.quote(self.id)}
|
36
|
-
SQL
|
37
30
|
join_sql = <<-SQL
|
38
31
|
JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
|
39
32
|
ON anc_hier.descendant_id = #{_ct.quoted_hierarchy_table_name}.descendant_id
|
@@ -42,20 +35,35 @@ module ClosureTree
|
|
42
35
|
JOIN #{_ct.quoted_hierarchy_table_name} depths
|
43
36
|
ON depths.ancestor_id = #{_ct.quote(self.id)} AND depths.descendant_id = anc.#{_ct.quoted_id_column_name}
|
44
37
|
SQL
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
38
|
+
|
39
|
+
self_and_descendants
|
40
|
+
.joins(join_sql)
|
41
|
+
.group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
|
42
|
+
.reorder(self.class._ct_sum_order_by(self))
|
49
43
|
end
|
50
44
|
|
51
45
|
module ClassMethods
|
52
|
-
|
53
|
-
|
46
|
+
|
47
|
+
# If node is nil, order the whole tree.
|
48
|
+
def _ct_sum_order_by(node = nil)
|
49
|
+
stats_sql = <<-SQL.strip_heredoc
|
54
50
|
SELECT
|
55
51
|
count(*) as total_descendants,
|
56
52
|
max(generations) as max_depth
|
57
53
|
FROM #{_ct.quoted_hierarchy_table_name}
|
58
54
|
SQL
|
55
|
+
stats_sql += " WHERE ancestor_id = #{_ct.quote(node.id)}" if node
|
56
|
+
h = _ct.connection.select_one(stats_sql)
|
57
|
+
|
58
|
+
depth_column = node ? 'depths.generations' : 'depths.max_depth'
|
59
|
+
|
60
|
+
node_score = "(1 + anc.#{_ct.quoted_order_column(false)}) * " +
|
61
|
+
"power(#{h['total_descendants']}, #{h['max_depth'].to_i + 1} - #{depth_column})"
|
62
|
+
|
63
|
+
"sum(#{node_score})"
|
64
|
+
end
|
65
|
+
|
66
|
+
def roots_and_descendants_preordered
|
59
67
|
join_sql = <<-SQL.strip_heredoc
|
60
68
|
JOIN #{_ct.quoted_hierarchy_table_name} anc_hier
|
61
69
|
ON anc_hier.descendant_id = #{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}
|
@@ -67,10 +75,9 @@ module ClosureTree
|
|
67
75
|
GROUP BY 1
|
68
76
|
) AS depths ON depths.descendant_id = anc.#{_ct.quoted_id_column_name}
|
69
77
|
SQL
|
70
|
-
|
71
|
-
"
|
72
|
-
|
73
|
-
joins(join_sql).group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}").reorder(order_by)
|
78
|
+
joins(join_sql)
|
79
|
+
.group("#{_ct.quoted_table_name}.#{_ct.quoted_id_column_name}")
|
80
|
+
.reorder(_ct_sum_order_by)
|
74
81
|
end
|
75
82
|
end
|
76
83
|
|
data/lib/closure_tree/support.rb
CHANGED
@@ -2,6 +2,7 @@ require 'closure_tree/support_flags'
|
|
2
2
|
require 'closure_tree/support_attributes'
|
3
3
|
require 'closure_tree/numeric_order_support'
|
4
4
|
require 'closure_tree/active_record_support'
|
5
|
+
require 'closure_tree/hash_tree_support'
|
5
6
|
require 'with_advisory_lock'
|
6
7
|
|
7
8
|
# This class and mixins are an effort to reduce the namespace pollution to models that act_as_tree.
|
@@ -10,6 +11,7 @@ module ClosureTree
|
|
10
11
|
include ClosureTree::SupportFlags
|
11
12
|
include ClosureTree::SupportAttributes
|
12
13
|
include ClosureTree::ActiveRecordSupport
|
14
|
+
include ClosureTree::HashTreeSupport
|
13
15
|
|
14
16
|
attr_reader :model_class
|
15
17
|
attr_reader :options
|
@@ -22,7 +24,7 @@ module ClosureTree
|
|
22
24
|
:name_column => 'name',
|
23
25
|
:with_advisory_lock => true
|
24
26
|
}.merge(options)
|
25
|
-
raise
|
27
|
+
raise ArgumentError, "name_column can't be 'path'" if options[:name_column] == 'path'
|
26
28
|
if order_is_numeric?
|
27
29
|
extend NumericOrderSupport.adapter_for_connection(connection)
|
28
30
|
end
|
@@ -59,10 +61,6 @@ module ClosureTree
|
|
59
61
|
ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix
|
60
62
|
end
|
61
63
|
|
62
|
-
def quote(field)
|
63
|
-
connection.quote(field)
|
64
|
-
end
|
65
|
-
|
66
64
|
def with_order_option(opts)
|
67
65
|
if order_option?
|
68
66
|
opts[:order] = [opts[:order], order_by].compact.join(",")
|
@@ -80,20 +78,12 @@ module ClosureTree
|
|
80
78
|
|
81
79
|
# lambda-ize the order, but don't apply the default order_option
|
82
80
|
def has_many_without_order_option(opts)
|
83
|
-
if ActiveRecord::VERSION::MAJOR > 3
|
84
81
|
[lambda { order(opts[:order]) }, opts.except(:order)]
|
85
|
-
else
|
86
|
-
[opts]
|
87
|
-
end
|
88
82
|
end
|
89
83
|
|
90
84
|
def has_many_with_order_option(opts)
|
91
|
-
|
92
|
-
|
93
|
-
[lambda { order(order_options) }, opts.except(:order)]
|
94
|
-
else
|
95
|
-
[with_order_option(opts)]
|
96
|
-
end
|
85
|
+
order_options = [opts[:order], order_by].compact
|
86
|
+
[lambda { order(order_options) }, opts.except(:order)]
|
97
87
|
end
|
98
88
|
|
99
89
|
def ids_from(scope)
|
data/lib/closure_tree/version.rb
CHANGED
data/spec/support/database.rb
CHANGED
@@ -45,7 +45,7 @@ require "#{database_folder}/models"
|
|
45
45
|
def count_queries(&block)
|
46
46
|
count = 0
|
47
47
|
counter_fn = ->(name, started, finished, unique_id, payload) do
|
48
|
-
count += 1 unless
|
48
|
+
count += 1 unless %w[CACHE SCHEMA].include? payload[:name]
|
49
49
|
end
|
50
50
|
ActiveSupport::Notifications.subscribed(counter_fn, 'sql.active_record', &block)
|
51
51
|
count
|
data/spec/tag_examples.rb
CHANGED
@@ -20,25 +20,25 @@ shared_examples_for Tag do
|
|
20
20
|
end
|
21
21
|
|
22
22
|
it 'should have a correct parent column name' do
|
23
|
-
expected_parent_column_name = tag_class == UUIDTag ?
|
23
|
+
expected_parent_column_name = tag_class == UUIDTag ? 'parent_uuid' : 'parent_id'
|
24
24
|
expect(tag_class._ct.parent_column_name).to eq(expected_parent_column_name)
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
-
describe
|
28
|
+
describe 'from empty db' do
|
29
29
|
|
30
|
-
context
|
31
|
-
it
|
30
|
+
context 'with no tags' do
|
31
|
+
it 'should return no entities' do
|
32
32
|
expect(tag_class.roots).to be_empty
|
33
33
|
expect(tag_class.leaves).to be_empty
|
34
34
|
end
|
35
35
|
|
36
|
-
it
|
36
|
+
it '#find_or_create_by_path with strings' do
|
37
37
|
a = tag_class.create!(name: 'a')
|
38
38
|
expect(a.find_or_create_by_path(%w{b c}).ancestry_path).to eq(%w{a b c})
|
39
39
|
end
|
40
40
|
|
41
|
-
it
|
41
|
+
it '#find_or_create_by_path with hashes' do
|
42
42
|
a = tag_class.create!(name: 'a', title: 'A')
|
43
43
|
subject = a.find_or_create_by_path([
|
44
44
|
{name: 'b', title: 'B'},
|
@@ -49,16 +49,16 @@ shared_examples_for Tag do
|
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
|
-
context
|
52
|
+
context 'with 1 tag' do
|
53
53
|
before do
|
54
|
-
@tag = tag_class.create!(name:
|
54
|
+
@tag = tag_class.create!(name: 'tag')
|
55
55
|
end
|
56
56
|
|
57
|
-
it
|
57
|
+
it 'should be a leaf' do
|
58
58
|
expect(@tag.leaf?).to be_truthy
|
59
59
|
end
|
60
60
|
|
61
|
-
it
|
61
|
+
it 'should be a root' do
|
62
62
|
expect(@tag.root?).to be_truthy
|
63
63
|
end
|
64
64
|
|
@@ -66,13 +66,13 @@ shared_examples_for Tag do
|
|
66
66
|
expect(@tag.parent).to be_nil
|
67
67
|
end
|
68
68
|
|
69
|
-
it
|
69
|
+
it 'should return the only entity as a root and leaf' do
|
70
70
|
expect(tag_class.all).to eq([@tag])
|
71
71
|
expect(tag_class.roots).to eq([@tag])
|
72
72
|
expect(tag_class.leaves).to eq([@tag])
|
73
73
|
end
|
74
74
|
|
75
|
-
context
|
75
|
+
context 'with child' do
|
76
76
|
before do
|
77
77
|
@child = tag_class.create!(name: 'tag 2')
|
78
78
|
end
|
@@ -90,13 +90,13 @@ shared_examples_for Tag do
|
|
90
90
|
expect(@tag.reload.children.to_a).to eq([@child])
|
91
91
|
end
|
92
92
|
|
93
|
-
it
|
93
|
+
it 'adds children through add_child' do
|
94
94
|
@tag.add_child @child
|
95
95
|
assert_roots_and_leaves
|
96
96
|
assert_parent_and_children
|
97
97
|
end
|
98
98
|
|
99
|
-
it
|
99
|
+
it 'adds children through collection' do
|
100
100
|
@tag.children << @child
|
101
101
|
assert_roots_and_leaves
|
102
102
|
assert_parent_and_children
|
@@ -104,48 +104,48 @@ shared_examples_for Tag do
|
|
104
104
|
end
|
105
105
|
end
|
106
106
|
|
107
|
-
context
|
107
|
+
context 'with 2 tags' do
|
108
108
|
before :each do
|
109
|
-
@root = tag_class.create!(name:
|
110
|
-
@leaf = @root.add_child(tag_class.create!(name:
|
109
|
+
@root = tag_class.create!(name: 'root')
|
110
|
+
@leaf = @root.add_child(tag_class.create!(name: 'leaf'))
|
111
111
|
end
|
112
|
-
it
|
112
|
+
it 'should return a simple root and leaf' do
|
113
113
|
expect(tag_class.roots).to eq([@root])
|
114
114
|
expect(tag_class.leaves).to eq([@leaf])
|
115
115
|
end
|
116
|
-
it
|
116
|
+
it 'should return child_ids for root' do
|
117
117
|
expect(@root.child_ids).to eq([@leaf.id])
|
118
118
|
end
|
119
119
|
|
120
|
-
it
|
120
|
+
it 'should return an empty array for leaves' do
|
121
121
|
expect(@leaf.child_ids).to be_empty
|
122
122
|
end
|
123
123
|
end
|
124
124
|
|
125
|
-
context
|
125
|
+
context '3 tag collection.create db' do
|
126
126
|
before :each do
|
127
|
-
@root = tag_class.create! name:
|
128
|
-
@mid = @root.children.create! name:
|
129
|
-
@leaf = @mid.children.create! name:
|
127
|
+
@root = tag_class.create! name: 'root'
|
128
|
+
@mid = @root.children.create! name: 'mid'
|
129
|
+
@leaf = @mid.children.create! name: 'leaf'
|
130
130
|
DestroyedTag.delete_all
|
131
131
|
end
|
132
132
|
|
133
|
-
it
|
133
|
+
it 'should create all tags' do
|
134
134
|
expect(tag_class.all.to_a).to match_array([@root, @mid, @leaf])
|
135
135
|
end
|
136
136
|
|
137
|
-
it
|
137
|
+
it 'should return a root and leaf without middle tag' do
|
138
138
|
expect(tag_class.roots).to eq([@root])
|
139
139
|
expect(tag_class.leaves).to eq([@leaf])
|
140
140
|
end
|
141
141
|
|
142
|
-
it
|
142
|
+
it 'should delete leaves' do
|
143
143
|
tag_class.leaves.destroy_all
|
144
144
|
expect(tag_class.roots).to eq([@root]) # untouched
|
145
145
|
expect(tag_class.leaves).to eq([@mid])
|
146
146
|
end
|
147
147
|
|
148
|
-
it
|
148
|
+
it 'should delete everything if you delete the roots' do
|
149
149
|
tag_class.roots.destroy_all
|
150
150
|
expect(tag_class.all).to be_empty
|
151
151
|
expect(tag_class.roots).to be_empty
|
@@ -167,7 +167,7 @@ shared_examples_for Tag do
|
|
167
167
|
end
|
168
168
|
|
169
169
|
it 'moves non-leaves' do
|
170
|
-
new_root = tag_class.create! name:
|
170
|
+
new_root = tag_class.create! name: 'new_root'
|
171
171
|
new_root.children << @mid
|
172
172
|
expect(@root.reload.descendants).to be_empty
|
173
173
|
expect(new_root.descendants).to eq([@mid, @leaf])
|
@@ -175,7 +175,7 @@ shared_examples_for Tag do
|
|
175
175
|
end
|
176
176
|
|
177
177
|
it 'moves leaves' do
|
178
|
-
new_root = tag_class.create! name:
|
178
|
+
new_root = tag_class.create! name: 'new_root'
|
179
179
|
new_root.children << @leaf
|
180
180
|
expect(new_root.descendants).to eq([@leaf])
|
181
181
|
expect(@root.reload.descendants).to eq([@mid])
|
@@ -183,46 +183,46 @@ shared_examples_for Tag do
|
|
183
183
|
end
|
184
184
|
end
|
185
185
|
|
186
|
-
context
|
186
|
+
context '3 tag explicit_create db' do
|
187
187
|
before :each do
|
188
|
-
@root = tag_class.create!(name:
|
189
|
-
@mid = @root.add_child(tag_class.create!(name:
|
190
|
-
@leaf = @mid.add_child(tag_class.create!(name:
|
188
|
+
@root = tag_class.create!(name: 'root')
|
189
|
+
@mid = @root.add_child(tag_class.create!(name: 'mid'))
|
190
|
+
@leaf = @mid.add_child(tag_class.create!(name: 'leaf'))
|
191
191
|
end
|
192
192
|
|
193
|
-
it
|
193
|
+
it 'should create all tags' do
|
194
194
|
expect(tag_class.all.to_a).to match_array([@root, @mid, @leaf])
|
195
195
|
end
|
196
196
|
|
197
|
-
it
|
197
|
+
it 'should return a root and leaf without middle tag' do
|
198
198
|
expect(tag_class.roots).to eq([@root])
|
199
199
|
expect(tag_class.leaves).to eq([@leaf])
|
200
200
|
end
|
201
201
|
|
202
|
-
it
|
202
|
+
it 'should prevent parental loops from torso' do
|
203
203
|
@mid.children << @root
|
204
204
|
expect(@root.valid?).to be_falsey
|
205
205
|
expect(@mid.reload.children).to eq([@leaf])
|
206
206
|
end
|
207
207
|
|
208
|
-
it
|
208
|
+
it 'should prevent parental loops from toes' do
|
209
209
|
@leaf.children << @root
|
210
210
|
expect(@root.valid?).to be_falsey
|
211
211
|
expect(@leaf.reload.children).to be_empty
|
212
212
|
end
|
213
213
|
|
214
|
-
it
|
214
|
+
it 'should support re-parenting' do
|
215
215
|
@root.children << @leaf
|
216
216
|
expect(tag_class.leaves).to eq([@leaf, @mid])
|
217
217
|
end
|
218
218
|
|
219
|
-
it
|
219
|
+
it 'cleans up hierarchy references for leaves' do
|
220
220
|
@leaf.destroy
|
221
221
|
expect(tag_hierarchy_class.where(ancestor_id: @leaf.id)).to be_empty
|
222
222
|
expect(tag_hierarchy_class.where(descendant_id: @leaf.id)).to be_empty
|
223
223
|
end
|
224
224
|
|
225
|
-
it
|
225
|
+
it 'cleans up hierarchy references' do
|
226
226
|
@mid.destroy
|
227
227
|
expect(tag_hierarchy_class.where(ancestor_id: @mid.id)).to be_empty
|
228
228
|
expect(tag_hierarchy_class.where(descendant_id: @mid.id)).to be_empty
|
@@ -233,17 +233,17 @@ shared_examples_for Tag do
|
|
233
233
|
expect(tag_hierarchy_class.where(descendant_id: @root.id)).to eq(root_hiers)
|
234
234
|
end
|
235
235
|
|
236
|
-
it
|
236
|
+
it 'should have different hash codes for each hierarchy model' do
|
237
237
|
hashes = tag_hierarchy_class.all.map(&:hash)
|
238
238
|
expect(hashes).to match_array(hashes.uniq)
|
239
239
|
end
|
240
240
|
|
241
|
-
it
|
241
|
+
it 'should return the same hash code for equal hierarchy models' do
|
242
242
|
expect(tag_hierarchy_class.first.hash).to eq(tag_hierarchy_class.first.hash)
|
243
243
|
end
|
244
244
|
end
|
245
245
|
|
246
|
-
it
|
246
|
+
it 'performs as the readme says it does' do
|
247
247
|
grandparent = tag_class.create(name: 'Grandparent')
|
248
248
|
parent = grandparent.children.create(name: 'Parent')
|
249
249
|
child1 = tag_class.create(name: 'First Child', parent: parent)
|
@@ -252,13 +252,13 @@ shared_examples_for Tag do
|
|
252
252
|
child3 = tag_class.new(name: 'Third Child')
|
253
253
|
parent.add_child child3
|
254
254
|
expect(grandparent.self_and_descendants.collect(&:name)).to eq(
|
255
|
-
[
|
255
|
+
['Grandparent', 'Parent', 'First Child', 'Second Child', 'Third Child']
|
256
256
|
)
|
257
257
|
expect(child1.ancestry_path).to eq(
|
258
|
-
[
|
258
|
+
['Grandparent', 'Parent', 'First Child']
|
259
259
|
)
|
260
260
|
expect(child3.ancestry_path).to eq(
|
261
|
-
[
|
261
|
+
['Grandparent', 'Parent', 'Third Child']
|
262
262
|
)
|
263
263
|
d = tag_class.find_or_create_by_path %w(a b c d)
|
264
264
|
h = tag_class.find_or_create_by_path %w(e f g h)
|
@@ -267,8 +267,8 @@ shared_examples_for Tag do
|
|
267
267
|
expect(h.ancestry_path).to eq(%w(a b c d e f g h))
|
268
268
|
end
|
269
269
|
|
270
|
-
it
|
271
|
-
expected = (
|
270
|
+
it 'roots sort alphabetically' do
|
271
|
+
expected = ('a'..'z').to_a
|
272
272
|
expected.shuffle.each { |ea| tag_class.create!(name: ea) }
|
273
273
|
expect(tag_class.roots.collect { |ea| ea.name }).to eq(expected)
|
274
274
|
end
|
@@ -403,7 +403,7 @@ shared_examples_for Tag do
|
|
403
403
|
expect(@child.self_and_ancestors).to eq([@child, @parent, @grandparent])
|
404
404
|
end
|
405
405
|
|
406
|
-
it
|
406
|
+
it 'should find by path' do
|
407
407
|
# class method:
|
408
408
|
expect(tag_class.find_by_path(%w{grandparent parent child})).to eq(@child)
|
409
409
|
# instance method:
|
@@ -412,7 +412,7 @@ shared_examples_for Tag do
|
|
412
412
|
expect(@parent.find_by_path(%w{child larvae})).to be_nil
|
413
413
|
end
|
414
414
|
|
415
|
-
it
|
415
|
+
it 'should respect attribute hashes with both selection and creation' do
|
416
416
|
expected_title = 'something else'
|
417
417
|
attrs = {title: expected_title}
|
418
418
|
existing_title = @grandparent.title
|
@@ -422,7 +422,7 @@ shared_examples_for Tag do
|
|
422
422
|
expect(@grandparent.reload.title).to eq(existing_title)
|
423
423
|
end
|
424
424
|
|
425
|
-
it
|
425
|
+
it 'should create a hierarchy with a given attribute' do
|
426
426
|
expected_title = 'unicorn rainbows'
|
427
427
|
attrs = {title: expected_title}
|
428
428
|
child = tag_class.find_or_create_by_path(%w{grandparent parent child}, attrs)
|
@@ -433,20 +433,20 @@ shared_examples_for Tag do
|
|
433
433
|
end
|
434
434
|
end
|
435
435
|
|
436
|
-
it
|
436
|
+
it 'finds correctly rooted paths' do
|
437
437
|
decoy = tag_class.find_or_create_by_path %w(a b c d)
|
438
438
|
b_d = tag_class.find_or_create_by_path %w(b c d)
|
439
439
|
expect(tag_class.find_by_path(%w(b c d))).to eq(b_d)
|
440
440
|
expect(tag_class.find_by_path(%w(c d))).to be_nil
|
441
441
|
end
|
442
442
|
|
443
|
-
it
|
443
|
+
it 'find_by_path for 1 node' do
|
444
444
|
b = tag_class.find_or_create_by_path %w(a b)
|
445
445
|
b2 = b.root.find_by_path(%w(b))
|
446
446
|
expect(b2).to eq(b)
|
447
447
|
end
|
448
448
|
|
449
|
-
it
|
449
|
+
it 'find_by_path for 2 nodes' do
|
450
450
|
path = %w(a b c)
|
451
451
|
c = tag_class.find_or_create_by_path path
|
452
452
|
permutations = path.permutation.to_a
|
@@ -457,34 +457,34 @@ shared_examples_for Tag do
|
|
457
457
|
end
|
458
458
|
end
|
459
459
|
|
460
|
-
it
|
460
|
+
it 'find_by_path for 3 nodes' do
|
461
461
|
d = tag_class.find_or_create_by_path %w(a b c d)
|
462
462
|
expect(d.root.find_by_path(%w(b c d))).to eq(d)
|
463
463
|
expect(tag_class.find_by_path(%w(a b c d))).to eq(d)
|
464
464
|
expect(tag_class.find_by_path(%w(d))).to be_nil
|
465
465
|
end
|
466
466
|
|
467
|
-
it
|
467
|
+
it 'should return nil for missing nodes' do
|
468
468
|
expect(tag_class.find_by_path(%w{missing})).to be_nil
|
469
469
|
expect(tag_class.find_by_path(%w{grandparent missing})).to be_nil
|
470
470
|
expect(tag_class.find_by_path(%w{grandparent parent missing})).to be_nil
|
471
471
|
expect(tag_class.find_by_path(%w{grandparent parent missing child})).to be_nil
|
472
472
|
end
|
473
473
|
|
474
|
-
describe
|
475
|
-
it
|
474
|
+
describe '.find_or_create_by_path' do
|
475
|
+
it 'uses existing records' do
|
476
476
|
grandparent = tag_class.find_or_create_by_path(%w{grandparent})
|
477
477
|
expect(grandparent).to eq(grandparent)
|
478
478
|
child = tag_class.find_or_create_by_path(%w{grandparent parent child})
|
479
479
|
expect(child).to eq(child)
|
480
480
|
end
|
481
481
|
|
482
|
-
it
|
482
|
+
it 'creates 2-deep trees with strings' do
|
483
483
|
subject = tag_class.find_or_create_by_path(%w{events anniversary})
|
484
484
|
expect(subject.ancestry_path).to eq(%w{events anniversary})
|
485
485
|
end
|
486
486
|
|
487
|
-
it
|
487
|
+
it 'creates 2-deep trees with hashes' do
|
488
488
|
subject = tag_class.find_or_create_by_path([
|
489
489
|
{name: 'test1', title: 'TEST1'},
|
490
490
|
{name: 'test2', title: 'TEST2'}
|
@@ -497,93 +497,129 @@ shared_examples_for Tag do
|
|
497
497
|
end
|
498
498
|
end
|
499
499
|
|
500
|
-
context
|
501
|
-
|
500
|
+
context 'hash_tree' do
|
502
501
|
before :each do
|
503
|
-
@
|
502
|
+
@d1 = tag_class.find_or_create_by_path %w(a b c1 d1)
|
503
|
+
@c1 = @d1.parent
|
504
|
+
@b = @c1.parent
|
504
505
|
@a = @b.parent
|
506
|
+
@a2 = tag_class.create(name: 'a2')
|
505
507
|
@b2 = tag_class.find_or_create_by_path %w(a b2)
|
506
|
-
@
|
507
|
-
@
|
508
|
-
@
|
509
|
-
@
|
510
|
-
|
508
|
+
@c3 = tag_class.find_or_create_by_path %w(a3 b3 c3)
|
509
|
+
@b3 = @c3.parent
|
510
|
+
@a3 = @b3.parent
|
511
|
+
@tree2 = {
|
512
|
+
@a => {@b => {}, @b2 => {}}, @a2 => {}, @a3 => {@b3 => {}}
|
513
|
+
}
|
514
|
+
|
515
|
+
@one_tree = {
|
516
|
+
@a => {},
|
517
|
+
@a2 => {},
|
518
|
+
@a3 => {}
|
519
|
+
}
|
520
|
+
@two_tree = {
|
521
|
+
@a => {
|
522
|
+
@b => {},
|
523
|
+
@b2 => {}
|
524
|
+
},
|
525
|
+
@a2 => {},
|
526
|
+
@a3 => {
|
527
|
+
@b3 => {}
|
528
|
+
}
|
529
|
+
}
|
530
|
+
@three_tree = {
|
531
|
+
@a => {
|
532
|
+
@b => {
|
533
|
+
@c1 => {},
|
534
|
+
},
|
535
|
+
@b2 => {}
|
536
|
+
},
|
537
|
+
@a2 => {},
|
538
|
+
@a3 => {
|
539
|
+
@b3 => {
|
540
|
+
@c3 => {}
|
541
|
+
}
|
542
|
+
}
|
543
|
+
}
|
544
|
+
@full_tree = {
|
545
|
+
@a => {
|
546
|
+
@b => {
|
547
|
+
@c1 => {
|
548
|
+
@d1 => {}
|
549
|
+
},
|
550
|
+
},
|
551
|
+
@b2 => {}
|
552
|
+
},
|
553
|
+
@a2 => {},
|
554
|
+
@a3 => {
|
555
|
+
@b3 => {
|
556
|
+
@c3 => {}
|
557
|
+
}
|
558
|
+
}
|
559
|
+
}
|
511
560
|
#File.open("example.dot", "w") { |f| f.write(tag_class.root.to_dot_digraph) }
|
512
561
|
end
|
513
562
|
|
514
|
-
context
|
515
|
-
it
|
563
|
+
context '#hash_tree' do
|
564
|
+
it 'returns {} for depth 0' do
|
516
565
|
expect(tag_class.hash_tree(limit_depth: 0)).to eq({})
|
517
566
|
end
|
518
|
-
it
|
519
|
-
expect(tag_class.hash_tree(limit_depth: 1)).to eq(
|
567
|
+
it 'limit_depth 1' do
|
568
|
+
expect(tag_class.hash_tree(limit_depth: 1)).to eq(@one_tree)
|
520
569
|
end
|
521
|
-
it
|
522
|
-
expect(tag_class.hash_tree(limit_depth: 2)).to eq(
|
570
|
+
it 'limit_depth 2' do
|
571
|
+
expect(tag_class.hash_tree(limit_depth: 2)).to eq(@two_tree)
|
523
572
|
end
|
524
|
-
it
|
525
|
-
expect(tag_class.hash_tree(limit_depth: 3)).to eq(
|
573
|
+
it 'limit_depth 3' do
|
574
|
+
expect(tag_class.hash_tree(limit_depth: 3)).to eq(@three_tree)
|
526
575
|
end
|
527
|
-
it
|
576
|
+
it 'limit_depth 4' do
|
528
577
|
expect(tag_class.hash_tree(limit_depth: 4)).to eq(@full_tree)
|
529
578
|
end
|
530
|
-
it
|
579
|
+
it 'no limit' do
|
531
580
|
expect(tag_class.hash_tree).to eq(@full_tree)
|
532
581
|
end
|
533
582
|
end
|
534
583
|
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
a = scope.collect { |ea| ea.id }
|
539
|
-
expect(a).to eq(a.uniq)
|
540
|
-
end
|
541
|
-
|
542
|
-
context "#hash_tree_scope" do
|
543
|
-
it "no dupes for any depth" do
|
544
|
-
(0..5).each do |ea|
|
545
|
-
assert_no_dupes(tag_class.hash_tree_scope(ea))
|
546
|
-
end
|
584
|
+
context '.hash_tree' do
|
585
|
+
it 'returns {} for depth 0' do
|
586
|
+
expect(@b.hash_tree(limit_depth: 0)).to eq({})
|
547
587
|
end
|
548
|
-
it
|
549
|
-
|
588
|
+
it 'limit_depth 1' do
|
589
|
+
expect(@b.hash_tree(limit_depth: 1)).to eq(@two_tree[@a].slice(@b))
|
550
590
|
end
|
551
|
-
|
552
|
-
|
553
|
-
context ".hash_tree_scope" do
|
554
|
-
it "no dupes for any depth" do
|
555
|
-
(0..5).each do |ea|
|
556
|
-
assert_no_dupes(@a.hash_tree_scope(ea))
|
557
|
-
end
|
591
|
+
it 'limit_depth 2' do
|
592
|
+
expect(@b.hash_tree(limit_depth: 2)).to eq(@three_tree[@a].slice(@b))
|
558
593
|
end
|
559
|
-
it
|
560
|
-
|
594
|
+
it 'limit_depth 3' do
|
595
|
+
expect(@b.hash_tree(limit_depth: 3)).to eq(@full_tree[@a].slice(@b))
|
561
596
|
end
|
562
|
-
|
563
|
-
|
564
|
-
context ".hash_tree" do
|
565
|
-
before :each do
|
597
|
+
it 'no limit from subsubroot' do
|
598
|
+
expect(@c1.hash_tree).to eq(@full_tree[@a][@b].slice(@c1))
|
566
599
|
end
|
567
|
-
it
|
568
|
-
expect(@b.hash_tree
|
600
|
+
it 'no limit from subroot' do
|
601
|
+
expect(@b.hash_tree).to eq(@full_tree[@a].slice(@b))
|
569
602
|
end
|
570
|
-
it
|
571
|
-
expect(@
|
603
|
+
it 'no limit from root' do
|
604
|
+
expect(@a.hash_tree.merge(@a2.hash_tree)).to eq(@full_tree.slice(@a, @a2))
|
572
605
|
end
|
573
|
-
|
574
|
-
|
606
|
+
end
|
607
|
+
|
608
|
+
context '.hash_tree from relations' do
|
609
|
+
it 'limit_depth 2 from chained activerecord association subroots' do
|
610
|
+
expect(@a.children.hash_tree(limit_depth: 2)).to eq(@three_tree[@a])
|
575
611
|
end
|
576
|
-
it
|
577
|
-
expect(@
|
612
|
+
it 'no limit from chained activerecord association subroots' do
|
613
|
+
expect(@a.children.hash_tree).to eq(@full_tree[@a])
|
578
614
|
end
|
579
|
-
it
|
580
|
-
expect(@
|
615
|
+
it 'limit_depth 3 from b.parent' do
|
616
|
+
expect(@b.parent.hash_tree(limit_depth: 3)).to eq(@three_tree.slice(@a))
|
581
617
|
end
|
582
|
-
it
|
583
|
-
expect(@b.hash_tree).to eq(
|
618
|
+
it 'no limit_depth from b.parent' do
|
619
|
+
expect(@b.parent.hash_tree).to eq(@full_tree.slice(@a))
|
584
620
|
end
|
585
|
-
it
|
586
|
-
expect(@
|
621
|
+
it 'no limit_depth from c.parent' do
|
622
|
+
expect(@c1.parent.hash_tree).to eq(@full_tree[@a].slice(@b))
|
587
623
|
end
|
588
624
|
end
|
589
625
|
end
|
@@ -600,7 +636,7 @@ shared_examples_for Tag do
|
|
600
636
|
|
601
637
|
describe 'DOT rendering' do
|
602
638
|
it 'should render for an empty scope' do
|
603
|
-
expect(tag_class.to_dot_digraph(tag_class.where(
|
639
|
+
expect(tag_class.to_dot_digraph(tag_class.where('0=1'))).to eq("digraph G {\n}\n")
|
604
640
|
end
|
605
641
|
it 'should render for an empty scope' do
|
606
642
|
tag_class.find_or_create_by_path(%w(a b1 c1))
|
data/tests.sh
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: closure_tree
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 6.0.0.
|
4
|
+
version: 6.0.0.gamma
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matthew McEachen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-09-
|
11
|
+
date: 2015-09-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -166,6 +166,7 @@ files:
|
|
166
166
|
- lib/closure_tree/finders.rb
|
167
167
|
- lib/closure_tree/has_closure_tree.rb
|
168
168
|
- lib/closure_tree/hash_tree.rb
|
169
|
+
- lib/closure_tree/hash_tree_support.rb
|
169
170
|
- lib/closure_tree/hierarchy_maintenance.rb
|
170
171
|
- lib/closure_tree/model.rb
|
171
172
|
- lib/closure_tree/numeric_deterministic_ordering.rb
|