nobrainer-tree 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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