locomotive-mongoid-tree 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ gem 'bson_ext', '>= 1.0.4', :platform => :ruby
6
+ gem 'SystemTimer', '>= 1.2.0', :platform => :ruby_18
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 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.
@@ -0,0 +1,192 @@
1
+ = mongoid-tree http://travis-ci.org/benedikt/mongoid-tree.png
2
+
3
+ A tree structure for Mongoid documents using the materialized path pattern
4
+
5
+ == Requirements
6
+
7
+ * mongoid (~> 2.0)
8
+
9
+
10
+ == Install
11
+
12
+ To install mongoid_tree, simply add it to your Gemfile:
13
+
14
+ gem 'mongoid-tree', :require => 'mongoid/tree'
15
+
16
+ In order to get the latest development version of mongoid-tree:
17
+
18
+ gem 'mongoid-tree', :git => 'git://github.com/benedikt/mongoid-tree', :require => 'mongoid/tree'
19
+
20
+ You might want to remove the <tt>:require => 'mongoid/tree'</tt> option and explicitly <tt>require 'mongoid/tree'</tt> where needed and finally run
21
+
22
+ bundle install
23
+
24
+
25
+ == Usage
26
+
27
+ Read the API documentation at http://benedikt.github.com/mongoid-tree and take a look at the Mongoid::Tree module
28
+
29
+ class Node
30
+ include Mongoid::Document
31
+ include Mongoid::Tree
32
+ end
33
+
34
+
35
+ === Utility methods
36
+
37
+ There are several utility methods that help getting to other related documents in the tree:
38
+
39
+ Node.root
40
+ Node.roots
41
+ Node.leaves
42
+
43
+ node.root
44
+ node.parent
45
+ node.children
46
+ node.ancestors
47
+ node.ancestors_and_self
48
+ node.descendants
49
+ node.descendants_and_self
50
+ node.siblings
51
+ node.siblings_and_self
52
+ node.leaves
53
+
54
+ In addition it's possible to check certain aspects of the document's position in the tree:
55
+
56
+ node.root?
57
+ node.leaf?
58
+ node.depth
59
+ node.ancestor_of?(other)
60
+ node.descendant_of?(other)
61
+ node.sibling_of?(other)
62
+
63
+ See Mongoid::Tree for more information on these methods.
64
+
65
+
66
+ === Ordering
67
+
68
+ Mongoid::Tree doesn't order children by default. To enable ordering of tree nodes include the Mongoid::Tree::Ordering module. This will add a <tt>position</tt> field to your document and provide additional utility methods:
69
+
70
+ node.lower_siblings
71
+ node.higher_siblings
72
+ node.first_sibling_in_list
73
+ node.last_sibling_in_list
74
+
75
+ node.move_up
76
+ node.move_down
77
+ node.move_to_top
78
+ node.move_to_bottom
79
+ node.move_above(other)
80
+ node.move_below(other)
81
+
82
+ node.at_top?
83
+ node.at_bottom?
84
+
85
+ Example:
86
+
87
+ class Node
88
+ include Mongoid::Document
89
+ include Mongoid::Tree
90
+ include Mongoid::Tree::Ordering
91
+ end
92
+
93
+ See Mongoid::Tree::Ordering for more information on these methods.
94
+
95
+ === Traversal
96
+
97
+ It's possible to traverse the tree using different traversal methods using the Mongoid::Tree::Traversal module.
98
+
99
+ Example:
100
+
101
+ class Node
102
+ include Mongoid::Document
103
+ include Mongoid::Tree
104
+ include Mongoid::Tree::Traversal
105
+ end
106
+
107
+ node.traverse(:breadth_first) do |n|
108
+ # Do something with Node n
109
+ end
110
+
111
+
112
+ === Destroying
113
+
114
+ Mongoid::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 <tt>before_destroy</tt> callbacks.
115
+
116
+ Available strategies are:
117
+
118
+ * :nullify_children -- Sets the children's parent_id to null
119
+ * :move_children_to_parent -- Moves the children to the current document's parent
120
+ * :destroy_children -- Destroys all children by calling their #destroy method (invokes callbacks)
121
+ * :delete_descendants -- Deletes all descendants using a database query (doesn't invoke callbacks)
122
+
123
+ Example:
124
+
125
+ class Node
126
+ include Mongoid::Document
127
+ include Mongoid::Tree
128
+
129
+ before_destroy :nullify_children
130
+ end
131
+
132
+
133
+ === Callbacks
134
+
135
+ 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 Mongoid::Tree for details.
136
+
137
+ Example:
138
+
139
+ class Page
140
+ include Mongoid::Document
141
+ include Mongoid::Tree
142
+
143
+ after_rearrange :rebuild_path
144
+
145
+ field :slug
146
+ field :path
147
+
148
+ private
149
+
150
+ def rebuild_path
151
+ self.path = self.ancestors_and_self.collect(&:slug).join('/')
152
+ end
153
+ end
154
+
155
+
156
+ === Validations
157
+
158
+ Mongoid::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 <tt>validates_associated</tt> validation.
159
+
160
+ Example
161
+
162
+ class Node
163
+ include Mongoid::Document
164
+ include Mongoid::Tree
165
+
166
+ validates_associated :parent, :children
167
+ end
168
+
169
+
170
+ == Build Status
171
+
172
+ mongoid-tree is on {Travis}[http://travis-ci.org/benedikt/mongoid-tree] running the specs on Ruby 1.8.7, Ruby 1.9.2 and Ruby Enterprise Edition.
173
+
174
+
175
+ == Known issues
176
+
177
+ See https://github.com/benedikt/mongoid-tree/issues
178
+
179
+
180
+ == Repository
181
+
182
+ See https://github.com/benedikt/mongoid-tree and feel free to fork it!
183
+
184
+
185
+ == Contributors
186
+
187
+ See a list of all contributors at https://github.com/benedikt/mongoid-tree/contributors. Thanks a lot everyone!
188
+
189
+
190
+ == Copyright
191
+
192
+ Copyright (c) 2010-2011 Benedikt Deicke. See LICENSE for details.
@@ -0,0 +1,31 @@
1
+ require 'rspec/core/rake_task'
2
+ require 'rdoc/task'
3
+
4
+ spec = Gem::Specification.load("locomotive-mongoid-tree.gemspec")
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default => :spec
9
+
10
+ RDoc::Task.new do |rdoc|
11
+ rdoc.rdoc_dir = 'doc'
12
+ rdoc.title = "#{spec.name} #{spec.version}"
13
+ rdoc.options += spec.rdoc_options
14
+ rdoc.rdoc_files.include(spec.extra_rdoc_files)
15
+ rdoc.rdoc_files.include('lib/**/*.rb')
16
+ end
17
+
18
+ desc "Build the .gem file"
19
+ task :build do
20
+ system "gem build #{spec.name}.gemspec"
21
+ end
22
+
23
+ desc "Push the .gem file to rubygems.org"
24
+ task :release => :build do
25
+ system "gem push #{spec.name}-#{spec.version}.gem"
26
+ end
27
+
28
+ desc "Open an irb session"
29
+ task :console do
30
+ sh "irb -rubygems -I lib -r ./spec/spec_helper.rb"
31
+ end
@@ -0,0 +1,318 @@
1
+ module Mongoid # :nodoc:
2
+ ##
3
+ # = Mongoid::Tree
4
+ #
5
+ # This module extends any Mongoid document with tree functionality.
6
+ #
7
+ # == Usage
8
+ #
9
+ # Simply include the module in any Mongoid document:
10
+ #
11
+ # class Node
12
+ # include Mongoid::Document
13
+ # include Mongoid::Tree
14
+ # end
15
+ #
16
+ # === Using the tree structure
17
+ #
18
+ # Each document references many children. You can access them using the <tt>#children</tt> method.
19
+ #
20
+ # node = Node.create
21
+ # node.children.create
22
+ # node.children.count # => 1
23
+ #
24
+ # Every document references one parent (unless it's a root document).
25
+ #
26
+ # node = Node.create
27
+ # node.parent # => nil
28
+ # node.children.create
29
+ # node.children.first.parent # => node
30
+ #
31
+ # === Destroying
32
+ #
33
+ # Mongoid::Tree does not handle destroying of nodes by default. However it provides
34
+ # several strategies that help you to deal with children of deleted documents. You can
35
+ # simply add them as <tt>before_destroy</tt> callbacks.
36
+ #
37
+ # Available strategies are:
38
+ #
39
+ # * :nullify_children -- Sets the children's parent_id to null
40
+ # * :move_children_to_parent -- Moves the children to the current document's parent
41
+ # * :destroy_children -- Destroys all children by calling their #destroy method (invokes callbacks)
42
+ # * :delete_descendants -- Deletes all descendants using a database query (doesn't invoke callbacks)
43
+ #
44
+ # Example:
45
+ #
46
+ # class Node
47
+ # include Mongoid::Document
48
+ # include Mongoid::Tree
49
+ #
50
+ # before_destroy :nullify_children
51
+ # end
52
+ #
53
+ # === Callbacks
54
+ #
55
+ # Mongoid::Tree offers callbacks for its rearranging process. This enables you to
56
+ # rebuild certain fields when the document was moved in the tree. Rearranging happens
57
+ # before the document is validated. This gives you a chance to validate your additional
58
+ # changes done in your callbacks. See ActiveModel::Callbacks and ActiveSupport::Callbacks
59
+ # for further details on callbacks.
60
+ #
61
+ # Example:
62
+ #
63
+ # class Page
64
+ # include Mongoid::Document
65
+ # include Mongoid::Tree
66
+ #
67
+ # after_rearrange :rebuild_path
68
+ #
69
+ # field :slug
70
+ # field :path
71
+ #
72
+ # private
73
+ #
74
+ # def rebuild_path
75
+ # self.path = self.ancestors_and_self.collect(&:slug).join('/')
76
+ # end
77
+ # end
78
+ #
79
+ module Tree
80
+ extend ActiveSupport::Concern
81
+
82
+ autoload :Ordering, 'mongoid/tree/ordering'
83
+ autoload :Traversal, 'mongoid/tree/traversal'
84
+
85
+ included do
86
+ references_many :children, :class_name => self.name, :foreign_key => :parent_id, :inverse_of => :parent, :autosave => true, :validate => false
87
+
88
+ referenced_in :parent, :class_name => self.name, :inverse_of => :children, :index => true, :validate => false
89
+
90
+ field :parent_ids, :type => Array, :default => []
91
+ index :parent_ids
92
+
93
+ set_callback :save, :after, :rearrange_children, :if => :rearrange_children?
94
+ set_callback :validation, :before do
95
+ run_callbacks(:rearrange) { rearrange }
96
+ end
97
+
98
+ validate :position_in_tree
99
+
100
+ define_model_callbacks :rearrange, :only => [:before, :after]
101
+
102
+ class_eval "def base_class; ::#{self.name}; end"
103
+ end
104
+
105
+ ##
106
+ # :singleton-method: root
107
+ # Returns the first root document
108
+
109
+ ##
110
+ # :singleton-method: roots
111
+ # Returns all root documents
112
+
113
+ ##
114
+ # :singleton-method: leaves
115
+ # Returns all leaves (be careful, currently involves two queries)
116
+
117
+ ##
118
+ # This module includes those methods documented above
119
+ module ClassMethods # :nodoc:
120
+
121
+ def root
122
+ first(:conditions => { :parent_id => nil })
123
+ end
124
+
125
+ def roots
126
+ where(:parent_id => nil)
127
+ end
128
+
129
+ def leaves
130
+ where(:_id.nin => only(:parent_id).collect(&:parent_id))
131
+ end
132
+
133
+ end
134
+
135
+ ##
136
+ # :singleton-method: before_rearrange
137
+ # Sets a callback that is called before the document is rearranged
138
+ # (Generated by ActiveSupport)
139
+
140
+ ##
141
+ # :singleton-method: after_rearrange
142
+ # Sets a callback that is called after the document is rearranged
143
+ # (Generated by ActiveSupport)
144
+
145
+ ##
146
+ # :method: children
147
+ # Returns a list of the document's children. It's a <tt>references_many</tt> association.
148
+ # (Generated by Mongoid)
149
+
150
+ ##
151
+ # :method: parent
152
+ # Returns the document's parent (unless it's a root document). It's a <tt>referenced_in</tt> association.
153
+ # (Generated by Mongoid)
154
+
155
+ ##
156
+ # :method: parent=
157
+ #call-seq:
158
+ # parent= document
159
+ #
160
+ # Sets this documents parent document.
161
+ # (Generated by Mongoid)
162
+
163
+ ##
164
+ # :method: parent_ids
165
+ # Returns a list of the document's parent_ids, starting with the root node.
166
+ # (Generated by Mongoid)
167
+
168
+ ##
169
+ # Is this document a root node (has no parent)?
170
+ def root?
171
+ parent_id.nil?
172
+ end
173
+
174
+ ##
175
+ # Is this document a leaf node (has no children)?
176
+ def leaf?
177
+ children.empty?
178
+ end
179
+
180
+ ##
181
+ # Returns the depth of this document (number of ancestors)
182
+ def depth
183
+ parent_ids.count
184
+ end
185
+
186
+ ##
187
+ # Returns this document's root node
188
+ def root
189
+ if parent_ids.present?
190
+ return base_class.find(parent_ids.first)
191
+ else
192
+ return self.root? ? self : self.parent.root
193
+ end
194
+ end
195
+
196
+ ##
197
+ # Returns a chainable criteria for this document's ancestors
198
+ def ancestors
199
+ base_class.where(:_id.in => parent_ids)
200
+ end
201
+
202
+ ##
203
+ # Returns an array of this document's ancestors and itself
204
+ def ancestors_and_self
205
+ ancestors + [self]
206
+ end
207
+
208
+ ##
209
+ # Is this document an ancestor of the other document?
210
+ def ancestor_of?(other)
211
+ other.parent_ids.include?(self.id)
212
+ end
213
+
214
+ ##
215
+ # Returns a chainable criteria for this document's descendants
216
+ def descendants
217
+ base_class.where(:parent_ids => self.id)
218
+ end
219
+
220
+ ##
221
+ # Returns and array of this document's descendants and itself
222
+ def descendants_and_self
223
+ [self] + descendants
224
+ end
225
+
226
+ ##
227
+ # Is this document a descendant of the other document?
228
+ def descendant_of?(other)
229
+ self.parent_ids.include?(other.id)
230
+ end
231
+
232
+ ##
233
+ # Returns this document's siblings
234
+ def siblings
235
+ siblings_and_self.excludes(:id => self.id)
236
+ end
237
+
238
+ ##
239
+ # Returns this document's siblings and itself
240
+ def siblings_and_self
241
+ base_class.where(:parent_id => self.parent_id)
242
+ end
243
+
244
+ ##
245
+ # Is this document a sibling of the other document?
246
+ def sibling_of?(other)
247
+ self.parent_id == other.parent_id
248
+ end
249
+
250
+ ##
251
+ # Returns all leaves of this document (be careful, currently involves two queries)
252
+ def leaves
253
+ base_class.where(:_id.nin => base_class.only(:parent_id).collect(&:parent_id)).and(:parent_ids => self.id)
254
+ end
255
+
256
+ ##
257
+ # Forces rearranging of all children after next save
258
+ def rearrange_children!
259
+ @rearrange_children = true
260
+ end
261
+
262
+ ##
263
+ # Will the children be rearranged after next save?
264
+ def rearrange_children?
265
+ !!@rearrange_children
266
+ end
267
+
268
+ ##
269
+ # Nullifies all children's parent_id
270
+ def nullify_children
271
+ children.each do |c|
272
+ c.parent = c.parent_id = nil
273
+ c.save
274
+ end
275
+ end
276
+
277
+ ##
278
+ # Moves all children to this document's parent
279
+ def move_children_to_parent
280
+ children.each do |c|
281
+ c.parent_id = self.parent_id
282
+ c.save
283
+ end
284
+ end
285
+
286
+ ##
287
+ # Deletes all descendants using the database (doesn't invoke callbacks)
288
+ def delete_descendants
289
+ base_class.delete_all(:conditions => { :parent_ids => self.id })
290
+ end
291
+
292
+ ##
293
+ # Destroys all children by calling their #destroy method (does invoke callbacks)
294
+ def destroy_children
295
+ children.destroy_all
296
+ end
297
+
298
+ private
299
+ def rearrange
300
+ if self.parent_id
301
+ self.parent_ids = parent.parent_ids + [self.parent_id]
302
+ else
303
+ self.parent_ids = []
304
+ end
305
+
306
+ rearrange_children! if self.parent_ids_changed?
307
+ end
308
+
309
+ def rearrange_children
310
+ @rearrange_children = false
311
+ self.children.each { |c| c.save }
312
+ end
313
+
314
+ def position_in_tree
315
+ errors.add(:parent_id, :invalid) if self.parent_ids.include?(self.id)
316
+ end
317
+ end
318
+ end