Commit 4aaceed0 authored by Zac Wood's avatar Zac Wood

Merge branch 'fix-columbus-day' into 'master'

Fix columbus day

See merge request srct/schedules!12
parents 69f6e0f7 b4635282
......@@ -21,5 +21,3 @@
/yarn-error.log
.byebug_history
*.xlsx
\ No newline at end of file
......@@ -15,6 +15,11 @@ class CalendarGeneratorController < ApplicationController
event = generate_event_from_section(section)
cal.add_event(event)
end
if section.days.start_with? "M"
col_day_makeup = generate_event_after_columbus_day(section)
cal.add_event(col_day_makeup)
end
end
render plain: cal.to_ical # render a plaintext iCal file
......@@ -90,6 +95,14 @@ class CalendarGeneratorController < ApplicationController
)
end
# 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
exdates
end
......@@ -102,4 +115,20 @@ class CalendarGeneratorController < ApplicationController
formatted_time = Time.parse(time).strftime("%H%M%S")
Icalendar::Values::DateTime.new("#{date}T#{formatted_time}")
end
# 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
end
......@@ -22,25 +22,28 @@ class ExcelLoader
end
configure_section?(row)&.save! # If this row contained a section, save it
end
load_closures
end
private
# create closures for the days there will be no classes
# see: https://registrar.gmu.edu/calendars/fall-2018/
def load_closures
Closure.create! date: Date.new(2018, 9, 3), semester: @semester
Closure.create! date: Date.new(2018, 10, 8), semester: @semester
(21..25).each { |n| Closure.create! date: Date.new(2018, 11, n), semester: @semester }
(10..19).each { |n| Closure.create! date: Date.new(2018, 12, n), semester: @semester }
end
# Prints the failure, deletes all data added during loading, and raises the failure error.
def fail(error)
logger.fatal error.message
logger.fatal error.backtrace
delete_all_records
raise error
end
# Deletes all records from the database.
def delete_all_records
Semester.delete_all
Course.delete_all
CourseSection.delete_all
end
# Tries to create a course from a given row.
def configure_course?(row)
course_name = row.cells[1]&.value
......@@ -64,27 +67,42 @@ class ExcelLoader
# Tries to create a section from a given row.
def configure_section?(row)
section_name = row.cells[2]&.value
# If there is no valid section name, just continue to the next row
unless section_name.blank? || section_name == 'Total'
# The time field in the spreadsheet uses the format "start_time - end_time" i.e. "12:00 PM - 1:15 PM".
# So, split the times string by the - character
times = row.cells[23]&.value
time_strs = times.split('-')
instructor_val = row.cells[16]
instructor = if instructor_val.nil? || instructor_val.value == "'-"
"TBA"
else
instructor_val.value
end
location_cell = row.cells[25]
location = if location_cell.nil? || location_cell.value.include?("'-")
"TBA"
else
location_cell.value
end
section = CourseSection.create name: section_name,
course: @current_course,
crn: row.cells[6]&.value,
section_type: row.cells[8]&.value,
title: row.cells[11]&.value,
instructor: row.cells[16]&.value,
start_date: row.cells[18]&.value,
end_date: row.cells[21]&.value,
days: row.cells[22]&.value,
start_time: time_strs[0].strip,
end_time: time_strs[1].strip,
location: row.cells[25]&.value
course: @current_course,
crn: row.cells[6]&.value,
section_type: row.cells[8]&.value,
title: row.cells[11]&.value,
instructor: instructor,
start_date: row.cells[18]&.value,
end_date: row.cells[21]&.value,
days: row.cells[22]&.value,
start_time: time_strs[0].strip,
end_time: time_strs[1].strip,
location: location
# TODO: Add campus, notes, and size limit fields
section
end
section
end
end
......@@ -36,7 +36,7 @@ module PatriotWeb
def parse_courses_in_subject(subject)
response = @networker.fetch_courses_in_subject(subject)
document = Nokogiri::HTML(response)
get_courses(document)
get_courses(document, subject)
end
private
......@@ -66,54 +66,74 @@ module PatriotWeb
# Parse all courses from the subject search page
# @param document [Nokogiri::HTML::Document]
# @return [Array] courses
def get_courses(document)
table = document.css('html body div.pagebodydiv table.datadisplaytable').first
rows = table.children.drop 2 # first two elements are junk
def get_courses(document, subject)
table = document.css('html body div.pagebodydiv table.datadisplaytable')
rows = table.css('tr')
# rows[100..110].each_with_index do |row, i|
# puts i
# puts row
# end
data_from rows
end
def data_from(rows)
i = 0
title_index = 0
result = []
# each section is represented by 6 rows in the table
(0..(rows.length/6 - 1)).map do |i|
start = i*6
data = {}
title = rows[start].text
# the title looks this: Survey of Accounting - 71117 - ACCT 203 - 001
# so split it by ' - ' and extract
title_elements = title.split(' - ')
data[:title] = title_elements[0].strip
data[:crn] = title_elements[1]
full_name = title_elements[2].split(' ')
next unless full_name.length == 2
data[:subj] = title_elements[2].split(' ')[0]
data[:course_number] = title_elements[2].split(' ')[1]
data[:section] = title_elements[3].strip
while i < rows.length
if is_title(rows[i].text) # check if the row is a title
data = {}
title_elements = rows[i].text.split(' - ')
data[:title] = title_elements[0].strip
data[:crn] = title_elements[1]
full_name = title_elements[2].split(' ')
next unless full_name.length == 2
data[:subj] = full_name[0]
data[:course_number] = full_name[1]
data[:section] = title_elements[3].strip
details = rows[i+2].css('td table tr td')
unless details.length > 0
puts "#{full_name.join(' ')} is fake news"
i += 1
next
end
times = details[1].text.split(' - ')
if (times.length == 1)
data[:start_time] = 'TBA'
data[:end_time] = 'TBA'
else
data[:start_time] = times[0]
data[:end_time] = times[1]
end
data[:days] = details[2].text.strip
data[:location] = details[3].text.strip
dates = details[4].text.split(' - ')
data[:start_date] = dates[0]
data[:end_date] = dates[1]
data[:type] = details[5].text
data[:instructor] = details[6].text
# rows 1 to 3 contain info about registration and drop dates.
# for now we're gonna ignore them and skip to row 4, which contains details
detail_rows = rows[start+4].css('tr')
next unless detail_rows.length > 0 # if there are no details, skip this item
details = detail_rows.last.text.split("\n").compact.reject(&:empty?) # skip empty strings
times = details[1].split(' - ')
if (times.length == 1)
data[:start_time] = 'TBA'
data[:end_time] = 'TBA'
result << data
i += 5 # skip to what we think is the next title
else
data[:start_time] = times[0]
data[:end_time] = times[1]
i += 1 # try the next row if this one was not a title
end
data[:days] = details[2].strip
data[:location] = details[3].strip
dates = details[4].split(' - ')
data[:start_date] = dates[0]
data[:end_date] = dates[1]
data[:type] = details[5]
data[:instructor] = details[6]
data
end
result
end
# a title looks this: Survey of Accounting - 71117 - ACCT 203 - 001
def is_title(text)
elements = text.split(' - ')
elements.length == 4 && elements[2].split(' ').length == 2
end
end
end
require_relative 'excel_loader'
# Deletes all records from the database.
Closure.delete_all
CourseSection.delete_all
Course.delete_all
Semester.delete_all
loader = ExcelLoader.new 'db/data/fall2018.xlsx'
loader.load_data
......@@ -19,17 +19,17 @@ puts "DDOSing Patriot Web, buckle up kids"
# parse all subjects and their courses in the semester
parser.parse_subjects(semester).each do |subject|
puts "Getting courses for #{subject}"
threads << Thread.new {
# threads << Thread.new {
total[subject] = parser.parse_courses_in_subject(subject)
}
# }
end
# For testing, only get first subject
# subject = parser.parse_subjects(semester).first
# subject = parser.parse_subjects(semester)[20]
# total[subject] = parser.parse_courses_in_subject(subject)
# wait for all the threads to finish
ThreadsWait.all_waits(*threads)
# ThreadsWait.all_waits(*threads)
# delete everything in the current database
Closure.delete_all
......@@ -44,7 +44,10 @@ semester.save!
total.each do |subject, sections|
puts "Adding courses for #{subject}..."
sections.each do |section|
next if section.nil? || !section.key?(:subj) || !section.key?(:course_number)
if section.nil? || !section.key?(:subj) || !section.key?(:course_number)
puts "#{subject} failed section: #{section.class}"
next
end
# Find or create a course and set its semester
# TODO: this breaks when you try to do more than one semester,
......@@ -57,7 +60,7 @@ total.each do |subject, sections|
section_name = "#{section[:subj]} #{section[:course_number]} #{section[:section]}"
puts "Adding #{section_name}..."
# puts "Adding #{section_name}..."
CourseSection.create!(name: section_name,
crn: section[:crn],
......
......@@ -13,6 +13,13 @@ EXDATE:20180530T120000
EXDATE:20180531T120000
END:VEVENT
BEGIN:VEVENT
DTSTART:20181009T120000
DTEND:20181009T130000
DESCRIPTION:MyString (Columbus Day makeup)
LOCATION:MyString
SUMMARY:MyString (Columbus Day makeup)
END:VEVENT
BEGIN:VEVENT
DTSTART:20180521T110000
DTEND:20180521T140000
DESCRIPTION:MyString
......@@ -22,5 +29,6 @@ RRULE:FREQ=WEEKLY;UNTIL=20180604T140000;BYDAY=TU,TH
EXDATE:20180530T110000
EXDATE:20180531T110000
EXDATE:20180521T110000
EXDATE:20181009T110000
END:VEVENT
END:VCALENDAR
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment