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'
gem 'puma', '~> 3.7'
# Use SCSS for stylesheets
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
gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use ActiveModel has_secure_password
......
......@@ -11,7 +11,7 @@
// about supported directives.
//
//= require rails-ujs
//= require turbolinks
// require turbolinks
//= require FileSaver
//= require_tree .
// require jquery3
......@@ -25,6 +25,7 @@ const elementFromString = string => {
document.addEventListener('DOMContentLoaded', () => {
this.cart = new Cart();
FontAwesome.dom.i2svg();
});
const setSemester = async select => {
......@@ -33,6 +34,7 @@ const setSemester = async select => {
};
/** Loads FontAwesome icons on load; fixes weird flickering */
document.addEventListener('turbolinks:load', () => {
FontAwesome.dom.i2svg();
});
FontAwesome.dom.watch({ observeMutationsRoot: document });
// document.addEventListener('turbolinks:load', () => {
// FontAwesome.dom.i2svg();
// });
class Cart {
constructor() {
this.isOpen = false;
this._courses = {}; // {title, id, sections: {id, crn}}
const cartData = document.querySelector('#cart-data');
const courses = Array.from(cartData.content.children);
for (const course of courses) {
const { id, title } = course.dataset;
const sections = Array.from(course.children).map(node => ({ ...node.dataset }));
this._courses[id] = { id, title, sections };
}
document.getElementById('course-counter').innerText = Object.keys(this._courses).length;
this._ids = Array.from(document.getElementById('cart-courses').children).map(e => e.dataset.crn);
}
get crns() {
return Object.keys(this._courses)
.map(cid => this._courses[cid].sections.map(s => s.crn))
.reduce((prev, curr) => [...prev, ...curr], []);
}
get ids() {
return Object.keys(this._courses)
.map(cid => this._courses[cid].sections.map(s => s.id))
.reduce((prev, curr) => [...prev, ...curr], []);
this._courses = {};
}
toggle() {
......@@ -42,111 +19,200 @@ class Cart {
this.isOpen = !this.isOpen;
}
addCourse(course) {
this._courses[course.id] = course;
const courseList = document.getElementById('cart-courses');
const courseNode = courseList.querySelector(`#schedule-${course.id}`);
const newNode = this._constructCourseNode(course);
if (courseNode !== null) courseList.replaceChild(newNode, courseNode);
else courseList.appendChild(newNode);
document.getElementById('course-counter').innerText = Object.keys(this._courses).length;
fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
}
removeCourse(id) {
const sectionIds = this._courses[id].sections.map(s => s.id);
for (const sectionId of sectionIds) {
const sectionCard = document.querySelector(`#section-${sectionId}`);
sectionCard && sectionCard.classList.remove('selected');
set courses(courses) {
this._courses = courses;
for (const courseId in this._courses) {
if (this._courses[courseId].length === 0) delete this._courses[courseId];
}
delete this._courses[id];
const courseList = document.getElementById('cart-courses');
const current = courseList.querySelector(`#schedule-${id}`);
courseList.removeChild(current);
console.log(courses);
document.getElementById('course-counter').innerText = Object.keys(this._courses).length;
fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
}
courseContainingSection(id) {
for (const courseId in this._courses) {
const course = this._courses[courseId];
for (const section of course.sections) {
if (section.id == id) return course;
}
}
return undefined;
async addSections(sections) {
const resp = await fetch(`/sessions/cart?course_id=${sections[0].cid}&section_ids=${sections.map(s => s.id).join(',')}`, { cache: 'no-store' });
const json = await resp.json();
this.courses = json;
}
includesSection(id) {
return !!this.courseContainingSection(id);
async addPair(sections) {
const resp = await fetch(`/sessions/cart?course_id=${sections[0].cid}&pair_ids=${sections[0].id},${sections[1].id}`, { cache: 'no-store' });
const json = await resp.json();
this.courses = json;
}
// section: { id, crn }
addSection(section) {
const course = this._courses[section.cid];
if (course) {
course.sections.push(section);
const courseNode = document.getElementById(`#schedule-${course.id}`);
const crnList = courseNode.querySelector('.crns');
crnList.innerText = course.sections.map(s => `#${s.crn}`);
fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
} else {
const courseCard = document.getElementById(`course-${section.cid}`);
const title = courseCard.querySelector('.title').innerText;
this.addCourse({ title, id: section.cid, sections: [section] });
}
}
includesPair(pair) {
const ids = pair.map(p => p.id);
for (const courseId in this._courses) {
const pairs = this._courses[courseId];
if (!Array.isArray(pairs[0])) continue;
removeSection(section) {
const course = this.courseContainingSection(section.id);
course.sections = course.sections.filter(s => s.id !== section.id);
const schedule = document.querySelector('#cart-courses');
const courseNode = schedule.querySelector(`#schedule-${course.id}`);
const crnList = courseNode.querySelector('.crns');
if (course.sections.length === 0) {
this.removeCourse(section.cid);
} else {
crnList.innerText = course.sections.map(s => `#${s.crn}`);
for (const otherPair of pairs) {
if (JSON.stringify(ids) == JSON.stringify(otherPair)) return true;
}
}
fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
return false;
}
async downloadIcs() {
const cal = await fetch(`/api/schedules?crns=${this.crns.join(',')}`);
const text = await cal.text();
var blob = new Blob([text], { type: 'text/calendar;charset=utf-8' });
saveAs(blob, 'test.ics');
}
async addToSystemCalendar() {
const url = `webcal://${window.location.hostname}/api/schedule?crns=${this.crns.join(',')}`;
window.open(url, '_self');
}
includesSection(obj) {
for (const key in this._courses) {
const list = this._courses[key];
if (list.includes(obj.id)) return true;
}
_constructCourseNode(course) {
let html = `<li id="schedule-${course.id}" class="list-group-item schedule-section-card" onclick="removeCourse(${course.id})">`;
html += `<div style="display: flex; justify-content: space-between;">`;
html += `<b style="min-width: 15%">${course.title}</b>`;
html += `<span class="crns" style="color: gray; font-size: 10pt;">`;
html += course.sections.map(s => `#${s.crn}`).join(', ');
html += `</span>`;
html += `</div>`;
html += `</li>`;
return elementFromString(html);
return false;
}
}
const removeCourse = id => {
this.cart.removeCourse(id);
};
// class Cart {
// constructor() {
// this.isOpen = false;
// this._courses = {}; {title, id, sections: {id, crn}}
// const cartData = document.getElementById('cart-data');
// const courses = Array.from(cartData.content.children);
// for (const course of courses) {
// const { id, title } = course.dataset;
// const sections = Array.from(course.children).map(node => ({ ...node.dataset }));
// this._courses[id] = { id, title, sections };
// }
// document.getElementById('course-counter').innerText = Object.keys(this._courses).length;
// this._ids = Array.from(document.getElementById('cart-courses').children).map(e => e.dataset.crn);
// }
// get crns() {
// return Object.keys(this._courses)
// .map(cid => this._courses[cid].sections.map(s => s.crn))
// .reduce((prev, curr) => [...prev, ...curr], []);
// }
// get ids() {
// return Object.keys(this._courses)
// .map(cid => this._courses[cid].sections.map(s => s.id))
// .reduce((prev, curr) => [...prev, ...curr], []);
// }
// 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;
// }
// addCourse(course) {
// this._courses[course.id] = course;
// const courseList = document.getElementById('cart-courses');
// const courseNode = courseList.querySelector(`#schedule-${course.id}`);
// const newNode = this._constructCourseNode(course);
// if (courseNode !== null) courseList.replaceChild(newNode, courseNode);
// else courseList.appendChild(newNode);
// document.getElementById('course-counter').innerText = Object.keys(this._courses).length;
// fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
// }
// removeCourse(id) {
// const sectionIds = this._courses[id].sections.map(s => s.id);
// for (const sectionId of sectionIds) {
// const sectionCard = document.querySelector(`#section-${sectionId}`);
// sectionCard && sectionCard.classList.remove('selected');
// }
// delete this._courses[id];
// const courseList = document.getElementById('cart-courses');
// const current = courseList.querySelector(`#schedule-${id}`);
// courseList.removeChild(current);
// document.getElementById('course-counter').innerText = Object.keys(this._courses).length;
// fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
// }
// courseContainingSection(id) {
// for (const courseId in this._courses) {
// const course = this._courses[courseId];
// for (const section of course.sections) {
// if (section.id == id) return course;
// }
// }
// return undefined;
// }
// includesSection(id) {
// return !!this.courseContainingSection(id);
// }
// section: { id, crn }
// addSection(section) {
// const course = this._courses[section.cid];
// if (course) {
// course.sections.push(section);
// const courseNode = document.getElementById(`schedule-${course.id}`);
// const crnList = courseNode.querySelector('.crns');
// crnList.innerText = course.sections.map(s => `#${s.crn}`);
// fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
// } else {
// const courseCard = document.getElementById(`course-${section.cid}`);
// const title = courseCard.querySelector('.title').innerText;
// this.addCourse({ title, id: section.cid, sections: [section] });
// }
// }
// removeSection(section) {
// const course = this.courseContainingSection(section.id);
// course.sections = course.sections.filter(s => s.id !== section.id);
// const schedule = document.querySelector('#cart-courses');
// const courseNode = schedule.querySelector(`#schedule-${course.id}`);
// const crnList = courseNode.querySelector('.crns');
// if (course.sections.length === 0) {
// this.removeCourse(section.cid);
// } else {
// crnList.innerText = course.sections.map(s => `#${s.crn}`);
// }
// fetch(`/sessions/update?section_ids=${this.ids.join(',')}`, { cache: 'no-store' });
// }
// async downloadIcs() {
// const cal = await fetch(`/api/schedules?crns=${this.crns.join(',')}`);
// const text = await cal.text();
// var blob = new Blob([text], { type: 'text/calendar;charset=utf-8' });
// saveAs(blob, 'test.ics');
// }
// async addToSystemCalendar() {
// const url = `webcal:${window.location.hostname}/api/schedule?crns=${this.crns.join(',')}`;
// window.open(url, '_self');
// }
// _constructCourseNode(course) {
// let html = `<li id="schedule-${course.id}" class="list-group-item schedule-section-card" onclick="removeCourse(${course.id})">`;
// html += `<div style="display: flex; justify-content: space-between;">`;
// html += `<b style="min-width: 15%">${course.title}</b>`;
// html += `<span class="crns" style="color: gray; font-size: 10pt;">`;
// html += course.sections.map(s => `#${s.crn}`).join(', ');
// html += `</span>`;
// html += `</div>`;
// html += `</li>`;
// return elementFromString(html);
// }
// }
// const removeCourse = id => {
// this.cart.removeCourse(id);
// };
......@@ -3,12 +3,39 @@ document.addEventListener('DOMContentLoaded', () => {
if (eventsTemplate) {
const eventsJSON = eventsTemplate.dataset.events;
const events = JSON.parse(eventsJSON);
window.events = events;
console.log(events);
$('#calendar').fullCalendar({
defaultDate: new Date(2019, 0, 14),
defaultView: 'agendaWeek',
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 @@
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 title = courseCard.querySelector('.title').innerText;
const sectionsItems = Array.from(courseCard.querySelectorAll('li'));
const sections = sectionsItems.map(li => ({ ...li.dataset }));
this.cart.addCourse({ title, id, sections });
sectionsItems.forEach(s => s.classList.add('selected'));
const filtered = sectionsItems.filter(li => {
return !li.parentNode.classList.contains('pair') || li.dataset.type === 'Lecture';
});
event.stopPropagation();
for (const section of filtered) {
await addOrRemoveFromCart(undefined, section);
}
};
/**
* Either adds or removes a section from the cart depending on
* if it is currently in the cart.
*/
const addOrRemoveFromCart = (event, sectionNode) => {
const addOrRemoveFromCart = async (event, sectionNode) => {
event && event.stopPropagation();
const section = { ...sectionNode.dataset };
if (this.cart.includesSection(section.id)) {
this.cart.removeSection(section);
sectionNode.classList.remove('selected');
const parent = sectionNode.parentNode;
if (parent.classList.contains('pair')) {
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 {
this.cart.addSection(section);
sectionNode.classList.add('selected');
await this.cart.addSections([section]);
if (this.cart.includesSection(section)) {
sectionNode.classList.add('selected');
} else {
sectionNode.classList.remove('selected');
}
}
event.stopPropagation();
};
/**
......@@ -49,7 +74,7 @@ const removeFromCart = section => {
*/
const toggleSections = course => {
const sections = course.querySelector('.sections');
console.log(sections);
if (sections.style.display === 'flex') {
sections.style.display = 'none';
} else {
......
require 'icalendar'
require 'time'
# Contains functionality for generating schedules.
class SchedulesController < ApplicationController
resource_description do
......@@ -16,35 +13,27 @@ class SchedulesController < ApplicationController
render plain: @schedule.to_ical # render a plaintext iCal file
end
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
include SchedulesHelper
def show
all_sections = @cart.values
# schedules = []
all_sections.each_with_index do |sections, i|
combined = {}
@cart.each do |cid, sections|
combined[cid] = []
sections.each do |section|
end
end
@events = @cart.map do |_cid, sections|
s = sections.first
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")
courses = @cart.values.group_by do |s|
s.course.id
end
{
title: s.name,
start: "#{formatted_date}T#{time}",
end: "#{formatted_date}T#{endtime}"
}
end
end.flatten
puts courses.keys
id_sets = generate_schedules(@cart.values)
@events = generate_fullcalender_events(id_sets)
end
# this works(?)
# recursively build a list of sets containing 1 section from each course chosen
end
......@@ -7,6 +7,50 @@ class SessionsController < ApplicationController
head :ok
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
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