has_hierarchy 0.2.2 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://badge.fury.io/rb/has_hierarchy.svg)](http://badge.fury.io/rb/has_hierarchy)
|
2
|
-
[![Build Status](https://travis-ci.org/kolesnikovde/has_hierarchy.svg
|
2
|
+
[![Build Status](https://api.travis-ci.org/kolesnikovde/has_hierarchy.svg)](https://travis-ci.org/kolesnikovde/has_hierarchy)
|
3
3
|
[![Code Climate](https://codeclimate.com/github/kolesnikovde/has_hierarchy/badges/gpa.svg)](https://codeclimate.com/github/kolesnikovde/has_hierarchy)
|
4
4
|
[![Test Coverage](https://codeclimate.com/github/kolesnikovde/has_hierarchy/badges/coverage.svg)](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
|