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

Merge branch 'revert-ea27d283' into 'master'

Revert "Merge branch '44-instructor-search' into 'master'"

See merge request !49
parents ea27d283 5ca634f2
Pipeline #4315 passed with stage
in 3 minutes and 51 seconds
import React from 'react';
import CourseList from 'src/CourseList';
import InstructorList from 'src/InstructorList';
export default class SearchList extends React.Component {
render() {
return (
<div>
<InstructorList instructors={this.props.instructors} />
<CourseList courses={this.props.courses} />
</div>
);
}
}
import React from 'react';
import Cart from 'src/Cart';
import Stars from 'src/Stars';
export default class Section extends React.Component {
constructor(props) {
super(props);
this.state = { inCart: Cart.includesCrn(this.props.crn) };
}
onClick = e => {
e.stopPropagation();
console.log(e.target.tagName);
if (e.target.tagName === 'A') return;
Cart.toggleCrn(this.props.crn);
this.setState({ inCart: Cart.includesCrn(this.props.crn) });
this.props.onClick && this.props.onClick(this.props.crn);
};
render() {
const { name, title, crn, instructor_name, instructor_url, teaching_rating, course_rating, location, days, start_time, end_time } = this.props;
const { inCart } = this.state;
const percent = teaching_rating ? <Stars percent={(teaching_rating[0] / 5) * 100} /> : null;
const remove = (
<span className="float-right text-center add-remove-btn" style={inCart ? {} : { display: 'none' }}>
<i id="icon" className="fas fa-minus" />
<br />
<span className="text">Remove</span>
</span>
);
const add = (
<span className="float-right text-center add-remove-btn" style={inCart ? { display: 'none' } : {}}>
<i id="icon" className="fas fa-plus" />
<br />
<span className="text">Add</span>
</span>
);
return (
<li className="list-group-item section-item" onClick={this.onClick}>
<p>
<b>{name}</b>: {title}{' '}
<em>
(#
{crn})
</em>
</p>
{remove}
{add}
<i className="fas fa-chalkboard-teacher" /> <a href={instructor_url}>{instructor_name}</a> {percent}
<br />
<i className="fas fa-map-marker-alt" /> {location} <br />
<i className="fas fa-clock" /> {days}, {start_time} - {end_time} <br />
</li>
);
}
}
import React from 'react';
import Chevron from 'src/Chevron';
import Section from 'src/Section';
export default class SectionList extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
{this.props.expandable ? <Chevron open={this.props.expanded} /> : null}
{this.props.expanded ? (
<div className="d-flex list-group list-group-flush sections">
{this.props.sections.map(section => (
<Section key={section.id} onClick={this.props.onClick} {...section} />
))}
</div>
) : (
<div />
)}
</div>
);
}
}
import React from 'react';
export default class Stars extends React.Component {
render() {
return (
<div className="star-rating">
<div className="back-stars">
<i className="fas fa-star" aria-hidden="true" />
<i className="fas fa-star" aria-hidden="true" />
<i className="fas fa-star" aria-hidden="true" />
<i className="fas fa-star" aria-hidden="true" />
<i className="fas fa-star" aria-hidden="true" />
<div className="front-stars" style={{ width: `${this.props.percent}%` }}>
<i className="fa fa-star" aria-hidden="true" />
<i className="fa fa-star" aria-hidden="true" />
<i className="fa fa-star" aria-hidden="true" />
<i className="fa fa-star" aria-hidden="true" />
<i className="fa fa-star" aria-hidden="true" />
</div>
</div>
</div>
);
}
}
import React from 'react';
import BigCalendar from 'react-big-calendar';
import Toolbar from 'react-big-calendar/lib/Toolbar';
import '!style-loader!css-loader!react-big-calendar/lib/css/react-big-calendar.css';
import withSizes from 'react-sizes';
class CustomToolbar extends Toolbar {
render() {
const { label, isMobile } = this.props;
if (isMobile && label === '') {
this.view('day');
}
return (
<div className="rbc-toolbar d-flex justify-content-between">
{!isMobile && (
<span className="rbc-btn-group">
<button type="button" onClick={() => this.view('day')}>
Day
</button>
<button type="button" onClick={() => this.view('week')}>
Week
</button>
</span>
)}
<span className="rbc-toolbar-label">{this.props.label}</span>
{this.props.view === 'day' && (
<span className="rbc-btn-group">
{this.props.label !== 'Sun' && (
<button type="button" onClick={() => this.navigate('PREV')}>
Back
</button>
)}
{this.props.label !== 'Sat' && (
<button type="button" onClick={() => this.navigate('NEXT')}>
Next
</button>
)}
</span>
)}
</div>
);
}
navigate = action => {
console.log(action);
this.props.onNavigate(action);
};
view = action => {
this.props.onView(action);
};
}
const mapSizesToProps = ({ width }) => ({
isMobile: width < 1000,
});
export default withSizes(mapSizesToProps)(CustomToolbar);
//import '@babel/polyfill';
class Cart {
constructor() {
document.addEventListener('DOMContentLoaded', () => (document.getElementById('cart-counter').innerText = this.crns.length));
}
this.isOpen = false;
this._courses = [];
get crns() {
const crnString = localStorage.getItem('crns');
if (!crnString) return [];
return JSON.parse(crnString);
const cartData = document.getElementById('cart-data');
if (cartData) {
this._courses = JSON.parse(cartData.dataset.cart);
}
}
set crns(crnList) {
localStorage.setItem('crns', JSON.stringify(crnList));
document.getElementById('cart-counter').innerText = crnList.length;
_parseData() {
const cartData = document.getElementById('cart-data');
if (cartData) {
this._courses = JSON.parse(cartData.dataset.cart);
}
}
addCrn(crn) {
if (!this.includesCrn(crn)) {
this.crns = [...this.crns, crn];
toggle() {
const list = document.getElementById('cart');
const icon = document.getElementById('schedule-icon');
if (this.isOpen) {
list.style.display = 'none';
icon.style.color = 'black';
} else {
list.style.display = 'block';
icon.style.color = 'green';
}
this.isOpen = !this.isOpen;
}
toggleCrn(crn) {
if (!this.includesCrn(crn)) {
this.crns = [...this.crns, crn];
} else {
this.crns = this.crns.filter(c => c != crn);
set courses(courses) {
this._courses = courses;
for (const courseId in this._courses) {
if (this._courses[courseId].length === 0) delete this._courses[courseId];
}
document.getElementById('course-counter').innerText = Object.keys(this._courses).length;
}
includesCrn(crn) {
return this.crns.filter(c => c == crn).length > 0;
async toggleSection(section) {
const resp = await fetch(`/sessions/cart?&crn=${section.crn}`, {
cache: 'no-store',
credentials: 'same-origin'
});
const json = await resp.json();
this.courses = json;
}
includesSection(obj) {
for (const key in this._courses) {
const list = this._courses[key];
if (list.includes(obj.crn)) return true;
}
return false;
}
}
export default new Cart();
const cart = new Cart();
document.addEventListener('DOMContentLoaded', () => cart._parseData());
export default cart;
......@@ -9,4 +9,53 @@ class Course < ApplicationRecord
def full_name
"#{subject} #{course_number}"
end
def self.from_subject(base_query, subject)
base_query.where("courses.subject = ?", subject.upcase)
end
def self.from_course_number(base_query, course_number)
base_query.where("courses.course_number = ?", course_number)
end
def self.from_title(base_query, title)
puts title
# Temporary really disgusting regex that I hate with all my heart
title = (title + " ").upcase.gsub(/(I+) +/, '\1$').gsub(/ +/, "% ").tr('$', ' ')
base_query.where("UPPER(courses.title) LIKE UPPER(?) or UPPER(courses.title) LIKE UPPER(?)", "%#{title.strip}", "%#{title}%")
end
# Given a list of filters, collect a list of matching elements. This makes it
# so you can just pass the arguments straight thru
def self.fetch(filters)
# join with course_sections so that we can get a section count for each course and then sort by that
query = Course.left_outer_joins(:course_sections)
.select("courses.*, COUNT(course_sections.id) AS section_count")
.group("courses.id")
.order("section_count DESC")
filters.each do |filter, value|
case filter
when "subject"
query = from_subject(query, value)
when "course_number"
query = from_course_number(query, value)
when "title"
query = from_title(query, value)
when "instructor"
query = Instructor.from_name(query.joins("INNER JOIN instructors ON course_sections.instructor_id = instructors.id"), value)
end
end
query
end
# build_set builds
def self.build_set(sections)
courses = [].to_set
sections.each do |s|
courses.add s.course
end
courses
end
end
......@@ -14,26 +14,6 @@ class CourseSection < ApplicationRecord
validates :course_id, presence: true
validates :semester_id, presence: true
serialize :rating_questions, Array
scope :in_semester, ->(semester) { where(semester: semester) }
def teaching_rating
if rating_questions.empty?
nil
else
"#{rating_questions[0]['instr_mean']} / #{rating_questions[0]['resp']} responses"
end
end
def course_rating
if rating_questions.empty?
nil
else
"#{rating_questions[1]['instr_mean']} / #{rating_questions[1]['resp']} responses"
end
end
def overlaps?(other)
t1_start, t1_end = Time.parse(start_time), Time.parse(end_time)
t2_start, t2_end = Time.parse(other.start_time), Time.parse(other.end_time)
......
class Instructor < ApplicationRecord
has_many :course_sections
scope :named, ->(name) {
name.split(' ').reduce(all) do |query, comp|
query.where("upper(instructors.name) LIKE ?", "%#{comp.upcase}%")
end
}
def self.from_name(base_query, name)
base_query.where("upper(instructors.name) LIKE ?", "%#{name.upcase}%")
end
def rating(question = 0, sections = CourseSection.where(instructor_id: id))
total = 0
resp = 0
sections.each do |s|
next if s.rating_questions.empty?
resp += s.rating_questions[question]["resp"].to_i
total += s.rating_questions[question]["instr_mean"].to_f * s.rating_questions[0]["resp"].to_i
end
[(total / resp).round(2), resp] unless resp.zero?
end
end
<div class="jumbotron text-center">
<h1><i class="fas fa-calendar-alt"></i>&nbsp;SRCT Schedules</h1>
<p class="lead">Version 3.0</p>
<hr />
Last updated: 2:00am, 4/14/19
</div>
<h3>Thank you to our contributors who make Schedules possible!</h3>
Zac Wood, David Haynes, Zach Perkins, Gilberto Barrientos, Michael Bailey, Nic Anderson
<br /><br/>
<h3>Questions?</h3>
All data in Schedules is sourced from data made publicly avaiable by GMU.
<ul>
<li>Course and section data can be found on <a href="https://patriotweb.gmu.edu/pls/prod/bwckschd.p_disp_dyn_sched">Patriot Web</a></li>
<li>Course review data can be found <a href="https://crserating.gmu.edu/ReportOnline/">here</a></li>
</ul>
Please contact SRCT at <a href="mailto:srct@gmu.edu">srct@gmu.edu</a> with any other questions.
<h1><%= @section.name %> - <%= @section.semester.to_s %> - <%= @section.instructor.name %></h1>
<ol>
<% @section.rating_questions.each do |q| %>
<b>
<li><%= q["q"] %></li>
</b>
Instructor mean: <%= q["instr_mean"] %>, Responses: <%= q["resp"] %>
<% end %>
</ol>
......@@ -29,5 +29,5 @@
</div>
<%= javascript_pack_tag 'instructor' %>
<%= javascript_pack_tag 'search' %>
<%= stylesheet_link_tag 'search' %>
<div class="jumbotron text-center">
<h1><i class="fas fa-calendar-alt"></i>&nbsp;SRCT Schedules</h1>
<h1>SRCT Schedules</h1>
<p class="lead">Build, share, and export your schedule. Search for classes and professors.</p>
<hr>
<p>
......@@ -36,6 +36,24 @@
</div>
</div>
<div id="quick-add" />
<%= javascript_pack_tag 'home' %>
<h3 class="quick-add-header">Quick add</h3>
<p>Want to quickly generate a calendar populated with your semester's classes? Enter the CRNs in a comma separated list below.</p>
<form action="/sessions/add_bulk" class="form">
<div class="input-group">
<input
id="crns"
name="crns"
type="text"
class="form-control"
placeholder="12345,54321,..."
aria-describedby="basic-addon2"
autocomplete="off"
>
<div class="input-group-append">
<button type="submit" class="btn btn-primary" type="button">
Populate Calendar
</button>
</div>
</div>
</form>
<div class="row">
<div class="col-lg-4 col-12 mb-4">
<div class="col-lg-4 col-12">
<h1><%= @instructor.name %></h1>
<% unless @rating[:teaching].nil? %>
Average teaching rating: <%= @rating[:teaching][0] %> / <%= @rating[:teaching][1] %> responses
<% if @past.count.positive? %>
<strong>Previously taught: </strong>
<ul>
<% @past.each do |c| %>
<li><%= link_to(c.full_name, course_path(c)) %></li>
<% end %>
</ul>
<% end %>
</div>
<div class="col-lg-8 col-12">
<% @semesters.each do |semester, sections| %>
<h2><%= semester %></h2>
<%= render(partial: 'shared/section', collection: sections) %>
<br/>
<div class ="col-lg-8 col-12">
<h3><%= @semester.to_s %></h3>
<% if @courses.any? %>
<%= render(partial: 'shared/course', collection: @courses, locals: { expanded: true }) %>
<% else %>
<p><%= @instructor.name %> is not teaching any courses this semester...</p>
<% end %>
</div>
</div>
<%= javascript_pack_tag 'instructor' %>
<%= javascript_pack_tag 'search' %>
<%= stylesheet_link_tag 'search' %>
......@@ -5,8 +5,6 @@
<%= csrf_meta_tags %>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<%= Gon::Base.render_data %>
<%= javascript_include_tag 'masonstrap.min' %>
<%= stylesheet_link_tag 'masonstrap.min' %>
......@@ -18,7 +16,7 @@
<meta property="og:url" content="https://schedules.gmu.edu/">
<meta property="og:type" content="website">
<meta property="og:title" content="SRCT Schedules">
<meta property="og:description" content="Browse the GMU catalog, see course reviews, build and share your schedule.">
<meta property="og:description" content="Easily generate a calendar with your class schedule.">
<meta property="og:site_name" content="SRCT Schedules">
<meta property="og:locale" content="en_US">
<meta property="article:author" content="SRCT">
......@@ -29,11 +27,7 @@
<meta name="twitter:creator" content="@MasonSRCT">
<meta name="twitter:url" content="https://schedules.gmu.edu/">
<meta name="twitter:title" content="SRCT Schedules">
<meta name="twitter:description" content="Browse the GMU catalog, see course reviews, build and share your schedule.">
<!-- favicons -->
<%= favicon_link_tag %>
<link href="<%= asset_path 'favicon-32x32.png' %>" sizes="32x32" rel="shortcut icon" type="image/png" />
<meta name="twitter:description" content="Easily generate a calendar with your class schedule.">
</head>
<body>
......
<%= javascript_pack_tag 'schedules' %>
<%= stylesheet_link_tag 'schedules' %>
<%= javascript_include_tag 'moment.min' %>
<%= stylesheet_link_tag 'fullcalendar.min' %>
<button id="open-modal-btn" type="button" class="btn btn-primary" data-toggle="modal" data-target="#exportModal">
Export Schedule
</button>
<button id="save-image" class="btn btn-secondary">Save Image</button>
<div id="root"></div>
<div id="calendar"></div>
<h3>Quick add</h3>
<p>Populate your calendar quickly by entering a comma separated list of CRNs.</p>
<form action="/sessions/add_bulk" class="form">
<div class="input-group">
<input
id="crns"
name="crns"
type="text"
class="form-control"
placeholder="12345,54321,..."
aria-describedby="basic-addon2"
autocomplete="off"
>
<div class="input-group-append">
<button type="submit" class="btn btn-primary" type="button">
Populate Calendar
</button>
</div>
</div>
</form>
<h3 id="share-header">Share</h3>
Want to share your schedule with your friends? Send them this link:<br/>
<a id="share-url"></a>
<template id="events" data-events="<%= @events.to_json %>"></template>
<hr />
<h2>Selected Courses</h2>
<%= render partial: 'shared/section', collection: @all %>
<!-- Export Modal -->
......
<div id="root"></div>
<% unless @instructors.nil? %>
<h2>Instructors</h2>
<div class="row">