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

Merge branch 'redesign' into 'master'

Redesign

See merge request !53
parents 80db8a9e 197c3aee
Pipeline #5030 passed with stages
in 27 minutes and 17 seconds
// Place all the styles related to the Sections controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
# Configures the application.
class ApplicationController < ActionController::Base
include BySemester
before_action :set_render_page
def set_render_page
@render_page = true
end
end
......@@ -12,14 +12,10 @@ module BySemester
#
# By default, load the most recent semester.
def set_semester
if params.key?(:semester_id)
@semester = Semester.find_by_id(params[:semester_id])
session[:semester_id] = @semester.id
elsif session[:semester_id].nil?
@semester = Semester.first
session[:semester_id] = @semester.id
@semester = if params.key?(:semester_id)
Semester.find_by_id(params[:semester_id])
else
@semester = Semester.find_by_id(session[:semester_id])
Semester.sorted_by_date.first
end
end
end
class CourseSectionsController < ApplicationController
def index
@render_page = false
crns = params[:crns].split(',')
@sections = crns.map { |crn| CourseSection.latest_by_crn(crn) }
@days = {
"M" => [], "T" => [], "W" => [],
"R" => [], "F" => []
}
@sections.each do |s|
s.days.split('').each do |day|
@days[day] << s unless s.start_time == "TBA"
end
end
@days_map = {
"M" => "Monday", "T" => "Tuesday", "W" => "Wednesday",
"R" => "Thursday", "F" => "Friday"
}
@days.each do |day, sections|
sections.sort! do |a,b|
Time.new(a.start_time) <=> Time.new(b.start_time)
end
end
end
def show
@section = CourseSection.find_by_id(params[:id])
end
......
......@@ -3,6 +3,40 @@ class CoursesController < ApplicationController
# Load the course with the id passed in the URL.
@course = Course.find_by_id(params[:id])
@rating = @course.rating
@sections = @course.course_sections.where(semester: @semester)
semester_ids = Set.new(@course.course_sections.map(&:semester_id)).to_a
@semesters = Semester.where(id: semester_ids)
@semesters = Semester.sorted_by_date(@semesters)
@taught_in = Set.new(@semesters.map(&:season))
@taught_in = if @taught_in.empty?
"Has not been offered since #{Semester.sorted_by_date.last.to_s}"
else
"Has been offered in #{(@taught_in.to_a).join(", ")}"
end
if @semesters.first != Semester.sorted_by_date.first
@semesters = [Semester.sorted_by_date.first, *@semesters]
end
@sections = @course.course_sections.where(semester: @semester).group_by { |s| s.section_type }
end
private
def sort_seasons(seasons)
seasons.sort do |s1, s2|
case
when s1 == "Fall"
-1
when s1 == "Summer" && s2 == "Fall"
1
when s1 == "Spring"
1
else
0
end
end
end
end
\ No newline at end of file
......@@ -8,8 +8,15 @@ class InstructorsController < ApplicationController
# find the courses being taught this semester
sections = CourseSection.where(instructor: @instructor)
@semesters = sections.group_by do |s|
s.semester.to_s
semester_ids = Set.new(sections.map(&:semester_id))
@semesters = Semester.where(id: semester_ids.to_a)
@semesters = Semester.sorted_by_date(@semesters)
@sections = sections.where(semester: @semester).group_by { |s| s.section_type }
if @semesters.first != Semester.sorted_by_date.first
@semesters = [Semester.sorted_by_date.first, *@semesters]
end
@rating = { teaching: @instructor.rating, respect: @instructor.rating(6) }
......
......@@ -5,10 +5,11 @@ class SchedulesController < ApplicationController
def show; end
def view
@all = params[:crns].split(',').map { |crn|
CourseSection.latest_by_crn(crn)
}
@all.reject!(&:nil?)
@all = params[:crns]
.split(',')
.map { |crn| CourseSection.latest_by_crn(crn) }
.reject(&:nil?)
@without_online = @all.reject { |s|
s.start_time == "TBA" || s.end_time == "TBA"
}
......
class SearchController < ApplicationController
def index
params[:query].strip!
redirect_to(home_url) unless params[:query].length > 1
if params[:query].casecmp('god').zero?
......@@ -10,39 +11,24 @@ class SearchController < ApplicationController
@instructors = nil
@courses = nil
/[[:alpha:]]{2,4} \d{3}/.match(params[:query]) do |m|
subj, num = m[0].split(' ')
/(?<subj>[[:alpha:]]{2,4}) ?(?<num>\d{3})/.match(params[:query]) do |m|
subj, num = m[:subj], m[:num]
course = Course.find_by(subject: subj.upcase, course_number: num)
redirect_to(course_url(course)) unless course.nil?
end
/[[:alpha:]]{2,4}/i.match(params[:query]) do |m|
@courses = Course.where(subject: m[0].upcase)
.joins(:course_sections)
.merge(CourseSection.in_semester(@semester))
.uniq
@courses = Course.where(subject: m[0].upcase).uniq
if @courses.empty?
@courses = Course.where("(courses.title LIKE ?)", "%#{params[:query]}%")
.joins(:course_sections)
.merge(CourseSection.in_semester(@semester))
.uniq
other = Course.where("(courses.description LIKE ?)", "%#{params[:query]}%")
.joins(:course_sections)
.merge(CourseSection.in_semester(@semester))
.uniq
@courses = [*@courses, *other].uniq
query = "%#{params[:query]}%"
@courses = Course.where("(courses.title LIKE ?) OR (courses.description LIKE ?)", query, query).uniq
@instructors = Instructor.named(params[:query])
end
@courses.map! do |c|
c.serializable_hash.merge(url: course_url(c))
end
gon.courses = @courses
gon.instructors = @instructors
end
/[0-9]{5}/.match(params[:query]) do |m|
......@@ -50,9 +36,9 @@ class SearchController < ApplicationController
end
if @courses&.count == 1 && @instructors&.count&.zero?
redirect_to course_url(@courses.first["id"])
redirect_to(course_url(@courses.first["id"]))
elsif @courses&.count&.zero? && @instructors&.count == 1
redirect_to instructor_url(@instructors.first)
redirect_to(instructor_url(@instructors.first))
end
end
end
module SearchHelper
class GenericQueryData
attr_reader :semester
attr_reader :sort_mode
attr_reader :search_string
def initialize(search_string, sort_mode, semester)
@semester = semester
@sort_mode = sort_mode
@search_string = search_string
end
end
class GenericItem
attr_reader :data
attr_reader :type
def initialize(type, data)
@type = type
@data = data
end
def self.fetchall(search_string, sort_mode: :auto, semester: :fall2018)
query_data = GenericQueryData.new(search_string, sort_mode, semester)
models = []
models += fetch_instructors(query_data)
models += fetch_courses(query_data)
build_list(models)
end
def self.fetch_instructors(query_data)
Instructor.from_name(Instructor.select("instructors.*, COUNT(courses.id) AS section_count").from("course_sections"), query_data.search_string)
.joins("LEFT OUTER JOIN instructors ON instructors.id = course_sections.instructor_id")
.joins("LEFT OUTER JOIN courses ON courses.id = course_sections.course_id AND course_sections.semester_id = #{query_data.semester.id}")
.group("instructors.id").all
end
def self.fetch_courses(query_data)
query_string = query_data.search_string
query_string.upcase!
CourseReplacementHelper.replace!(query_string)
base_query = Course.select("courses.*, count(course_sections.id) AS section_count")
.left_outer_joins(:course_sections)
.having("count(course_sections.id) > 0")
.where("course_sections.semester_id = ?", query_data.semester)
.group("courses.id")
subj = nil
query_string.scan(/(?<= |^)([a-zA-Z]{2,4})(?=$| )/).each do |a|
s = a[0]
next unless get_count(Course.from_subject(base_query, s)).positive?
# next unless Course.from_subject(base_query, s).count.positive?
subj = s
base_query = Course.from_subject(base_query, subj)
query_string.remove!(s)
end
query_string.scan(/(?<= |^)(\d{3})(?=$| )/).each do |a|
s = a[0]
next unless !subj.nil? && get_count(Course.from_course_number(base_query, s)).positive?
# next unless !subj.nil? && Course.from_course_number(base_query, s).count.positive?
base_query = Course.from_course_number(base_query, s)
return base_query.all
end
stripped_query_string = query_string.gsub(/ +/, " ").strip
# There's more to parse
base_query = if stripped_query_string.length.positive?
Course.from_title(base_query, stripped_query_string)
.order("section_count DESC")
else
base_query.order("courses.course_number ASC")
end
base_query.all
end
# Given a set of models, create a list of GenericItems for each model's data
def self.build_list(models)
list = []
models.each do |model|
list.push(GenericItem.new(model.class.name.underscore.to_sym, model))
end
list
end
def self.get_count(base_query)
# I think I finally hit a limit of active record
ActiveRecord::Base.connection.execute("SELECT COUNT(*) AS count FROM (#{base_query.to_sql}) as q")[0]["count"]
end
def to_s
@type
end
end
end
/* eslint no-console:0 */
// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
......@@ -7,47 +6,72 @@
// To reference this file, add <%= javascript_pack_tag 'application' %> to the appropriate
// layout file, like app/views/layouts/application.html.erb
import '@babel/polyfill';
import 'url-polyfill';
import 'url-polyfill'
import React from 'react';
import Cart from 'src/Cart';
import Turbolinks from 'turbolinks'
Turbolinks.start()
const elementFromString = string => {
const html = new DOMParser().parseFromString(string, 'text/html');
return html.body.firstChild;
};
document.addEventListener('DOMContentLoaded', () => {
initGlobalListeners();
});
const setSemester = async select => {
const url = new URL(window.location.href);
url.searchParams.set('semester_id', select.value);
window.open(url.toString(), '_self');
};
const initGlobalListeners = () => {
const semesterSelect = document.getElementById('semester-select');
semesterSelect.onchange = () => setSemester(semesterSelect);
};
if (!HTMLCanvasElement.prototype.toBlob) {
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
value: function(callback, type, quality) {
var canvas = this;
setTimeout(function() {
var binStr = atob(canvas.toDataURL(type, quality).split(',')[1]),
len = binStr.length,
arr = new Uint8Array(len);
for (var i = 0; i < len; i++) {
arr[i] = binStr.charCodeAt(i);
window.addEventListener('turbolinks:load', () => {
setInitialLinks()
addListeners()
document.querySelector('#count').innerText = getCart().length
})
function setInitialLinks() {
getCart().forEach(writeLink)
}
function addListeners() {
const links = Array.from(document.querySelectorAll('.add-section'))
for (const link of links) {
link.addEventListener('click', e => {
e.preventDefault()
const crn = link.dataset.crn
toggleSection(crn)
writeLink(crn)
})
}
}
function getCart() {
return JSON.parse(localStorage.getItem('cart') || '[]')
}
function toggleSection(crn) {
if (getCart().includes(crn)) {
removeSection(crn)
} else {
addSection(crn)
}
console.log(getCart())
document.querySelector('#count').innerText = getCart().length
}
function addSection(crn) {
const newCart = [...getCart(), crn]
localStorage.setItem('cart', JSON.stringify(newCart))
}
function removeSection(crn) {
const newCart = getCart().filter(c => c !== crn)
localStorage.setItem('cart', JSON.stringify(newCart))
}
callback(new Blob([arr], { type: type || 'image/png' }));
});
},
});
function writeLink(crn) {
const item = document.querySelector(`[data-crn="${crn}"]`)
if (!item) return
const icon = item.querySelector('.add-remove-link a i')
const link = item.querySelector('.add-remove-link a span')
if (getCart().includes(crn)) {
link.innerText = ' Remove from cart'
icon.className = 'fas fa-minus'
} else {
link.innerText = ' Add Section to Cart'
icon.className = 'fas fa-plus'
}
}
const elementFromString = string => {
const html = new DOMParser().parseFromString(string, 'text/html')
return html.body.firstChild
}
import React from 'react';
import ReactDOM from 'react-dom';
import Cart from 'src/Cart';
import QuickAdd from 'src/QuickAdd';
document.addEventListener('DOMContentLoaded', () => {
//const calendarUrl = `${window.location.protocol}//${window.location.hostname}${window.location.port == 3000 ? ':3000' : ''}/schedule`;
const calendarUrl = '/schedule';
ReactDOM.render(
<QuickAdd
loadCalendar={() => {
window.location.href = calendarUrl;
}}
/>,
document.getElementById('quick-add')
);
});
// /**
// * Either adds or removes a section from the cart depending on
// * if it is currently in the cart.
// */
import $ from 'jquery';
import Cart from 'src/Cart';
const addOrRemoveFromCart = async (event, sectionNode) => {
event && event.stopPropagation();
if (event.target.tagName === 'A') return;
const section = { ...sectionNode.dataset };
Cart.toggleCrn(section.crn);
const icon = $(sectionNode.querySelector('.add-remove-btn #icon'));
const text = sectionNode.querySelector('.add-remove-btn .text');
if (Cart.includesCrn(section.crn)) {
icon.addClass('fa-minus').removeClass('fa-plus');
text.innerText = 'Remove';
} else {
icon.addClass('fa-plus').removeClass('fa-minus');
text.innerText = 'Add';
}
};
const initSearchListeners = () => {
const sectionItems = Array.from(document.querySelectorAll('.section-item'));
sectionItems.forEach(item => {
item.onclick = event => addOrRemoveFromCart(event, item);
});
setTimeout(() => {
sectionItems.forEach(item => {
const icon = $(item.querySelector('.add-remove-btn #icon'));
const text = item.querySelector('.add-remove-btn .text');
if (Cart.includesCrn(item.dataset.crn)) {
icon.addClass('fa-minus').removeClass('fa-plus');
text.innerText = 'Remove';
} else {
icon.addClass('fa-plus').removeClass('fa-minus');
text.innerText = 'Add';
}
});
}, 100);
};
document.addEventListener('DOMContentLoaded', initSearchListeners);
import React from 'react';
import ReactDOM from 'react-dom';
import CalendarPage from 'src/CalendarPage';
import { saveAs } from 'file-saver'
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(<CalendarPage />, document.getElementById('root'));
});
const initPage = () => {
if (getCart().length != 0) {
document.getElementById('root').innerHTML = '<i class="fas fa-spinner fa-spin"></i>'
fetch(`/course_sections?crns=${getCart().join(',')}`)
.then(resp => resp.text())
.then(text => {
const tree = elementFromString(text)
const body = tree.querySelector('.page')
document.getElementById('root').innerHTML = body.innerHTML
setInitialLinks()
addListeners()
})
} else {
document.getElementById('root').innerHTML = 'Add classes to your cart to see them here!'
}
document.querySelector('#count').innerText = getCart().length
document.getElementById('add-to-system').addEventListener('click', () => {
window.open(
`webcal://${window.location.hostname}${
window.location.port === '3000' ? ':3000' : ''
}/api/schedules?crns=${getCart().join(',')}`
)
})
document.getElementById('download-ics').addEventListener('click', () => {
fetch(
`${window.location.protocol}//${window.location.hostname}${
window.location.port === '3000' ? ':3000' : ''
}/api/schedules?crns=${getCart().join(',')}`
)
.then(resp => resp.text())
.then(text => {
const blob = new Blob([text], { type: 'text/calendar;charset=utf-8' })
saveAs(blob, 'GMU Schedule.ics')
})
})
}
window.addEventListener('DOMContentLoaded', initPage)
function setInitialLinks() {
getCart().forEach(writeLink)
}
function addListeners() {
const links = Array.from(document.querySelectorAll('.add-section'))
for (const link of links) {
link.addEventListener('click', e => {
e.preventDefault()
const crn = link.dataset.crn
toggleSection(crn)
writeLink(crn)
})
}
}
function getCart() {
return JSON.parse(localStorage.getItem('cart') || '[]')
}
function toggleSection(crn) {
if (getCart().includes(crn)) {
removeSection(crn)
} else {
addSection(crn)
}
console.log(getCart())
document.querySelector('#count').innerText = getCart().length
}
function addSection(crn) {
const newCart = [...getCart(), crn]
localStorage.setItem('cart', JSON.stringify(newCart))