class RankedModel::Ranker::Mapper

Attributes

instance[RW]
ranker[RW]

Public Class Methods

new(ranker, instance) click to toggle source
# File lib/ranked-model/ranker.rb, line 26
def initialize ranker, instance
  self.ranker   = ranker
  self.instance = instance

  validate_ranker_for_instance!
end

Public Instance Methods

current_at_position(_pos) click to toggle source
# File lib/ranked-model/ranker.rb, line 90
def current_at_position _pos
  if (ordered_instance = finder.offset(_pos).first)
    RankedModel::Ranker::Mapper.new ranker, ordered_instance
  end
end
handle_ranking() click to toggle source
# File lib/ranked-model/ranker.rb, line 52
def handle_ranking
  case ranker.unless
  when Proc
    return if ranker.unless.call(instance)
  when Symbol
    return if instance.send(ranker.unless)
  end

  update_index_from_position
  assure_unique_position
end
has_rank?() click to toggle source
# File lib/ranked-model/ranker.rb, line 96
def has_rank?
  !rank.nil?
end
position() click to toggle source
# File lib/ranked-model/ranker.rb, line 76
def position
  instance.send "#{ranker.name}_position"
end
rank() click to toggle source
# File lib/ranked-model/ranker.rb, line 86
def rank
  instance.send "#{ranker.column}"
end
relative_rank() click to toggle source
# File lib/ranked-model/ranker.rb, line 80
def relative_rank
  escaped_column = instance_class.connection.quote_column_name ranker.column

  finder.where("#{escaped_column} < #{rank}").count(:all)
end
reset_ranks!() click to toggle source
# File lib/ranked-model/ranker.rb, line 72
def reset_ranks!
  finder.update_all(ranker.column => nil)
end
update_rank!(value) click to toggle source
# File lib/ranked-model/ranker.rb, line 64
def update_rank! value
  # Bypass callbacks
  #
  instance_class.
    where(instance_class.primary_key => instance.id).
    update_all(ranker.column => value)
end
validate_ranker_for_instance!() click to toggle source
# File lib/ranked-model/ranker.rb, line 33
def validate_ranker_for_instance!
  if ranker.scope && !instance_class.respond_to?(ranker.scope)
    raise RankedModel::InvalidScope, %Q{No scope called "#{ranker.scope}" found in model}
  end

  if ranker.with_same
    if (case ranker.with_same
          when Symbol
            !instance.respond_to?(ranker.with_same)
          when Array
            array_element = ranker.with_same.detect {|attr| !instance.respond_to?(attr) }
          else
            false
        end)
      raise RankedModel::InvalidField, %Q{No field called "#{array_element || ranker.with_same}" found in model}
    end
  end
end

Private Instance Methods

assure_unique_position() click to toggle source
# File lib/ranked-model/ranker.rb, line 182
def assure_unique_position
  if ( new_record? || rank_changed? )
    if (rank > RankedModel::MAX_RANK_VALUE) || rank_taken?
      rearrange_ranks
    end
  end
end
current_first() click to toggle source
# File lib/ranked-model/ranker.rb, line 287
def current_first
  @current_first ||= begin
    if (ordered_instance = finder.first)
      RankedModel::Ranker::Mapper.new ranker, ordered_instance
    end
  end
end
current_last() click to toggle source
# File lib/ranked-model/ranker.rb, line 295
def current_last
  @current_last ||= begin
    if (ordered_instance = finder.
                             reverse.
                             first)
      RankedModel::Ranker::Mapper.new ranker, ordered_instance
    end
  end
end
current_order() click to toggle source
# File lib/ranked-model/ranker.rb, line 279
def current_order
  @current_order ||= begin
    finder.unscope(where: instance_class.primary_key.to_sym).collect { |ordered_instance|
      RankedModel::Ranker::Mapper.new ranker, ordered_instance
    }
  end
end
find_next_two(_rank) click to toggle source
# File lib/ranked-model/ranker.rb, line 330
def find_next_two _rank
  ordered_instances = finder.where(instance_class.arel_table[ranker.column].gt _rank).limit(2)
  if ordered_instances[1]
    { :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ),
      :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[1] ) }
  elsif ordered_instances[0]
    { :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ) }
  else
    {}
  end
end
find_previous_two(_rank) click to toggle source
# File lib/ranked-model/ranker.rb, line 342
def find_previous_two _rank
  ordered_instances = finder(:desc).where(instance_class.arel_table[ranker.column].lt _rank).limit(2)
  if ordered_instances[1]
    { :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ),
      :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[1] ) }
  elsif ordered_instances[0]
    { :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ) }
  else
    {}
  end
end
finder(order = :asc) click to toggle source
# File lib/ranked-model/ranker.rb, line 249
def finder(order = :asc)
  @finder ||= {}
  @finder[order] ||= begin
    _finder = instance_class
    columns = [instance_class.primary_key.to_sym, ranker.column]

    if ranker.scope
      _finder = _finder.send ranker.scope
    end

    case ranker.with_same
    when Symbol
      columns << ranker.with_same
      _finder = _finder.where \
        ranker.with_same => instance.attributes[ranker.with_same.to_s]
    when Array
      ranker.with_same.each do |column|
        columns << column
        _finder = _finder.where column => instance.attributes[column.to_s]
      end
    end

    unless new_record?
      _finder = _finder.where.not instance_class.primary_key.to_sym => instance.id
    end

    _finder.reorder(ranker.column.to_sym => order).select(columns)
  end
end
instance_class() click to toggle source
# File lib/ranked-model/ranker.rb, line 106
def instance_class
  ranker.class_name.nil? ? instance.class : ranker.class_name.constantize
end
neighbors_at_position(_pos) click to toggle source
# File lib/ranked-model/ranker.rb, line 309
def neighbors_at_position _pos
  if _pos > 0
    if (ordered_instances = finder.offset(_pos-1).limit(2).to_a)
      if ordered_instances[1]
        { :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ),
          :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[1] ) }
      elsif ordered_instances[0]
        { :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ) }
      else
        { :lower => current_last }
      end
    end
  else
    if (ordered_instance = finder.first)
      { :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instance ) }
    else
      {}
    end
  end
end
new_record?() click to toggle source
# File lib/ranked-model/ranker.rb, line 123
def new_record?
  instance.new_record?
end
position_at(value) click to toggle source
# File lib/ranked-model/ranker.rb, line 110
def position_at value
  instance.send "#{ranker.name}_position=", value
  update_index_from_position
end
rank_at(value) click to toggle source
# File lib/ranked-model/ranker.rb, line 115
def rank_at value
  instance.send "#{ranker.column}=", value
end
rank_at_average(min, max) click to toggle source
# File lib/ranked-model/ranker.rb, line 173
def rank_at_average(min, max)
  if (max - min).between?(-1, 1) # No room at the inn...
    rebalance_ranks
    position_at position
  else
    rank_at( ( ( max - min ).to_f / 2 ).ceil + min )
  end
end
rank_changed?() click to toggle source
# File lib/ranked-model/ranker.rb, line 119
def rank_changed?
  instance.send "#{ranker.column}_changed?"
end
rank_taken?() click to toggle source
# File lib/ranked-model/ranker.rb, line 305
def rank_taken?
  finder.except(:order).where(ranker.column => rank).exists?
end
rearrange_ranks() click to toggle source
# File lib/ranked-model/ranker.rb, line 190
def rearrange_ranks
  _scope = finder
  escaped_column = instance_class.connection.quote_column_name ranker.column
  # If there is room at the bottom of the list and we're added to the very top of the list...
  if current_first.rank && current_first.rank > RankedModel::MIN_RANK_VALUE && rank == RankedModel::MAX_RANK_VALUE
    # ...then move everyone else down 1 to make room for us at the end
    _scope.
      where( instance_class.arel_table[ranker.column].lteq(rank) ).
      update_all( "#{escaped_column} = #{escaped_column} - 1" )
  # If there is room at the top of the list and we're added below the last value in the list...
  elsif current_last.rank && current_last.rank < (RankedModel::MAX_RANK_VALUE - 1) && rank < current_last.rank
    # ...then move everyone else at or above our desired rank up 1 to make room for us
    _scope.
      where( instance_class.arel_table[ranker.column].gteq(rank) ).
      update_all( "#{escaped_column} = #{escaped_column} + 1" )
  # If there is room at the bottom of the list and we're added above the lowest value in the list...
  elsif current_first.rank && current_first.rank > RankedModel::MIN_RANK_VALUE && rank > current_first.rank
    # ...then move everyone else below us down 1 and change our rank down 1 to avoid the collission
    _scope.
      where( instance_class.arel_table[ranker.column].lt(rank) ).
      update_all( "#{escaped_column} = #{escaped_column} - 1" )
    rank_at( rank - 1 )
  else
    rebalance_ranks
  end
end
rebalance_ranks() click to toggle source
# File lib/ranked-model/ranker.rb, line 217
def rebalance_ranks
  ActiveRecord::Base.transaction do
    if rank && instance.persisted?
      origin = current_order.index { |item| item.instance.id == instance.id }
      if origin
        destination = current_order.index { |item| rank <= item.rank }
        destination -= 1 if origin < destination

        current_order.insert destination, current_order.delete_at(origin)
      end
    end

    gaps = current_order.size + 1
    range = (RankedModel::MAX_RANK_VALUE - RankedModel::MIN_RANK_VALUE).to_f
    gap_size = (range / gaps).ceil

    reset_ranks!

    current_order.each.with_index(1) do |item, position|
      new_rank = (gap_size * position) + RankedModel::MIN_RANK_VALUE

      if item.instance.id == instance.id
        rank_at new_rank
      else
        item.update_rank! new_rank
      end
    end

    reset_cache
  end
end
reset_cache() click to toggle source
# File lib/ranked-model/ranker.rb, line 102
def reset_cache
  @finder, @current_order, @current_first, @current_last = nil
end
update_index_from_position() click to toggle source
# File lib/ranked-model/ranker.rb, line 127
def update_index_from_position
  case position
    when :first, 'first'
      if current_first && current_first.rank
        rank_at_average current_first.rank, RankedModel::MIN_RANK_VALUE
      else
        position_at :middle
      end
    when :last, 'last'
      if current_last && current_last.rank
        rank_at_average current_last.rank, RankedModel::MAX_RANK_VALUE
      else
        position_at :middle
      end
    when :middle, 'middle'
      rank_at_average RankedModel::MIN_RANK_VALUE, RankedModel::MAX_RANK_VALUE
    when :down, 'down'
      neighbors = find_next_two(rank)
      if neighbors[:lower]
        min = neighbors[:lower].rank
        max = neighbors[:upper] ? neighbors[:upper].rank : RankedModel::MAX_RANK_VALUE
        rank_at_average min, max
      end
    when :up, 'up'
      neighbors = find_previous_two(rank)
      if neighbors[:upper]
        max = neighbors[:upper].rank
        min = neighbors[:lower] ? neighbors[:lower].rank : RankedModel::MIN_RANK_VALUE
        rank_at_average min, max
      end
    when String
      position_at position.to_i
    when 0
      position_at :first
    when Integer
      neighbors = neighbors_at_position(position)
      min = ((neighbors[:lower] && neighbors[:lower].has_rank?) ? neighbors[:lower].rank : RankedModel::MIN_RANK_VALUE)
      max = ((neighbors[:upper] && neighbors[:upper].has_rank?) ? neighbors[:upper].rank : RankedModel::MAX_RANK_VALUE)
      rank_at_average min, max
    when NilClass
      if !rank
        position_at :last
      end
  end
end