mongoid-tree-rational 0.1.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.
@@ -0,0 +1,443 @@
1
+ module Mongoid
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
+ autoload :RationalNumbering, 'mongoid/tree/rational_numbering'
85
+
86
+ included do
87
+ has_many :children, :class_name => self.name, :foreign_key => :parent_id, :inverse_of => :parent, :validate => false
88
+
89
+ belongs_to :parent, :class_name => self.name, :inverse_of => :children, :index => true, :validate => false
90
+
91
+ field :parent_ids, :type => Array, :default => []
92
+ index :parent_ids => 1
93
+
94
+ set_callback :save, :after, :rearrange_children, :if => :rearrange_children?
95
+ set_callback :validation, :before do
96
+ run_callbacks(:rearrange) { rearrange }
97
+ end
98
+
99
+ validate :position_in_tree
100
+
101
+ define_model_callbacks :rearrange, :only => [:before, :after]
102
+
103
+ class_eval "def base_class; ::#{self.name}; end"
104
+ end
105
+
106
+ ##
107
+ # This module implements class methods that will be available
108
+ # on the document that includes Mongoid::Tree
109
+ module ClassMethods
110
+
111
+ ##
112
+ # Returns the first root document
113
+ #
114
+ # @example
115
+ # Node.root
116
+ #
117
+ # @return [Mongoid::Document] The first root document
118
+ def root
119
+ roots.first
120
+ end
121
+
122
+ ##
123
+ # Returns all root documents
124
+ #
125
+ # @example
126
+ # Node.roots
127
+ #
128
+ # @return [Mongoid::Criteria] Mongoid criteria to retrieve all root documents
129
+ def roots
130
+ where(:parent_id => nil)
131
+ end
132
+
133
+ ##
134
+ # Returns all leaves (be careful, currently involves two queries)
135
+ #
136
+ # @example
137
+ # Node.leaves
138
+ #
139
+ # @return [Mongoid::Criteria] Mongoid criteria to retrieve all leave nodes
140
+ def leaves
141
+ where(:_id.nin => only(:parent_id).collect(&:parent_id))
142
+ end
143
+
144
+ end
145
+
146
+ ##
147
+ # @!method before_rearrange
148
+ # @!scope class
149
+ #
150
+ # Sets a callback that is called before the document is rearranged
151
+ #
152
+ # @example
153
+ # class Node
154
+ # include Mongoid::Document
155
+ # include Mongoid::Tree
156
+ #
157
+ # before_rearrage :do_something
158
+ #
159
+ # private
160
+ #
161
+ # def do_something
162
+ # # ...
163
+ # end
164
+ # end
165
+ #
166
+ # @note Generated by ActiveSupport
167
+ #
168
+ # @return [undefined]
169
+
170
+ ##
171
+ # @!method after_rearrange
172
+ # @!scope class
173
+ #
174
+ # Sets a callback that is called after the document is rearranged
175
+ #
176
+ # @example
177
+ # class Node
178
+ # include Mongoid::Document
179
+ # include Mongoid::Tree
180
+ #
181
+ # after_rearrange :do_something
182
+ #
183
+ # private
184
+ #
185
+ # def do_something
186
+ # # ...
187
+ # end
188
+ # end
189
+ #
190
+ # @note Generated by ActiveSupport
191
+ #
192
+ # @return [undefined]
193
+
194
+ ##
195
+ # @!method children
196
+ # Returns a list of the document's children. It's a <tt>references_many</tt> association.
197
+ #
198
+ # @note Generated by Mongoid
199
+ #
200
+ # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's children
201
+
202
+ ##
203
+ # @!method parent
204
+ # Returns the document's parent (unless it's a root document). It's a <tt>referenced_in</tt> association.
205
+ #
206
+ # @note Generated by Mongoid
207
+ #
208
+ # @return [Mongoid::Document] The document's parent document
209
+
210
+ ##
211
+ # @!method parent=(document)
212
+ # Sets this documents parent document.
213
+ #
214
+ # @note Generated by Mongoid
215
+ #
216
+ # @param [Mongoid::Tree] document
217
+
218
+ ##
219
+ # @!method parent_ids
220
+ # Returns a list of the document's parent_ids, starting with the root node.
221
+ #
222
+ # @note Generated by Mongoid
223
+ #
224
+ # @return [Array<BSON::ObjectId>] The ids of the document's ancestors
225
+
226
+ ##
227
+ # Is this document a root node (has no parent)?
228
+ #
229
+ # @return [Boolean] Whether the document is a root node
230
+ def root?
231
+ parent_id.nil?
232
+ end
233
+
234
+ ##
235
+ # Is this document a leaf node (has no children)?
236
+ #
237
+ # @return [Boolean] Whether the document is a leaf node
238
+ def leaf?
239
+ children.empty?
240
+ end
241
+
242
+ ##
243
+ # Returns the depth of this document (number of ancestors)
244
+ #
245
+ # @example
246
+ # Node.root.depth # => 0
247
+ # Node.root.children.first.depth # => 1
248
+ #
249
+ # @return [Fixnum] Depth of this document
250
+ def depth
251
+ parent_ids.count
252
+ end
253
+
254
+ ##
255
+ # Returns this document's root node. Returns `self` if the
256
+ # current document is a root node
257
+ #
258
+ # @example
259
+ # node = Node.find(...)
260
+ # node.root
261
+ #
262
+ # @return [Mongoid::Document] The documents root node
263
+ def root
264
+ if parent_ids.present?
265
+ base_class.find(parent_ids.first)
266
+ else
267
+ self.root? ? self : self.parent.root
268
+ end
269
+ end
270
+
271
+ ##
272
+ # Returns a chainable criteria for this document's ancestors
273
+ #
274
+ # @return [Mongoid::Criteria] Mongoid criteria to retrieve the documents ancestors
275
+ def ancestors
276
+ if parent_ids.any?
277
+ base_class.and({
278
+ '$or' => parent_ids.map { |id| { :_id => id } }
279
+ })
280
+ else
281
+ base_class.where(:_id.in => [])
282
+ end
283
+ end
284
+
285
+ ##
286
+ # Returns an array of this document's ancestors and itself
287
+ #
288
+ # @return [Array<Mongoid::Document>] Array of the document's ancestors and itself
289
+ def ancestors_and_self
290
+ ancestors + [self]
291
+ end
292
+
293
+ ##
294
+ # Is this document an ancestor of the other document?
295
+ #
296
+ # @param [Mongoid::Tree] other document to check against
297
+ #
298
+ # @return [Boolean] The document is an ancestor of the other document
299
+ def ancestor_of?(other)
300
+ other.parent_ids.include?(self.id)
301
+ end
302
+
303
+ ##
304
+ # Returns a chainable criteria for this document's descendants
305
+ #
306
+ # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's descendants
307
+ def descendants
308
+ base_class.where(:parent_ids => self.id)
309
+ end
310
+
311
+ ##
312
+ # Returns and array of this document and it's descendants
313
+ #
314
+ # @return [Array<Mongoid::Document>] Array of the document itself and it's descendants
315
+ def descendants_and_self
316
+ [self] + descendants
317
+ end
318
+
319
+ ##
320
+ # Is this document a descendant of the other document?
321
+ #
322
+ # @param [Mongoid::Tree] other document to check against
323
+ #
324
+ # @return [Boolean] The document is a descendant of the other document
325
+ def descendant_of?(other)
326
+ self.parent_ids.include?(other.id)
327
+ end
328
+
329
+ ##
330
+ # Returns this document's siblings
331
+ #
332
+ # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's siblings
333
+ def siblings
334
+ siblings_and_self.excludes(:id => self.id)
335
+ end
336
+
337
+ ##
338
+ # Returns this document's siblings and itself
339
+ #
340
+ # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's siblings and itself
341
+ def siblings_and_self
342
+ base_class.where(:parent_id => self.parent_id)
343
+ end
344
+
345
+ ##
346
+ # Is this document a sibling of the other document?
347
+ #
348
+ # @param [Mongoid::Tree] other document to check against
349
+ #
350
+ # @return [Boolean] The document is a sibling of the other document
351
+ def sibling_of?(other)
352
+ self.parent_id == other.parent_id
353
+ end
354
+
355
+ ##
356
+ # Returns all leaves of this document (be careful, currently involves two queries)
357
+ #
358
+ # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's leaves
359
+ def leaves
360
+ base_class.where(:_id.nin => base_class.only(:parent_id).collect(&:parent_id)).and(:parent_ids => self.id)
361
+ end
362
+
363
+ ##
364
+ # Forces rearranging of all children after next save
365
+ #
366
+ # @return [undefined]
367
+ def rearrange_children!
368
+ @rearrange_children = true
369
+ end
370
+
371
+ ##
372
+ # Will the children be rearranged after next save?
373
+ #
374
+ # @return [Boolean] Whether the children will be rearranged
375
+ def rearrange_children?
376
+ !!@rearrange_children
377
+ end
378
+
379
+ ##
380
+ # Nullifies all children's parent_id
381
+ #
382
+ # @return [undefined]
383
+ def nullify_children
384
+ children.each do |c|
385
+ c.parent = c.parent_id = nil
386
+ c.save
387
+ end
388
+ end
389
+
390
+ ##
391
+ # Moves all children to this document's parent
392
+ #
393
+ # @return [undefined]
394
+ def move_children_to_parent
395
+ children.each do |c|
396
+ c.parent = self.parent
397
+ c.save
398
+ end
399
+ end
400
+
401
+ ##
402
+ # Deletes all descendants using the database (doesn't invoke callbacks)
403
+ #
404
+ # @return [undefined]
405
+ def delete_descendants
406
+ base_class.delete_all(:conditions => { :parent_ids => self.id })
407
+ end
408
+
409
+ ##
410
+ # Destroys all children by calling their #destroy method (does invoke callbacks)
411
+ #
412
+ # @return [undefined]
413
+ def destroy_children
414
+ children.destroy_all
415
+ end
416
+
417
+ private
418
+
419
+ ##
420
+ # Updates the parent_ids and marks the children for
421
+ # rearrangement when the parent_ids changed
422
+ #
423
+ # @private
424
+ # @return [undefined]
425
+ def rearrange
426
+ if self.parent_id
427
+ self.parent_ids = parent.parent_ids + [self.parent_id]
428
+ else
429
+ self.parent_ids = []
430
+ end
431
+ rearrange_children! if self.parent_ids_changed?
432
+ end
433
+
434
+ def rearrange_children
435
+ @rearrange_children = false
436
+ self.children.each { |c| c.save }
437
+ end
438
+
439
+ def position_in_tree
440
+ errors.add(:parent_id, :invalid) if self.parent_ids.include?(self.id)
441
+ end
442
+ end
443
+ end
@@ -0,0 +1,236 @@
1
+ module Mongoid
2
+ module Tree
3
+ ##
4
+ # = Mongoid::Tree::Ordering
5
+ #
6
+ # Mongoid::Tree doesn't order the tree by default. To enable ordering of children
7
+ # include both Mongoid::Tree and Mongoid::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 asc(:position)
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
+ ##
46
+ # Returns a chainable criteria for this document's ancestors
47
+ #
48
+ # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's ancestors
49
+ def ancestors
50
+ base_class.unscoped { super }
51
+ end
52
+
53
+ ##
54
+ # Returns siblings below the current document.
55
+ # Siblings with a position greater than this document's position.
56
+ #
57
+ # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's lower siblings
58
+ def lower_siblings
59
+ self.siblings.where(:position.gt => self.position)
60
+ end
61
+
62
+ ##
63
+ # Returns siblings above the current document.
64
+ # Siblings with a position lower than this document's position.
65
+ #
66
+ # @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's higher siblings
67
+ def higher_siblings
68
+ self.siblings.where(:position.lt => self.position)
69
+ end
70
+
71
+ ##
72
+ # Returns siblings between the current document and the other document
73
+ # Siblings with a position between this document's position and the other document's position.
74
+ #
75
+ # @return [Mongoid::Criteria] Mongoid criteria to retrieve the documents between this and the other document
76
+ def siblings_between(other)
77
+ range = [self.position, other.position].sort
78
+ self.siblings.where(:position.gt => range.first, :position.lt => range.last)
79
+ end
80
+
81
+ ##
82
+ # Returns the lowest sibling (could be self)
83
+ #
84
+ # @return [Mongoid::Document] The lowest sibling
85
+ def last_sibling_in_list
86
+ siblings_and_self.last
87
+ end
88
+
89
+ ##
90
+ # Returns the highest sibling (could be self)
91
+ #
92
+ # @return [Mongoid::Document] The highest sibling
93
+ def first_sibling_in_list
94
+ siblings_and_self.first
95
+ end
96
+
97
+ ##
98
+ # Is this the highest sibling?
99
+ #
100
+ # @return [Boolean] Whether the document is the highest sibling
101
+ def at_top?
102
+ higher_siblings.empty?
103
+ end
104
+
105
+ ##
106
+ # Is this the lowest sibling?
107
+ #
108
+ # @return [Boolean] Whether the document is the lowest sibling
109
+ def at_bottom?
110
+ lower_siblings.empty?
111
+ end
112
+
113
+ ##
114
+ # Move this node above all its siblings
115
+ #
116
+ # @return [undefined]
117
+ def move_to_top
118
+ return true if at_top?
119
+ move_above(first_sibling_in_list)
120
+ end
121
+
122
+ ##
123
+ # Move this node below all its siblings
124
+ #
125
+ # @return [undefined]
126
+ def move_to_bottom
127
+ return true if at_bottom?
128
+ move_below(last_sibling_in_list)
129
+ end
130
+
131
+ ##
132
+ # Move this node one position up
133
+ #
134
+ # @return [undefined]
135
+ def move_up
136
+ switch_with_sibling_at_offset(-1) unless at_top?
137
+ end
138
+
139
+ ##
140
+ # Move this node one position down
141
+ #
142
+ # @return [undefined]
143
+ def move_down
144
+ switch_with_sibling_at_offset(1) unless at_bottom?
145
+ end
146
+
147
+ ##
148
+ # Move this node above the specified node
149
+ #
150
+ # This method changes the node's parent if nescessary.
151
+ #
152
+ # @param [Mongoid::Tree] other document to move this document above
153
+ #
154
+ # @return [undefined]
155
+ def move_above(other)
156
+ ensure_to_be_sibling_of(other)
157
+
158
+ if position > other.position
159
+ new_position = other.position
160
+ self.siblings_between(other).inc(:position, 1)
161
+ other.inc(:position, 1)
162
+ else
163
+ new_position = other.position - 1
164
+ self.siblings_between(other).inc(:position, -1)
165
+ end
166
+
167
+ self.position = new_position
168
+ save!
169
+ end
170
+
171
+ ##
172
+ # Move this node below the specified node
173
+ #
174
+ # This method changes the node's parent if nescessary.
175
+ #
176
+ # @param [Mongoid::Tree] other document to move this document below
177
+ #
178
+ # @return [undefined]
179
+ def move_below(other)
180
+ ensure_to_be_sibling_of(other)
181
+
182
+ if position > other.position
183
+ new_position = other.position + 1
184
+ self.siblings_between(other).inc(:position, 1)
185
+ else
186
+ new_position = other.position
187
+ self.siblings_between(other).inc(:position, -1)
188
+ other.inc(:position, -1)
189
+ end
190
+
191
+ self.position = new_position
192
+ save!
193
+ end
194
+
195
+ private
196
+
197
+ def switch_with_sibling_at_offset(offset)
198
+ siblings.where(:position => self.position + offset).first.inc(:position, -offset)
199
+ inc(:position, offset)
200
+ end
201
+
202
+ def ensure_to_be_sibling_of(other)
203
+ return if sibling_of?(other)
204
+ self.parent_id = other.parent_id
205
+ save!
206
+ end
207
+
208
+ def move_lower_siblings_up
209
+ lower_siblings.inc(:position, -1)
210
+ end
211
+
212
+ def reposition_former_siblings
213
+ former_siblings = base_class.where(:parent_id => attribute_was('parent_id')).
214
+ and(:position.gt => (attribute_was('position') || 0)).
215
+ excludes(:id => self.id)
216
+ former_siblings.inc(:position, -1)
217
+ end
218
+
219
+ def sibling_reposition_required?
220
+ parent_id_changed? && persisted?
221
+ end
222
+
223
+ def assign_default_position
224
+ self.position = if self.siblings.where(:position.ne => nil).any?
225
+ self.last_sibling_in_list.position + 1
226
+ else
227
+ 0
228
+ end
229
+ end
230
+
231
+ def assign_default_position?
232
+ self.position.nil? || self.parent_id_changed?
233
+ end
234
+ end
235
+ end
236
+ end