Commit ec853376 authored by Zac Wood's avatar Zac Wood
Browse files

Started adding docs, cleaned up, ensured tests pass

parent 91a2305f
...@@ -5,17 +5,19 @@ class CalendarGeneratorController < ApplicationController ...@@ -5,17 +5,19 @@ class CalendarGeneratorController < ApplicationController
def generate def generate
cal = Icalendar::Calendar.new cal = Icalendar::Calendar.new
params[:_json].each do |crn| params[:_json].each do |crn| # for each CRN sent by the post request
section = Section.find_by_crn(crn) section = Section.find_by_crn(crn)
event = generate_event_from_section(section) event = generate_event_from_section(section)
cal.add_event(event) cal.add_event(event)
end end
render plain: cal.to_ical render plain: cal.to_ical # render a plaintext iCal file
end end
private private
# Configures a calendar event from a given section
# @param section [Section]
def generate_event_from_section(section) def generate_event_from_section(section)
event = Icalendar::Event.new event = Icalendar::Event.new
...@@ -30,6 +32,10 @@ class CalendarGeneratorController < ApplicationController ...@@ -30,6 +32,10 @@ class CalendarGeneratorController < ApplicationController
event event
end end
# Format a DateTime string based on a given date and time
# @param date [String]
# @param time [String]
# @return [String]
def formatted_datetime_str(date, time) def formatted_datetime_str(date, time)
formatted_date = date.to_s.tr('-', '') formatted_date = date.to_s.tr('-', '')
formatted_time = Time.parse(time).strftime("%H%M%S") formatted_time = Time.parse(time).strftime("%H%M%S")
...@@ -37,6 +43,7 @@ class CalendarGeneratorController < ApplicationController ...@@ -37,6 +43,7 @@ class CalendarGeneratorController < ApplicationController
"#{formatted_date}T#{formatted_time}" "#{formatted_date}T#{formatted_time}"
end end
# Mapping of days as represented by GMU to the iCal standard
DAYS = { DAYS = {
"M" => "MO", "M" => "MO",
"T" => "TU", "T" => "TU",
...@@ -47,6 +54,10 @@ class CalendarGeneratorController < ApplicationController ...@@ -47,6 +54,10 @@ class CalendarGeneratorController < ApplicationController
"U" => "SU" "U" => "SU"
}.freeze }.freeze
# Generates a recurrence rule string descripting which day the class event
# should take place on
# @param section [Section]
# @return [String]
def recurrence_rule_str(section) def recurrence_rule_str(section)
days = section.days.split("").map do |day| days = section.days.split("").map do |day|
DAYS[day] DAYS[day]
...@@ -55,7 +66,11 @@ class CalendarGeneratorController < ApplicationController ...@@ -55,7 +66,11 @@ class CalendarGeneratorController < ApplicationController
"FREQ=WEEKLY;UNTIL=#{formatted_datetime_str(section.end_date, section.end_time)};BYDAY=#{days.join(',')}" "FREQ=WEEKLY;UNTIL=#{formatted_datetime_str(section.end_date, section.end_time)};BYDAY=#{days.join(',')}"
end end
# Get all dates that should excluded from the schedule
# @param section [Section]
# @return [Array]
def exdates_for_section(section) def exdates_for_section(section)
# Generate exdates for all closures in a semester
exdates = Closure.where(semester: section.course.semester).map { |closure| exdates = Closure.where(semester: section.course.semester).map { |closure|
generate_exdate(closure.date.to_formatted_s(:number), section.start_time) generate_exdate(closure.date.to_formatted_s(:number), section.start_time)
} }
...@@ -72,7 +87,12 @@ class CalendarGeneratorController < ApplicationController ...@@ -72,7 +87,12 @@ class CalendarGeneratorController < ApplicationController
exdates exdates
end end
# Generate a DataTime to use as an exdate
# @param date [String]
# @param time [String]
# @return [Icalendar::Values::DateTime]
def generate_exdate(date, time) def generate_exdate(date, time)
# format the time for use in a DateTime
formatted_time = Time.parse(time).strftime("%H%M%S") formatted_time = Time.parse(time).strftime("%H%M%S")
Icalendar::Values::DateTime.new("#{date}T#{formatted_time}") Icalendar::Values::DateTime.new("#{date}T#{formatted_time}")
end end
......
# Defines actions for the homepage
class HomeController < ApplicationController
def index; end
end
...@@ -11,6 +11,7 @@ class Course < ApplicationRecord ...@@ -11,6 +11,7 @@ class Course < ApplicationRecord
validates :semester_id, presence: true validates :semester_id, presence: true
# Returns all +Section+ objects that belong to this course. # Returns all +Section+ objects that belong to this course.
# @return [Array]
def sections def sections
Section.where course_id: id Section.where course_id: id
end end
......
...@@ -8,11 +8,6 @@ class Section < ApplicationRecord ...@@ -8,11 +8,6 @@ class Section < ApplicationRecord
# Ensure all necessary fields are present. # Ensure all necessary fields are present.
validates :name, presence: true validates :name, presence: true
validates :crn, presence: true validates :crn, presence: true
# Unsure if necessary
# validates :section_type, presence: true
validates :title, presence: true validates :title, presence: true
# validates :start_date, presence: true
# validates :end_date, presence: true
# validates :days, presence: true
validates :course_id, presence: true validates :course_id, presence: true
end end
# This file is no longer being used.
# Data is now being parsed from Patriot Web.
require 'rubyXL' require 'rubyXL'
# Provides utilities for loading schedules from GMU's excel files. # Provides utilities for loading schedules from GMU's excel files.
......
require 'httparty' require 'httparty'
module PatriotWeb module PatriotWeb
# Contains utilities for making HTTP requests to PatriotWeb
class Networker class Networker
def fetch_page_containing_semester_data def fetch_page_containing_semester_data
HTTParty.get('https://patriotweb.gmu.edu/pls/prod/bwckschd.p_disp_dyn_sched') HTTParty.get('https://patriotweb.gmu.edu/pls/prod/bwckschd.p_disp_dyn_sched')
......
...@@ -2,61 +2,75 @@ require_relative 'patriot_web_networker' ...@@ -2,61 +2,75 @@ require_relative 'patriot_web_networker'
require 'nokogiri' require 'nokogiri'
class String class String
# Checks if a String is a alphanumeric
def alpha? def alpha?
!!match(/^[[:alpha:]]+$/) !!match(/^[[:alpha:]]+$/)
end end
end end
module PatriotWeb module PatriotWeb
# Contains methods for parsing data retrieved from Patriot Web
class Parser class Parser
def initialize def initialize
@networker = PatriotWeb::Networker.new @networker = PatriotWeb::Networker.new
end end
# Parses all semesters avaliable on Patriot Web
def parse_semesters def parse_semesters
response = @networker.fetch_page_containing_semester_data response = @networker.fetch_page_containing_semester_data
searcher = Nokogiri::HTML(response) document = Nokogiri::HTML(response) # parse the document from the HTTP response
get_semesters_from_option_values(searcher).compact get_semesters_from_option_values(document).compact
end end
# Parses subjects belonging to a given semester id
# @param semester_id [Integer]
def parse_subjects(semester_id) def parse_subjects(semester_id)
response = @networker.fetch_subjects(semester_id) response = @networker.fetch_subjects(semester_id)
searcher = Nokogiri::HTML(response) document = Nokogiri::HTML(response)
get_subject_codes_from_option_values(document)
get_alpha_option_values(searcher)
end end
# Parses all courses belonging to a given subject
# @param subject [String]
def parse_courses_in_subject(subject) def parse_courses_in_subject(subject)
resp = @networker.fetch_courses_in_subject(subject) response = @networker.fetch_courses_in_subject(subject)
searcher = Nokogiri::HTML(resp) document = Nokogiri::HTML(response)
feed_course_info(searcher) feed_course_info(document)
end end
private private
def get_alpha_option_values(searcher) # Parse the values of all different options on the Patriot Web
searcher.xpath('//*[@id="subj_id"]/option').map do |opt| # semester select page
if opt.attr('value').strip.alpha? # @param document [Nokogiri::HTML::Document]
opt.attr('value') def get_semesters_from_option_values(document)
document.css('option').map do |opt| # for each option value
if opt.attr('value').start_with? '20' # ensure it is a semester value
opt.attr('value') # return the value
end end
end end
end end
def get_semesters_from_option_values(searcher) # Parse all subject codes from the select element on the Patriot Web
searcher.css('option').map do |opt| # subject select page
if opt.attr('value').start_with? '20' # @param document [Nokogiri::HTML::Document]
opt.attr('value') def get_subject_codes_from_option_values(document)
document.xpath('//*[@id="subj_id"]/option').map do |opt| # for each option value under "subj_id"
if opt.attr('value').strip.alpha? # if the value is alphanumeric
opt.attr('value') # return the value
end end
end end
end end
# TODO write docs
def feed_course_info(searcher) def feed_course_info(searcher)
# find the table containing the courses
table = searcher.css('html body div.pagebodydiv table.datadisplaytable') table = searcher.css('html body div.pagebodydiv table.datadisplaytable')
data = {} data = {}
currentobj = nil currentobj = nil
table.css('table.datadisplaytable').first.children.each do |row| table.css('table.datadisplaytable').first.children.each do |row| # for each row in the table
next unless row.name == 'tr' next unless row.name == 'tr' # only search table rows, ignore headers
row.children.each do |item| row.children.each do |item|
currentobj = sort_item(item, currentobj, data) currentobj = sort_item(item, currentobj, data)
end end
...@@ -64,6 +78,7 @@ module PatriotWeb ...@@ -64,6 +78,7 @@ module PatriotWeb
data data
end end
# TODO break this up and write docs
def sort_item(item, currentobj, data) def sort_item(item, currentobj, data)
if item.name == 'th' if item.name == 'th'
if item.to_html.include? '-' if item.to_html.include? '-'
...@@ -105,6 +120,7 @@ module PatriotWeb ...@@ -105,6 +120,7 @@ module PatriotWeb
currentobj currentobj
end end
# TODO break this up and write docs
def get_details(data, titledetails, titledata) def get_details(data, titledetails, titledata)
crn = titledetails[1].strip crn = titledetails[1].strip
data[crn] = {} unless data[titledetails[1]] data[crn] = {} unless data[titledetails[1]]
......
# This file should contain all the record creation needed to seed the database with its default values. # This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
#
# Examples:
#
# movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
# Character.create(name: 'Luke', movie: movies.first)
require_relative 'patriot_web_parser' require_relative 'patriot_web_parser'
require 'thwait' require 'thwait'
...@@ -16,7 +11,10 @@ threads = [] ...@@ -16,7 +11,10 @@ threads = []
total = [] total = []
parser = PatriotWeb::Parser.new parser = PatriotWeb::Parser.new
# get the first semester only -- no need to ddos patriot web
semester = parser.parse_semesters.first semester = parser.parse_semesters.first
# parse all subjects and their courses in the semester
parser.parse_subjects(semester).each do |subject| parser.parse_subjects(semester).each do |subject|
threads << Thread.new { threads << Thread.new {
total << parser.parse_courses_in_subject(subject) total << parser.parse_courses_in_subject(subject)
...@@ -27,20 +25,25 @@ end ...@@ -27,20 +25,25 @@ end
# subject = parser.parse_subjects(semester).first # subject = parser.parse_subjects(semester).first
# total << parser.parse_courses_in_subject(subject) # total << 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 Closure.delete_all
Section.delete_all Section.delete_all
Course.delete_all Course.delete_all
Semester.delete_all Semester.delete_all
# create a semester for the next semester
semester = Semester.create! season: 'Fall', year: 2018 semester = Semester.create! season: 'Fall', year: 2018
semester.save! semester.save!
total.each do |subject| total.each do |subject| # for each course
subject.each_value do |section| subject.each_value do |section| # for each value in the subject hash
# ensure all necessary fields are present
next unless (section.key? "date_range") && (section.key? "instructors") && (section.key? "days") next unless (section.key? "date_range") && (section.key? "instructors") && (section.key? "days")
# create a course and set its semester
course = Course.find_or_create_by(subject: section[:subj], course = Course.find_or_create_by(subject: section[:subj],
course_number: section[:code]) course_number: section[:code])
...@@ -50,6 +53,8 @@ total.each do |subject| ...@@ -50,6 +53,8 @@ total.each do |subject|
section_name = "#{section[:subj]} #{section[:code]} #{section[:sect]}" section_name = "#{section[:subj]} #{section[:code]} #{section[:sect]}"
puts "Adding #{section_name}..." puts "Adding #{section_name}..."
# the start and end times are located in the "time" key and look like START_TIME - END_TIME
# so, split them by the dash and add them
start_time = if section.key? "time" start_time = if section.key? "time"
section["time"].split(' - ').first section["time"].split(' - ').first
else else
...@@ -76,6 +81,8 @@ total.each do |subject| ...@@ -76,6 +81,8 @@ total.each do |subject|
end end
end end
# create closures for the days there will be no classes
# see: https://registrar.gmu.edu/calendars/fall-2018/
Closure.create! date: Date.new(2018, 9, 3), semester: semester Closure.create! date: Date.new(2018, 9, 3), semester: semester
Closure.create! date: Date.new(2018, 10, 8), 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 } (21..25).each { |n| Closure.create! date: Date.new(2018, 11, n), semester: semester }
......
require 'test_helper'
class HomeControllerTest < ActionDispatch::IntegrationTest
test 'should get index' do
get home_index_url
assert_response :success
end
end
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