vol1ura/Sat_9am_5km

View on GitHub
app/models/athlete.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
# frozen_string_literal: true

class Athlete < ApplicationRecord
  include PgSearch::Model

  pg_search_scope :search_by_name, against: :name, using: {
    trigram: { threshold: 0.7, word_similarity: true },
  }

  audited associated_with: :user, max_audits: 20, except: [:stats]

  PARKZHRUN_BORDER = 690_000_000
  SAT_9AM_5KM_BORDER = 770_000_000
  FIVE_VERST_BORDER = 790_000_000
  RUN_PARK_BORDER = 7_000_000_000

  RAGE_BADGE_LIMIT = 3

  PersonalCode = Struct.new(:code) do
    def code_type
      @code_type ||=
        if code < PARKZHRUN_BORDER
          :parkrun_code
        elsif code < SAT_9AM_5KM_BORDER
          :parkzhrun_code
        elsif code > RUN_PARK_BORDER
          :runpark_code
        elsif code > FIVE_VERST_BORDER
          :fiveverst_code
        else
          :id
        end
    end

    def id
      @id ||= code.between?(SAT_9AM_5KM_BORDER, FIVE_VERST_BORDER) ? code - SAT_9AM_5KM_BORDER : code
    end

    def to_params
      @to_params ||= { code_type => id }
    end
  end

  belongs_to :club, optional: true
  belongs_to :user, optional: true
  belongs_to :event, optional: true

  has_many :trophies, dependent: :destroy
  has_many :badges, through: :trophies
  has_many :results, dependent: :nullify
  has_many :activities, through: :results
  has_many :events, through: :activities
  has_many :volunteering, -> { published.order(date: :desc) },
           dependent: :destroy, class_name: 'Volunteer', inverse_of: :athlete

  validates :parkrun_code,
            uniqueness: true,
            numericality: { only_integer: true, less_than: PARKZHRUN_BORDER },
            allow_nil: true
  validates :fiveverst_code,
            uniqueness: true,
            numericality: { only_integer: true, greater_than: FIVE_VERST_BORDER, less_than: RUN_PARK_BORDER },
            allow_nil: true
  validates :runpark_code,
            uniqueness: true,
            numericality: { only_integer: true, greater_than: RUN_PARK_BORDER },
            allow_nil: true
  validates :parkzhrun_code,
            uniqueness: true,
            numericality: { only_integer: true, greater_than: PARKZHRUN_BORDER, less_than: SAT_9AM_5KM_BORDER },
            allow_nil: true

  before_save :remove_name_extra_spaces, if: :will_save_change_to_name?
  after_commit :refresh_home_trophies, if: :saved_change_to_event_id?

  def self.duplicates
    sql = <<~SQL.squish
      SELECT id, parkrun_code, fiveverst_code, l_name FROM (
        SELECT id, parkrun_code, fiveverst_code, l_name, COUNT(id) OVER (PARTITION BY l_name) AS cnt FROM (
          SELECT *, array(SELECT unnest(string_to_array(LOWER(name), ' ')) ORDER BY 1) AS l_name FROM athletes
        ) AS q1
      ) AS q2
      WHERE q2.cnt > 1
    SQL
    namesakes_ids =
      find_by_sql(sql)
        .pluck(:l_name, :id, :parkrun_code, :fiveverst_code)
        .group_by(&:first)
        .reject { |_, arr| arr.all?(&:third) || arr.all?(&:last) }
        .flat_map { |_, arr| arr.map(&:second) }
    where(id: namesakes_ids)
  end

  def self.find_or_scrape_by_code!(code)
    personal_code = PersonalCode.new(code)
    code_type = personal_code.code_type
    athlete = find_by(**personal_code.to_params)
    return athlete if athlete && (athlete.name || code_type == :id)
    return create if code_type == :id

    athlete_name = Athletes::Finder.call(personal_code)
    athlete ||= find_or_initialize_by(name: athlete_name, code_type => nil)
    athlete.update!(name: athlete_name, **personal_code.to_params)
    athlete
  end

  def self.ransackable_attributes(_auth_object = nil)
    %w[club_id event_id id male name parkrun_code fiveverst_code runpark_code updated_at created_at]
  end

  def code
    parkrun_code || fiveverst_code || runpark_code || (SAT_9AM_5KM_BORDER + id if id)
  end

  def award_by_rage_badge?
    last_total_times = results.published.order(date: :desc).limit(RAGE_BADGE_LIMIT).pluck(:total_time).compact

    last_total_times.size == RAGE_BADGE_LIMIT &&
      last_total_times.each_cons(2).all? { |next_time, prev_time| next_time < prev_time }
  end

  def award_by_five_plus_badge?
    initial_date = Date.current.saturday? ? Date.current : Date.tomorrow.prev_week(:saturday)
    Activity
      .where(id: results.select(:activity_id))
      .or(Activity.where(id: Volunteer.where(athlete: self).select(:activity_id)))
      .where(date: Array.new(5) { |k| initial_date - k.weeks })
      .published
      .select(:date)
      .distinct
      .size == 5
  end

  def gender
    return if male.nil?

    male ? 'мужчина' : 'женщина'
  end

  private

  def remove_name_extra_spaces
    trimmed_name = name.gsub(/\s+/, ' ').gsub(/^ | $|(?<= ) /, '')
    self.name = trimmed_name unless name == trimmed_name
  end

  def refresh_home_trophies
    trophies.joins(:badge).where(badge: { kind: :home_participating }).destroy_all
    HomeBadgeAwardingJob.perform_later(id) if event_id
  end
end