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