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.
- data/.rspec +2 -0
- data/.travis.yml +12 -0
- data/Gemfile +23 -0
- data/Guardfile +6 -0
- data/LICENSE +21 -0
- data/README.md +287 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/lib/mongoid/locale/en.yml +11 -0
- data/lib/mongoid/locale/nb.yml +8 -0
- data/lib/mongoid/tree.rb +443 -0
- data/lib/mongoid/tree/ordering.rb +236 -0
- data/lib/mongoid/tree/rational_numbering.rb +805 -0
- data/lib/mongoid/tree/traversal.rb +122 -0
- data/mongoid-tree-rational.gemspec +103 -0
- data/spec/mongoid/tree/ordering_spec.rb +342 -0
- data/spec/mongoid/tree/rational_numbering_spec.rb +765 -0
- data/spec/mongoid/tree/traversal_spec.rb +177 -0
- data/spec/mongoid/tree_spec.rb +444 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/macros/tree_macros.rb +53 -0
- data/spec/support/models/node.rb +47 -0
- metadata +320 -0
data/lib/mongoid/tree.rb
ADDED
@@ -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
|