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