schedule.rb 4.14 KB
Newer Older
Zac Wood's avatar
Zac Wood committed
1
2
3
require 'icalendar'
require 'time'

4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Schedule
  def initialize(crns)
    @cal = Icalendar::Calendar.new
    @cal.x_wr_calname = 'GMU Fall 2018'
    
    @course_sections = crns.map do |crn|
      CourseSection.find_by crn: crn
    end
    @course_sections.compact!

    load_events
  end

  def to_ical
    @cal.to_ical
  end

  private

  def load_events
    @course_sections.each do |section|
25
26
      unless section.start_time == "TBA" || section.end_time == "TBA"
        event = generate_event_from_section(section) 
27
        @cal.add_event(event)
28
      end
Zac Wood's avatar
Zac Wood committed
29
30
31

      if section.days.start_with? "M"
        col_day_makeup = generate_event_after_columbus_day(section)
32
        @cal.add_event(col_day_makeup)
Zac Wood's avatar
Zac Wood committed
33
      end
Zac Wood's avatar
Zac Wood committed
34
35
36
    end
  end

37
  # Configures a calendar event from a given section
Zac Wood's avatar
Zac Wood committed
38
  # @param section [CourseSection]
Zac Wood's avatar
Zac Wood committed
39
40
  def generate_event_from_section(section)
    event = Icalendar::Event.new
Zac Wood's avatar
Zac Wood committed
41

Zac Wood's avatar
Zac Wood committed
42
43
    event.summary = section.name
    event.description = section.title
Zac Wood's avatar
Zac Wood committed
44
    event.location = section.location
Zac Wood's avatar
Zac Wood committed
45
46
    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
47
    event.rrule = Icalendar::Values::Recur.new(recurrence_rule_str(section))
Zac Wood's avatar
Zac Wood committed
48
    event.exdate = exdates_for_section(section)
Zac Wood's avatar
Zac Wood committed
49
50
51
52

    event
  end

53
54
55
56
  # 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
57
58
59
  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
60

Zac Wood's avatar
Zac Wood committed
61
62
63
    "#{formatted_date}T#{formatted_time}"
  end

64
  # Mapping of days as represented by GMU to the iCal standard
Zac Wood's avatar
Zac Wood committed
65
66
67
68
69
70
71
72
73
74
  DAYS = {
    "M" => "MO",
    "T" => "TU",
    "W" => "WE",
    "R" => "TH",
    "F" => "FR",
    "S" => "SA",
    "U" => "SU"
  }.freeze

75
76
  # Generates a recurrence rule string descripting which day the class event
  # should take place on
Zac Wood's avatar
Zac Wood committed
77
  # @param section [CourseSection]
78
  # @return [String]
Zac Wood's avatar
Zac Wood committed
79
  def recurrence_rule_str(section)
Zac Wood's avatar
Zac Wood committed
80
81
82
83
84
85
    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
86

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

    # 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
Zac Wood's avatar
Zac Wood committed
98
99
100
    unless section.days.start_with? "M"
      exdates << generate_exdate(
        section.start_date.to_formatted_s(:number),
Zac Wood's avatar
Zac Wood committed
101
        section.start_time
Zac Wood's avatar
Zac Wood committed
102
103
      )
    end
Zac Wood's avatar
Zac Wood committed
104

Zac Wood's avatar
Zac Wood committed
105
106
107
108
109
110
111
112
    # If the section meets on Tuesdays, add an exdate for the day after columbus day
    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
113
    exdates
Zac Wood's avatar
Zac Wood committed
114
115
  end

116
117
118
119
  # 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
120
  def generate_exdate(date, time)
121
    # format the time for use in a DateTime
Zac Wood's avatar
Zac Wood committed
122
123
124
    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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140

  # 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
141
end