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

Merge branch 'api-cleanup' into 2-pretty-馃拝

parents a5565466 7ac3f9f2
stages:
- test_api
api:
image: ruby:2.5
stage: test_api
script:
- cd schedules_api
- bundle install
- rails db:migrate
- rails test
...@@ -37,7 +37,9 @@ Execute `cd schedules_api/` to enter the API directory. ...@@ -37,7 +37,9 @@ Execute `cd schedules_api/` to enter the API directory.
To install the project dependencies, run the `bundle install` command. To install the project dependencies, run the `bundle install` command.
### Database ### Database
To populate your local database, run `rake db:migrate` and `rake db:seed`. This sets up your local database and loads it with data from the Excel speadsheet(s) of GMU courses. **NOTE:** This may take a while! To populate your local database, run `rake db:migrate` and `rake db:seed`. This sets up your local database and loads it with data from Patriot Web.
**NOTE:** Sometimes Patriot Web doesn't appriciate being parsed. If you're having problems,
please let us know in [Slack](https://srct.slack.com/)!
## Setting up client ## Setting up client
......
version: "3"
services:
api:
image: schedules_api
ports:
- "3000:3000"
command: ./start.sh
volumes:
- .:/api
FROM ruby:2.5
RUN mkdir /api
WORKDIR /api
ADD . /api
RUN bundle install
...@@ -12,11 +12,6 @@ gem 'sqlite3' ...@@ -12,11 +12,6 @@ gem 'sqlite3'
# Use Puma as the app server # Use Puma as the app server
gem 'puma', '~> 3.7' gem 'puma', '~> 3.7'
# Use SCSS for stylesheets # Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
# gem 'therubyracer', platforms: :ruby
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks # Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5' gem 'turbolinks', '~> 5'
......
...@@ -55,15 +55,15 @@ GEM ...@@ -55,15 +55,15 @@ GEM
ffi (~> 1.0, >= 1.0.11) ffi (~> 1.0, >= 1.0.11)
coderay (1.1.2) coderay (1.1.2)
concurrent-ruby (1.0.5) concurrent-ruby (1.0.5)
crass (1.0.3) crass (1.0.4)
erubi (1.7.1) erubi (1.7.1)
execjs (2.7.0) execjs (2.7.0)
ffi (1.9.23) ffi (1.9.25)
globalid (0.4.1) globalid (0.4.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
httparty (0.16.2) httparty (0.16.2)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
i18n (1.0.0) i18n (1.0.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
icalendar (2.4.1) icalendar (2.4.1)
jbuilder (2.7.0) jbuilder (2.7.0)
...@@ -84,8 +84,8 @@ GEM ...@@ -84,8 +84,8 @@ GEM
minitest (5.11.3) minitest (5.11.3)
multi_json (1.13.1) multi_json (1.13.1)
multi_xml (0.6.0) multi_xml (0.6.0)
nio4r (2.3.0) nio4r (2.3.1)
nokogiri (1.8.2) nokogiri (1.8.3)
mini_portile2 (~> 2.3.0) mini_portile2 (~> 2.3.0)
pry (0.11.3) pry (0.11.3)
coderay (~> 1.1.0) coderay (~> 1.1.0)
...@@ -94,8 +94,8 @@ GEM ...@@ -94,8 +94,8 @@ GEM
pry (~> 0.11) pry (~> 0.11)
yard (~> 0.9.11) yard (~> 0.9.11)
public_suffix (3.0.2) public_suffix (3.0.2)
puma (3.11.3) puma (3.11.4)
rack (2.0.4) rack (2.0.5)
rack-cors (1.0.2) rack-cors (1.0.2)
rack-test (1.0.0) rack-test (1.0.0)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
...@@ -142,7 +142,7 @@ GEM ...@@ -142,7 +142,7 @@ GEM
sprockets (>= 2.8, < 4.0) sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0) sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3) tilt (>= 1.1, < 3)
selenium-webdriver (3.11.0) selenium-webdriver (3.13.0)
childprocess (~> 0.5) childprocess (~> 0.5)
rubyzip (~> 1.2) rubyzip (~> 1.2)
spring (2.0.2) spring (2.0.2)
...@@ -150,7 +150,7 @@ GEM ...@@ -150,7 +150,7 @@ GEM
spring-watcher-listen (2.0.1) spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0) listen (>= 2.7, < 4.0)
spring (>= 1.2, < 3.0) spring (>= 1.2, < 3.0)
sprockets (3.7.1) sprockets (3.7.2)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (> 1, < 3)
sprockets-rails (3.2.1) sprockets-rails (3.2.1)
...@@ -161,14 +161,14 @@ GEM ...@@ -161,14 +161,14 @@ GEM
thor (0.20.0) thor (0.20.0)
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.0.8) tilt (2.0.8)
turbolinks (5.1.0) turbolinks (5.1.1)
turbolinks-source (~> 5.1) turbolinks-source (~> 5.1)
turbolinks-source (5.1.0) turbolinks-source (5.1.0)
tzinfo (1.2.5) tzinfo (1.2.5)
thread_safe (~> 0.1) thread_safe (~> 0.1)
uglifier (4.1.8) uglifier (4.1.12)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
web-console (3.5.1) web-console (3.6.2)
actionview (>= 5.0) actionview (>= 5.0)
activemodel (>= 5.0) activemodel (>= 5.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
...@@ -176,9 +176,9 @@ GEM ...@@ -176,9 +176,9 @@ GEM
websocket-driver (0.6.5) websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3) websocket-extensions (0.1.3)
xpath (3.0.0) xpath (3.1.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
yard (0.9.12) yard (0.9.14)
PLATFORMS PLATFORMS
ruby ruby
......
require 'icalendar' require 'icalendar'
require 'time' require 'time'
# Contains functionality for generating schedules.
class CalendarGeneratorController < ApplicationController class CalendarGeneratorController < ApplicationController
def generate # Render an iCal file containing the schedules of all the
# course sections with the given CRNs.
def new
cal = Icalendar::Calendar.new cal = Icalendar::Calendar.new
params[:_json].each do |crn| # the intended format for the json is a list of CRNs
section = Section.find_by_crn(crn) params[:_json].each do |crn| # for each CRN sent by the post request
section = CourseSection.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 [CourseSection]
def generate_event_from_section(section) def generate_event_from_section(section)
event = Icalendar::Event.new event = Icalendar::Event.new
...@@ -30,6 +36,10 @@ class CalendarGeneratorController < ApplicationController ...@@ -30,6 +36,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 +47,7 @@ class CalendarGeneratorController < ApplicationController ...@@ -37,6 +47,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 +58,10 @@ class CalendarGeneratorController < ApplicationController ...@@ -47,6 +58,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 [CourseSection]
# @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 +70,11 @@ class CalendarGeneratorController < ApplicationController ...@@ -55,7 +70,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 [CourseSection]
# @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 +91,12 @@ class CalendarGeneratorController < ApplicationController ...@@ -72,7 +91,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
......
# Contains all actions having to do with Sections. # Contains all actions having to do with CourseSections.
# This is a nested controller -- see +config/routes.rb+ for details # This is a nested controller -- see +config/routes.rb+ for details
class SectionsController < ApplicationController class CourseSectionsController < ApplicationController
# Render JSON of all Sections belonging to a given Course. # Render JSON of all Sections belonging to a given Course.
def index def index
@course = Course.find(params[:course_id]) @sections = CourseSection.where(course_id: params[:course_id])
@sections = @course.sections
render json: @sections render json: @sections
end end
end end
# Contains all actions having to do with Courses. # Contains all actions having to do with Courses.
class CoursesController < ApplicationController class CoursesController < ApplicationController
# Renders JSON of all courses. # Renders JSON of courses matching params.
def index def index
@courses = Course.all @courses = Course.all
# filter by subject + course number if the params are included
@courses = @courses.where(subject: params[:subject].upcase) if params.key?(:subject)
@courses = @courses.where(course_number: params[:course_number]) if params.key?(:course_number)
render json: @courses render json: @courses
end end
# Renders JSON of details of a singluar course, such as its sections
def show
@sections = CourseSection.where(course_id: params[:id])
render json: @sections
end
end end
# Defines actions for the homepage
class HomeController < ApplicationController
def index; end
end
class SearchController < ApplicationController
def index
if params.key?(:crn)
crn = params[:crn]
@sections = Section.find_by_crn(crn)
render json: @sections
else
render status: 404
end
end
end
...@@ -4,14 +4,16 @@ ...@@ -4,14 +4,16 @@
class Course < ApplicationRecord class Course < ApplicationRecord
# Each course belongs to a +Semester+ # Each course belongs to a +Semester+
belongs_to :semester belongs_to :semester
has_many :course_sections
# Ensure all necessary fields are present. # Ensure all necessary are fields present.
validates :course_number, presence: true validates :course_number, presence: true
validates :subject, presence: true validates :subject, presence: true
validates :semester_id, presence: true validates :semester_id, presence: true
# Returns all +Section+ objects that belong to this course. # Returns all +CourseSection+ objects that belong to this course.
def sections # @return [Array]
Section.where course_id: id def course_sections
CourseSection.where course_id: id
end end
end end
# Contains logic belonging to the +Section+ model. # Contains logic belonging to the +CourseSection+ model.
# #
# TODO: Add more docs # TODO: Add more docs
class Section < ApplicationRecord class CourseSection < ApplicationRecord
# Each +Section+ belongs to a +Course+. # Each +CourseSection+ belongs to a +Course+.
belongs_to :course belongs_to :course
# 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
...@@ -2,7 +2,13 @@ ...@@ -2,7 +2,13 @@
# #
# A +Semester+ is a simple model that consists of a +year+ and a +season+, e.g. "Fall 2018". # A +Semester+ is a simple model that consists of a +year+ and a +season+, e.g. "Fall 2018".
class Semester < ApplicationRecord class Semester < ApplicationRecord
has_many :courses
# Ensure necessary fields are present. # Ensure necessary fields are present.
validates :year, presence: true validates :year, presence: true
validates :season, presence: true validates :season, presence: true
def courses
Course.where semester_id: id
end
end end
...@@ -17,7 +17,7 @@ Rails.application.configure do ...@@ -17,7 +17,7 @@ Rails.application.configure do
# Attempt to read encrypted secrets from `config/secrets.yml.enc`. # Attempt to read encrypted secrets from `config/secrets.yml.enc`.
# Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
# `config/secrets.yml.key`. # `config/secrets.yml.key`.
config.read_encrypted_secrets = true # config.read_encrypted_secrets = true
# Disable serving static files from the `/public` folder by default since # Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this. # Apache or NGINX already handles this.
......
# Registers all routes for the app. # Registers all routes for the app.
Rails.application.routes.draw do Rails.application.routes.draw do
scope :api do # Register /api routes scope :api do # Register /api routes
resources :courses, only: [:index] do # GET /api/courses resources :courses, only: [:index, :show]
resources :sections, only: [:index] # GET /api/courses/:course_id/sections resources :course_sections, only: [:index]
end
get 'search', controller: 'search', action: 'index' post 'generate', controller: 'calendar_generator', action: 'new'
post 'generate', controller: 'calendar_generator', action: 'generate'
end end
root 'courses#index' # Set the root to be the courses API endpoint root 'courses#index' # Set the root to be the courses API endpoint
......
# 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.
...@@ -35,7 +38,7 @@ class ExcelLoader ...@@ -35,7 +38,7 @@ class ExcelLoader
def delete_all_records def delete_all_records
Semester.delete_all Semester.delete_all
Course.delete_all Course.delete_all
Section.delete_all CourseSection.delete_all
end end
# Tries to create a course from a given row. # Tries to create a course from a given row.
...@@ -67,7 +70,7 @@ class ExcelLoader ...@@ -67,7 +70,7 @@ class ExcelLoader
# So, split the times string by the - character # So, split the times string by the - character
times = row.cells[23]&.value times = row.cells[23]&.value
time_strs = times.split('-') time_strs = times.split('-')
section = Section.create name: section_name, section = CourseSection.create name: section_name,
course: @current_course, course: @current_course,
crn: row.cells[6]&.value, crn: row.cells[6]&.value,
section_type: row.cells[8]&.value, section_type: row.cells[8]&.value,
......
class ChangeSectionToCourseSection < ActiveRecord::Migration[5.1]
def change
rename_table :sections, :course_sections
end
end
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')
......
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