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
@@ -0,0 +1,805 @@
|
|
1
|
+
I18n.load_path << File.expand_path('../../locale/en.yml', __FILE__)
|
2
|
+
|
3
|
+
# TODO: ADD VALIDATIONS!
|
4
|
+
|
5
|
+
require 'rational_number'
|
6
|
+
|
7
|
+
module Mongoid
|
8
|
+
module Tree
|
9
|
+
##
|
10
|
+
# = Mongoid::Tree::RationalNumbering
|
11
|
+
#
|
12
|
+
# Mongoid::Tree doesn't use rational numbers by default. To enable rational numbering
|
13
|
+
# of children include both Mongoid::Tree and Mongoid::Tree::RationalNumbering into
|
14
|
+
# your document.
|
15
|
+
#
|
16
|
+
# == Utility methods
|
17
|
+
#
|
18
|
+
|
19
|
+
module RationalNumbering
|
20
|
+
extend ActiveSupport::Concern
|
21
|
+
|
22
|
+
@@_disable_timestamp_count = 0
|
23
|
+
|
24
|
+
included do
|
25
|
+
field :rational_number_nv, :type => Integer, :default => 0
|
26
|
+
field :rational_number_dv, :type => Integer, :default => 1
|
27
|
+
field :rational_number_snv, :type => Integer, :default => 1
|
28
|
+
field :rational_number_sdv, :type => Integer, :default => 0
|
29
|
+
field :rational_number_value, :type => BigDecimal
|
30
|
+
|
31
|
+
validate :validate_rational_hierarchy
|
32
|
+
|
33
|
+
# after_rearrange :assign_initial_rational_number, :if => :assign_initial_rational_number?
|
34
|
+
|
35
|
+
# after_rearrange :set_initial_rational_number, :if :set_initial_rational_number?
|
36
|
+
|
37
|
+
after_rearrange :update_rational_number, :if => :update_rational_number?
|
38
|
+
|
39
|
+
# Rekey former siblings to avoid gaps in the rational structure
|
40
|
+
after_save :rekey_former_siblings, :if => :rekey_former_siblings?
|
41
|
+
|
42
|
+
# Rekey all the children of the node if needed
|
43
|
+
after_save :rekey_children, :if => :rekey_children?
|
44
|
+
|
45
|
+
### ??? ADD SOME STRATEGY???
|
46
|
+
# before_destroy :destroy_descendants
|
47
|
+
|
48
|
+
after_destroy :move_lower_siblings
|
49
|
+
|
50
|
+
default_scope asc(:rational_number_value)
|
51
|
+
|
52
|
+
end # included do
|
53
|
+
|
54
|
+
module ClassMethods
|
55
|
+
|
56
|
+
# helper metods for nv/dv
|
57
|
+
|
58
|
+
##
|
59
|
+
# Force all rational number keys to update
|
60
|
+
#
|
61
|
+
# Can be used to remove any "gaps" that are in a tree
|
62
|
+
#
|
63
|
+
# For large collections, this uses a lot of resources, and should probably be used
|
64
|
+
# in a backround job on production sites. As a rational tree works just fine even
|
65
|
+
# if there are missing items, this shouldn't be necessary to do that often.
|
66
|
+
|
67
|
+
def rekey_all!
|
68
|
+
# rekey keys for each root. will do children
|
69
|
+
_pos = 1
|
70
|
+
root_rational = RationalNumber.new
|
71
|
+
self.roots.each do |root|
|
72
|
+
new_rational = root_rational.child_from_position(_pos)
|
73
|
+
if new_rational != root.rational_number
|
74
|
+
root.move_to_rational_number(new_rational.nv, new_rational.dv, {:force => true})
|
75
|
+
root.save_with_force_rational_numbers!
|
76
|
+
# root.reload # Should caller be responsible for reloading?
|
77
|
+
end
|
78
|
+
root.rekey_children
|
79
|
+
_pos += 1
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end # Classmethods
|
84
|
+
|
85
|
+
|
86
|
+
##
|
87
|
+
# Initialize the rational tree document
|
88
|
+
#
|
89
|
+
# @return [undefined]
|
90
|
+
#
|
91
|
+
def initialize(*args)
|
92
|
+
@_forced_rational_number = false
|
93
|
+
@_rational_moving_nodes = false
|
94
|
+
super
|
95
|
+
end
|
96
|
+
|
97
|
+
##
|
98
|
+
#
|
99
|
+
# Validate that this document has the correct parent document through a query
|
100
|
+
# If not, the parent must be set before setting nv/dv driectly
|
101
|
+
#
|
102
|
+
# @return true for valid, else false
|
103
|
+
#
|
104
|
+
def validate_rational_hierarchy
|
105
|
+
if self.rational_number_nv_changed? && self.rational_number_dv_changed?
|
106
|
+
# puts "#{self.name} #{self.changes.inspect}"
|
107
|
+
unless correct_rational_parent?(self.rational_number_nv, self.rational_number_dv)
|
108
|
+
errors.add(:base, I18n.t(:cyclic, :scope => [:mongoid, :errors, :messages, :tree]))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
##
|
115
|
+
#
|
116
|
+
# [INTERNAL] Move the document to a given position (integer based, starting with 1)
|
117
|
+
#
|
118
|
+
# if a document exists on the new position, all siblings are shifted right before moving this document
|
119
|
+
# can move without updating conflicting siblings by using :force in options
|
120
|
+
#
|
121
|
+
# @param [Integer] The positional value
|
122
|
+
# @param [Hash] Options: :force (defaults to false)
|
123
|
+
#
|
124
|
+
# @return [undefined]
|
125
|
+
#
|
126
|
+
def move_to_position(_position, opts = {})
|
127
|
+
new_rational_number = parent_rational_number.child_from_position(_position)
|
128
|
+
move_to_rational_number(new_rational_number.nv, new_rational_number.dv, opts)
|
129
|
+
end
|
130
|
+
|
131
|
+
##
|
132
|
+
#
|
133
|
+
# [INTERNAL] Move the document to a given rational_number position
|
134
|
+
#
|
135
|
+
# if a document exists on the new position, all siblings are shifted right before moving this document
|
136
|
+
# can move without updating conflicting siblings by using :ignore_conflicts in options
|
137
|
+
#
|
138
|
+
# @param [Integer] The nominator value
|
139
|
+
# @param [Integer] The denominator value
|
140
|
+
# @param [Hash] Options: :force (defaults to false)
|
141
|
+
#
|
142
|
+
# @return [undefined]
|
143
|
+
#
|
144
|
+
def move_to_rational_number(nv, dv, opts = {})
|
145
|
+
# don't check for conflict if forced move
|
146
|
+
move_conflicting_nodes(nv,dv) unless !!opts[:force]
|
147
|
+
|
148
|
+
# shouldn't be any conflicting sibling now...
|
149
|
+
self.from_rational_number(RationalNumber.new(nv,dv))
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
153
|
+
#
|
154
|
+
# This can be used to set a rational number directly
|
155
|
+
# The node will be moved to the correct parent
|
156
|
+
#
|
157
|
+
# If the given nv/dv does not find an existing parent, it will add an validation error
|
158
|
+
#
|
159
|
+
# If the given nv/dv is higher than the last sibling under the parent, the nv/dv will be recalculated
|
160
|
+
# to appropriate nv/dv values
|
161
|
+
#
|
162
|
+
def set_rational_number(nv,dv, do_save = true)
|
163
|
+
# return true of already at the right spot
|
164
|
+
# puts "#{self.name} - set_rational_number #{nv}/#{dv} self:#{self.rational_number_nv}/#{self.rational_number_dv}"
|
165
|
+
return true if self.rational_number_nv == nv && self.rational_number_dv == dv && (!self.rational_number_nv_changed? && !self.rational_number_dv_changed?)
|
166
|
+
# check if parent exist
|
167
|
+
# puts " parent exists: #{parent_exists?(nv,dv).inspect}"
|
168
|
+
unless parent_exists?(nv,dv)
|
169
|
+
errors.add(:base, I18n.t(:parent_does_not_exist, :scope => [:mongoid, :errors, :messages, :tree, :rational], nv: nv, dv: dv) )
|
170
|
+
return false
|
171
|
+
end
|
172
|
+
# find other/conflicting sibling
|
173
|
+
other = base_class.where(:rational_number_nv => nv).where(:rational_number_dv => dv).excludes(:id => self.id).first
|
174
|
+
already_sibling_of = other.nil? ? false : self.sibling_of?(other)
|
175
|
+
|
176
|
+
# puts " conflict: #{other.nil?} already_sibling_of :#{already_sibling_of}"
|
177
|
+
return false if ensure_to_have_correct_parent(nv,dv) == false
|
178
|
+
|
179
|
+
move_to_rational = RationalNumber.new(nv,dv)
|
180
|
+
|
181
|
+
unless other.nil?
|
182
|
+
if already_sibling_of
|
183
|
+
# puts " already sibling of other, so moving down"
|
184
|
+
return if other.position == self.position + 1
|
185
|
+
# If there are nodes between this and other before move, make sure they are shifted upwards before moving
|
186
|
+
_direction = (self.position > other.position ? 1 : -1)
|
187
|
+
_position = (_direction < 0 ? other.position + _direction : other.position)
|
188
|
+
shift_nodes_position(other, _direction, (_direction > 0 ? false : true))
|
189
|
+
|
190
|
+
# There should not be conflicting nodes at this stage.
|
191
|
+
move_to_position(_position)
|
192
|
+
else
|
193
|
+
# puts " shifting lower nodes from other"
|
194
|
+
shift_lower_nodes_from_other(other, 1)
|
195
|
+
end
|
196
|
+
else
|
197
|
+
# make sure the new position is the next rational value under the parent
|
198
|
+
# as there was no "other" to move
|
199
|
+
new_parent = base_class.where(:id => self.parent_id).first
|
200
|
+
if new_parent.nil?
|
201
|
+
# count roots
|
202
|
+
root_count = base_class.roots.count
|
203
|
+
move_to_rational = RationalNumber.new.child_from_position(root_count+1)
|
204
|
+
# puts " new parent is root root_count: #{root_count} new 'correct position' is : #{move_to_rational.nv}/#{move_to_rational.dv}"
|
205
|
+
else
|
206
|
+
child_count = new_parent.children.count
|
207
|
+
move_to_rational = new_parent.rational_number.child_from_position(child_count+1)
|
208
|
+
# puts " new parent is not root child_count: #{child_count} new 'correct position' is : #{move_to_rational.nv}/#{move_to_rational.dv}"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
move_to_rational_number(move_to_rational.nv, move_to_rational.dv, {:force => true})
|
212
|
+
if do_save
|
213
|
+
save
|
214
|
+
else
|
215
|
+
true
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
##
|
220
|
+
#
|
221
|
+
# Check if a parent exists for the given nv/dv values
|
222
|
+
#
|
223
|
+
# Will return true if the parent is "root" and the node should be created
|
224
|
+
# as a root element
|
225
|
+
#
|
226
|
+
def parent_exists?(nv,dv)
|
227
|
+
q_parent = base_class.where(:rational_number_nv => nv).where(:rational_number_dv => dv).excludes(:id => self.id).first
|
228
|
+
if q_parent.nil?
|
229
|
+
return true if RationalNumber.new(nv,dv).parent.root?
|
230
|
+
else
|
231
|
+
return true
|
232
|
+
end
|
233
|
+
false
|
234
|
+
end
|
235
|
+
|
236
|
+
##
|
237
|
+
#
|
238
|
+
# Move conflicting nodes for a given value
|
239
|
+
#
|
240
|
+
# @param [Integer] The nominator value
|
241
|
+
# @param [Integer] The denominator value
|
242
|
+
#
|
243
|
+
def move_conflicting_nodes(nv,dv)
|
244
|
+
# As we are moving to the position of the conflicting sibling, it all items can be shifted similar to "move_above"
|
245
|
+
|
246
|
+
conflicting_sibling = base_class.where(:rational_number_nv => nv).where(:rational_number_dv => dv).excludes(:id => self.id).first
|
247
|
+
if (conflicting_sibling != nil)
|
248
|
+
# ensure_to_be_sibling_of(conflicting_sibling)
|
249
|
+
return if conflicting_sibling.position == self.position + 1
|
250
|
+
# If there are nodes between this and conflicting_sibling before move, make sure their position shifted before moving
|
251
|
+
_direction = (self.position > conflicting_sibling.position ? 1 : -1)
|
252
|
+
_position = (_direction < 0 ? conflicting_sibling.position + _direction : conflicting_sibling.position)
|
253
|
+
shift_nodes_position(conflicting_sibling, _direction, (_direction > 0 ? false : true))
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
##
|
258
|
+
#
|
259
|
+
# Query the ancestor rational number
|
260
|
+
#
|
261
|
+
# @return [RationalNumber] returns the rational number for the ancestor or nil for "not found"
|
262
|
+
#
|
263
|
+
def query_ancestor_rational_number
|
264
|
+
check_parent = base_class.where(:_id => self.parent_id).first
|
265
|
+
return nil if (check_parent.nil? || check_parent == [])
|
266
|
+
check_parent.rational_number
|
267
|
+
end
|
268
|
+
|
269
|
+
##
|
270
|
+
#
|
271
|
+
# Verifies parent keys from calculation and query
|
272
|
+
#
|
273
|
+
# @return [Boolean] true for correct, else false
|
274
|
+
#
|
275
|
+
def correct_rational_parent?(nv, dv)
|
276
|
+
q_rational_number = query_ancestor_rational_number
|
277
|
+
if q_rational_number.nil?
|
278
|
+
if RationalNumber.new(nv,dv).parent.root?
|
279
|
+
return true
|
280
|
+
else
|
281
|
+
return false
|
282
|
+
end
|
283
|
+
end
|
284
|
+
return true if self.rational_number.parent == q_rational_number
|
285
|
+
false
|
286
|
+
end
|
287
|
+
|
288
|
+
##
|
289
|
+
# Check if children needs to be rekeyed
|
290
|
+
#
|
291
|
+
def rekey_children?
|
292
|
+
persisted? && self.children? && ( self.previous_changes.include?("rational_number_nv") || self.previous_changes.include?("parent_ids") || self.changes.include?("rational_number_nv") || self.changes.include?("parent_ids") )
|
293
|
+
end
|
294
|
+
|
295
|
+
##
|
296
|
+
#
|
297
|
+
# Rekey each of the children (usually forcefully if a tree has gone "crazy")
|
298
|
+
#
|
299
|
+
# @return [undefined]
|
300
|
+
#
|
301
|
+
def rekey_children
|
302
|
+
_pos = 1
|
303
|
+
this_rational_number = self.rational_number
|
304
|
+
self.children.each do |child|
|
305
|
+
new_rational_number = this_rational_number.child_from_position(_pos)
|
306
|
+
move_node_and_save_if_changed(child, new_rational_number)
|
307
|
+
_pos += 1
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def rekey_former_siblings?
|
312
|
+
persisted? && self.previous_changes.include?("parent_id")
|
313
|
+
end
|
314
|
+
|
315
|
+
def rekey_former_siblings
|
316
|
+
former_siblings = base_class.where(:parent_id => attribute_was('parent_id')).
|
317
|
+
and(:rational_number_value.gt => (attribute_was('rational_number_value') || 0)).
|
318
|
+
excludes(:id => self.id)
|
319
|
+
former_siblings.each do |prev_sibling|
|
320
|
+
new_rational_number = prev_sibling.parent_rational_number.child_from_position(prev_sibling.position - 1)
|
321
|
+
move_node_and_save_if_changed(prev_sibling, new_rational_number)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def move_node_and_save_if_changed(node, new_rational_number)
|
326
|
+
if new_rational_number != node.rational_number
|
327
|
+
node.move_to_rational_number(new_rational_number.nv, new_rational_number.dv, {:force => true})
|
328
|
+
node.save_with_force_rational_numbers!
|
329
|
+
# node.reload # Should caller be responsible for reloading?
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
##
|
334
|
+
#
|
335
|
+
# Not needed, as each child gets the rational number updated after updating path?
|
336
|
+
# @return [undefined]
|
337
|
+
#
|
338
|
+
# def children_update_rational_number
|
339
|
+
# if rearrange_children?
|
340
|
+
# _position = 0
|
341
|
+
# # self.disable_timestamp_callback()
|
342
|
+
# self.children.each do |child|
|
343
|
+
# child.update_rational_number!(:position => _position)
|
344
|
+
# _position += 1
|
345
|
+
# end
|
346
|
+
# # self.enable_timestamp_callback()
|
347
|
+
# end
|
348
|
+
# end
|
349
|
+
|
350
|
+
##
|
351
|
+
# Enable timestamping callback if existing
|
352
|
+
#
|
353
|
+
def enable_timestamp_callback
|
354
|
+
|
355
|
+
end
|
356
|
+
|
357
|
+
##
|
358
|
+
# Disable timestamping callback if existing
|
359
|
+
#
|
360
|
+
def disable_timestamp_callback
|
361
|
+
|
362
|
+
end
|
363
|
+
|
364
|
+
##
|
365
|
+
# Convert to rational number
|
366
|
+
#
|
367
|
+
# @return [RationalNumber] The rational number for this node
|
368
|
+
#
|
369
|
+
def rational_number
|
370
|
+
RationalNumber.new(self.rational_number_nv, self.rational_number_dv, self.rational_number_snv, self.rational_number_sdv)
|
371
|
+
end
|
372
|
+
|
373
|
+
##
|
374
|
+
# Convert from rational number and set keys accordingly
|
375
|
+
#
|
376
|
+
# @param [RationalNumber] The rational number for this node
|
377
|
+
# @return [undefined]
|
378
|
+
#
|
379
|
+
def from_rational_number(rational_number)
|
380
|
+
self.rational_number_nv = rational_number.nv
|
381
|
+
self.rational_number_dv = rational_number.dv
|
382
|
+
self.rational_number_snv = rational_number.snv
|
383
|
+
self.rational_number_sdv = rational_number.sdv
|
384
|
+
self.rational_number_value = rational_number.number
|
385
|
+
end
|
386
|
+
|
387
|
+
##
|
388
|
+
# Returns a chainable criteria for this document's ancestors
|
389
|
+
#
|
390
|
+
# @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's ancestors
|
391
|
+
def ancestors
|
392
|
+
base_class.unscoped { super }
|
393
|
+
end
|
394
|
+
|
395
|
+
##
|
396
|
+
#
|
397
|
+
# Returns the positional value for the current node
|
398
|
+
#
|
399
|
+
def position
|
400
|
+
self.rational_number.position
|
401
|
+
end
|
402
|
+
|
403
|
+
##
|
404
|
+
# Returns siblings below the current document.
|
405
|
+
# Siblings with a position greater than this document's position.
|
406
|
+
#
|
407
|
+
# @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's lower siblings
|
408
|
+
def lower_siblings
|
409
|
+
self.siblings.where(:rational_number_value.gt => self.rational_number_value)
|
410
|
+
end
|
411
|
+
|
412
|
+
##
|
413
|
+
# Returns siblings above the current document.
|
414
|
+
# Siblings with a position lower than this document's position.
|
415
|
+
#
|
416
|
+
# @return [Mongoid::Criteria] Mongoid criteria to retrieve the document's higher siblings
|
417
|
+
def higher_siblings
|
418
|
+
self.siblings.where(:rational_number_value.lt => self.rational_number_value)
|
419
|
+
end
|
420
|
+
|
421
|
+
##
|
422
|
+
# Returns siblings between the current document and the other document
|
423
|
+
# Siblings with a position between this document's position and the other document's position.
|
424
|
+
#
|
425
|
+
# @return [Mongoid::Criteria] Mongoid criteria to retrieve the documents between this and the other document
|
426
|
+
def siblings_between(other)
|
427
|
+
range = [self.rational_number_value, other.rational_number_value].sort
|
428
|
+
self.siblings.where(:rational_number_value.gt => range.first, :rational_number_value.lt => range.last)
|
429
|
+
end
|
430
|
+
|
431
|
+
##
|
432
|
+
# Return the siblings between this and other + other
|
433
|
+
#
|
434
|
+
def siblings_between_including_other(other)
|
435
|
+
range = [self.rational_number_value, other.rational_number_value].sort
|
436
|
+
self.siblings.where(:rational_number_value.gte => range.first, :rational_number_value.lte => range.last)
|
437
|
+
end
|
438
|
+
|
439
|
+
##
|
440
|
+
# Returns the lowest sibling (could be self)
|
441
|
+
#
|
442
|
+
# @return [Mongoid::Document] The lowest sibling
|
443
|
+
def last_sibling_in_list
|
444
|
+
siblings_and_self.last
|
445
|
+
end
|
446
|
+
|
447
|
+
##
|
448
|
+
# Returns the highest sibling (could be self)
|
449
|
+
#
|
450
|
+
# @return [Mongoid::Document] The highest sibling
|
451
|
+
def first_sibling_in_list
|
452
|
+
siblings_and_self.first
|
453
|
+
end
|
454
|
+
|
455
|
+
##
|
456
|
+
# Is this the highest sibling?
|
457
|
+
#
|
458
|
+
# @return [Boolean] Whether the document is the highest sibling
|
459
|
+
def at_top?
|
460
|
+
higher_siblings.empty?
|
461
|
+
end
|
462
|
+
|
463
|
+
##
|
464
|
+
# Is this the lowest sibling?
|
465
|
+
#
|
466
|
+
# @return [Boolean] Whether the document is the lowest sibling
|
467
|
+
def at_bottom?
|
468
|
+
lower_siblings.empty?
|
469
|
+
end
|
470
|
+
|
471
|
+
##
|
472
|
+
# Move this node above all its siblings
|
473
|
+
#
|
474
|
+
# @return [undefined]
|
475
|
+
def move_to_top
|
476
|
+
return true if at_top?
|
477
|
+
move_above(first_sibling_in_list)
|
478
|
+
end
|
479
|
+
|
480
|
+
##
|
481
|
+
# Move this node below all its siblings
|
482
|
+
#
|
483
|
+
# @return [undefined]
|
484
|
+
def move_to_bottom
|
485
|
+
return true if at_bottom?
|
486
|
+
move_below(last_sibling_in_list)
|
487
|
+
end
|
488
|
+
|
489
|
+
##
|
490
|
+
# Move this node one position up
|
491
|
+
#
|
492
|
+
# @return [undefined]
|
493
|
+
def move_up
|
494
|
+
unless at_top?
|
495
|
+
prev_sibling = higher_siblings.last
|
496
|
+
switch_with_sibling(prev_sibling) unless prev_sibling.nil?
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
##
|
501
|
+
# Move this node one position down
|
502
|
+
#
|
503
|
+
# @return [undefined]
|
504
|
+
def move_down
|
505
|
+
unless at_bottom?
|
506
|
+
next_sibling = lower_siblings.first
|
507
|
+
switch_with_sibling(next_sibling) unless next_sibling.nil?
|
508
|
+
end
|
509
|
+
end
|
510
|
+
|
511
|
+
##
|
512
|
+
# Shift nodes between self and other (or including other) in one or the other direction
|
513
|
+
#
|
514
|
+
# @param [Mongoid::Tree] other document to move this document above
|
515
|
+
# @param [Integer] +1 / -1 for the direction to shift nodes
|
516
|
+
# @param [Boolean] exclude the other object in the shift or not.
|
517
|
+
#
|
518
|
+
#
|
519
|
+
def shift_nodes_position(other, direction, exclude_other = false)
|
520
|
+
# puts "#{self.name} shift_nodes_position other: #{other.name} direction #{direction} exclude_other: #{exclude_other}"
|
521
|
+
if exclude_other
|
522
|
+
nodes_to_shift = siblings_between(other)
|
523
|
+
else
|
524
|
+
nodes_to_shift = siblings_between_including_other(other)
|
525
|
+
end
|
526
|
+
shift_nodes(nodes_to_shift, direction)
|
527
|
+
end
|
528
|
+
|
529
|
+
def shift_lower_nodes_from_other(other, direction)
|
530
|
+
# puts "#{self.name} shift_lower_nodes_from_other other: #{other.name} direction: #{direction}"
|
531
|
+
range = [other.rational_number_value, other.siblings.last.rational_number_value].sort
|
532
|
+
nodes_to_shift = other.siblings_and_self.where(:rational_number_value.gte => range.first, :rational_number_value.lte => range.last)
|
533
|
+
shift_nodes(nodes_to_shift, direction)
|
534
|
+
end
|
535
|
+
|
536
|
+
def shift_nodes(nodes_to_shift, direction)
|
537
|
+
# puts "#{self.name} shift_nodes direction: #{direction}"
|
538
|
+
nodes_to_shift.each do |node_to_shift|
|
539
|
+
pos = node_to_shift.position + direction
|
540
|
+
# puts " shifting #{node_to_shift.name} from position #{node_to_shift.position} to #{pos}"
|
541
|
+
node_to_shift.move_to_position(pos, {:force => true})
|
542
|
+
node_to_shift.save_with_force_rational_numbers!
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
##
|
547
|
+
# Move this node above the specified node
|
548
|
+
#
|
549
|
+
# This method changes the node's parent if nescessary.
|
550
|
+
#
|
551
|
+
# @param [Mongoid::Tree] other document to move this document above
|
552
|
+
#
|
553
|
+
# @return [undefined]
|
554
|
+
def move_above(other)
|
555
|
+
ensure_to_be_sibling_of(other)
|
556
|
+
return if other.position == self.position + 1
|
557
|
+
@_rational_moving_nodes = true
|
558
|
+
# If there are nodes between this and other before move, make sure they are shifted upwards before moving
|
559
|
+
_direction = (self.position > other.position ? 1 : -1)
|
560
|
+
_position = (_direction < 0 ? other.position + _direction : other.position)
|
561
|
+
shift_nodes_position(other, _direction, (_direction > 0 ? false : true))
|
562
|
+
|
563
|
+
# There should not be conflicting nodes at this stage.
|
564
|
+
move_to_position(_position)
|
565
|
+
save!
|
566
|
+
@_rational_moving_nodes = false
|
567
|
+
end
|
568
|
+
|
569
|
+
##
|
570
|
+
# Move this node below the specified node
|
571
|
+
#
|
572
|
+
# This method changes the node's parent if nescessary.
|
573
|
+
#
|
574
|
+
# @param [Mongoid::Tree] other document to move this document below
|
575
|
+
#
|
576
|
+
# @return [undefined]
|
577
|
+
def move_below(other)
|
578
|
+
ensure_to_be_sibling_of(other)
|
579
|
+
return if other.position + 1 == self.position
|
580
|
+
|
581
|
+
@_rational_moving_nodes = true
|
582
|
+
|
583
|
+
_direction = (self.position > other.position ? 1 : -1)
|
584
|
+
_position = (_direction > 0 ? other.position + _direction : other.position)
|
585
|
+
shift_nodes_position(other, _direction, (_direction > 0 ? true : false))
|
586
|
+
|
587
|
+
move_to_position(_position)
|
588
|
+
save!
|
589
|
+
@_rational_moving_nodes = false
|
590
|
+
end
|
591
|
+
|
592
|
+
##
|
593
|
+
# Disable the timestamps for the document type, and increase the disable count
|
594
|
+
# Will only disable once, even if called multiple times
|
595
|
+
#
|
596
|
+
# @return [undefined]
|
597
|
+
def disable_timestamp_callback
|
598
|
+
if self.respond_to?("updated_at")
|
599
|
+
self.class.skip_callback(:save, :before, :update_timestamps ) if @@_disable_timestamp_count == 0
|
600
|
+
@@_disable_timestamp_count += 1
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
##
|
605
|
+
# Enable the timestamps for the document type, and decrease the disable count
|
606
|
+
# Will only enable once, even if called multiple times
|
607
|
+
#
|
608
|
+
# @return [undefined]
|
609
|
+
def enable_timestamp_callback
|
610
|
+
if self.respond_to?("updated_at")
|
611
|
+
@@_disable_timestamp_count -= 1
|
612
|
+
self.class.set_callback(:save, :before, :update_timestamps ) if @@_disable_timestamp_count == 0
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
|
617
|
+
##
|
618
|
+
#
|
619
|
+
# Update the rational numbers on the document if changes to parent
|
620
|
+
# or rational number has been changed
|
621
|
+
#
|
622
|
+
# Should calculate next free nv/dv and set that if parent has changed.
|
623
|
+
# (set values to "missing and call missing function should work")
|
624
|
+
#
|
625
|
+
# If there are both changes to nv/dv and parent_id, nv/dv settings takes
|
626
|
+
# precedence over parent_id changes
|
627
|
+
#
|
628
|
+
# @return [undefined]
|
629
|
+
#
|
630
|
+
def update_rational_number
|
631
|
+
if self.rational_number_nv_changed? && self.rational_number_dv_changed? && !self.rational_number_value.nil? && !set_initial_rational_number?
|
632
|
+
self.set_rational_number(self.rational_number_nv, self.rational_number_dv, false)
|
633
|
+
elsif self.parent_id_changed? || set_initial_rational_number?
|
634
|
+
# only changed parent, needs to find next free position
|
635
|
+
# Get rational number from new parent
|
636
|
+
|
637
|
+
last_sibling = self.siblings.last
|
638
|
+
|
639
|
+
if (last_sibling.nil?)
|
640
|
+
new_rational_number = parent_rational_number.child_from_position(1)
|
641
|
+
else
|
642
|
+
new_rational_number = parent_rational_number.child_from_position(last_sibling.rational_number.position + 1)
|
643
|
+
end
|
644
|
+
|
645
|
+
self.move_to_rational_number(new_rational_number.nv, new_rational_number.dv)
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
##
|
650
|
+
# Get the parent rational number or "root" rational number if no parent
|
651
|
+
#
|
652
|
+
def parent_rational_number
|
653
|
+
if root?
|
654
|
+
RationalNumber.new
|
655
|
+
else
|
656
|
+
self.parent.rational_number
|
657
|
+
end
|
658
|
+
end
|
659
|
+
|
660
|
+
##
|
661
|
+
#
|
662
|
+
# Check if the rational number should be updated
|
663
|
+
#
|
664
|
+
# @return true if it should be updated, else false
|
665
|
+
#
|
666
|
+
def update_rational_number?
|
667
|
+
(set_initial_rational_number? || self.parent_id_changed? || (self.rational_number_nv_changed? && self.rational_number_dv_changed?)) && !self.forced_rational_number? && !self.moving_nodes?
|
668
|
+
end
|
669
|
+
|
670
|
+
##
|
671
|
+
# Should the initial rational number value
|
672
|
+
#
|
673
|
+
def set_initial_rational_number?
|
674
|
+
self.rational_number_value.nil?
|
675
|
+
end
|
676
|
+
|
677
|
+
##
|
678
|
+
# Was the changed forced?
|
679
|
+
#
|
680
|
+
def forced_rational_number?
|
681
|
+
!!@_forced_rational_number
|
682
|
+
end
|
683
|
+
|
684
|
+
##
|
685
|
+
# Currently moving nodes around?
|
686
|
+
#
|
687
|
+
def moving_nodes?
|
688
|
+
!!@_rational_moving_nodes
|
689
|
+
end
|
690
|
+
|
691
|
+
##
|
692
|
+
# save when forcing rational numbers
|
693
|
+
#
|
694
|
+
def save_with_force_rational_numbers!
|
695
|
+
@_forced_rational_number = true
|
696
|
+
self.save!
|
697
|
+
@_forced_rational_number = false
|
698
|
+
end
|
699
|
+
|
700
|
+
##
|
701
|
+
# Get the tree under the given node
|
702
|
+
#
|
703
|
+
def tree
|
704
|
+
low_rational_number = self.rational_number_value
|
705
|
+
high_rational_number = self.rational_number.parent.child_from_position(self.position+1).number
|
706
|
+
|
707
|
+
base_class.where(:rational_number_value.gt => low_rational_number, :rational_number_value.lt => high_rational_number)
|
708
|
+
end
|
709
|
+
|
710
|
+
##
|
711
|
+
# Get the tree under the given node
|
712
|
+
#
|
713
|
+
def tree_and_self
|
714
|
+
low_rational_number = self.rational_number_value
|
715
|
+
high_rational_number = self.rational_number.parent.child_from_position(self.position+1).number
|
716
|
+
|
717
|
+
base_class.where(:rational_number_value.gte => low_rational_number, :rational_number_value.lt => high_rational_number)
|
718
|
+
end
|
719
|
+
|
720
|
+
private
|
721
|
+
|
722
|
+
##
|
723
|
+
#
|
724
|
+
# Switch location with a given sibling
|
725
|
+
#
|
726
|
+
# @param [Mongoid::Tree] other document to switch places with
|
727
|
+
#
|
728
|
+
# @return [undefined]
|
729
|
+
#
|
730
|
+
def switch_with_sibling(sibling)
|
731
|
+
self_pos = self.position
|
732
|
+
sibling_pos = sibling.position
|
733
|
+
sibling.move_to_position(self_pos, {:force => true})
|
734
|
+
self.move_to_position(sibling_pos, {:force => true})
|
735
|
+
sibling.save_with_force_rational_numbers!
|
736
|
+
self.save_with_force_rational_numbers!
|
737
|
+
end
|
738
|
+
|
739
|
+
##
|
740
|
+
#
|
741
|
+
# Ensure this is a sibling of given other, if not, move it to the same parent
|
742
|
+
#
|
743
|
+
# @param [Mongoid::Tree] other document to ensure sibling relation
|
744
|
+
#
|
745
|
+
# @return [undefined]
|
746
|
+
#
|
747
|
+
def ensure_to_be_sibling_of(other)
|
748
|
+
return if sibling_of?(other)
|
749
|
+
self.parent = other.parent
|
750
|
+
save!
|
751
|
+
end
|
752
|
+
|
753
|
+
def ensure_to_have_correct_parent(nv,dv)
|
754
|
+
# puts "#{self.name} ensure_to_have_correct_parent #{nv}/#{dv}"
|
755
|
+
new_rational_number = RationalNumber.new(nv,dv)
|
756
|
+
new_parent = nil
|
757
|
+
# puts " root: #{new_rational_number.root?} #{("parent: " + self.parent.name + " nv/dv : "+ self.parent.rational_number.nv.to_s+ "/"+ self.parent.rational_number.dv.to_s) unless self.parent.nil?}#{"parent: nil" if self.parent.nil?}"
|
758
|
+
if self.parent == nil
|
759
|
+
return true if new_rational_number.parent == RationalNumber.new
|
760
|
+
elsif new_rational_number.parent.root?
|
761
|
+
new_parent = nil
|
762
|
+
else
|
763
|
+
return true if self.parent.rational_number == new_rational_number.parent
|
764
|
+
new_parent = base_class.where(:rational_number_nv => new_rational_number.parent.nv, :rational_number_dv => new_rational_number.dv)
|
765
|
+
return false if new_parent.nil? # INVALID PARENT
|
766
|
+
end
|
767
|
+
# If entered here, the parent needs to change
|
768
|
+
# puts " changing parent to #{new_parent.name if !new_parent.nil?} #{"nil" if new_parent.nil?}"
|
769
|
+
self.parent = new_parent
|
770
|
+
end
|
771
|
+
|
772
|
+
# FIX THESE SHIT CASE FUCK!
|
773
|
+
|
774
|
+
def move_lower_siblings
|
775
|
+
lower_siblings.each do |sibling|
|
776
|
+
disable_timestamp_callback
|
777
|
+
sibling.move_to_position(sibling.position - 1)
|
778
|
+
sibling.save_with_force_rational_numbers!
|
779
|
+
enable_timestamp_callback
|
780
|
+
end
|
781
|
+
end
|
782
|
+
|
783
|
+
# def reposition_former_siblings
|
784
|
+
# former_siblings = base_class.where(:parent_id => attribute_was('parent_id')).
|
785
|
+
# and(:position.gt => (attribute_was('position') || 0)).
|
786
|
+
# excludes(:id => self.id)
|
787
|
+
# former_siblings.inc(:position, -1)
|
788
|
+
# end
|
789
|
+
|
790
|
+
# def sibling_reposition_required?
|
791
|
+
# parent_id_changed? && persisted?
|
792
|
+
# end
|
793
|
+
|
794
|
+
|
795
|
+
|
796
|
+
end # RationalNumbering
|
797
|
+
end # Tree
|
798
|
+
end # Mongoid
|
799
|
+
|
800
|
+
##
|
801
|
+
# The rational number is root and therefore has no siblings
|
802
|
+
#
|
803
|
+
class InvalidParentError < StandardError
|
804
|
+
end
|
805
|
+
|