nobrainer-tree 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0a7bca7cff2679bd8fbd69bc4ccdbeba8e59f50d
4
+ data.tar.gz: 75eae734d058953c9e63840e07176b1e325defde
5
+ SHA512:
6
+ metadata.gz: 3149e67069144dfd3bf675501c7d8ce41f7eb33e152396e07ff14b8cdce77d722f258a3212a6106b536b9ddbf3d34177062916c985b2ae4c0c78d3a030dd8799
7
+ data.tar.gz: 3eb4a08d9067691283368b9cc04e04f1a64c947366ad7902ae5ffb11e6a4a721ee7a5b40d3a7051e186c2f0e46902eea6b345b96ff34b8c58563a25ae2b54b03
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'guard-rspec', '>= 0.6.0'
6
+ gem 'ruby_gntp', '>= 0.3.4'
7
+ gem 'rb-fsevent' if RUBY_PLATFORM =~ /darwin/
8
+ gem 'nobrainer', :github => 'nviennot/nobrainer'
9
+
10
+ platforms :rbx do
11
+ gem 'rubysl-rake', '~> 2.0'
12
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010-2013 Benedikt Deicke
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,220 @@
1
+ # nobrainer-tree ![Build Status](https://travis-ci.org/secondimpression/nobrainer-tree.svg?branch=nobrainer) [![Dependency Status](https://gemnasium.com/secondimpression/nobrainer-tree.png)](https://gemnasium.com/secondimpression/nobrainer-tree)
2
+
3
+ A tree structure for NoBrainer documents using the materialized path pattern
4
+
5
+ ## Requirements
6
+
7
+ * nobrainer (~> 0.20.0)
8
+
9
+
10
+ ## Install
11
+
12
+ To install nobrainer-tree, simply add it to your Gemfile:
13
+
14
+ gem 'nobrainer-tree', :require => 'nobrainer/tree'
15
+
16
+ In order to get the latest development version of nobrainer-tree:
17
+
18
+ gem 'nobrainer-tree', :git => 'git://github.com/secondimpression/nobrainer-tree'
19
+
20
+ You might want to add `:require => nil` option and explicitly `require 'nobrainer/tree'` where needed and finally run
21
+
22
+ bundle install
23
+
24
+
25
+ ## Usage
26
+
27
+ Read the API documentation at https://github.com/secondimpression/nobrainer-tree and take a look at the `NoBrainer::Tree` module
28
+
29
+ ```ruby
30
+ class Node
31
+ include NoBrainer::Document
32
+ include NoBrainer::Tree
33
+ end
34
+ ```
35
+
36
+
37
+ ### Utility methods
38
+
39
+ There are several utility methods that help getting to other related documents in the tree:
40
+
41
+ ```ruby
42
+ Node.root
43
+ Node.roots
44
+ Node.leaves
45
+
46
+ node.root
47
+ node.parent
48
+ node.children
49
+ node.ancestors
50
+ node.ancestors_and_self
51
+ node.descendants
52
+ node.descendants_and_self
53
+ node.siblings
54
+ node.siblings_and_self
55
+ node.leaves
56
+ ```
57
+
58
+ In addition it's possible to check certain aspects of the document's position in the tree:
59
+
60
+ ```ruby
61
+ node.root?
62
+ node.leaf?
63
+ node.depth
64
+ node.ancestor_of?(other)
65
+ node.descendant_of?(other)
66
+ node.sibling_of?(other)
67
+ ```
68
+
69
+ See `NoBrainer::Tree` for more information on these methods.
70
+
71
+
72
+ ### Ordering
73
+
74
+ `NoBrainer::Tree` doesn't order children by default. To enable ordering of tree nodes include the `NoBrainer::Tree::Ordering` module. This will add a `position` field to your document and provide additional utility methods:
75
+
76
+ ```ruby
77
+ node.lower_siblings
78
+ node.higher_siblings
79
+ node.first_sibling_in_list
80
+ node.last_sibling_in_list
81
+
82
+ node.move_up
83
+ node.move_down
84
+ node.move_to_top
85
+ node.move_to_bottom
86
+ node.move_above(other)
87
+ node.move_below(other)
88
+
89
+ node.at_top?
90
+ node.at_bottom?
91
+ ```
92
+
93
+ Example:
94
+
95
+ ```ruby
96
+ class Node
97
+ include NoBrainer::Document
98
+ include NoBrainer::Tree
99
+ include NoBrainer::Tree::Ordering
100
+ end
101
+ ```
102
+
103
+ See `NoBrainer::Tree::Ordering` for more information on these methods.
104
+
105
+
106
+ ### Traversal
107
+
108
+ It's possible to traverse the tree using different traversal methods using the `NoBrainer::Tree::Traversal` module.
109
+
110
+ Example:
111
+
112
+ ```ruby
113
+ class Node
114
+ include NoBrainer::Document
115
+ include NoBrainer::Tree
116
+ include NoBrainer::Tree::Traversal
117
+ end
118
+
119
+ node.traverse(:breadth_first) do |n|
120
+ # Do something with Node n
121
+ end
122
+ ```
123
+
124
+
125
+ ### Destroying
126
+
127
+ `NoBrainer::Tree` does not handle destroying of nodes by default. However it provides several strategies that help you to deal with children of deleted documents. You can simply add them as `before_destroy` callbacks.
128
+
129
+ Available strategies are:
130
+
131
+ * `:nullify_children` -- Sets the children's parent_id to null
132
+ * `:move_children_to_parent` -- Moves the children to the current document's parent
133
+ * `:destroy_children` -- Destroys all children by calling their `#destroy` method (invokes callbacks)
134
+ * `:delete_descendants` -- Deletes all descendants using a database query (doesn't invoke callbacks)
135
+
136
+ Example:
137
+
138
+ ```ruby
139
+ class Node
140
+ include NoBrainer::Document
141
+ include NoBrainer::Tree
142
+
143
+ before_destroy :nullify_children
144
+ end
145
+ ```
146
+
147
+
148
+ ### Callbacks
149
+
150
+ There are two callbacks that are called before and after the rearranging process. This enables you to do additional computations after the documents position in the tree is updated. See `NoBrainer::Tree` for details.
151
+
152
+ Example:
153
+
154
+ ```ruby
155
+ class Page
156
+ include NoBrainer::Document
157
+ include NoBrainer::Tree
158
+
159
+ after_rearrange :rebuild_path
160
+
161
+ field :slug
162
+ field :path
163
+
164
+ private
165
+
166
+ def rebuild_path
167
+ self.path = self.ancestors_and_self.collect(&:slug).join('/')
168
+ end
169
+ end
170
+ ```
171
+
172
+
173
+ ### Validations
174
+
175
+ `NoBrainer::Tree` currently does not validate the document's children or parent associations by default. To explicitly enable validation for children and parent documents it's required to add a `validates_associated` validation.
176
+
177
+ Example:
178
+
179
+ ```ruby
180
+ class Node
181
+ include NoBrainer::Document
182
+ include NoBrainer::Tree
183
+
184
+ validates_associated :parent, :children
185
+ end
186
+ ```
187
+
188
+
189
+ ## Build Status
190
+
191
+ nobrainer-tree is on [Travis CI](http://travis-ci.org/secondimpression/nobrainer-tree) running the specs on Ruby Head, Ruby 1.9.3, Ruby 2.0 and Ruby 2.1
192
+
193
+
194
+ ## Known issues
195
+
196
+ See [https://github.com/secondimpression/nobrainer-tree/issues](https://github.com/secondimpression/nobrainer-tree/issues)
197
+
198
+
199
+ ## Repository
200
+
201
+ See [https://github.com/secondimpression/nobrainer-tree](https://github.com/secondimpression/nobrainer-tree) and feel free to fork it!
202
+
203
+
204
+ ## Contributors
205
+
206
+ See a list of all contributors at [https://github.com/benedikt/mongoid-tree/contributors](https://github.com/benedikt/mongoid-tree/contributors) and [https://github.com/secondimpression/nobrainer-tree/contributors](https://github.com/secondimpression/nobrainer-tree/contributors). Thanks a lot everyone!
207
+
208
+
209
+ ## Support
210
+
211
+ If you like nobrainer-tree and want to support the development, the original author would appreciate a small donation:
212
+
213
+ [![Pledgie](http://www.pledgie.com/campaigns/12137.png?skin_name=chrome)](http://www.pledgie.com/campaigns/12137)
214
+
215
+ [![Flattr](https://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=benediktdeicke&url=https://github.com/benedikt/mongoid-tree&title=mongoid-tree&language=&tags=github&category=software)
216
+
217
+
218
+ ## Copyright
219
+
220
+ Copyright (c) 2010-2013 Benedikt Deicke. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'yard'
3
+
4
+ spec = Gem::Specification.load("nobrainer-tree.gemspec")
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default => :spec
9
+
10
+ YARD::Rake::YardocTask.new(:doc)
11
+
12
+ desc "Build the .gem file"
13
+ task :build do
14
+ system "gem build #{spec.name}.gemspec"
15
+ end
16
+
17
+ desc "Push the .gem file to rubygems.org"
18
+ task :release => :build do
19
+ system "gem push #{spec.name}-#{spec.version}.gem"
20
+ end
21
+
22
+ desc "Open an irb session"
23
+ task :console do
24
+ sh "irb -rubygems -I lib -r ./spec/spec_helper.rb"
25
+ end
@@ -0,0 +1,238 @@
1
+ module NoBrainer
2
+ module Tree
3
+ ##
4
+ # = NoBrainer::Tree::Ordering
5
+ #
6
+ # NoBrainer::Tree doesn't order the tree by default. To enable ordering of children
7
+ # include both NoBrainer::Tree and NoBrainer::Tree::Ordering into your document.
8
+ #
9
+ # == Utility methods
10
+ #
11
+ # This module adds methods to get related siblings depending on their position:
12
+ #
13
+ # node.lower_siblings
14
+ # node.higher_siblings
15
+ # node.first_sibling_in_list
16
+ # node.last_sibling_in_list
17
+ #
18
+ # There are several methods to move nodes around in the list:
19
+ #
20
+ # node.move_up
21
+ # node.move_down
22
+ # node.move_to_top
23
+ # node.move_to_bottom
24
+ # node.move_above(other)
25
+ # node.move_below(other)
26
+ #
27
+ # Additionally there are some methods to check aspects of the document
28
+ # in the list of children:
29
+ #
30
+ # node.at_top?
31
+ # node.at_bottom?
32
+ module Ordering
33
+ extend ActiveSupport::Concern
34
+
35
+ included do
36
+ field :position, :type => Integer
37
+
38
+ default_scope -> { order_by(:position => :asc) }
39
+
40
+ before_save :assign_default_position, :if => :assign_default_position?
41
+ before_save :reposition_former_siblings, :if => :sibling_reposition_required?
42
+ after_destroy :move_lower_siblings_up
43
+ end
44
+
45
+ def inc(increments)
46
+ update(increments.symbolize_keys.map{ |field, value| { field => (self[field] || 0) + value } }.first)
47
+ self
48
+ end
49
+
50
+ ##
51
+ # Returns a chainable criteria for this document's ancestors
52
+ #
53
+ # @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document's ancestors
54
+ def ancestors
55
+ base_class.unscoped.without_index.where(:id.in => self.parent_ids).order_by(:depth => :asc)
56
+ end
57
+
58
+ ##
59
+ # Returns siblings below the current document.
60
+ # Siblings with a position greater than this document's position.
61
+ #
62
+ # @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document's lower siblings
63
+ def lower_siblings
64
+ self.siblings.where(:position.gt => self.position)
65
+ end
66
+
67
+ ##
68
+ # Returns siblings above the current document.
69
+ # Siblings with a position lower than this document's position.
70
+ #
71
+ # @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document's higher siblings
72
+ def higher_siblings
73
+ self.siblings.where(:position.lt => self.position)
74
+ end
75
+
76
+ ##
77
+ # Returns siblings between the current document and the other document
78
+ # Siblings with a position between this document's position and the other document's position.
79
+ #
80
+ # @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the documents between this and the other document
81
+ def siblings_between(other)
82
+ range = [self.position, other.position].sort
83
+ self.siblings.where(:position.gt => range.first, :position.lt => range.last)
84
+ end
85
+
86
+ ##
87
+ # Returns the lowest sibling (could be self)
88
+ #
89
+ # @return [NoBrainer::Document] The lowest sibling
90
+ def last_sibling_in_list
91
+ siblings_and_self.last
92
+ end
93
+
94
+ ##
95
+ # Returns the highest sibling (could be self)
96
+ #
97
+ # @return [NoBrainer::Document] The highest sibling
98
+ def first_sibling_in_list
99
+ siblings_and_self.first
100
+ end
101
+
102
+ ##
103
+ # Is this the highest sibling?
104
+ #
105
+ # @return [Boolean] Whether the document is the highest sibling
106
+ def at_top?
107
+ higher_siblings.empty?
108
+ end
109
+
110
+ ##
111
+ # Is this the lowest sibling?
112
+ #
113
+ # @return [Boolean] Whether the document is the lowest sibling
114
+ def at_bottom?
115
+ lower_siblings.empty?
116
+ end
117
+
118
+ ##
119
+ # Move this node above all its siblings
120
+ #
121
+ # @return [undefined]
122
+ def move_to_top
123
+ return true if at_top?
124
+ move_above(first_sibling_in_list)
125
+ end
126
+
127
+ ##
128
+ # Move this node below all its siblings
129
+ #
130
+ # @return [undefined]
131
+ def move_to_bottom
132
+ return true if at_bottom?
133
+ move_below(last_sibling_in_list)
134
+ end
135
+
136
+ ##
137
+ # Move this node one position up
138
+ #
139
+ # @return [undefined]
140
+ def move_up
141
+ switch_with_sibling_at_offset(-1) unless at_top?
142
+ end
143
+
144
+ ##
145
+ # Move this node one position down
146
+ #
147
+ # @return [undefined]
148
+ def move_down
149
+ switch_with_sibling_at_offset(1) unless at_bottom?
150
+ end
151
+
152
+ ##
153
+ # Move this node above the specified node
154
+ #
155
+ # This method changes the node's parent if nescessary.
156
+ #
157
+ # @param [NoBrainer::Tree] other document to move this document above
158
+ #
159
+ # @return [undefined]
160
+ def move_above(other)
161
+ ensure_to_be_sibling_of(other)
162
+
163
+ if position > other.position
164
+ new_position = other.position
165
+ self.siblings_between(other).each{ |object| object.inc(:position => 1) }
166
+ other.inc(:position => 1)
167
+ else
168
+ new_position = other.position - 1
169
+ self.siblings_between(other).each{ |object| object.inc(:position => -1) }
170
+ end
171
+
172
+ self.position = new_position
173
+ save
174
+ end
175
+
176
+ ##
177
+ # Move this node below the specified node
178
+ #
179
+ # This method changes the node's parent if nescessary.
180
+ #
181
+ # @param [NoBrainer::Tree] other document to move this document below
182
+ #
183
+ # @return [undefined]
184
+ def move_below(other)
185
+ ensure_to_be_sibling_of(other)
186
+
187
+ if position > other.position
188
+ new_position = other.position + 1
189
+ self.siblings_between(other).each{ |object| object.inc(:position => 1) }
190
+ else
191
+ new_position = other.position
192
+ self.siblings_between(other).each{ |object| object.inc(:position => -1) }
193
+ other.inc(:position => -1)
194
+ end
195
+
196
+ self.position = new_position
197
+ save
198
+ end
199
+
200
+ private
201
+
202
+ def switch_with_sibling_at_offset(offset)
203
+ siblings.where(:position => self.position + offset).first.inc(:position => -offset)
204
+ inc(:position => offset)
205
+ end
206
+
207
+ def ensure_to_be_sibling_of(other)
208
+ return if sibling_of?(other)
209
+ self.parent_id = other.parent_id
210
+ save
211
+ end
212
+
213
+ def move_lower_siblings_up
214
+ lower_siblings.each{ |object| object.inc(:position => -1) }
215
+ end
216
+
217
+ def reposition_former_siblings
218
+ former_siblings = base_class.where(:parent_id => self.parent_id_was).
219
+ where(:position.gt => (self.position_was || 0)).
220
+ where(:id.ne => self.id)
221
+ former_siblings.to_a.each{ |object| object.inc(:position => -1) }
222
+ end
223
+
224
+ def sibling_reposition_required?
225
+ parent_id_changed? && persisted?
226
+ end
227
+
228
+ def assign_default_position
229
+ self.position = self.last_sibling_in_list.position + 1 rescue 0
230
+ end
231
+
232
+ def assign_default_position?
233
+ self.position.nil? || self.parent_id_changed?
234
+ end
235
+
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,122 @@
1
+ module NoBrainer
2
+ module Tree
3
+ ##
4
+ # = NoBrainer::Tree::Traversal
5
+ #
6
+ # NoBrainer::Tree::Traversal provides a #traverse method to walk through the tree.
7
+ # It supports these traversal methods:
8
+ #
9
+ # * depth_first
10
+ # * breadth_first
11
+ #
12
+ # == Depth First Traversal
13
+ #
14
+ # See http://en.wikipedia.org/wiki/Depth-first_search for a proper description.
15
+ #
16
+ # Given a tree like:
17
+ #
18
+ # node1:
19
+ # - node2:
20
+ # - node3
21
+ # - node4:
22
+ # - node5
23
+ # - node6
24
+ # - node7
25
+ #
26
+ # Traversing the tree using depth first traversal would visit each node in this order:
27
+ #
28
+ # node1, node2, node3, node4, node5, node6, node7
29
+ #
30
+ # == Breadth First Traversal
31
+ #
32
+ # See http://en.wikipedia.org/wiki/Breadth-first_search for a proper description.
33
+ #
34
+ # Given a tree like:
35
+ #
36
+ # node1:
37
+ # - node2:
38
+ # - node5
39
+ # - node3:
40
+ # - node6
41
+ # - node7
42
+ # - node4
43
+ #
44
+ # Traversing the tree using breadth first traversal would visit each node in this order:
45
+ #
46
+ # node1, node2, node3, node4, node5, node6, node7
47
+ #
48
+ module Traversal
49
+ extend ActiveSupport::Concern
50
+
51
+
52
+ ##
53
+ # This module implements class methods that will be available
54
+ # on the document that includes NoBrainer::Tree::Traversal
55
+ module ClassMethods
56
+ ##
57
+ # Traverses the entire tree, one root at a time, using the given traversal
58
+ # method (Default is :depth_first).
59
+ #
60
+ # See NoBrainer::Tree::Traversal for available traversal methods.
61
+ #
62
+ # @example
63
+ #
64
+ # # Say we have the following tree, and want to print its hierarchy:
65
+ # # root_1
66
+ # # child_1_a
67
+ # # root_2
68
+ # # child_2_a
69
+ # # child_2_a_1
70
+ #
71
+ # Node.traverse(:depth_first) do |node|
72
+ # indentation = ' ' * node.depth
73
+ #
74
+ # puts "#{indentation}#{node.name}"
75
+ # end
76
+ #
77
+ def traverse(type = :depth_first, &block)
78
+ roots.collect { |root| root.traverse(type, &block) }.flatten
79
+ end
80
+ end
81
+
82
+ ##
83
+ # Traverses the tree using the given traversal method (Default is :depth_first)
84
+ # and passes each document node to the block.
85
+ #
86
+ # See NoBrainer::Tree::Traversal for available traversal methods.
87
+ #
88
+ # @example
89
+ #
90
+ # results = []
91
+ # root.traverse(:depth_first) do |node|
92
+ # results << node
93
+ # end
94
+ #
95
+ # root.traverse(:depth_first).map(&:name)
96
+ # root.traverse(:depth_first, &:name)
97
+ #
98
+ def traverse(type = :depth_first, &block)
99
+ block ||= lambda { |node| node }
100
+ send("#{type}_traversal", &block)
101
+ end
102
+
103
+ private
104
+
105
+ def depth_first_traversal(&block)
106
+ result = [block.call(self)] + self.children.collect { |c| c.send(:depth_first_traversal, &block) }
107
+ result.flatten
108
+ end
109
+
110
+ def breadth_first_traversal(&block)
111
+ result = []
112
+ queue = [self]
113
+ while queue.any? do
114
+ node = queue.shift
115
+ result << block.call(node)
116
+ queue += node.children
117
+ end
118
+ result
119
+ end
120
+ end
121
+ end
122
+ end