nilbus/pinewood-derby

View on GitHub
app/models/contestant.rb

Summary

Maintainability
A
25 mins
Test Coverage
class Contestant < ActiveRecord::Base
  has_many :runs, dependent: :destroy
  has_many :heats, through: :runs

  scope :ranked, -> do
    with_average_time.
    where('heats.sequence >= 0').
    order('average_time')
  end

  scope :with_average_time, -> do
    select('contestants.*, avg(runs.time) AS average_time').
    group('contestants.id').
    joins('LEFT JOIN runs ON runs.contestant_id = contestants.id LEFT JOIN heats ON heats.id = runs.heat_id')
  end

  validates :name, presence: true

  def self.next_suitable(options = {})
    exclude = Array(options[:exclude])
    exclude = [Contestant.new] if exclude.empty?
    exclude = exclude.map { |contestant| contestant.respond_to?(:id) ? contestant.id.to_i : contestant.to_i }
    lane = options[:lane].to_i
    max_runs_per_contestant = DerbyConfig.lane_count

    # select
    s = select('contestants.*')
    # filter
    s = s.where('contestants.id NOT IN (?)', exclude) if exclude.any?
    s = s.where("contestants.retired IS NULL OR contestants.retired = ?", false)
    s = s.group('contestants.id')
    s = s.joins('LEFT JOIN runs ON runs.contestant_id = contestants.id')
    s = s.having('count(DISTINCT heats.id) < ?', max_runs_per_contestant)
    s = s.joins("LEFT JOIN runs AS runs_in_lane ON runs.contestant_id = contestants.id AND runs.lane = #{lane}")
    s = s.having("count(runs_in_lane.id) = 0")
    # order
    s = s.joins('LEFT JOIN heats ON heats.id = runs.heat_id')
    s = s.joins("LEFT JOIN runs AS runs_with_chosen ON runs_with_chosen.heat_id = heats.id AND runs_with_chosen.contestant_id IN (#{exclude.join(',')})")
    s = s.select('count(runs_with_chosen.id) AS heat_count_with_chosen')
    s = s.select('count(DISTINCT heats.id) AS heat_count')
    s = s.select('avg(runs.time) AS average_run_time')
    s = s.order('heat_count_with_chosen, heat_count, average_run_time DESC, contestants.created_at')

    return s if options[:raw]
    s.first
  end

  def self.generate(i = 26)
    (?a..?z).to_a[0, i].map{|a|a.upcase + a*10}.each{|a|Contestant.create name: a}
  end

  def average_time
    average_time = self[:average_time] || calculate_average_time

    average_time.try :round, 4
  end

  def retire
    transaction do
      update_attributes! retired: true
      runs.upcoming.readonly(false).each &:postpone

      self
    end
  end

  def reactivate
    update_attributes! retired: false

    self
  end

private

  def calculate_average_time
    (self.class.ranked.where(id: id).first || {})[:average_time]
  end

end