AgileVentures/WebsiteOne

View on GitHub
app/models/event.rb

Summary

Maintainability
B
6 hrs
Test Coverage
A
90%
# frozen_string_literal: true

class Event < ApplicationRecord
  has_many :event_instances
  belongs_to :project, optional: true
  belongs_to :creator, class_name: 'User', optional: true
  has_and_belongs_to_many :slack_channels

  extend FriendlyId
  friendly_id :name, use: :slugged

  include IceCube
  validates :name, :time_zone, :repeats, :category, :start_datetime, :duration, presence: true
  validates :url, uri: true, allow_blank: true
  validates :repeats_every_n_weeks, presence: true, if: lambda { |e|
                                                          e.repeats == 'weekly' or e.repeats == 'biweekly'
                                                        }
  validates :repeat_ends_on, presence: true, allow_blank: false, if: lambda { |e|
                                                                       (e.repeats == 'weekly' or e.repeats == 'biweekly') and e.repeat_ends_string == 'on'
                                                                     }
  validate :must_have_at_least_one_repeats_weekly_each_days_of_the_week, if: lambda { |e|
                                                                               e.repeats == 'weekly' or e.repeats == 'biweekly'
                                                                             }
  validates :repeat_ends, inclusion: { in: [true, false] }
  attr_accessor :next_occurrence_time_attr, :repeat_ends_string

  COLLECTION_TIME_FUTURE = 10.days
  COLLECTION_TIME_PAST = 300.minutes
  NEXT_SCRUM_COLLECTION_TIME_PAST = 15.minutes

  REPEATS_OPTIONS = %w(never weekly biweekly).freeze
  REPEAT_ENDS_OPTIONS = %w(on never).freeze
  DAYS_OF_THE_WEEK = %w(monday tuesday wednesday thursday friday saturday sunday).freeze

  def set_repeat_ends_string
    @repeat_ends_string = repeat_ends ? 'on' : 'never'
  end

  def self.base_future_events(project)
    project.nil? ? Event.future_events : Event.future_events.where(project_id: project)
  end

  def self.future_events
    Event.where('start_datetime > ? OR repeat_ends = false AND repeats != ? OR repeat_ends = false AND repeat_ends_on IS NULL OR repeat_ends_on > ?', 1.day.ago, 'never', 1.day.ago)
         .where.not('repeats = ? AND start_datetime < ?', 'never', 1.day.ago)
  end

  def repeats?
    schedule.recurrence_rules.map { |rule| rule.class.name }.include?('IceCube::WeeklyRule')
  end

  def self.upcoming_events(project = nil)
    events = Event.base_future_events(project).inject([]) do |memo, event|
      memo << event.next_occurrences
    end.flatten.sort_by { |e| e[:time] }
    Event.remove_past_events(events)
  end

  def self.remove_past_events(events)
    events.delete_if do |event|
      (event[:time] + event[:event].duration.minutes) < Time.current &&
        !event[:event].event_instances.last.try(:live?)
    end
  end

  def self.hookups
    Event.where(category: 'PairProgramming')
  end

  def self.pending_hookups
    pending = []
    hookups.each do |h|
      started = h.last_hangout&.started?
      expired_without_starting = !h.last_hangout && Time.now.utc > h.instance_end_time
      pending << h if !started && !expired_without_starting
    end
    pending
  end

  def event_date
    start_datetime
  end

  def start_time
    start_datetime
  end

  def series_end_time
    repeat_ends && repeat_ends_on.present? ? repeat_ends_on.to_time : nil
  end

  def instance_end_time
    (start_datetime + (duration * 60)).utc
  end

  def end_date
    if series_end_time < start_time
      (event_date.to_datetime + 1.day).strftime('%Y-%m-%d')
    else
      event_date
    end
  end

  def live?
    last_hangout.present? && last_hangout.live?
  end

  def final_datetime_for_collection(options = {})
    final_datetime = if repeating_and_ends? && options[:end_time].present?
                       [options[:end_time], repeat_ends_on.to_datetime].min
                     elsif repeating_and_ends?
                       repeat_ends_on.to_datetime
                     else
                       options[:end_time]
                     end
    final_datetime ? final_datetime.to_datetime.utc : COLLECTION_TIME_FUTURE.from_now
  end

  def start_datetime_for_collection(options = {})
    first_datetime = options.fetch(:start_time, COLLECTION_TIME_PAST.ago)
    first_datetime = [start_datetime, first_datetime.to_datetime].max
    first_datetime.to_datetime.utc
  end

  def next_occurrence_time_method(start = Time.now)
    next_occurrence = next_event_occurrence_with_time(start)
    next_occurrence.present? ? next_occurrence[:time] : nil
  end

  def self.next_occurrence(event_type, begin_time = NEXT_SCRUM_COLLECTION_TIME_PAST.ago)
    events_with_times = []
    events_with_times = Event.where(category: [event_type, event_type.to_s.humanize]).map do |event|
      event.next_event_occurrence_with_time(begin_time)
    end.compact
    return nil if events_with_times.empty?

    events_with_times = events_with_times.sort_by { |e| e[:time] }
    events_with_times[0][:event].next_occurrence_time_attr = events_with_times[0][:time]
    events_with_times[0][:event]
  end

  # The IceCube Schedule's occurrences_between method requires a time range as input to find the next time
  # Most of the time, the next instance will be within the next weeek.do
  # But some event instances may have been excluded, so there's not guarantee that the next time for an event will be within the next week, or even the next month
  # To cover these cases, the while loop looks farther and farther into the future for the next event occurrence, just in case there are many exclusions.
  def next_event_occurrence_with_time(start = Time.now, final = 2.months.from_now)
    begin_datetime = start_datetime_for_collection(start_time: start)
    final_datetime = repeating_and_ends? ? repeat_ends_on : final
    n_days = 8
    end_datetime = n_days.days.from_now
    event = nil
    return next_event_occurrence_with_time_inner(start, final_datetime) if repeats == 'never'

    while event.nil? && end_datetime < final_datetime
      event = next_event_occurrence_with_time_inner(start, final_datetime)
      n_days *= 2
      end_datetime = n_days.days.from_now
    end
    event
  end

  def next_event_occurrence_with_time_inner(start_time, end_time)
    occurrences = occurrences_between(start_time, end_time)
    { event: self, time: occurrences.first.start_time } if occurrences.present?
  end

  def next_occurrences(options = {})
    begin_datetime = start_datetime_for_collection(options)
    final_datetime = final_datetime_for_collection(options)
    limit = options.fetch(:limit, 100)
    [].tap do |occurences|
      occurrences_between(begin_datetime, final_datetime).each do |time|
        occurences << { event: self, time: time }
        return occurences if occurences.count >= limit
      end
    end
  end

  def occurrences_between(start_time, end_time)
    schedule.occurrences_between(start_time.to_time, end_time.to_time)
  end

  def repeats_weekly_each_days_of_the_week=(repeats_weekly_each_days_of_the_week)
    self.repeats_weekly_each_days_of_the_week_mask = (repeats_weekly_each_days_of_the_week & DAYS_OF_THE_WEEK).map do |r|
      2**DAYS_OF_THE_WEEK.index(r)
    end.inject(0, :+)
  end

  def repeats_weekly_each_days_of_the_week
    DAYS_OF_THE_WEEK.reject do |r|
      ((repeats_weekly_each_days_of_the_week_mask || 0) & (2**DAYS_OF_THE_WEEK.index(r))).zero?
    end
  end

  def remove_from_schedule(timedate)
    # best if schedule is serialized into the events record...  and an attribute.
    if timedate >= Time.now && timedate == next_occurrence_time_method
      _next_occurrences = next_occurrences(limit: 2)
      self.start_datetime = _next_occurrences.size > 1 ? _next_occurrences[1][:time] : timedate + 1.day
    elsif timedate >= Time.now
      self.exclusions ||= []
      self.exclusions << timedate
    end
    save!
  end

  def schedule
    sched = if series_end_time.nil? || !repeat_ends
              IceCube::Schedule.new(start_datetime)
            else
              IceCube::Schedule.new(
                start_datetime, end_time: series_end_time
              )
            end
    case repeats
    when 'never'
      sched.add_recurrence_time(start_datetime)
    when 'weekly', 'biweekly'
      days = repeats_weekly_each_days_of_the_week.map(&:to_sym)
      sched.add_recurrence_rule IceCube::Rule.weekly(repeats_every_n_weeks).day(*days)
    end
    self.exclusions ||= []
    self.exclusions.each do |ex|
      sched.add_exception_time(ex)
    end
    sched
  end

  def start_time_with_timezone
    DateTime.parse(start_time.strftime('%k:%M ')).in_time_zone(time_zone)
  end

  def last_hangout
    event_instances.order(:created_at).last
  end

  def recent_hangouts
    event_instances
      .where('created_at BETWEEN ? AND ?', 1.day.ago + duration.minutes, DateTime.now.end_of_day)
      .order(created_at: :desc)
  end

  def less_than_ten_till_start?
    return true if within_current_event_duration?

    Time.now > next_event_occurrence_with_time[:time] - 10.minutes
  rescue StandardError
    false
  end

  def within_current_event_duration?
    after_current_start_time? and before_current_end_time?
  end

  def current_start_time
    schedule.occurrences_between(1.month.ago, Time.current).last
  end

  def current_end_time
    schedule.occurrences_between(1.month.ago, Time.current).last + (duration * 60)
  end

  def before_current_end_time?
    Time.current < current_end_time
  rescue StandardError
    false
  end

  def after_current_start_time?
    Time.current > current_start_time
  rescue StandardError
    false
  end

  def jitsi_room_link
    "https://meet.jit.si/AV_#{name.tr(' ', '_').gsub(/[^0-9a-zA-Z_]/i, '')}"
  end

  def modifier
    User.find modifier_id
  end

  def slack_channel_codes
    slack_channels.pluck(:code)
  end

  private

  def must_have_at_least_one_repeats_weekly_each_days_of_the_week
    return unless repeats_weekly_each_days_of_the_week.empty?

    errors.add(:base, 'You must have at least one repeats weekly each days of the week')
  end

  def repeating_and_ends?
    repeats != 'never' && repeat_ends && repeat_ends_on.present?
  end
end