scottwillson/racing_on_rails

View on GitHub
app/models/multi_day_event.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# frozen_string_literal: true

# Event that spans more than one day: stage races, six days, omniums
# MultiDayEvents represent events that occur on concurrent days, and Series and
# WeeklySeries subclasses represent events that do not occur on concurrent days, though
# this is just a convention.
#
# Calculate start_date, end_date, and date from children.
# date = start_date
#
# Cannot have a parent event
#
# New child event should populate child event with parent data, but they don't
class MultiDayEvent < Event
  validates :name, :date, presence: true
  validate on: :create do |event|
    event.parent.nil?
  end
  validate on: :update do |event|
    event.parent.nil?
  end

  before_save :update_children

  # TODO: Default first child event date to start date, next child to first child date + 1, additional children to next day if adjacent,
  # same day of next week if not adjacent (Series/WeeklySeries)
  has_many :children,
           lambda {
             where("type is null or type = 'SingleDayEvent'")
               .order(:date)
           },
           class_name: "Event",
           foreign_key: "parent_id",
           after_add: %i[children_changed update_date],
           after_remove: %i[children_changed update_date] do
             def create!(attributes = {})
               owner = proxy_association.owner
               attributes[:parent_id] = owner.id
               attributes[:parent] = owner
               event = SingleDayEvent.new(attributes)
               (event.date = owner.date) unless attributes[:date]
               event.parent = owner
               event.save!
               owner.children << event
               event
             end

             def create(attributes = {})
               owner = proxy_association.owner
               attributes[:parent_id] = owner.id
               attributes[:parent] = owner
               event = SingleDayEvent.new(attributes)
               (event.date = owner.date) unless attributes[:date]
               event.parent = owner
               event.save
               owner.children << event
               event
             end
           end

  # Create MultiDayEvent from several SingleDayEvents.
  # Use first SingleDayEvent to populate date, name, promoter, etc.
  # Guess subclass (MultiDayEvent, Series, WeeklySeries) from SingleDayEvent dates
  def self.create_from_children(children)
    raise ArgumentError, "children cannot be empty" if children.empty?

    first_event = children.first
    new_event_attributes = {
      city: first_event.city,
      discipline: first_event.discipline,
      email: first_event.email,
      flyer: first_event.flyer,
      name: first_event.name,
      phone: first_event.phone,
      promoter: first_event.promoter,
      prize_list: first_event.prize_list,
      sanctioned_by: first_event.sanctioned_by,
      state: first_event.state,
      team: first_event.team,
      velodrome: first_event.velodrome
    }

    new_multi_day_event_class = MultiDayEvent.guess_type(children)
    multi_day_event = new_multi_day_event_class.create!(new_event_attributes)

    children.each do |child|
      multi_day_event.children << child
    end

    multi_day_event
  end

  # Expects a value from Date::DAYNAMES: Monday, Tuesday, etc., or an array of same. Example: ["Saturday", "Sunday"]
  def self.create_for_every!(days_of_week, params)
    raise(ArgumentError) unless days_of_week && (params[:date] || params[:start_date]) && params[:end_date]

    days_of_week = Array.wrap(days_of_week)
    days_of_week.each do |day|
      raise ArgumentError, "'#{day}' must be in #{Date::DAYNAMES.join(', ')}" unless Date::DAYNAMES.index(day)
    end

    event = create!(params)

    days_of_week_indexes = days_of_week.map { |day| Date::DAYNAMES.index(day) }
    start_date = event.date
    start_date = start_date.next until days_of_week_indexes.include?(start_date.wday)

    start_date.step(event.end_date, 1) do |date|
      event.children.create!(date: date) if days_of_week_indexes.include?(date.wday)
    end

    event
  end

  def self.guess_type(name, date)
    events = SingleDayEvent.where(name: name).year(date.year)
    MultiDayEvent.guess_type(events)
  end

  # This fails if a stage race spans more than one day but has more events than days
  # we could look for events that span contiguous days...but a long stage race could have a rest day...
  def self.guess_type(events)
    length = events.last.date - events.first.date
    if events.size - 1 == length
      MultiDayEvent
    elsif (events.first.date.wday == 0) || (events.first.date.wday == 6)
      Series
    else
      WeeklySeries
    end
  end

  def self.same_name_and_year(event)
    raise ArgumentError, "'event' cannot be nil" if event.nil?

    MultiDayEvent.where(name: event.name).year(event.date.year).first
  end

  # Uses SQL query to set +date+ from child events. Callbacks pass in unsued child parameter.
  def update_date(_child = nil)
    return true if new_record? || destroyed?

    child_dates = child_dates_query.pluck(:date)
    if child_dates.present?
      self.date = child_dates.min
      self.end_date = child_dates.max
      if date_changed? || end_date_changed?
        # Don't trigger callbacks
        MultiDayEvent.where(id: id).update_all(date: date, end_date: end_date)
      end
    end
  end

  def child_dates_query
    if is_a?(Competitions::Competition) || calculation.present?
      Event.where(parent_id: id).where(canceled: false).where(postponed: false)
    else
      SingleDayEvent
        .where(parent_id: id)
        .where(canceled: false)
        .where(postponed: false)
    end
  end

  def set_end_date
    children_for_date = children

    unless is_a?(Competitions::Competition) || calculation.present?
      children_for_date = children_for_date.reject { |child| child.is_a?(Competitions::Competition) }
    end

    self.end_date = children_for_date.map(&:date).max || date if self[:end_date].nil?
  end

  def end_date_s
    "#{end_date.month}/#{end_date.day}"
  end

  # +format+:
  # * :short: 6/7-6/12
  # * :long: 6/7/2010-6/12/2010
  def date_range_s(format = :short)
    return date.year if all_year?

    if format == :long
      if start_date == end_date
        date.strftime("%-m/%-d/%Y")
      else
        "#{start_date.strftime('%-m/%-d/%Y')}-#{end_date.strftime('%-m/%-d/%Y')}"
      end
    else
      start_date_s = "#{start_date.month}/#{start_date.day}"
      if start_date == end_date
        start_date_s
      elsif start_date.month == end_date.month
        "#{start_date_s}-#{end_date.day}"
      else
        "#{start_date_s}-#{end_date.month}/#{end_date.day}"
      end
    end
  end

  def date_short_with_week_day
    date_range_s :short
  end

  # Unassociated events with same name in same year
  def missing_children
    return [] unless name && date

    @missing_children ||= SingleDayEvent.where(parent_id: nil).where(name: name).year(date.year)
  end

  # All children have results?
  def completed?
    children.present? && (children_with_results.size == children.count)
  end

  # Synch Races with children. More accurately: create a new Race on each child Event for each Race on the parent.
  def propagate_races
    children.each do |event|
      races.map(&:category).each do |category|
        event.races.create!(category: category) unless event.races.map(&:category).include?(category)
      end
    end
  end

  def to_s
    "<#{self.class} #{id} #{discipline} #{name} #{date} #{children.size}>"
  end
end