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
def generate
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)
event = generate_event_from_section(section)
cal.add_event(event)
end
render plain: cal.to_ical
render plain: cal.to_ical # render a plaintext iCal file
end
private
# Configures a calendar event from a given section
# @param section [Section]
def generate_event_from_section(section)
event = Icalendar::Event.new
......@@ -30,6 +32,10 @@ class CalendarGeneratorController < ApplicationController
event
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)
formatted_date = date.to_s.tr('-', '')
formatted_time = Time.parse(time).strftime("%H%M%S")
......@@ -37,6 +43,7 @@ class CalendarGeneratorController < ApplicationController
"#{formatted_date}T#{formatted_time}"
end
# Mapping of days as represented by GMU to the iCal standard
DAYS = {
"M" => "MO",
"T" => "TU",
......@@ -47,6 +54,10 @@ class CalendarGeneratorController < ApplicationController
"U" => "SU"
}.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)
days = section.days.split("").map do |day|
DAYS[day]
......@@ -55,7 +66,11 @@ class CalendarGeneratorController < ApplicationController
"FREQ=WEEKLY;UNTIL=#{formatted_datetime_str(section.end_date, section.end_time)};BYDAY=#{days.join(',')}"
end
# Get all dates that should excluded from the schedule
# @param section [Section]
# @return [Array]
def exdates_for_section(section)
# Generate exdates for all closures in a semester
exdates = Closure.where(semester: section.course.semester).map { |closure|
generate_exdate(closure.date.to_formatted_s(:number), section.start_time)
}
......@@ -72,7 +87,12 @@ class CalendarGeneratorController < ApplicationController
exdates
end
# Generate a DataTime to use as an exdate
# @param date [String]
# @param time [String]
# @return [Icalendar::Values::DateTime]
def generate_exdate(date, time)
# format the time for use in a DateTime
formatted_time = Time.parse(time).strftime("%H%M%S")
Icalendar::Values::DateTime.new("#{date}T#{formatted_time}")
end
......
# Defines actions for the homepage
class HomeController < ApplicationController
def index; end
end
......@@ -11,6 +11,7 @@ class Course < ApplicationRecord
validates :semester_id, presence: true
# Returns all +Section+ objects that belong to this course.
# @return [Array]
def sections
Section.where course_id: id
end
......
......@@ -8,11 +8,6 @@ class Section < ApplicationRecord
# Ensure all necessary fields are present.
validates :name, presence: true
validates :crn, presence: true
# Unsure if necessary
# validates :section_type, 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
end
# This file is no longer being used.
# Data is now being parsed from Patriot Web.
require 'rubyXL'
# Provides utilities for loading schedules from GMU's excel files.
......
require 'httparty'
module PatriotWeb
# Contains utilities for making HTTP requests to PatriotWeb
class Networker
def fetch_page_containing_semester_data
HTTParty.get('https://patriotweb.gmu.edu/pls/prod/bwckschd.p_disp_dyn_sched')
......
......@@ -2,61 +2,75 @@ require_relative 'patriot_web_networker'
require 'nokogiri'
class String
# Checks if a String is a alphanumeric
def alpha?
!!match(/^[[:alpha:]]+$/)
end
end
module PatriotWeb
# Contains methods for parsing data retrieved from Patriot Web
class Parser
def initialize
@networker = PatriotWeb::Networker.new
end
# Parses all semesters avaliable on Patriot Web
def parse_semesters
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
# Parses subjects belonging to a given semester id
# @param semester_id [Integer]
def parse_subjects(semester_id)
response = @networker.fetch_subjects(semester_id)
searcher = Nokogiri::HTML(response)
get_alpha_option_values(searcher)
document = Nokogiri::HTML(response)
get_subject_codes_from_option_values(document)
end
# Parses all courses belonging to a given subject
# @param subject [String]
def parse_courses_in_subject(subject)
resp = @networker.fetch_courses_in_subject(subject)
searcher = Nokogiri::HTML(resp)
feed_course_info(searcher)
response = @networker.fetch_courses_in_subject(subject)
document = Nokogiri::HTML(response)
feed_course_info(document)
end
private
def get_alpha_option_values(searcher)
searcher.xpath('//*[@id="subj_id"]/option').map do |opt|
if opt.attr('value').strip.alpha?
opt.attr('value')
# Parse the values of all different options on the Patriot Web
# semester select page
# @param document [Nokogiri::HTML::Document]
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
def get_semesters_from_option_values(searcher)
searcher.css('option').map do |opt|
if opt.attr('value').start_with? '20'
opt.attr('value')
# Parse all subject codes from the select element on the Patriot Web
# subject select page
# @param document [Nokogiri::HTML::Document]
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
# TODO write docs
def feed_course_info(searcher)
# find the table containing the courses
table = searcher.css('html body div.pagebodydiv table.datadisplaytable')
data = {}
currentobj = nil
table.css('table.datadisplaytable').first.children.each do |row|
next unless row.name == 'tr'
table.css('table.datadisplaytable').first.children.each do |row| # for each row in the table
next unless row.name == 'tr' # only search table rows, ignore headers
row.children.each do |item|
currentobj = sort_item(item, currentobj, data)
end
......@@ -64,6 +78,7 @@ module PatriotWeb
data
end
# TODO break this up and write docs
def sort_item(item, currentobj, data)
if item.name == 'th'
if item.to_html.include? '-'
......@@ -105,6 +120,7 @@ module PatriotWeb
currentobj
end
# TODO break this up and write docs
def get_details(data, titledetails, titledata)
crn = titledetails[1].strip
data[crn] = {} unless data[titledetails[1]]
......
# 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).
#
# 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 'thwait'
......@@ -16,7 +11,10 @@ threads = []
total = []
parser = PatriotWeb::Parser.new
# get the first semester only -- no need to ddos patriot web
semester = parser.parse_semesters.first
# parse all subjects and their courses in the semester
parser.parse_subjects(semester).each do |subject|
threads << Thread.new {
total << parser.parse_courses_in_subject(subject)
......@@ -27,20 +25,25 @@ end
# subject = parser.parse_subjects(semester).first
# total << parser.parse_courses_in_subject(subject)
# wait for all the threads to finish
ThreadsWait.all_waits(*threads)
# delete everything in the current database
Closure.delete_all
Section.delete_all
Course.delete_all
Semester.delete_all
# create a semester for the next semester
semester = Semester.create! season: 'Fall', year: 2018
semester.save!
total.each do |subject|
subject.each_value do |section|
total.each do |subject| # for each course
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")
# create a course and set its semester
course = Course.find_or_create_by(subject: section[:subj],
course_number: section[:code])
......@@ -50,6 +53,8 @@ total.each do |subject|
section_name = "#{section[:subj]} #{section[:code]} #{section[:sect]}"
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"
section["time"].split(' - ').first
else
......@@ -76,6 +81,8 @@ total.each do |subject|
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, 10, 8), 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