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