scottwillson/racing_on_rails

View on GitHub
app/models/person.rb

Summary

Maintainability
C
1 day
Test Coverage
# frozen_string_literal: true

require "sentient_user/sentient_user"

# Someone who either appears in race results or who is added as a member of a racing association
#
# Names are _not_ unique. In fact, there are many business rules about names. See Aliases and Names.
class Person < ApplicationRecord
  LOGIN_FORMAT = /\A\w[\w.+\-_@ ]+\z/.freeze

  include Comparable
  include Export::People
  include Names::Nameable
  include People::Aliases
  include People::Ages
  include People::Authorization
  include People::Export
  include People::Gender
  include People::Membership
  include People::Merge
  include People::Names
  include People::Numbers
  include RacingOnRails::PaperTrail::Versions
  include SentientUser

  acts_as_authentic do |config|
    config.crypto_provider = Authlogic::CryptoProviders::SCrypt
    config.disable_perishable_token_maintenance true
    config.log_in_after_create false
    config.log_in_after_password_change false
    config.transition_from_crypto_providers = [Authlogic::CryptoProviders::Sha512]
  end

  validates :login,
            allow_blank: true,
            format: { with: LOGIN_FORMAT, message: "should use only letters, numbers, spaces, and .-_@ please" },
            length: { in: 3..100 },
            uniqueness: { case_sensitive: false }

  validates :password,
            allow_blank: true,
            length: { minimum: 4 }

  before_validation :find_associated_records
  before_validation :set_membership_dates
  before_save { |r| r.login = nil if login.blank? }
  before_save { |r| r.license = nil if license.blank? }
  validates :license, uniqueness: { allow_blank: true, case_sensitive: false }
  validate :membership_dates
  before_destroy :ensure_no_results

  has_and_belongs_to_many :editable_events, class_name: "Event", foreign_key: "editor_id", join_table: "editors_events"
  has_many :events, foreign_key: "promoter_id"
  has_many :event_team_memberships, dependent: :destroy
  has_many :event_teams, through: :event_team_memberships
  has_many :results
  belongs_to :team, optional: true

  attr_accessor :year

  CATEGORY_FIELDS = %i[bmx_category ccx_category dh_category mtb_category road_category track_category].freeze

  def self.where_name_or_number_like(name)
    return Person.none if name.blank?

    Person
      .where(
        "people.name like :name_like or aliases.name like :name_like or race_numbers.value = :name",
        name_like: "%#{name.strip}%", name: name.strip
      )
      .includes(:aliases)
      .includes(:race_numbers)
      .includes(:team)
      .references(:aliases)
      .references(:race_numbers)
      .order(:last_name, :first_name)
  end

  def self.first_by_info(name, email = nil, home_phone = nil)
    if name.present?
      Person.find_by(name: name)
    else
      Person.where(
        "(email = ? and email <> '' and email is not null) or (home_phone = ? and home_phone <> '' and home_phone is not null)",
        email, home_phone
      ).first
    end
  end

  # interprets dates returned in sql above for member export
  def self.lic_check(lic, lic_date)
    if lic_date && lic.to_i > 0
      case lic_date
      when Date, Time, DateTime
        lic_date > Time.zone.today ? "current" : "CHECK LIC!"
      else
        Date.strptime(lic_date, "%m/%d/%Y") > Time.zone.today ? "current" : "CHECK LIC!"
      end
    else
      "NOT ON FILE"
    end
  end

  # Find Person with most recent. If no results, select the most recently updated Person.
  def self.select_by_recent_activity(people)
    results = people.map(&:results).flatten
    if results.empty?
      people.to_a.max_by(&:updated_at)
    else
      results = results.sort_by(&:date)
      results.last.person
    end
  end

  def self.deliver_password_reset_instructions!(people)
    people.each(&:reset_perishable_token!)
    Notifier.password_reset_instructions(people).deliver_now
  end

  # Cannot have promoters with duplicate contact information
  def unique_info
    person = Person.first_by_info(name, email, home_phone)
    errors.add("existing person with name '#{name}'") if person && person != self
  end

  def team_name
    if team
      team.name || ""
    else
      ""
    end
  end

  def team_name=(value)
    if value.blank? || value == "N/A"
      self.team = nil
    else
      self.team = Team.find_by_name_or_alias(value)
      self.team = Team.new(name: value, updater: new_record? ? updater : nil) unless team
    end
  end

  def gender_pronoun
    if female?
      "herself"
    elsif non_binary?
      "themself"
    else
      "himself"
    end
  end

  def possessive_pronoun
    if female?
      "her"
    elsif non_binary?
      "their"
    else
      "his"
    end
  end

  def third_person_pronoun
    if female?
      "her"
    elsif non_binary?
      "them"
    else
      "him"
    end
  end

  # Non-nil for happier sorting
  def gender
    self[:gender] || ""
  end

  def gender=(value)
    if value.nil?
      self[:gender] = nil
    else
      value = value.upcase
      case value
      when "M", "MALE", "BOY"
        self[:gender] = "M"
      when "F", "FEMALE", "GIRL"
        self[:gender] = "F"
      when "NB", "NON-BINARY", "NONBINARY"
        self[:gender] = "NB"
      else
        self[:gender] = "M"
      end
    end
  end

  def category(discipline)
    _discipline = if discipline.is_a?(String)
                    Discipline[discipline]
                  else
                    discipline
                  end

    case _discipline
    when Discipline[:road], Discipline[:criterium], Discipline[:time_trial], Discipline[:circuit]
      self["road_category"]
    when Discipline[:track]
      self["track_category"]
    when Discipline[:cyclocross]
      self["ccx_category"]
    when Discipline[:dh]
      self["dh_category"]
    when Discipline[:bmx]
      self["bmx_category"]
    when Discipline[:mtb]
      self["xc_category"]
    end
  end

  def state=(value)
    value = value.to_s.upcase if value && value.to_s.size == 2
    super value
  end

  def city_state
    if city.present?
      if state.present?
        "#{city}, #{state}"
      else
        city.to_s
      end
    elsif state.present?
      state.to_s
    end
  end

  def city_state_zip
    if city.present?
      if state.present?
        "#{city}, #{state} #{zip}"
      else
        "#{city} #{zip}"
      end
    elsif state.present?
      "#{state} #{zip}"
    else
      zip || ""
    end
  end

  def hometown
    if city.blank?
      if state.blank?
        ""
      elsif state == RacingAssociation.current.state
        ""
      else
        state
      end
    elsif state.blank?
      city
    elsif state == RacingAssociation.current.state
      city
    else
      "#{city}, #{state}"
    end
  end

  def hometown=(value)
    self.city = nil
    self.state = nil
    return value if value.blank?

    parts = value.split(",")
    self.state = parts.last.strip if parts.size > 1
    self.city = parts.first.strip
  end

  # Hack around in-place editing
  def toggle!(attribute)
    if attribute.try(:to_s) == "member"
      self.member = !member?
      save!
    else
      super
    end
  end

  # All non-Competition results
  # reload does an optimized load with joins
  def event_results(reload = true)
    if reload
      return Result
             .includes(:team, :person, :scores, :category, race: %i[event category])
             .where("people.id" => id)
             .reject(&:competition_result?)
    end
    results.reject(&:competition_result?)
  end

  # BAR, Oregon Cup, Ironman
  def competition_results
    results.select(&:competition_result?)
  end

  # Replace +team+ with exising Team if current +team+ is an unsaved duplicate of an existing Team
  def find_associated_records
    if team&.new_record?
      if team.name.blank? || (team.name == "N/A")
        self.team = nil
      else
        existing_team = Team.find_by_name_or_alias(team.name)
        self.team = existing_team if existing_team
      end
    end
  end

  def ensure_no_results
    if results.present?
      errors.add :base, "Can't delete person with results"
      throw :abort
    end
  end

  # TODO: Any reason not to change this to last name, first name?
  def <=>(other)
    if other
      id <=> other.id
    else
      -1
    end
  end

  def to_s
    "#<Person #{id} #{first_name} #{last_name} #{team_id}>"
  end
end