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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c92de68d598ce6447ce4688b7425659aea75bc7c
4
- data.tar.gz: 6d7c9fc5daadf07893bfbacec56ac7dce23476e0
3
+ metadata.gz: abe351194af5c2a75cbaad480fce586064fddad1
4
+ data.tar.gz: 283c8fd7fd5b8d27fdbe3b15bc4af195a90b15b5
5
5
  SHA512:
6
- metadata.gz: e508fdf364899edbbad77b0ab59a5243442f2829622c82d0f709edb4b36e20dfbbec8c815cf8c7616ac1989f0ce9ea4bef3009c67274d43fb99717933dedc81d
7
- data.tar.gz: e0fe3d89fbf0e180bc88be851c96aabc7fc6ad576b3273fba992547dd49ee9dcddc906cc1d8651513d70f442c63d2c1917347f40a9c13c66c2bc57c8b82c544f
6
+ metadata.gz: 8b529a4e0c3a0937d215feb5c77647c7ef9e7877ecdca68365bc888789d6abc050675e29ac0526dc27b14f7ece94b610849fdb2a6b982093f6e4ba779a414376
7
+ data.tar.gz: 12978bfbbbf31fd2d8ffcaed16c67fe1c029076af8365708ff54cb56de6accfe11879cb80b82fc5fb71880bf20d07ce777fc97550ffcd5cda54a3af3979e8497
data/.gitignore CHANGED
@@ -1,6 +1,6 @@
1
- /*.gem
1
+ *.gem
2
2
  /.bundle
3
3
  /Gemfile.lock
4
- /log
4
+ /pkg
5
5
  /tmp
6
6
  /coverage
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
- - has_children-0.2.2
6
- - Rewrited specs.
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
- - has_children-0.2.0
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 README.md
15
- - Updated dependencies.
16
- - has_children-0.1.3
17
- - has_order-0.1.2
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
- - has_children-0.1.2
45
+ - Fixed scopes (always using lambdas).
22
46
 
23
47
  ## 0.1.0
24
48
 
25
- - Added `#set_default_position?` support.
26
- - Updated dependencies.
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?branch=master)](https://travis-ci.org/kolesnikovde/has_hierarchy)
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 sortable tree behavior to active_record models.
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 migration CreateItems \
24
+ $ rails g model Item \
24
25
  name:string \
25
- parent:belongs_to \
26
+ path:string \
27
+ depth:integer \
26
28
  position:integer \
27
- node_path:string
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
- Item.tree # => {
41
- # foo => {},
42
- # bar => {
43
- # qux => {
44
- # quux => {}
45
- # },
46
- # baz => {}
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
- foo.move_after(quux)
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
- Item.tree # => {
53
- # bar => {
54
- # qux => {
55
- # quux => {},
56
- # foo => {}
57
- # },
58
- # baz => {}
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
- See [has_children](https://github.com/kolesnikovde/has_children) and
65
- [has_order](https://github.com/kolesnikovde/has_order) for details.
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
 
@@ -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 sortable tree behavior to active_record models.'
13
- spec.summary = 'Provides sortable tree behavior to active_record models.'
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
@@ -1,3 +1,3 @@
1
1
  module HasHierarchy
2
- VERSION = '0.2.2'
2
+ VERSION = '0.3.0'
3
3
  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 options = {}
8
- has_order options.merge(scope: :parent_id)
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 InstanceMethods
17
- def move_before node
18
- accept_parent(node)
19
- super
30
+ module ClassMethods
31
+ def roots
32
+ where(parent_id: nil)
20
33
  end
21
34
 
22
- def move_after node
23
- accept_parent(node)
24
- super
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 accept_parent node
30
- self.parent_id = node.parent_id
31
- @parent_accepted = true
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 reset_parent_acceptance
35
- @parent_accepted = false
75
+ def parent_of?(node)
76
+ node.parent_id == id
36
77
  end
37
78
 
38
- def set_default_position?
39
- super or parent_id_changed? and not @parent_accepted
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 :node_path
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
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- shared_examples 'ordered tree' do
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
- it do
15
- expect(described_class.tree).to be_arranged_like({
16
- foo => {},
17
- bar => {
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 Item do
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
@@ -1,5 +1,29 @@
1
1
  require 'has_hierarchy'
2
2
 
3
3
  class Item < ActiveRecord::Base
4
- has_hierarchy
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.2.2
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-04 00:00:00.000000000 Z
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
- - !ruby/object:Gem::Dependency
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 sortable tree behavior to active_record models.
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