Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
ranked-model / lib / ranked-model / ranker.rb
Size: Mime:
module RankedModel

  class InvalidScope < StandardError; end
  class InvalidField < StandardError; end

  class Ranker
    attr_accessor :name, :column, :scope, :with_same, :class_name, :unless,
      :on_rank_rearrange, :on_rank_rebalance

    def initialize name, options={}
      self.name = name.to_sym
      self.column = options[:column] || name
      self.class_name = options[:class_name]

      [ :scope, :with_same, :unless, :on_rank_rearrange, :on_rank_rebalance ].each do |key|
        self.send "#{key}=", options[key]
      end
    end

    def with instance
      Mapper.new self, instance
    end

    class Mapper
      attr_accessor :ranker, :instance

      def initialize ranker, instance
        self.ranker   = ranker
        self.instance = instance

        validate_ranker_for_instance!
      end

      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

      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

      def update_rank! value
        # Bypass callbacks
        #
        instance_class.
          where(instance_class.primary_key => instance.id).
          update_all([%Q{#{ranker.column} = ?}, value])
      end

      def position
        instance.send "#{ranker.name}_position"
      end

      def rank
        instance.send "#{ranker.column}"
      end

      def current_at_position _pos
        if (ordered_instance = finder.offset(_pos).first)
          RankedModel::Ranker::Mapper.new ranker, ordered_instance
        end
      end

      def has_rank?
        !rank.nil?
      end

    private

      def instance_class
        ranker.class_name.nil? ? instance.class : ranker.class_name.constantize
      end

      def position_at value
        instance.send "#{ranker.name}_position=", value
        update_index_from_position
      end

      def rank_at value
        instance.send "#{ranker.column}=", value
      end

      def rank_changed?
        instance.send "#{ranker.column}_changed?"
      end

      def new_record?
        instance.new_record?
      end

      def update_index_from_position
        case position
          when :first, 'first'
            if current_first && current_first.rank
              rank_at( ( ( RankedModel::MIN_RANK_VALUE - current_first.rank ).to_f / 2 ).ceil + current_first.rank)
            else
              position_at :middle
            end
          when :last, 'last'
            if current_last && current_last.rank
              rank_at( ( ( RankedModel::MAX_RANK_VALUE - current_last.rank ).to_f / 2 ).ceil + current_last.rank )
            else
              position_at :middle
            end
          when :middle, 'middle'
            rank_at( ( ( RankedModel::MAX_RANK_VALUE - RankedModel::MIN_RANK_VALUE ).to_f / 2 ).ceil + RankedModel::MIN_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( ( ( max - min ).to_f / 2 ).ceil + min )
            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( ( ( max - min ).to_f / 2 ).ceil + min )
            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( ( ( max - min ).to_f / 2 ).ceil + min )
          when NilClass
            if !rank
              position_at :last
            end
        end
      end

      def assure_unique_position
        if ( new_record? || rank_changed? )
          unless rank
            rank_at( RankedModel::MAX_RANK_VALUE )
          end

          if (rank > RankedModel::MAX_RANK_VALUE) || current_at_rank(rank)
            rearrange_ranks
          end
        end
      end

      def rearrange_ranks
        _scope = finder
        unless instance.id.nil?
          # Never update ourself, shift others around us.
          _scope = _scope.where( instance_class.arel_table[instance_class.primary_key].not_eq(instance.id) )
        end
        instances_to_update, update = if current_first.rank && current_first.rank > RankedModel::MIN_RANK_VALUE && rank == RankedModel::MAX_RANK_VALUE
          [
            _scope.where( instance_class.arel_table[ranker.column].lteq(rank) ),
            %Q{#{ranker.column} = #{ranker.column} - 1}
          ]
        elsif current_last.rank && current_last.rank < (RankedModel::MAX_RANK_VALUE - 1) && rank < current_last.rank
          [
            _scope.where( instance_class.arel_table[ranker.column].gteq(rank) ),
            %Q{#{ranker.column} = #{ranker.column} + 1}
          ]
        elsif current_first.rank && current_first.rank > RankedModel::MIN_RANK_VALUE && rank > current_first.rank
          rank_at(rank - 1)
          [
            _scope.where( instance_class.arel_table[ranker.column].lt(rank) ),
            %Q{#{ranker.column} = #{ranker.column} - 1}
          ]
        end

        if instances_to_update
          instances_to_update.update_all(update)
          ranker.on_rank_rearrange.call(instances_to_update.pluck(:id)) if ranker.on_rank_rearrange
        else
          rebalance_ranks
        end
      end

      def rebalance_ranks
        total = current_order.size + 2
        has_set_self = false
        total.times do |index|
          next if index == 0 || index == total
          rank_value = ((((RankedModel::MAX_RANK_VALUE - RankedModel::MIN_RANK_VALUE).to_f / total) * index ).ceil + RankedModel::MIN_RANK_VALUE)
          index = index - 1
          if has_set_self
            index = index - 1
          else
            if !current_order[index] ||
               ( !current_order[index].rank.nil? &&
                 current_order[index].rank >= rank )
              rank_at rank_value
              has_set_self = true
              next
            end
          end
          current_order[index].update_rank! rank_value
        end

        ranker.on_rank_rebalance.call(current_order.map(&:instance).map(&:id)) if ranker.on_rank_rebalance
      end

      def finder(order = :asc)
        @finder ||= begin
          _finder = instance_class
          columns = [instance_class.arel_table[instance_class.primary_key], instance_class.arel_table[ranker.column]]
          if ranker.scope
            _finder = _finder.send ranker.scope
          end
          case ranker.with_same
            when Symbol
              columns << instance_class.arel_table[ranker.with_same]
              _finder = _finder.where \
                instance_class.arel_table[ranker.with_same].eq(instance.attributes["#{ranker.with_same}"])
            when Array
              ranker.with_same.each {|c| columns.push instance_class.arel_table[c] }
              _finder = _finder.where(
                ranker.with_same[1..-1].inject(
                  instance_class.arel_table[ranker.with_same.first].eq(
                    instance.attributes["#{ranker.with_same.first}"]
                  )
                ) {|scoper, attr|
                  scoper.and(
                    instance_class.arel_table[attr].eq(
                      instance.attributes["#{attr}"]
                    )
                  )
                }
              )
          end
          if !new_record?
            _finder = _finder.where \
              instance_class.arel_table[instance_class.primary_key].not_eq(instance.id)
          end
          _finder.order(instance_class.arel_table[ranker.column].send(order)).select(columns)
        end
      end

      def current_order
        @current_order ||= begin
          finder.collect { |ordered_instance|
            RankedModel::Ranker::Mapper.new ranker, ordered_instance
          }
        end
      end

      def current_first
        @current_first ||= begin
          if (ordered_instance = finder.first)
            RankedModel::Ranker::Mapper.new ranker, ordered_instance
          end
        end
      end

      def current_last
        @current_last ||= begin
          if (ordered_instance = finder.
                                   reverse.
                                   first)
            RankedModel::Ranker::Mapper.new ranker, ordered_instance
          end
        end
      end

      def current_at_rank _rank
        if (ordered_instance = finder.
                                 except( :order ).
                                 where( ranker.column => _rank ).
                                 first)
          RankedModel::Ranker::Mapper.new ranker, ordered_instance
        end
      end

      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

      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

      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

    end

  end

end