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 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