has_hierarchy 0.2.2 → 0.3.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 +4 -4
- data/.gitignore +2 -2
- data/CHANGELOG.md +35 -10
- data/README.md +75 -27
- data/has_hierarchy.gemspec +2 -3
- data/lib/has_hierarchy/depth_cache.rb +25 -0
- data/lib/has_hierarchy/order.rb +38 -0
- data/lib/has_hierarchy/path.rb +120 -0
- data/lib/has_hierarchy/version.rb +1 -1
- data/lib/has_hierarchy.rb +89 -21
- data/spec/db/schema.rb +4 -1
- data/spec/has_hierarchy_spec.rb +264 -9
- data/spec/support/models.rb +25 -1
- metadata +7 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: abe351194af5c2a75cbaad480fce586064fddad1
|
4
|
+
data.tar.gz: 283c8fd7fd5b8d27fdbe3b15bc4af195a90b15b5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b529a4e0c3a0937d215feb5c77647c7ef9e7877ecdca68365bc888789d6abc050675e29ac0526dc27b14f7ece94b610849fdb2a6b982093f6e4ba779a414376
|
7
|
+
data.tar.gz: 12978bfbbbf31fd2d8ffcaed16c67fe1c029076af8365708ff54cb56de6accfe11879cb80b82fc5fb71880bf20d07ce777fc97550ffcd5cda54a3af3979e8497
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,26 +1,51 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.3.0
|
4
|
+
|
5
|
+
- Added "path_separator" option.
|
6
|
+
- Added ordering support.
|
7
|
+
- Cleaned up options.
|
8
|
+
- Renamed to has_hierarchy.
|
9
|
+
|
3
10
|
## 0.2.1
|
4
11
|
|
5
|
-
-
|
6
|
-
-
|
12
|
+
- Fixed `.find_by_node_path`.
|
13
|
+
- Added tree rebuilding on node_id change.
|
14
|
+
- Added depth caching.
|
7
15
|
|
8
16
|
## 0.2.0
|
9
17
|
|
10
|
-
-
|
18
|
+
- Added custom node path values.
|
19
|
+
- Added `.find_by_node_path`.
|
20
|
+
- Added `#child_of?`.
|
21
|
+
- Updated "node_path_column" option (renamed to "node_path_cache").
|
22
|
+
- Rewrited specs.
|
23
|
+
|
24
|
+
## 0.1.3
|
25
|
+
|
26
|
+
- Added README.md.
|
27
|
+
- Added `#leaf?`.
|
28
|
+
- Added `has_children_options` accessor.
|
29
|
+
- Added counter cache and root scope specs.
|
30
|
+
- Updated "orphan_strategy" option (renamed to "dependent").
|
31
|
+
- Updated rake tasks.
|
32
|
+
- Updated .gitignore.
|
33
|
+
- Updated codestyle.
|
34
|
+
- Fixed rspec deprication warnings.
|
11
35
|
|
12
36
|
## 0.1.2
|
13
37
|
|
14
|
-
- Added
|
15
|
-
-
|
16
|
-
-
|
17
|
-
-
|
38
|
+
- Added root association.
|
39
|
+
- Added `#root_id`.
|
40
|
+
- Added `#root_of?`.
|
41
|
+
- Added `#parent_of?`.
|
18
42
|
|
19
43
|
## 0.1.1
|
20
44
|
|
21
|
-
-
|
45
|
+
- Fixed scopes (always using lambdas).
|
22
46
|
|
23
47
|
## 0.1.0
|
24
48
|
|
25
|
-
- Added `#
|
26
|
-
-
|
49
|
+
- Added `#move_children_to_parent`.
|
50
|
+
- `#ancestor_tokens` renamed to `#ancestor_ids`.
|
51
|
+
- Added lambda scopes support.
|
data/README.md
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
[](http://badge.fury.io/rb/has_hierarchy)
|
2
|
-
[](https://travis-ci.org/kolesnikovde/has_hierarchy)
|
3
3
|
[](https://codeclimate.com/github/kolesnikovde/has_hierarchy)
|
4
4
|
[](https://codeclimate.com/github/kolesnikovde/has_hierarchy)
|
5
5
|
|
6
6
|
# has_hierarchy
|
7
7
|
|
8
|
-
Provides
|
8
|
+
Provides tree behavior to active_record models.
|
9
9
|
|
10
10
|
## Installation
|
11
11
|
|
@@ -19,16 +19,21 @@ And then execute:
|
|
19
19
|
|
20
20
|
## Usage
|
21
21
|
|
22
|
+
Example tree:
|
22
23
|
```sh
|
23
|
-
$ rails g
|
24
|
+
$ rails g model Item \
|
24
25
|
name:string \
|
25
|
-
|
26
|
+
path:string \
|
27
|
+
depth:integer \
|
26
28
|
position:integer \
|
27
|
-
|
29
|
+
parent:belongs_to \
|
30
|
+
children_count:integer
|
28
31
|
```
|
29
32
|
```ruby
|
30
33
|
class Item < ActiveRecord::Base
|
31
|
-
has_hierarchy
|
34
|
+
has_hierarchy path_part: :name,
|
35
|
+
counter_cache: :children_count,
|
36
|
+
dependent: :destroy
|
32
37
|
end
|
33
38
|
|
34
39
|
foo = Item.create!(name: 'foo')
|
@@ -36,33 +41,76 @@ bar = Item.create!(name: 'bar')
|
|
36
41
|
qux = bar.children.create!(name: 'qux')
|
37
42
|
baz = bar.children.create!(name: 'baz')
|
38
43
|
quux = qux.children.create!(name: 'quux')
|
44
|
+
```
|
39
45
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
46
|
+
Options:
|
47
|
+
```
|
48
|
+
scope - optional, proc, symbol or an array of symbols.
|
49
|
+
order - optional, column name or boolean, default :position.
|
50
|
+
path_cache - optional, column name or boolean, default :path.
|
51
|
+
path_part - optional, column name, default :id.
|
52
|
+
path_separator - optional, string, default '/'.
|
53
|
+
depth_cache - optional, column name or boolean, default :depth.
|
54
|
+
counter_cache - optional, :counter_cache option for parent association.
|
55
|
+
dependent - optional, :dependent option for children association.
|
56
|
+
```
|
49
57
|
|
50
|
-
|
58
|
+
Operations on the tree:
|
59
|
+
```ruby
|
60
|
+
Item.roots
|
61
|
+
# => [ foo, bar ]
|
62
|
+
|
63
|
+
Item.ordered.tree
|
64
|
+
# => {
|
65
|
+
# foo => {},
|
66
|
+
# bar => {
|
67
|
+
# qux => {
|
68
|
+
# quux => {}
|
69
|
+
# },
|
70
|
+
# baz => {}
|
71
|
+
# }
|
72
|
+
# }
|
73
|
+
|
74
|
+
Item.find_by_node_path('bar/qux/quux')
|
75
|
+
# => quux
|
76
|
+
```
|
51
77
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
78
|
+
Operations on nodes:
|
79
|
+
```ruby
|
80
|
+
bar.children # => [ qux, baz ]
|
81
|
+
qux.parent # => bar
|
82
|
+
foo.siblings # => [ bar ]
|
83
|
+
bar.parent_of?(quux) # => false
|
84
|
+
qux.child_of?(bar) # => true
|
85
|
+
bar.sibling_of?(foo) # => true
|
86
|
+
bar.root? # => true
|
87
|
+
qux.leaf? # => false
|
88
|
+
```
|
61
89
|
|
90
|
+
Path cache is required for following methods:
|
91
|
+
```ruby
|
92
|
+
bar.root_of?(quux) # => true
|
93
|
+
bar.ancestor_of?(quux) # => true
|
94
|
+
qux.descendant_of?(bar) # => true
|
95
|
+
quux.root # => bar
|
96
|
+
quux.ancestors # => [ qux, bar ]
|
97
|
+
bar.descendants # => [ qux, quux, baz ]
|
62
98
|
```
|
63
99
|
|
64
|
-
|
65
|
-
|
100
|
+
Ordering (see [has_order](https://github.com/kolesnikovde/has_order)):
|
101
|
+
```ruby
|
102
|
+
foo.move_after(quux)
|
103
|
+
Item.ordered.tree
|
104
|
+
# => {
|
105
|
+
# bar => {
|
106
|
+
# qux => {
|
107
|
+
# quux => {},
|
108
|
+
# foo => {}
|
109
|
+
# },
|
110
|
+
# baz => {}
|
111
|
+
# }
|
112
|
+
# }
|
113
|
+
```
|
66
114
|
|
67
115
|
## License
|
68
116
|
|
data/has_hierarchy.gemspec
CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
|
|
9
9
|
|
10
10
|
spec.authors = ['Kolesnikov Danil']
|
11
11
|
spec.email = ['kolesnikovde@gmail.com']
|
12
|
-
spec.description = 'Provides
|
13
|
-
spec.summary = 'Provides
|
12
|
+
spec.description = 'Provides tree behavior to active_record models.'
|
13
|
+
spec.summary = 'Provides tree behavior to active_record models.'
|
14
14
|
spec.homepage = 'https://github.com/kolesnikovde/has_hierarchy'
|
15
15
|
spec.license = 'MIT'
|
16
16
|
|
@@ -27,5 +27,4 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_runtime_dependency 'activerecord', '~> 4'
|
28
28
|
spec.add_runtime_dependency 'activesupport', '~> 4'
|
29
29
|
spec.add_runtime_dependency 'has_order', '~> 0.1'
|
30
|
-
spec.add_runtime_dependency 'has_children', '~> 0.2.1'
|
31
30
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module HasHierarchy
|
2
|
+
module DepthCache
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
before_save :cache_depth
|
7
|
+
|
8
|
+
cattr_accessor :depth_column do
|
9
|
+
column = has_hierarchy_options[:depth_cache]
|
10
|
+
column = :depth if column == true
|
11
|
+
column
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def depth
|
18
|
+
self[depth_column] || 0
|
19
|
+
end
|
20
|
+
|
21
|
+
def cache_depth
|
22
|
+
self[depth_column] = root? ? 0 : (parent.depth + 1)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'has_order'
|
2
|
+
|
3
|
+
module HasHierarchy
|
4
|
+
module Order
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
options = has_hierarchy_options
|
9
|
+
|
10
|
+
has_order scope: Array(options[:scope]).concat([ :parent_id ]),
|
11
|
+
position_column: options[:order]
|
12
|
+
|
13
|
+
include HasOrderOverrides
|
14
|
+
end
|
15
|
+
|
16
|
+
module HasOrderOverrides
|
17
|
+
def move_before(node)
|
18
|
+
self.parent_id = node.parent_id
|
19
|
+
@prevent_default_position = true
|
20
|
+
super
|
21
|
+
@prevent_default_position = false
|
22
|
+
end
|
23
|
+
|
24
|
+
def move_after(node)
|
25
|
+
self.parent_id = node.parent_id
|
26
|
+
@prevent_default_position = true
|
27
|
+
super
|
28
|
+
@prevent_default_position = false
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def set_default_position?
|
34
|
+
super or (parent_id_changed? and not @prevent_default_position)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module HasHierarchy
|
2
|
+
module Path
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
before_create :populate_path
|
7
|
+
before_update :rebuild_subtree, if: :need_to_rebuild_subtree?
|
8
|
+
|
9
|
+
cattr_accessor :path_column do
|
10
|
+
column = has_hierarchy_options[:path_cache]
|
11
|
+
column = :path if column.nil? or column == true
|
12
|
+
column
|
13
|
+
end
|
14
|
+
|
15
|
+
cattr_accessor :path_separator do
|
16
|
+
has_hierarchy_options[:path_separator] || '/'
|
17
|
+
end
|
18
|
+
|
19
|
+
cattr_accessor :path_part_column do
|
20
|
+
has_hierarchy_options[:path_part] || :id
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
def find_by_path(path)
|
26
|
+
sep = path_separator
|
27
|
+
parts = path.split(sep)
|
28
|
+
part = parts.pop
|
29
|
+
path = parts.length > 0 ? parts.join(sep) + sep : ''
|
30
|
+
|
31
|
+
where(path_part_column => part, path_column => path).first
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def root
|
36
|
+
self.class.find_by(path_part_column => path_parts.first)
|
37
|
+
end
|
38
|
+
|
39
|
+
def root_of?(node)
|
40
|
+
node.path_parts.first == path_part if path_part.present?
|
41
|
+
end
|
42
|
+
|
43
|
+
def ancestors
|
44
|
+
tree_scope.where(ancestors_conditions)
|
45
|
+
end
|
46
|
+
|
47
|
+
def ancestor_of?(node)
|
48
|
+
node.path_parts.include?(path_part)
|
49
|
+
end
|
50
|
+
|
51
|
+
def descendants
|
52
|
+
tree_scope.where(descendants_conditions)
|
53
|
+
end
|
54
|
+
|
55
|
+
def descendant_of?(node)
|
56
|
+
path_parts.include?(node.path_part)
|
57
|
+
end
|
58
|
+
|
59
|
+
def subtree
|
60
|
+
tree_scope.where(subtree_conditions)
|
61
|
+
end
|
62
|
+
|
63
|
+
def depth
|
64
|
+
path_parts.size
|
65
|
+
end
|
66
|
+
|
67
|
+
def path
|
68
|
+
self[path_column]
|
69
|
+
end
|
70
|
+
|
71
|
+
def path=(path)
|
72
|
+
self[path_column] = path
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def path_part
|
78
|
+
self[path_part_column].to_s
|
79
|
+
end
|
80
|
+
|
81
|
+
def path_parts
|
82
|
+
path.split(path_separator)
|
83
|
+
end
|
84
|
+
|
85
|
+
def path_for_children
|
86
|
+
[ path, path_part, path_separator ].join
|
87
|
+
end
|
88
|
+
|
89
|
+
def populate_path
|
90
|
+
self.path = root? ? '' : parent.path_for_children
|
91
|
+
end
|
92
|
+
|
93
|
+
def ancestors_conditions
|
94
|
+
{ path_part_column => path_parts }
|
95
|
+
end
|
96
|
+
|
97
|
+
def descendants_conditions
|
98
|
+
arel_path = self.class.arel_table[path_column]
|
99
|
+
arel_path.matches("#{path_for_children}%")
|
100
|
+
end
|
101
|
+
|
102
|
+
def subtree_conditions
|
103
|
+
arel_path_part = self.class.arel_table[path_part_column]
|
104
|
+
arel_path_part.eq(path_part).or(descendants_conditions)
|
105
|
+
end
|
106
|
+
|
107
|
+
def rebuild_subtree
|
108
|
+
populate_path
|
109
|
+
|
110
|
+
children.each do |child|
|
111
|
+
child.rebuild_subtree
|
112
|
+
child.save!
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def need_to_rebuild_subtree?
|
117
|
+
parent_id_changed? or changed_attributes.include?(path_part_column)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
data/lib/has_hierarchy.rb
CHANGED
@@ -1,42 +1,110 @@
|
|
1
1
|
require 'active_record'
|
2
|
-
require 'has_order'
|
3
|
-
require 'has_children'
|
4
2
|
require 'has_hierarchy/version'
|
3
|
+
require 'has_hierarchy/order'
|
4
|
+
require 'has_hierarchy/path'
|
5
|
+
require 'has_hierarchy/depth_cache'
|
5
6
|
|
6
7
|
module HasHierarchy
|
7
|
-
def has_hierarchy
|
8
|
-
|
9
|
-
has_children options
|
10
|
-
|
11
|
-
after_save :reset_parent_acceptance
|
8
|
+
def has_hierarchy(options = {})
|
9
|
+
cattr_accessor(:has_hierarchy_options) { options }
|
12
10
|
|
11
|
+
extend ClassMethods
|
13
12
|
include InstanceMethods
|
13
|
+
|
14
|
+
include Order unless options[:order] == false
|
15
|
+
include Path unless options[:path_cache] == false
|
16
|
+
include DepthCache if options[:depth_cache]
|
17
|
+
|
18
|
+
belongs_to :parent, class_name: self.name,
|
19
|
+
inverse_of: :children,
|
20
|
+
counter_cache: options[:counter_cache]
|
21
|
+
|
22
|
+
has_many :children, class_name: self.name,
|
23
|
+
foreign_key: :parent_id,
|
24
|
+
inverse_of: :parent,
|
25
|
+
dependent: options[:dependent]
|
26
|
+
|
27
|
+
define_tree_scope(options[:scope])
|
14
28
|
end
|
15
29
|
|
16
|
-
module
|
17
|
-
def
|
18
|
-
|
19
|
-
super
|
30
|
+
module ClassMethods
|
31
|
+
def roots
|
32
|
+
where(parent_id: nil)
|
20
33
|
end
|
21
34
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
35
|
+
def tree
|
36
|
+
nodes = all
|
37
|
+
index = {}
|
38
|
+
arranged = {}
|
39
|
+
|
40
|
+
nodes.each do |node|
|
41
|
+
struct = node.root? ? arranged : (index[node.parent_id] ||= {})
|
42
|
+
struct[node] = (index[node.id] ||= {})
|
43
|
+
end
|
44
|
+
|
45
|
+
arranged
|
25
46
|
end
|
26
47
|
|
27
48
|
protected
|
28
49
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
50
|
+
def define_tree_scope(tree_scope)
|
51
|
+
scope :tree_scope, case tree_scope
|
52
|
+
when Proc
|
53
|
+
tree_scope
|
54
|
+
when nil
|
55
|
+
->(model) { where(nil) }
|
56
|
+
else
|
57
|
+
->(model) { where(Hash[Array(tree_scope).map{ |s| [ s, model[s] ] }]) }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
module InstanceMethods
|
63
|
+
def leaf?
|
64
|
+
if counter_cache = has_hierarchy_options[:counter_cache]
|
65
|
+
self[counter_cache] == 0
|
66
|
+
else
|
67
|
+
children.empty?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def root?
|
72
|
+
parent_id.nil?
|
32
73
|
end
|
33
74
|
|
34
|
-
def
|
35
|
-
|
75
|
+
def parent_of?(node)
|
76
|
+
node.parent_id == id
|
36
77
|
end
|
37
78
|
|
38
|
-
def
|
39
|
-
|
79
|
+
def child_of?(node)
|
80
|
+
node.id == parent_id
|
81
|
+
end
|
82
|
+
|
83
|
+
def sibling_of?(node)
|
84
|
+
parent_id == node.parent_id and id != node.id
|
85
|
+
end
|
86
|
+
|
87
|
+
def siblings
|
88
|
+
tree_scope.where(siblings_conditions)
|
89
|
+
end
|
90
|
+
|
91
|
+
def move_children_to_parent
|
92
|
+
children.each do |c|
|
93
|
+
c.parent = self.parent
|
94
|
+
c.save
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
protected
|
99
|
+
|
100
|
+
def tree_scope
|
101
|
+
self.class.tree_scope(self)
|
102
|
+
end
|
103
|
+
|
104
|
+
def siblings_conditions
|
105
|
+
t = self.class.arel_table
|
106
|
+
|
107
|
+
t[:parent_id].eq(parent_id).and(t[:id].not_eq(id))
|
40
108
|
end
|
41
109
|
end
|
42
110
|
end
|
data/spec/db/schema.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
ActiveRecord::Schema.define(version: 0) do
|
2
2
|
create_table :items, force: true do |t|
|
3
3
|
t.string :name
|
4
|
-
t.string :
|
4
|
+
t.string :category
|
5
|
+
t.string :path
|
6
|
+
t.integer :children_count, default: 0
|
7
|
+
t.integer :depth
|
5
8
|
t.integer :position
|
6
9
|
|
7
10
|
t.belongs_to :parent
|
data/spec/has_hierarchy_spec.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
|
3
|
+
shared_context 'example tree' do
|
4
4
|
let!(:foo) { described_class.create!(name: 'foo') }
|
5
5
|
let!(:bar) { described_class.create!(name: 'bar') }
|
6
6
|
let!(:qux) { bar.children.create!(name: 'qux') }
|
@@ -11,18 +11,256 @@ shared_examples 'ordered tree' do
|
|
11
11
|
[ foo, bar, baz, qux, quux ].each(&:reload)
|
12
12
|
end
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
before do
|
15
|
+
reload_items
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
shared_examples 'adjacency list' do
|
20
|
+
include_context 'example tree'
|
21
|
+
|
22
|
+
describe '.tree' do
|
23
|
+
it 'arranges tree' do
|
24
|
+
expect(described_class.tree).to be_arranged_like({
|
25
|
+
foo => {},
|
26
|
+
bar => {
|
27
|
+
qux => {
|
28
|
+
quux => {}
|
29
|
+
},
|
30
|
+
baz => {}
|
31
|
+
}
|
32
|
+
})
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'allows custom order' do
|
36
|
+
expect(described_class.alphabetic.tree).to be_arranged_like({
|
37
|
+
bar => {
|
38
|
+
baz => {},
|
39
|
+
qux => {
|
40
|
+
quux => {}
|
41
|
+
}
|
42
|
+
},
|
43
|
+
foo => {}
|
44
|
+
})
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '.roots' do
|
49
|
+
it 'returns roots' do
|
50
|
+
expect(described_class.roots).to match_array([ foo, bar ])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#move_children_to_parent' do
|
55
|
+
it 'changes children parent' do
|
56
|
+
bar.move_children_to_parent
|
57
|
+
|
58
|
+
expect(described_class.tree).to be_arranged_like({
|
59
|
+
foo => {},
|
60
|
+
bar => {},
|
18
61
|
qux => {
|
19
|
-
quux => {}
|
62
|
+
quux => {}
|
20
63
|
},
|
21
64
|
baz => {}
|
22
|
-
}
|
23
|
-
|
65
|
+
})
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '#root?' do
|
70
|
+
it 'returns true if node has parent' do
|
71
|
+
expect(bar).to be_root
|
72
|
+
expect(baz).not_to be_root
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#leaf?' do
|
77
|
+
it 'returns true if node does not have children' do
|
78
|
+
expect(quux).to be_leaf
|
79
|
+
expect(qux).not_to be_leaf
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '#parent_of?' do
|
84
|
+
it 'returns true if node is a parent of given node' do
|
85
|
+
expect(bar).to be_parent_of(qux)
|
86
|
+
expect(bar).not_to be_parent_of(quux)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe '#child_of?' do
|
91
|
+
it 'returns true if node is a child of given node' do
|
92
|
+
expect(qux).to be_child_of(bar)
|
93
|
+
expect(qux).not_to be_child_of(quux)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe '#sibling_of?' do
|
98
|
+
it 'returns true if both nodes have same parent' do
|
99
|
+
expect(foo).to be_sibling_of(bar)
|
100
|
+
expect(baz).to be_sibling_of(qux)
|
101
|
+
expect(foo).not_to be_sibling_of(qux)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
shared_examples 'materialized path' do
|
107
|
+
include_context 'example tree'
|
108
|
+
it_behaves_like 'adjacency list'
|
109
|
+
|
110
|
+
describe '.find_by_path' do
|
111
|
+
it 'returns node' do
|
112
|
+
expect(described_class.find_by_path('bar')).to eq(bar)
|
113
|
+
expect(described_class.find_by_path('bar/qux')).to eq(qux)
|
114
|
+
expect(described_class.find_by_path('bar/qux/quux')).to eq(quux)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#root' do
|
119
|
+
it 'returns first node ancestor' do
|
120
|
+
expect(baz.root).to eq(bar)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'returns nil if node is a root' do
|
124
|
+
expect(bar.root).to be nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
describe '#ancestors' do
|
129
|
+
it 'returns node ancestors' do
|
130
|
+
expect(quux.ancestors).to match_array([ qux, bar ])
|
131
|
+
expect(qux.ancestors).to match_array([ bar ])
|
132
|
+
expect(bar.ancestors).to be_empty
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe '#descendants' do
|
137
|
+
it 'returns node descendants' do
|
138
|
+
expect(bar.descendants).to match_array([ qux, quux, baz ])
|
139
|
+
expect(qux.descendants).to match_array([ quux ])
|
140
|
+
expect(quux.descendants).to be_empty
|
141
|
+
end
|
24
142
|
end
|
25
143
|
|
144
|
+
describe '#subtree' do
|
145
|
+
it 'returns node with descendants' do
|
146
|
+
expect(bar.subtree.tree).to be_arranged_like({
|
147
|
+
bar => {
|
148
|
+
qux => {
|
149
|
+
quux => {}
|
150
|
+
},
|
151
|
+
baz => {}
|
152
|
+
}
|
153
|
+
})
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'returns node if node is a leaf' do
|
157
|
+
expect(baz.subtree).to eq([ baz ])
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe '#root_of?' do
|
162
|
+
it 'returns true of node is a root of given node' do
|
163
|
+
expect(bar).to be_root_of(qux)
|
164
|
+
expect(bar).to be_root_of(quux)
|
165
|
+
expect(bar).not_to be_root_of(bar)
|
166
|
+
expect(bar).not_to be_root_of(foo)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe '#ancestor_of?' do
|
171
|
+
it 'returns true if node is an ancestors of given node' do
|
172
|
+
expect(bar).to be_ancestor_of(qux)
|
173
|
+
expect(bar).to be_ancestor_of(quux)
|
174
|
+
expect(bar).not_to be_ancestor_of(bar)
|
175
|
+
expect(bar).not_to be_ancestor_of(foo)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
describe '#descendant_of?' do
|
180
|
+
it 'returns true if node is a descendant of given node' do
|
181
|
+
expect(quux).to be_descendant_of(qux)
|
182
|
+
expect(quux).to be_descendant_of(bar)
|
183
|
+
expect(quux).not_to be_descendant_of(quux)
|
184
|
+
expect(quux).not_to be_descendant_of(foo)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
describe '#depth' do
|
189
|
+
it 'returns ancestors count' do
|
190
|
+
expect(bar.depth).to eq(0)
|
191
|
+
expect(qux.depth).to eq(1)
|
192
|
+
expect(quux.depth).to eq(2)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
describe 'node id column change' do
|
197
|
+
before do
|
198
|
+
bar.name = 'bor'
|
199
|
+
bar.save!
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'updates children pathes' do
|
203
|
+
expect(described_class.find_by_path('bor')).to eq(bar)
|
204
|
+
expect(described_class.find_by_path('bor/qux')).to eq(qux)
|
205
|
+
expect(described_class.find_by_path('bor/qux/quux')).to eq(quux)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
describe 'parent change' do
|
210
|
+
let(:prev_parent) { baz.parent }
|
211
|
+
let(:new_parent) { foo }
|
212
|
+
let(:new_ancestors) { [ foo ] }
|
213
|
+
|
214
|
+
before do
|
215
|
+
baz.parent = new_parent
|
216
|
+
baz.save!
|
217
|
+
reload_items
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'updates counter_cache' do
|
221
|
+
expect(prev_parent.children_count).to eq(prev_parent.children.count)
|
222
|
+
expect(new_parent.children_count).to eq(new_parent.children.count)
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'changes ancestors' do
|
226
|
+
expect(baz.ancestors).to eq(new_ancestors)
|
227
|
+
end
|
228
|
+
|
229
|
+
it 'applies to all descendants' do
|
230
|
+
baz.children.each do |child|
|
231
|
+
expect(child).to be_descendant_of(new_parent)
|
232
|
+
|
233
|
+
child.children.each do |subchild|
|
234
|
+
expect(subchild).to be_descendant_of(new_parent)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
shared_examples 'tree with cached depth' do
|
242
|
+
include_context 'example tree'
|
243
|
+
it_behaves_like 'adjacency list'
|
244
|
+
|
245
|
+
it 'stores node level' do
|
246
|
+
expect(described_class.where(depth: 0)).to match_array([ foo, bar ])
|
247
|
+
expect(described_class.where(depth: 1)).to match_array([ qux, baz ])
|
248
|
+
expect(described_class.where(depth: 2)).to match_array([ quux ])
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
shared_examples 'scoped tree' do
|
253
|
+
let!(:foo) { described_class.create!(name: 'foo', category: 'foo') }
|
254
|
+
let!(:bar) { described_class.create!(name: 'bar', category: 'bar') }
|
255
|
+
|
256
|
+
it 'restricts scope' do
|
257
|
+
expect(bar.siblings).to be_empty
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
shared_examples 'ordered tree' do
|
262
|
+
include_context 'example tree'
|
263
|
+
|
26
264
|
it '#move_after' do
|
27
265
|
quux.move_after(foo)
|
28
266
|
reload_items
|
@@ -53,6 +291,23 @@ shared_examples 'ordered tree' do
|
|
53
291
|
end
|
54
292
|
end
|
55
293
|
|
56
|
-
describe
|
294
|
+
describe AdjacencyListTreeItem do
|
295
|
+
it_behaves_like 'adjacency list'
|
57
296
|
it_behaves_like 'ordered tree'
|
58
297
|
end
|
298
|
+
|
299
|
+
describe MaterializedPathTreeItem do
|
300
|
+
it_behaves_like 'materialized path'
|
301
|
+
end
|
302
|
+
|
303
|
+
describe CachedDepthTreeItem do
|
304
|
+
it_behaves_like 'tree with cached depth'
|
305
|
+
end
|
306
|
+
|
307
|
+
describe ScopedWithColumnTreeItem do
|
308
|
+
it_behaves_like 'scoped tree'
|
309
|
+
end
|
310
|
+
|
311
|
+
describe ScopedWithLambdaTreeItem do
|
312
|
+
it_behaves_like 'scoped tree'
|
313
|
+
end
|
data/spec/support/models.rb
CHANGED
@@ -1,5 +1,29 @@
|
|
1
1
|
require 'has_hierarchy'
|
2
2
|
|
3
3
|
class Item < ActiveRecord::Base
|
4
|
-
|
4
|
+
scope :alphabetic, ->{ order('name asc') }
|
5
|
+
end
|
6
|
+
|
7
|
+
class AdjacencyListTreeItem < Item
|
8
|
+
has_hierarchy counter_cache: :children_count,
|
9
|
+
path_cache: false
|
10
|
+
end
|
11
|
+
|
12
|
+
class MaterializedPathTreeItem < Item
|
13
|
+
has_hierarchy counter_cache: :children_count,
|
14
|
+
path_part: :name
|
15
|
+
end
|
16
|
+
|
17
|
+
class CachedDepthTreeItem < Item
|
18
|
+
has_hierarchy depth_cache: true
|
19
|
+
end
|
20
|
+
|
21
|
+
class ScopedWithColumnTreeItem < Item
|
22
|
+
has_hierarchy scope: :category
|
23
|
+
end
|
24
|
+
|
25
|
+
class ScopedWithLambdaTreeItem < Item
|
26
|
+
has_hierarchy scope: ->(item){ where(category: item.category) },
|
27
|
+
# Ordering scope (parent_id) cannot be combined with lambda.
|
28
|
+
order: false
|
5
29
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: has_hierarchy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kolesnikov Danil
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-09-
|
11
|
+
date: 2014-09-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -122,21 +122,7 @@ dependencies:
|
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
124
|
version: '0.1'
|
125
|
-
|
126
|
-
name: has_children
|
127
|
-
requirement: !ruby/object:Gem::Requirement
|
128
|
-
requirements:
|
129
|
-
- - "~>"
|
130
|
-
- !ruby/object:Gem::Version
|
131
|
-
version: 0.2.1
|
132
|
-
type: :runtime
|
133
|
-
prerelease: false
|
134
|
-
version_requirements: !ruby/object:Gem::Requirement
|
135
|
-
requirements:
|
136
|
-
- - "~>"
|
137
|
-
- !ruby/object:Gem::Version
|
138
|
-
version: 0.2.1
|
139
|
-
description: Provides sortable tree behavior to active_record models.
|
125
|
+
description: Provides tree behavior to active_record models.
|
140
126
|
email:
|
141
127
|
- kolesnikovde@gmail.com
|
142
128
|
executables: []
|
@@ -153,6 +139,9 @@ files:
|
|
153
139
|
- Rakefile
|
154
140
|
- has_hierarchy.gemspec
|
155
141
|
- lib/has_hierarchy.rb
|
142
|
+
- lib/has_hierarchy/depth_cache.rb
|
143
|
+
- lib/has_hierarchy/order.rb
|
144
|
+
- lib/has_hierarchy/path.rb
|
156
145
|
- lib/has_hierarchy/version.rb
|
157
146
|
- spec/db/schema.rb
|
158
147
|
- spec/has_hierarchy_spec.rb
|
@@ -182,7 +171,7 @@ rubyforge_project:
|
|
182
171
|
rubygems_version: 2.2.2
|
183
172
|
signing_key:
|
184
173
|
specification_version: 4
|
185
|
-
summary: Provides
|
174
|
+
summary: Provides tree behavior to active_record models.
|
186
175
|
test_files:
|
187
176
|
- spec/db/schema.rb
|
188
177
|
- spec/has_hierarchy_spec.rb
|