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
Zac Wood's avatar
Zac Wood committed
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
98
    exdates = Closure.where(semester: section.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

Zac Wood's avatar
Zac Wood committed
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
Zac Wood's avatar
Zac Wood committed
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
Zac Wood's avatar
Zac Wood committed
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