Module: Mongoid::Tree::RationalNumbering

Extended by:
ActiveSupport::Concern
Defined in:
lib/mongoid/tree/rational_numbering.rb

Overview

Mongoid::Tree::RationalNumbering

Mongoid::Tree doesn’t use rational numbers by default. To enable rational numbering of children include both Mongoid::Tree and Mongoid::Tree::RationalNumbering into your document.

Utility methods

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

@@_disable_timestamp_count =
0

Instance Method Summary collapse

Instance Method Details

#ancestorsMongoid::Criteria

Returns a chainable criteria for this document’s ancestors

Returns:

  • (Mongoid::Criteria)

    Mongoid criteria to retrieve the document’s ancestors



391
392
393
# File 'lib/mongoid/tree/rational_numbering.rb', line 391

def ancestors
  base_class.unscoped { super }
end

#at_bottom?Boolean

Is this the lowest sibling?

Returns:

  • (Boolean)

    Whether the document is the lowest sibling



467
468
469
# File 'lib/mongoid/tree/rational_numbering.rb', line 467

def at_bottom?
  lower_siblings.empty?
end

#at_top?Boolean

Is this the highest sibling?

Returns:

  • (Boolean)

    Whether the document is the highest sibling



459
460
461
# File 'lib/mongoid/tree/rational_numbering.rb', line 459

def at_top?
  higher_siblings.empty?
end

#correct_rational_parent?(nv, dv) ⇒ Boolean

Verifies parent keys from calculation and query

Returns:

  • (Boolean)

    true for correct, else false



275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/mongoid/tree/rational_numbering.rb', line 275

def correct_rational_parent?(nv, dv)
  q_rational_number = query_ancestor_rational_number
  if q_rational_number.nil?
    if RationalNumber.new(nv,dv).parent.root?
      return true
    else
      return false
    end
  end
  return true if self.rational_number.parent == q_rational_number
  false
end

#disable_timestamp_callbackundefined

Disable the timestamps for the document type, and increase the disable count Will only disable once, even if called multiple times

Returns:

  • (undefined)


360
361
362
# File 'lib/mongoid/tree/rational_numbering.rb', line 360

def disable_timestamp_callback

end

#enable_timestamp_callbackundefined

Enable the timestamps for the document type, and decrease the disable count Will only enable once, even if called multiple times

Returns:

  • (undefined)


353
354
355
# File 'lib/mongoid/tree/rational_numbering.rb', line 353

def enable_timestamp_callback

end

#first_sibling_in_listMongoid::Document

Returns the highest sibling (could be self)

Returns:

  • (Mongoid::Document)

    The highest sibling



451
452
453
# File 'lib/mongoid/tree/rational_numbering.rb', line 451

def first_sibling_in_list
  siblings_and_self.first
end

#forced_rational_number?Boolean

Was the changed forced?

Returns:

  • (Boolean)


680
681
682
# File 'lib/mongoid/tree/rational_numbering.rb', line 680

def forced_rational_number?
  !!@_forced_rational_number
end

#from_rational_number(rational_number) ⇒ undefined

Convert from rational number and set keys accordingly

Parameters:

  • The (RationalNumber)

    rational number for this node

Returns:

  • (undefined)


379
380
381
382
383
384
385
# File 'lib/mongoid/tree/rational_numbering.rb', line 379

def from_rational_number(rational_number)
  self.rational_number_nv    = rational_number.nv
  self.rational_number_dv    = rational_number.dv
  self.rational_number_snv   = rational_number.snv
  self.rational_number_sdv   = rational_number.sdv
  self.rational_number_value = rational_number.number
end

#higher_siblingsMongoid::Criteria

Returns siblings above the current document. Siblings with a position lower than this document’s position.

Returns:

  • (Mongoid::Criteria)

    Mongoid criteria to retrieve the document’s higher siblings



417
418
419
# File 'lib/mongoid/tree/rational_numbering.rb', line 417

def higher_siblings
  self.siblings.where(:rational_number_value.lt => self.rational_number_value)
end

#initialize(*args) ⇒ undefined

Initialize the rational tree document

Returns:

  • (undefined)


91
92
93
94
95
# File 'lib/mongoid/tree/rational_numbering.rb', line 91

def initialize(*args)
  @_forced_rational_number = false
  @_rational_moving_nodes  = false
  super
end

#last_sibling_in_listMongoid::Document

Returns the lowest sibling (could be self)

Returns:

  • (Mongoid::Document)

    The lowest sibling



443
444
445
# File 'lib/mongoid/tree/rational_numbering.rb', line 443

def last_sibling_in_list
  siblings_and_self.last
end

#lower_siblingsMongoid::Criteria

Returns siblings below the current document. Siblings with a position greater than this document’s position.

Returns:

  • (Mongoid::Criteria)

    Mongoid criteria to retrieve the document’s lower siblings



408
409
410
# File 'lib/mongoid/tree/rational_numbering.rb', line 408

def lower_siblings
  self.siblings.where(:rational_number_value.gt => self.rational_number_value)
end

#move_above(other) ⇒ undefined

Move this node above the specified node

This method changes the node’s parent if nescessary.

Parameters:

Returns:

  • (undefined)


554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/mongoid/tree/rational_numbering.rb', line 554

def move_above(other)
  ensure_to_be_sibling_of(other)
  return if other.position == self.position + 1
  @_rational_moving_nodes = true
  # If there are nodes between this and other before move, make sure they are shifted upwards before moving
  _direction = (self.position > other.position ? 1 : -1)
  _position = (_direction < 0 ? other.position + _direction : other.position)
  shift_nodes_position(other, _direction, (_direction > 0 ? false : true))

  # There should not be conflicting nodes at this stage.
  move_to_position(_position)
  save!
  @_rational_moving_nodes = false
end

#move_below(other) ⇒ undefined

Move this node below the specified node

This method changes the node’s parent if nescessary.

Parameters:

Returns:

  • (undefined)


577
578
579
580
581
582
583
584
585
586
587
588
589
590
# File 'lib/mongoid/tree/rational_numbering.rb', line 577

def move_below(other)
  ensure_to_be_sibling_of(other)
  return if other.position + 1 == self.position

  @_rational_moving_nodes = true

  _direction = (self.position > other.position ? 1 : -1)
  _position = (_direction > 0 ? other.position + _direction : other.position)
  shift_nodes_position(other, _direction, (_direction > 0 ? true : false))

  move_to_position(_position)
  save!
  @_rational_moving_nodes = false
end

#move_conflicting_nodes(nv, dv) ⇒ Object

Move conflicting nodes for a given value

Parameters:

  • The (Integer)

    nominator value

  • The (Integer)

    denominator value



243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/mongoid/tree/rational_numbering.rb', line 243

def move_conflicting_nodes(nv,dv)
  # As we are moving to the position of the conflicting sibling, it all items can be shifted similar to "move_above"

  conflicting_sibling = base_class.where(:rational_number_nv => nv).where(:rational_number_dv => dv).excludes(:id => self.id).first
  if (conflicting_sibling != nil)
    # ensure_to_be_sibling_of(conflicting_sibling)
    return if conflicting_sibling.position == self.position + 1
    # If there are nodes between this and conflicting_sibling before move, make sure their position shifted before moving
    _direction = (self.position > conflicting_sibling.position ? 1 : -1)
    _position = (_direction < 0 ? conflicting_sibling.position + _direction : conflicting_sibling.position)
    shift_nodes_position(conflicting_sibling, _direction, (_direction > 0 ? false : true))
  end
end

#move_downundefined

Move this node one position down

Returns:

  • (undefined)


504
505
506
507
508
509
# File 'lib/mongoid/tree/rational_numbering.rb', line 504

def move_down
  unless at_bottom?
    next_sibling = lower_siblings.first
    switch_with_sibling(next_sibling) unless next_sibling.nil?
  end
end

#move_node_and_save_if_changed(node, new_rational_number) ⇒ Object



325
326
327
328
329
330
331
# File 'lib/mongoid/tree/rational_numbering.rb', line 325

def move_node_and_save_if_changed(node, new_rational_number)
  if new_rational_number != node.rational_number
    node.move_to_rational_number(new_rational_number.nv, new_rational_number.dv, {:force => true})
    node.save_with_force_rational_numbers!
    # node.reload # Should caller be responsible for reloading?
  end
end

#move_to_bottomundefined

Move this node below all its siblings

Returns:

  • (undefined)


484
485
486
487
# File 'lib/mongoid/tree/rational_numbering.rb', line 484

def move_to_bottom
  return true if at_bottom?
  move_below(last_sibling_in_list)
end

#move_to_position(_position, opts = {}) ⇒ undefined

INTERNAL

Move the document to a given position (integer based, starting with 1)

if a document exists on the new position, all siblings are shifted right before moving this document can move without updating conflicting siblings by using :force in options

Parameters:

  • The (Integer)

    positional value

  • Options: (Hash)

    :force (defaults to false)

Returns:

  • (undefined)


126
127
128
129
# File 'lib/mongoid/tree/rational_numbering.rb', line 126

def move_to_position(_position, opts = {})
  new_rational_number = parent_rational_number.child_from_position(_position)
  move_to_rational_number(new_rational_number.nv, new_rational_number.dv, opts)
end

#move_to_rational_number(nv, dv, opts = {}) ⇒ undefined

INTERNAL

Move the document to a given rational_number position

if a document exists on the new position, all siblings are shifted right before moving this document can move without updating conflicting siblings by using :ignore_conflicts in options

Parameters:

  • The (Integer)

    nominator value

  • The (Integer)

    denominator value

  • Options: (Hash)

    :force (defaults to false)

Returns:

  • (undefined)


144
145
146
147
148
149
150
# File 'lib/mongoid/tree/rational_numbering.rb', line 144

def move_to_rational_number(nv, dv, opts = {})
  # don't check for conflict if forced move
  move_conflicting_nodes(nv,dv) unless !!opts[:force]

  # shouldn't be any conflicting sibling now...
  self.from_rational_number(RationalNumber.new(nv,dv))
end

#move_to_topundefined

Move this node above all its siblings

Returns:

  • (undefined)


475
476
477
478
# File 'lib/mongoid/tree/rational_numbering.rb', line 475

def move_to_top
  return true if at_top?
  move_above(first_sibling_in_list)
end

#move_upundefined

Move this node one position up

Returns:

  • (undefined)


493
494
495
496
497
498
# File 'lib/mongoid/tree/rational_numbering.rb', line 493

def move_up
  unless at_top?
    prev_sibling = higher_siblings.last
    switch_with_sibling(prev_sibling) unless prev_sibling.nil?
  end
end

#moving_nodes?Boolean

Currently moving nodes around?

Returns:

  • (Boolean)


687
688
689
# File 'lib/mongoid/tree/rational_numbering.rb', line 687

def moving_nodes?
  !!@_rational_moving_nodes
end

#parent_exists?(nv, dv) ⇒ Boolean

Check if a parent exists for the given nv/dv values

Will return true if the parent is “root” and the node should be created as a root element

Returns:

  • (Boolean)


226
227
228
229
230
231
232
233
234
# File 'lib/mongoid/tree/rational_numbering.rb', line 226

def parent_exists?(nv,dv)
  q_parent = base_class.where(:rational_number_nv => nv).where(:rational_number_dv => dv).excludes(:id => self.id).first
  if q_parent.nil?
    return true if RationalNumber.new(nv,dv).parent.root?
  else
    return true
  end
  false
end

#parent_rational_numberObject

Get the parent rational number or “root” rational number if no parent



652
653
654
655
656
657
658
# File 'lib/mongoid/tree/rational_numbering.rb', line 652

def parent_rational_number
  if root?
    RationalNumber.new
  else
    self.parent.rational_number
  end
end

#positionObject

Returns the positional value for the current node



399
400
401
# File 'lib/mongoid/tree/rational_numbering.rb', line 399

def position
  self.rational_number.position
end

#query_ancestor_rational_numberRationalNumber

Query the ancestor rational number

Returns:

  • (RationalNumber)

    returns the rational number for the ancestor or nil for “not found”



263
264
265
266
267
# File 'lib/mongoid/tree/rational_numbering.rb', line 263

def query_ancestor_rational_number
  check_parent = base_class.where(:_id => self.parent_id).first
  return nil if (check_parent.nil? || check_parent == [])
  check_parent.rational_number
end

#rational_numberRationalNumber

Convert to rational number

Returns:

  • (RationalNumber)

    The rational number for this node



369
370
371
# File 'lib/mongoid/tree/rational_numbering.rb', line 369

def rational_number
  RationalNumber.new(self.rational_number_nv, self.rational_number_dv, self.rational_number_snv, self.rational_number_sdv)
end

#rekey_childrenundefined

Rekey each of the children (usually forcefully if a tree has gone “crazy”)

Returns:

  • (undefined)


301
302
303
304
305
306
307
308
309
# File 'lib/mongoid/tree/rational_numbering.rb', line 301

def rekey_children
  _pos = 1
  this_rational_number = self.rational_number
  self.children.each do |child|
    new_rational_number = this_rational_number.child_from_position(_pos)
    move_node_and_save_if_changed(child, new_rational_number)
    _pos += 1
  end
end

#rekey_children?Boolean

Check if children needs to be rekeyed

Returns:

  • (Boolean)


291
292
293
# File 'lib/mongoid/tree/rational_numbering.rb', line 291

def rekey_children?
  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") )
end

#rekey_former_siblingsObject



315
316
317
318
319
320
321
322
323
# File 'lib/mongoid/tree/rational_numbering.rb', line 315

def rekey_former_siblings
  former_siblings = base_class.where(:parent_id => attribute_was('parent_id')).
                               and(:rational_number_value.gt => (attribute_was('rational_number_value') || 0)).
                               excludes(:id => self.id)
  former_siblings.each do |prev_sibling|
    new_rational_number = prev_sibling.parent_rational_number.child_from_position(prev_sibling.position - 1)
    move_node_and_save_if_changed(prev_sibling, new_rational_number)
  end
end

#rekey_former_siblings?Boolean

Returns:

  • (Boolean)


311
312
313
# File 'lib/mongoid/tree/rational_numbering.rb', line 311

def rekey_former_siblings?
  persisted? && self.previous_changes.include?("parent_id")
end

#save_with_force_rational_numbers!Object

save when forcing rational numbers



694
695
696
697
698
# File 'lib/mongoid/tree/rational_numbering.rb', line 694

def save_with_force_rational_numbers!
  @_forced_rational_number = true
  self.save!
  @_forced_rational_number = false
end

#set_initial_rational_number?Boolean

Should the initial rational number value

Returns:

  • (Boolean)


673
674
675
# File 'lib/mongoid/tree/rational_numbering.rb', line 673

def set_initial_rational_number?
  self.rational_number_value.nil?
end

#set_rational_number(nv, dv, do_save = true) ⇒ Object

This can be used to set a rational number directly The node will be moved to the correct parent

If the given nv/dv does not find an existing parent, it will add an validation error

If the given nv/dv is higher than the last sibling under the parent, the nv/dv will be recalculated to appropriate nv/dv values



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/mongoid/tree/rational_numbering.rb', line 162

def set_rational_number(nv,dv, do_save = true)
  # return true of already at the right spot
  # puts "#{self.name} - set_rational_number #{nv}/#{dv} self:#{self.rational_number_nv}/#{self.rational_number_dv}"
  return true if self.rational_number_nv == nv && self.rational_number_dv == dv && (!self.rational_number_nv_changed? && !self.rational_number_dv_changed?)
  # check if parent exist
  # puts "  parent exists: #{parent_exists?(nv,dv).inspect}"
  unless parent_exists?(nv,dv)
    errors.add(:base, I18n.t(:parent_does_not_exist, :scope => [:mongoid, :errors, :messages, :tree, :rational], nv: nv, dv: dv) )
    return false
  end
  # find other/conflicting sibling
  other = base_class.where(:rational_number_nv => nv).where(:rational_number_dv => dv).excludes(:id => self.id).first
  already_sibling_of = other.nil? ? false : self.sibling_of?(other)

  # puts "  conflict: #{other.nil?} already_sibling_of :#{already_sibling_of}"
  return false if ensure_to_have_correct_parent(nv,dv) == false

  move_to_rational = RationalNumber.new(nv,dv)

  unless other.nil?
    if already_sibling_of
      # puts "  already sibling of other, so moving down"
      return if other.position == self.position + 1
      # If there are nodes between this and other before move, make sure they are shifted upwards before moving
      _direction = (self.position > other.position ? 1 : -1)
      _position = (_direction < 0 ? other.position + _direction : other.position)
      shift_nodes_position(other, _direction, (_direction > 0 ? false : true))

      # There should not be conflicting nodes at this stage.
      move_to_position(_position)
    else
      # puts "  shifting lower nodes from other"
      shift_lower_nodes_from_other(other, 1)
    end
  else
    # make sure the new position is the next rational value under the parent
    # as there was no "other" to move
    new_parent = base_class.where(:id => self.parent_id).first
    if new_parent.nil?
      # count roots
      root_count = base_class.roots.count
      move_to_rational = RationalNumber.new.child_from_position(root_count+1)
      # puts "  new parent is root root_count: #{root_count} new 'correct position' is : #{move_to_rational.nv}/#{move_to_rational.dv}"
    else
      child_count = new_parent.children.count
      move_to_rational = new_parent.rational_number.child_from_position(child_count+1)
      # puts "  new parent is not root child_count: #{child_count} new 'correct position' is : #{move_to_rational.nv}/#{move_to_rational.dv}"
    end
  end
  move_to_rational_number(move_to_rational.nv, move_to_rational.dv, {:force => true})
  if do_save
    save
  else
    true
  end
end

#shift_lower_nodes_from_other(other, direction) ⇒ Object



529
530
531
532
533
534
# File 'lib/mongoid/tree/rational_numbering.rb', line 529

def shift_lower_nodes_from_other(other, direction)
  # puts "#{self.name} shift_lower_nodes_from_other other: #{other.name} direction: #{direction}"
  range = [other.rational_number_value, other.siblings.last.rational_number_value].sort
  nodes_to_shift = other.siblings_and_self.where(:rational_number_value.gte => range.first, :rational_number_value.lte => range.last)
  shift_nodes(nodes_to_shift, direction)
end

#shift_nodes(nodes_to_shift, direction) ⇒ Object



536
537
538
539
540
541
542
543
544
# File 'lib/mongoid/tree/rational_numbering.rb', line 536

def shift_nodes(nodes_to_shift, direction)
  # puts "#{self.name} shift_nodes direction: #{direction}"
  nodes_to_shift.each do |node_to_shift|
    pos = node_to_shift.position + direction
    # puts "  shifting #{node_to_shift.name} from position #{node_to_shift.position} to #{pos}"
    node_to_shift.move_to_position(pos, {:force => true})
    node_to_shift.save_with_force_rational_numbers!
  end
end

#shift_nodes_position(other, direction, exclude_other = false) ⇒ Object

Shift nodes between self and other (or including other) in one or the other direction

Parameters:

  • other (Mongoid::Tree)

    document to move this document above

  • +1 (Integer)

    / -1 for the direction to shift nodes

  • exclude (Boolean)

    the other object in the shift or not.



519
520
521
522
523
524
525
526
527
# File 'lib/mongoid/tree/rational_numbering.rb', line 519

def shift_nodes_position(other, direction, exclude_other = false)
  # puts "#{self.name} shift_nodes_position other: #{other.name} direction #{direction} exclude_other: #{exclude_other}"
  if exclude_other
    nodes_to_shift = siblings_between(other)
  else
    nodes_to_shift = siblings_between_including_other(other)
  end
  shift_nodes(nodes_to_shift, direction)
end

#siblings_between(other) ⇒ Mongoid::Criteria

Returns siblings between the current document and the other document Siblings with a position between this document’s position and the other document’s position.

Returns:

  • (Mongoid::Criteria)

    Mongoid criteria to retrieve the documents between this and the other document



426
427
428
429
# File 'lib/mongoid/tree/rational_numbering.rb', line 426

def siblings_between(other)
  range = [self.rational_number_value, other.rational_number_value].sort
  self.siblings.where(:rational_number_value.gt => range.first, :rational_number_value.lt => range.last)
end

#siblings_between_including_other(other) ⇒ Object

Return the siblings between this and other + other



434
435
436
437
# File 'lib/mongoid/tree/rational_numbering.rb', line 434

def siblings_between_including_other(other)
  range = [self.rational_number_value, other.rational_number_value].sort
  self.siblings.where(:rational_number_value.gte => range.first, :rational_number_value.lte => range.last)
end

#treeObject

Get the tree under the given node



703
704
705
706
707
708
# File 'lib/mongoid/tree/rational_numbering.rb', line 703

def tree
  low_rational_number  = self.rational_number_value
  high_rational_number = self.rational_number.parent.child_from_position(self.position+1).number

  base_class.where(:rational_number_value.gt => low_rational_number, :rational_number_value.lt => high_rational_number)
end

#tree_and_selfObject

Get the tree under the given node



713
714
715
716
717
718
# File 'lib/mongoid/tree/rational_numbering.rb', line 713

def tree_and_self
  low_rational_number  = self.rational_number_value
  high_rational_number = self.rational_number.parent.child_from_position(self.position+1).number

  base_class.where(:rational_number_value.gte => low_rational_number, :rational_number_value.lt => high_rational_number)
end

#update_rational_numberundefined

Update the rational numbers on the document if changes to parent or rational number has been changed

Should calculate next free nv/dv and set that if parent has changed. (set values to “missing and call missing function should work”)

If there are both changes to nv/dv and parent_id, nv/dv settings takes precedence over parent_id changes

Returns:

  • (undefined)


630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
# File 'lib/mongoid/tree/rational_numbering.rb', line 630

def update_rational_number
  if self.rational_number_nv_changed? && self.rational_number_dv_changed? && !self.rational_number_value.nil? && !set_initial_rational_number?
    self.set_rational_number(self.rational_number_nv, self.rational_number_dv, false)
  elsif self.parent_id_changed? || set_initial_rational_number?
    # only changed parent, needs to find next free position
    # Get rational number from new parent

    last_sibling = self.siblings.last

    if (last_sibling.nil?)
      new_rational_number = parent_rational_number.child_from_position(1)
    else
      new_rational_number = parent_rational_number.child_from_position(last_sibling.rational_number.position + 1)
    end

    self.move_to_rational_number(new_rational_number.nv, new_rational_number.dv)
  end
end

#update_rational_number?Boolean

Check if the rational number should be updated

Returns:

  • (Boolean)

    true if it should be updated, else false



666
667
668
# File 'lib/mongoid/tree/rational_numbering.rb', line 666

def update_rational_number?
  (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?
end

#validate_rational_hierarchyObject

Validate that this document has the correct parent document through a query If not, the parent must be set before setting nv/dv driectly

Returns:

  • true for valid, else false



104
105
106
107
108
109
110
111
# File 'lib/mongoid/tree/rational_numbering.rb', line 104

def validate_rational_hierarchy
  if self.rational_number_nv_changed? && self.rational_number_dv_changed?
    # puts "#{self.name} #{self.changes.inspect}"
    unless correct_rational_parent?(self.rational_number_nv, self.rational_number_dv)
      errors.add(:base, I18n.t(:cyclic, :scope => [:mongoid, :errors, :messages, :tree]))
    end
  end
end