Commit b32595db authored by Zac Wood's avatar Zac Wood

Use pairs if there are recitations/labs

parent fa9ffff2
...@@ -13,10 +13,9 @@ gem 'sqlite3' ...@@ -13,10 +13,9 @@ gem 'sqlite3'
gem 'puma', '~> 3.7' gem 'puma', '~> 3.7'
# Use SCSS for stylesheets # Use SCSS for stylesheets
gem 'sass-rails', '~> 5.0' gem 'sass-rails', '~> 5.0'
# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks
gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5' gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production # Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0' # gem 'redis', '~> 4.0'
# Use ActiveModel has_secure_password # Use ActiveModel has_secure_password
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
// about supported directives. // about supported directives.
// //
//= require rails-ujs //= require rails-ujs
//= require turbolinks // require turbolinks
//= require FileSaver //= require FileSaver
//= require_tree . //= require_tree .
// require jquery3 // require jquery3
...@@ -25,6 +25,7 @@ const elementFromString = string => { ...@@ -25,6 +25,7 @@ const elementFromString = string => {
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
this.cart = new Cart(); this.cart = new Cart();
FontAwesome.dom.i2svg();
}); });
const setSemester = async select => { const setSemester = async select => {
...@@ -33,6 +34,7 @@ const setSemester = async select => { ...@@ -33,6 +34,7 @@ const setSemester = async select => {
}; };
/** Loads FontAwesome icons on load; fixes weird flickering */ /** Loads FontAwesome icons on load; fixes weird flickering */
document.addEventListener('turbolinks:load', () => { FontAwesome.dom.watch({ observeMutationsRoot: document });
FontAwesome.dom.i2svg(); // document.addEventListener('turbolinks:load', () => {
}); // FontAwesome.dom.i2svg();
// });
This diff is collapsed.
...@@ -3,12 +3,39 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -3,12 +3,39 @@ document.addEventListener('DOMContentLoaded', () => {
if (eventsTemplate) { if (eventsTemplate) {
const eventsJSON = eventsTemplate.dataset.events; const eventsJSON = eventsTemplate.dataset.events;
const events = JSON.parse(eventsJSON); const events = JSON.parse(eventsJSON);
window.events = events;
console.log(events); console.log(events);
$('#calendar').fullCalendar({ $('#calendar').fullCalendar({
defaultDate: new Date(2019, 0, 14), defaultDate: new Date(2019, 0, 14),
defaultView: 'agendaWeek', defaultView: 'agendaWeek',
header: false, header: false,
events: events, events: renderEvents,
}); });
document.getElementById('numSchedules').innerText = window.events.length;
} }
}); });
let i = 0;
const renderEvents = (start, end, timezone, callback) => {
console.log(window.events[i]);
document.getElementById('currentSchedule').innerText = i + 1;
callback(window.events[i]);
};
const nextSchedule = () => {
if (i + 1 < window.events.length) i++;
$('#calendar').fullCalendar('refetchEvents');
console.log(window.events[i]);
};
const prevSchedule = () => {
if (i > 0) i--;
$('#calendar').fullCalendar('refetchEvents');
console.log(window.events[i]);
};
...@@ -3,33 +3,58 @@ ...@@ -3,33 +3,58 @@
const sectionWithCrn = crn => document.getElementById('search-list').querySelector(`[data-crn="${crn}"]`); const sectionWithCrn = crn => document.getElementById('search-list').querySelector(`[data-crn="${crn}"]`);
const addCourse = (event, id) => { const addCourse = async (event, id) => {
event.stopPropagation();
const courseCard = document.getElementById(`course-${id}`); const courseCard = document.getElementById(`course-${id}`);
const title = courseCard.querySelector('.title').innerText; const title = courseCard.querySelector('.title').innerText;
const sectionsItems = Array.from(courseCard.querySelectorAll('li')); const sectionsItems = Array.from(courseCard.querySelectorAll('li'));
const sections = sectionsItems.map(li => ({ ...li.dataset })); const filtered = sectionsItems.filter(li => {
return !li.parentNode.classList.contains('pair') || li.dataset.type === 'Lecture';
this.cart.addCourse({ title, id, sections }); });
sectionsItems.forEach(s => s.classList.add('selected'));
event.stopPropagation(); for (const section of filtered) {
await addOrRemoveFromCart(undefined, section);
}
}; };
/** /**
* Either adds or removes a section from the cart depending on * Either adds or removes a section from the cart depending on
* if it is currently in the cart. * if it is currently in the cart.
*/ */
const addOrRemoveFromCart = (event, sectionNode) => { const addOrRemoveFromCart = async (event, sectionNode) => {
event && event.stopPropagation();
const section = { ...sectionNode.dataset }; const section = { ...sectionNode.dataset };
if (this.cart.includesSection(section.id)) { const parent = sectionNode.parentNode;
this.cart.removeSection(section); if (parent.classList.contains('pair')) {
sectionNode.classList.remove('selected'); const otherNode = Array.from(parent.children).filter(c => c != sectionNode)[0];
const other = { ...otherNode.dataset };
let pair;
if (section.type == 'Lecture') {
pair = [section, other];
await this.cart.addPair(pair);
} else {
pair = [other, section];
await this.cart.addPair(pair);
}
if (this.cart.includesPair(pair)) {
console.log('found');
[sectionNode, otherNode].forEach(s => s.classList.add('selected'));
} else {
console.log('not found');
[sectionNode, otherNode].forEach(s => s.classList.remove('selected'));
}
} else { } else {
this.cart.addSection(section); await this.cart.addSections([section]);
sectionNode.classList.add('selected'); if (this.cart.includesSection(section)) {
sectionNode.classList.add('selected');
} else {
sectionNode.classList.remove('selected');
}
} }
event.stopPropagation();
}; };
/** /**
...@@ -49,7 +74,7 @@ const removeFromCart = section => { ...@@ -49,7 +74,7 @@ const removeFromCart = section => {
*/ */
const toggleSections = course => { const toggleSections = course => {
const sections = course.querySelector('.sections'); const sections = course.querySelector('.sections');
console.log(sections);
if (sections.style.display === 'flex') { if (sections.style.display === 'flex') {
sections.style.display = 'none'; sections.style.display = 'none';
} else { } else {
......
require 'icalendar'
require 'time'
# Contains functionality for generating schedules. # Contains functionality for generating schedules.
class SchedulesController < ApplicationController class SchedulesController < ApplicationController
resource_description do resource_description do
...@@ -16,35 +13,27 @@ class SchedulesController < ApplicationController ...@@ -16,35 +13,27 @@ class SchedulesController < ApplicationController
render plain: @schedule.to_ical # render a plaintext iCal file render plain: @schedule.to_ical # render a plaintext iCal file
end end
DAYS = { include SchedulesHelper
"M": Date.new(2019, 1, 14),
"T": Date.new(2019, 1, 15),
"W": Date.new(2019, 1, 16),
"R": Date.new(2019, 1, 17),
"F": Date.new(2019, 1, 18),
"S": Date.new(2019, 1, 19),
"U": Date.new(2019, 1, 20)
}.freeze
def show def show
all_sections = @cart.values combined = {}
# schedules = [] @cart.each do |cid, sections|
all_sections.each_with_index do |sections, i| combined[cid] = []
sections.each do |section|
end
end end
@events = @cart.map do |_cid, sections|
s = sections.first
s.days.split('').map do |day| courses = @cart.values.group_by do |s|
formatted_date = DAYS[day.to_sym].to_s.tr('-', '') s.course.id
time = Time.parse(s.start_time).strftime("%H%M%S") end
endtime = Time.parse(s.end_time).strftime("%H%M%S")
{ puts courses.keys
title: s.name,
start: "#{formatted_date}T#{time}", id_sets = generate_schedules(@cart.values)
end: "#{formatted_date}T#{endtime}" @events = generate_fullcalender_events(id_sets)
}
end
end.flatten
end end
# this works(?)
# recursively build a list of sets containing 1 section from each course chosen
end end
...@@ -7,6 +7,50 @@ class SessionsController < ApplicationController ...@@ -7,6 +7,50 @@ class SessionsController < ApplicationController
head :ok head :ok
end end
def cart
cart = if cookies[:cart].nil?
{}
else
JSON.parse cookies[:cart]
end
course_id, section_ids, pair_ids = params[:course_id], params[:section_ids], params[:pair_ids]
cart[course_id] ||= []
unless section_ids.nil?
ids = section_ids.split(',')
ids.each do |section_id|
if cart[course_id].include?(section_id)
cart[course_id] = cart[course_id].reject do |a|
a == section_id
end
else
cart[course_id].push(section_id)
end
end
end
unless pair_ids.nil?
pair = pair_ids.split(',')
if cart[course_id].include?(pair)
cart[course_id] = cart[course_id].reject do |a|
a == pair
end
else
cart[course_id].push(pair)
end
end
to_delete = cart.keys.select do |cid|
cart[cid].empty?
end
to_delete.each { |key| cart.delete(key) }
cookies[:cart] = cart.to_json
render json: cart.to_json
end
private private
def update_cookie(sym) def update_cookie(sym)
......
module SchedulesHelper
DAYS = {
"M": Date.new(2019, 1, 14),
"T": Date.new(2019, 1, 15),
"W": Date.new(2019, 1, 16),
"R": Date.new(2019, 1, 17),
"F": Date.new(2019, 1, 18),
"S": Date.new(2019, 1, 19),
"U": Date.new(2019, 1, 20)
}.freeze
def generate_fullcalender_events(id_sets)
id_sets.map do |id_set|
id_set.to_a.map do |s|
s.days.split('').map do |day|
formatted_date = DAYS[day.to_sym].to_s.tr('-', '')
time = Time.parse(s.start_time).strftime("%H%M%S")
endtime = Time.parse(s.end_time).strftime("%H%M%S")
{
title: s.name,
start: "#{formatted_date}T#{time}",
end: "#{formatted_date}T#{endtime}"
}
end
end.flatten
end
end
def generate_schedules(all_sections)
recur_build(all_sections, 0, Set.new).flatten!.select do |s|
s.to_a.size == all_sections.count
end
end
def recur_build(all_sections, i, set)
num_courses = all_sections.count
course_sections = all_sections[i]
course_sections.map do |section|
new_set = Set.new(set)
fits = true
set.to_a.each do |s|
fits = !section.overlaps?(s)
break if !fits
end
new_set << section if fits
if i == num_courses - 1
new_set
else
recur_build(all_sections, i + 1, new_set)
end
end
end
end
...@@ -12,6 +12,14 @@ class Course < ApplicationRecord ...@@ -12,6 +12,14 @@ class Course < ApplicationRecord
validates :subject, presence: true validates :subject, presence: true
validates :semester_id, presence: true validates :semester_id, presence: true
def has_labs?
course_sections.reject(&:is_lecture?).count.positive?
end
def lab_course?
course_sections.select(&:is_lecture?).count.zero?
end
def self.from_subject(base_query, subject) def self.from_subject(base_query, subject)
base_query.where("courses.subject = ?", subject.upcase) base_query.where("courses.subject = ?", subject.upcase)
end end
......
...@@ -10,13 +10,32 @@ class CourseSection < ApplicationRecord ...@@ -10,13 +10,32 @@ class CourseSection < ApplicationRecord
validates :title, presence: true validates :title, presence: true
validates :course_id, presence: true validates :course_id, presence: true
def is_lecture?
section_type == "Lecture"
end
def labs def labs
return nil unless section_type == "Lecture" return nil unless is_lecture?
# Lectures have names formatted like "MATH 214 001"
# Labs/recitations have the title format "Recitation for Lecture 001"
# so, match all the sections in the same course which have the same number
# as the last element of their titles
lecture_number = name.split[name.split.length - 1] lecture_number = name.split[name.split.length - 1]
course.course_sections.select do |s| labs_for_section = course.course_sections.select do |s|
s.title.split[s.title.split.length - 1] == lecture_number s.title.split[s.title.split.length - 1] == lecture_number
end end
labs_for_section.map do |lab|
[self, lab]
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)
(t1_start <= t2_end && t2_start <= t1_end) && Set.new(days.split).intersect?(Set.new(other.days.split))
end end
# Select all course sections that have an instructor that matches the given name # Select all course sections that have an instructor that matches the given name
......
<!DOCTYPE html> <!DOCTYPE html>
<html data-turbolinks="false"> <html>
<head> <head>
<title>Schedules</title> <title>Schedules</title>
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
<%= stylesheet_link_tag 'application' %> <%= stylesheet_link_tag 'application' %>
</head> </head>
<body data-turbolinks="false"> <body>
<%= render partial: 'shared/navbar' %> <%= render partial: 'shared/navbar' %>
<%= yield %> <%= yield %>
<%= render partial: 'shared/cart'%> <%= render partial: 'shared/cart'%>
......
<button onclick="nextSchedule()">Next</button>
<button onclick="prevSchedule()">Prev</button>
<span id="currentSchedule">1</span> / <span id="numSchedules">1</span>
<div id="calendar"></div> <div id="calendar"></div>
<template id="events" data-events="<%= @events.to_json %>"></template> <template id="events" data-events="<%= @events.to_json %>"></template>
<script>
/* const cal = document.querySelector('#calendar');
* var calendar = new Calendar(cal, {
* defaultView: 'agendaWeek'
* }); */
</script>
...@@ -12,9 +12,9 @@ ...@@ -12,9 +12,9 @@
<li id="schedule-<%= cid %>" class="list-group-item" onclick="removeCourse(<%= cid %>)"> <li id="schedule-<%= cid %>" class="list-group-item" onclick="removeCourse(<%= cid %>)">
<div class="cart-course"> <div class="cart-course">
<b class="title"><%= "#{course.subject} #{course.course_number}" %></b> <b class="title"><%= "#{course.subject} #{course.course_number}" %></b>
<span class="crns"> <!-- <span class="crns">
<%= sections.map { |s| "##{s.crn}" }.join(', ') %> <%= sections.map { |s| "##{s.crn}" }.join(', ') %>
</span> </span> -->
</div> </div>
</li> </li>
<% end %> <% end %>
......
...@@ -47,8 +47,19 @@ ...@@ -47,8 +47,19 @@
<!-- List of Course Sections --> <!-- List of Course Sections -->
<div class="list-group list-group-flush sections" style="display: <%= expanded ? "flex" : "none" %>"> <div class="list-group list-group-flush sections" style="display: <%= expanded ? "flex" : "none" %>">
<% if defined?(@instructor) %> <% if course.has_labs? && !course.lab_course? %>
<%= render partial: 'shared/section', collection: course.course_sections.where(instructor: @instructor), locals: { course: course } %> <% course.course_sections.each do |section| %>
<% lecture_list = section.labs %>
<% unless lecture_list.nil? %>
<% lecture_list.each do |list| %>
<div class="pair">
<%= render partial: 'shared/section', object: list.first, locals: { course: course } %>
<%= render partial: 'shared/section', object: list.last, locals: { course: course } %>
</div>
<hr />
<% end %>
<% end %>
<% end %>
<% else %> <% else %>
<%= render partial: 'shared/section', collection: course.course_sections, locals: { course: course } %> <%= render partial: 'shared/section', collection: course.course_sections, locals: { course: course } %>
<% end %> <% end %>
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
data-crn="<%= section.crn %>" data-crn="<%= section.crn %>"
data-id="<%= section.id %>" data-id="<%= section.id %>"
data-cid="<%= course.id %>" data-cid="<%= course.id %>"
data-type="<%= section.section_type %>"
onclick="addOrRemoveFromCart(event, this)" onclick="addOrRemoveFromCart(event, this)"
> >
<span style="float:left"><b class="subj"><%= "#{section.name}" %></b>: <%= section.title %></span> <span style="float:left"><b class="subj"><%= "#{section.name}" %></b>: <%= section.title %></span>
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
Rails.application.routes.draw do Rails.application.routes.draw do
get 'search', to: 'search#index' get 'search', to: 'search#index'
get 'sessions/update', as: 'update_session' get 'sessions/update', as: 'update_session'
get 'sessions/cart'
resources :instructors, only: [:index, :show] resources :instructors, only: [:index, :show]
get 'schedule', to: 'schedules#show', as: 'schedule' get 'schedule', to: 'schedules#show', as: 'schedule'
......
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