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.
@@ -0,0 +1,442 @@
1
+ require 'active_support/concern'
2
+
3
+ module NoBrainer
4
+ ##
5
+ # = NoBrainer::Tree
6
+ #
7
+ # This module extends any NoBrainer document with tree functionality.
8
+ #
9
+ # == Usage
10
+ #
11
+ # Simply include the module in any NoBrainer document:
12
+ #
13
+ # class Node
14
+ # include NoBrainer::Document
15
+ # include NoBrainer::Tree
16
+ # end
17
+ #
18
+ # === Using the tree structure
19
+ #
20
+ # Each document references many children. You can access them using the <tt>#children</tt> method.
21
+ #
22
+ # node = Node.create
23
+ # node.children.create
24
+ # node.children.count # => 1
25
+ #
26
+ # Every document references one parent (unless it's a root document).
27
+ #
28
+ # node = Node.create
29
+ # node.parent # => nil
30
+ # node.children.create
31
+ # node.children.first.parent # => node
32
+ #
33
+ # === Destroying
34
+ #
35
+ # NoBrainer::Tree does not handle destroying of nodes by default. However it provides
36
+ # several strategies that help you to deal with children of deleted documents. You can
37
+ # simply add them as <tt>before_destroy</tt> callbacks.
38
+ #
39
+ # Available strategies are:
40
+ #
41
+ # * :nullify_children -- Sets the children's parent_id to null
42
+ # * :move_children_to_parent -- Moves the children to the current document's parent
43
+ # * :destroy_children -- Destroys all children by calling their #destroy method (invokes callbacks)
44
+ # * :delete_descendants -- Deletes all descendants using a database query (doesn't invoke callbacks)
45
+ #
46
+ # Example:
47
+ #
48
+ # class Node
49
+ # include NoBrainer::Document
50
+ # include NoBrainer::Tree
51
+ #
52
+ # before_destroy :nullify_children
53
+ # end
54
+ #
55
+ # === Callbacks
56
+ #
57
+ # NoBrainer::Tree offers callbacks for its rearranging process. This enables you to
58
+ # rebuild certain fields when the document was moved in the tree. Rearranging happens
59
+ # before the document is validated. This gives you a chance to validate your additional
60
+ # changes done in your callbacks. See ActiveModel::Callbacks and ActiveSupport::Callbacks
61
+ # for further details on callbacks.
62
+ #
63
+ # Example:
64
+ #
65
+ # class Page
66
+ # include NoBrainer::Document
67
+ # include NoBrainer::Tree
68
+ #
69
+ # after_rearrange :rebuild_path
70
+ #
71
+ # field :slug
72
+ # field :path
73
+ #
74
+ # private
75
+ #
76
+ # def rebuild_path
77
+ # self.path = self.ancestors_and_self.collect(&:slug).join('/')
78
+ # end
79
+ # end
80
+ #
81
+ module Tree
82
+ extend ActiveSupport::Concern
83
+
84
+ autoload :Ordering, 'nobrainer/tree/ordering'
85
+ autoload :Traversal, 'nobrainer/tree/traversal'
86
+
87
+ included do
88
+ has_many :children, :class_name => self.name, :foreign_key => :parent_id
89
+
90
+ # TODO: Use index when nil is allowed in index queries
91
+ belongs_to :parent, :class_name => self.name
92
+
93
+ field :parent_ids, :type => Array, :default => []
94
+ index :parent_ids, :multi => true
95
+
96
+ field :depth, :type => Integer
97
+ index :depth
98
+
99
+ set_callback :save, :after, :rearrange_children, :if => :rearrange_children?
100
+ set_callback :validation, :before do
101
+ run_callbacks(:rearrange) { rearrange }
102
+ end
103
+
104
+ validate :position_in_tree
105
+
106
+ define_model_callbacks :rearrange, :only => [:before, :after]
107
+
108
+ class_eval "def base_class; ::#{self.name}; end"
109
+ end
110
+
111
+ ##
112
+ # This module implements class methods that will be available
113
+ # on the document that includes NoBrainer::Tree
114
+ module ClassMethods
115
+
116
+ ##
117
+ # Returns the first root document
118
+ #
119
+ # @example
120
+ # Node.root
121
+ #
122
+ # @return [NoBrainer::Document] The first root document
123
+ def root
124
+ roots.first
125
+ end
126
+
127
+ ##
128
+ # Returns all root documents
129
+ #
130
+ # @example
131
+ # Node.roots
132
+ #
133
+ # @return [NoBrainer::Criteria] NoBrainer criteria to retrieve all root documents
134
+ def roots
135
+ where(:parent_id => nil)
136
+ end
137
+
138
+ ##
139
+ # Returns all leaves (be careful, currently involves two queries)
140
+ #
141
+ # @example
142
+ # Node.leaves
143
+ #
144
+ # @return [NoBrainer::Criteria] NoBrainer criteria to retrieve all leave nodes
145
+ def leaves
146
+ where(:id.nin => pluck(:id, :_type, :parent_id).collect(&:parent_id).compact)
147
+ end
148
+
149
+ end
150
+
151
+ ##
152
+ # @!method before_rearrange
153
+ # @!scope class
154
+ #
155
+ # Sets a callback that is called before the document is rearranged
156
+ #
157
+ # @example
158
+ # class Node
159
+ # include NoBrainer::Document
160
+ # include NoBrainer::Tree
161
+ #
162
+ # before_rearrage :do_something
163
+ #
164
+ # private
165
+ #
166
+ # def do_something
167
+ # # ...
168
+ # end
169
+ # end
170
+ #
171
+ # @note Generated by ActiveSupport
172
+ #
173
+ # @return [undefined]
174
+
175
+ ##
176
+ # @!method after_rearrange
177
+ # @!scope class
178
+ #
179
+ # Sets a callback that is called after the document is rearranged
180
+ #
181
+ # @example
182
+ # class Node
183
+ # include NoBrainer::Document
184
+ # include NoBrainer::Tree
185
+ #
186
+ # after_rearrange :do_something
187
+ #
188
+ # private
189
+ #
190
+ # def do_something
191
+ # # ...
192
+ # end
193
+ # end
194
+ #
195
+ # @note Generated by ActiveSupport
196
+ #
197
+ # @return [undefined]
198
+
199
+ ##
200
+ # @!method children
201
+ # Returns a list of the document's children. It's a <tt>references_many</tt> association.
202
+ #
203
+ # @note Generated by NoBrainer
204
+ #
205
+ # @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document's children
206
+
207
+ ##
208
+ # @!method parent
209
+ # Returns the document's parent (unless it's a root document). It's a <tt>referenced_in</tt> association.
210
+ #
211
+ # @note Generated by NoBrainer
212
+ #
213
+ # @return [NoBrainer::Document] The document's parent document
214
+
215
+ ##
216
+ # @!method parent=(document)
217
+ # Sets this documents parent document.
218
+ #
219
+ # @note Generated by NoBrainer
220
+ #
221
+ # @param [NoBrainer::Tree] document
222
+
223
+ ##
224
+ # @!method parent_ids
225
+ # Returns a list of the document's parent_ids, starting with the root node.
226
+ #
227
+ # @note Generated by NoBrainer
228
+ #
229
+ # @return [Array<BSON::ObjectId>] The ids of the document's ancestors
230
+
231
+ ##
232
+ # Returns the depth of this document (number of ancestors)
233
+ #
234
+ # @example
235
+ # Node.root.depth # => 0
236
+ # Node.root.children.first.depth # => 1
237
+ #
238
+ # @return [Fixnum] Depth of this document
239
+ def depth
240
+ super || self.parent_ids.count
241
+ end
242
+
243
+ ##
244
+ # Is this document a root node (has no parent)?
245
+ #
246
+ # @return [Boolean] Whether the document is a root node
247
+ def root?
248
+ self.parent_id.nil?
249
+ end
250
+
251
+ ##
252
+ # Is this document a leaf node (has no children)?
253
+ #
254
+ # @return [Boolean] Whether the document is a leaf node
255
+ def leaf?
256
+ self.children.empty?
257
+ end
258
+
259
+ ##
260
+ # Returns this document's root node. Returns `self` if the
261
+ # current document is a root node
262
+ #
263
+ # @example
264
+ # node = Node.find(...)
265
+ # node.root
266
+ #
267
+ # @return [NoBrainer::Document] The documents root node
268
+ def root
269
+ if self.parent_ids.present?
270
+ base_class.find(self.parent_ids.first)
271
+ else
272
+ self.root? ? self : self.parent.root
273
+ end
274
+ end
275
+
276
+ ##
277
+ # Returns a chainable criteria for this document's ancestors
278
+ #
279
+ # @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the documents ancestors
280
+ def ancestors
281
+ base_class.without_index.where(:id.in => self.parent_ids).order_by(:depth => :asc)
282
+ end
283
+
284
+ ##
285
+ # Returns an array of this document's ancestors and itself
286
+ #
287
+ # @return [Array<NoBrainer::Document>] Array of the document's ancestors and itself
288
+ def ancestors_and_self
289
+ ancestors + [self]
290
+ end
291
+
292
+ ##
293
+ # Is this document an ancestor of the other document?
294
+ #
295
+ # @param [NoBrainer::Tree] other document to check against
296
+ #
297
+ # @return [Boolean] The document is an ancestor of the other document
298
+ def ancestor_of?(other)
299
+ other.parent_ids.include?(self.id)
300
+ end
301
+
302
+ ##
303
+ # Returns a chainable criteria for this document's descendants
304
+ #
305
+ # @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document's descendants
306
+ def descendants
307
+ base_class.where(:parent_ids.any => self.id)
308
+ end
309
+
310
+ ##
311
+ # Returns and array of this document and it's descendants
312
+ #
313
+ # @return [Array<NoBrainer::Document>] Array of the document itself and it's descendants
314
+ def descendants_and_self
315
+ [self] + descendants
316
+ end
317
+
318
+ ##
319
+ # Is this document a descendant of the other document?
320
+ #
321
+ # @param [NoBrainer::Tree] other document to check against
322
+ #
323
+ # @return [Boolean] The document is a descendant of the other document
324
+ def descendant_of?(other)
325
+ self.parent_ids.include?(other.id)
326
+ end
327
+
328
+ ##
329
+ # Returns this document's siblings
330
+ #
331
+ # @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document's siblings
332
+ def siblings
333
+ siblings_and_self.where(:id.ne => self.id)
334
+ end
335
+
336
+ ##
337
+ # Returns this document's siblings and itself
338
+ #
339
+ # @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document's siblings and itself
340
+ def siblings_and_self
341
+ base_class.where(:parent_id => self.parent_id)
342
+ end
343
+
344
+ ##
345
+ # Is this document a sibling of the other document?
346
+ #
347
+ # @param [NoBrainer::Tree] other document to check against
348
+ #
349
+ # @return [Boolean] The document is a sibling of the other document
350
+ def sibling_of?(other)
351
+ self.parent_id == other.parent_id
352
+ end
353
+
354
+ ##
355
+ # Returns all leaves of this document (be careful, currently involves two queries)
356
+ #
357
+ # @return [NoBrainer::Criteria] NoBrainer criteria to retrieve the document's leaves
358
+ def leaves
359
+ base_class.where(:id.nin => base_class.pluck(:id, :_type, :parent_id).collect(&:parent_id).compact, :parent_ids.any => self.id)
360
+ end
361
+
362
+ ##
363
+ # Forces rearranging of all children after next save
364
+ #
365
+ # @return [undefined]
366
+ def rearrange_children!
367
+ @rearrange_children = true
368
+ end
369
+
370
+ ##
371
+ # Will the children be rearranged after next save?
372
+ #
373
+ # @return [Boolean] Whether the children will be rearranged
374
+ def rearrange_children?
375
+ !!@rearrange_children
376
+ end
377
+
378
+ ##
379
+ # Nullifies all children's parent_id
380
+ #
381
+ # @return [undefined]
382
+ def nullify_children
383
+ children.each do |c|
384
+ c.parent = c.parent_id = nil
385
+ c.save
386
+ end
387
+ end
388
+
389
+ ##
390
+ # Moves all children to this document's parent
391
+ #
392
+ # @return [undefined]
393
+ def move_children_to_parent
394
+ children.each do |c|
395
+ c.parent = self.parent
396
+ c.save
397
+ end
398
+ end
399
+
400
+ ##
401
+ # Deletes all descendants using the database (doesn't invoke callbacks)
402
+ #
403
+ # @return [undefined]
404
+ #
405
+ def delete_descendants
406
+ self.descendants.delete_all
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
+ self.parent_ids = (parent.parent_ids + [self.parent_id]) rescue []
427
+
428
+ self.depth = parent_ids.size
429
+
430
+ rearrange_children! if self.parent_ids_changed?
431
+ end
432
+
433
+ def rearrange_children
434
+ @rearrange_children = false
435
+ self.children.each { |c| c.save }
436
+ end
437
+
438
+ def position_in_tree
439
+ errors.add(:parent_id, :invalid) if self.parent_ids.include?(self.id)
440
+ end
441
+ end
442
+ end