schedule.rb 4.38 KB
Newer Older
Zac Wood's avatar
Zac Wood committed
1
require 'icalendar'
2
require 'icalendar/tzinfo'
Zac Wood's avatar
Zac Wood committed
3 4
require 'time'

Zac Wood's avatar
Zac Wood committed
5
# Creates a iCal object given a list of section ids
6
class Schedule
7
  def initialize(crns)
8
    @cal = Icalendar::Calendar.new
Zac Wood's avatar
Zac Wood committed
9
    @cal.x_wr_calname = 'GMU Schedule'
Zac Wood's avatar
Zac Wood committed
10

11 12 13 14
    tzid = "America/New_York"
    tz = TZInfo::Timezone.get tzid
    @cal.add_timezone tz.ical_timezone(Time.now)

15 16 17
    @course_sections = crns.map { |crn|
      CourseSection.latest_by_crn(crn)
    }
18 19 20 21 22 23 24 25 26 27 28 29 30
    @course_sections.compact!

    load_events
  end

  def to_ical
    @cal.to_ical
  end

  private

  def load_events
    @course_sections.each do |section|
31
      unless section.start_time == "TBA" || section.end_time == "TBA"
Zac Wood's avatar
Zac Wood committed
32
        event = generate_event_from_section(section)
33
        @cal.add_event(event)
34
      end
35

36 37 38 39
      # if section.days.start_with? "M"
      #   col_day_makeup = generate_event_after_columbus_day(section)
      #   @cal.add_event(col_day_makeup)
      # end
Zac Wood's avatar
Zac Wood committed
40 41 42
    end
  end

43
  # Configures a calendar event from a given section
Zac Wood's avatar
Zac Wood committed
44
  # @param section [CourseSection]
Zac Wood's avatar
Zac Wood committed
45 46
  def generate_event_from_section(section)
    event = Icalendar::Event.new
Zac Wood's avatar
Zac Wood committed
47

Zac Wood's avatar
Zac Wood committed
48
    event.summary = section.name
49
    event.description = "#{section.title}\nTaught by #{section.instructor.name}"
Zac Wood's avatar
Zac Wood committed
50
    event.location = section.location
Zac Wood's avatar
Zac Wood committed
51 52
    event.dtstart = Icalendar::Values::DateTime.new(formatted_datetime_str(section.start_date, section.start_time))
    event.dtend = Icalendar::Values::DateTime.new(formatted_datetime_str(section.start_date, section.end_time))
Zac Wood's avatar
Zac Wood committed
53
    event.rrule = Icalendar::Values::Recur.new(recurrence_rule_str(section))
Zac Wood's avatar
Zac Wood committed
54
    event.exdate = exdates_for_section(section)
Zac Wood's avatar
Zac Wood committed
55 56 57 58

    event
  end

59 60 61 62
  # Format a DateTime string based on a given date and time
  # @param date [String]
  # @param time [String]
  # @return [String]
Zac Wood's avatar
Zac Wood committed
63 64 65
  def formatted_datetime_str(date, time)
    formatted_date = date.to_s.tr('-', '')
    formatted_time = Time.parse(time).strftime("%H%M%S")
Zac Wood's avatar
Zac Wood committed
66

Zac Wood's avatar
Zac Wood committed
67 68 69
    "#{formatted_date}T#{formatted_time}"
  end

70
  # Mapping of days as represented by GMU to the iCal standard
Zac Wood's avatar
Zac Wood committed
71 72 73 74 75 76 77 78 79 80
  DAYS = {
    "M" => "MO",
    "T" => "TU",
    "W" => "WE",
    "R" => "TH",
    "F" => "FR",
    "S" => "SA",
    "U" => "SU"
  }.freeze

81 82
  # Generates a recurrence rule string descripting which day the class event
  # should take place on
Zac Wood's avatar
Zac Wood committed
83
  # @param section [CourseSection]
84
  # @return [String]
Zac Wood's avatar
Zac Wood committed
85
  def recurrence_rule_str(section)
Zac Wood's avatar
Zac Wood committed
86 87 88 89 90 91
    days = section.days.split("").map do |day|
      DAYS[day]
    end

    "FREQ=WEEKLY;UNTIL=#{formatted_datetime_str(section.end_date, section.end_time)};BYDAY=#{days.join(',')}"
  end
Zac Wood's avatar
Zac Wood committed
92

93
  # Get all dates that should excluded from the schedule
Zac Wood's avatar
Zac Wood committed
94
  # @param section [CourseSection]
95
  # @return [Array]
Zac Wood's avatar
Zac Wood committed
96
  def exdates_for_section(section)
97
    # Generate exdates for all closures in a semester
Zac Wood's avatar
Zac Wood committed
98
    exdates = Closure.where(semester: section.course.semester).map { |closure|
Zac Wood's avatar
Zac Wood committed
99
      generate_exdate(closure.date.to_formatted_s(:number), section.start_time)
Zac Wood's avatar
Zac Wood committed
100
    }
Zac Wood's avatar
Zac Wood committed
101 102 103

    # Every section's start_date is the first Monday of the semester.
    # So we need to add an exclusion for that day unless the class is held on Mondays
104
    unless section.days.start_with? "T"
Zac Wood's avatar
Zac Wood committed
105 106
      exdates << generate_exdate(
        section.start_date.to_formatted_s(:number),
Zac Wood's avatar
Zac Wood committed
107
        section.start_time
Zac Wood's avatar
Zac Wood committed
108 109
      )
    end
Zac Wood's avatar
Zac Wood committed
110

111
    # If the section meets on Tuesdays, add an exdate for the day after columbus day
112 113 114 115 116 117
    # if section.days.start_with? "T"
    #   exdates << generate_exdate(
    #     Date.new(2018, 10, 9).to_formatted_s(:number),
    #     section.start_time
    #   )
    # end
118

Zac Wood's avatar
Zac Wood committed
119
    exdates
Zac Wood's avatar
Zac Wood committed
120 121
  end

122 123 124 125
  # Generate a DataTime to use as an exdate
  # @param date [String]
  # @param time [String]
  # @return [Icalendar::Values::DateTime]
Zac Wood's avatar
Zac Wood committed
126
  def generate_exdate(date, time)
127
    # format the time for use in a DateTime
Zac Wood's avatar
Zac Wood committed
128 129 130
    formatted_time = Time.parse(time).strftime("%H%M%S")
    Icalendar::Values::DateTime.new("#{date}T#{formatted_time}")
  end
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146

  # Configures a calendar event for the day after columbus day
  # @param section [CourseSection]
  def generate_event_after_columbus_day(section)
    event = Icalendar::Event.new

    event.summary = section.name + " (Columbus Day makeup)"
    event.description = section.title + " (Columbus Day makeup)"
    event.location = section.location

    after_columbus_day = Date.new 2018, 10, 9
    event.dtstart = Icalendar::Values::DateTime.new(formatted_datetime_str(after_columbus_day, section.start_time))
    event.dtend = Icalendar::Values::DateTime.new(formatted_datetime_str(after_columbus_day, section.end_time))

    event
  end
147
end