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();
// });
This diff is collapsed.
......@@ -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
end
end
end
......@@ -12,6 +12,14 @@ class Course < ApplicationRecord
validates :subject, 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)
base_query.where("courses.subject = ?", subject.upcase)
end
......
......@@ -10,13 +10,32 @@ class CourseSection < ApplicationRecord
validates :title, presence: true
validates :course_id, presence: true
def is_lecture?
section_type == "Lecture"
end
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]
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
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
# Select all course sections that have an instructor that matches the given name
......
<!DOCTYPE html>
<html data-turbolinks="false">
<html>
<head>
<title>Schedules</title>
<%= csrf_meta_tags %>
......@@ -15,7 +15,7 @@
<%= stylesheet_link_tag 'application' %>
</head>
<body data-turbolinks="false">
<body>
<%= render partial: 'shared/navbar' %>
<%= yield %>
<%= 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>
<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 @@
<li id="schedule-<%= cid %>" class="list-group-item" onclick="removeCourse(<%= cid %>)">
<div class="cart-course">
<b class="title"><%= "#{course.subject} #{course.course_number}" %></b>
<span class="crns">
<%= sections.map { |s| "##{s.crn}" }.join(', ') %>
</span>
<!-- <span class="crns">
<%= sections.map { |s| "##{s.crn}" }.join(', ') %>
</span> -->
</div>
</li>
<% end %>
......
......@@ -47,8 +47,19 @@
<!-- List of Course Sections -->
<div class="list-group list-group-flush sections" style="display: <%= expanded ? "flex" : "none" %>">
<% if defined?(@instructor) %>
<%= render partial: 'shared/section', collection: course.course_sections.where(instructor: @instructor), locals: { course: course } %>
<% if course.has_labs? && !course.lab_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 %>
<%= render partial: 'shared/section', collection: course.course_sections, locals: { course: course } %>
<% end %>
......
......@@ -3,6 +3,7 @@
data-crn="<%= section.crn %>"
data-id="<%= section.id %>"
data-cid="<%= course.id %>"
data-type="<%= section.section_type %>"
onclick="addOrRemoveFromCart(event, this)"
>
<span style="float:left"><b class="subj"><%= "#{section.name}" %></b>: <%= section.title %></span>
......
......@@ -2,6 +2,7 @@
Rails.application.routes.draw do
get 'search', to: 'search#index'
get 'sessions/update', as: 'update_session'
get 'sessions/cart'
resources :instructors, only: [:index, :show]
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