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.
@@ -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
+