nobrainer-tree 0.0.1

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