Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
SRCT
schedules
Commits
b32595db
Commit
b32595db
authored
Oct 21, 2018
by
Zac Wood
Browse files
Use pairs if there are recitations/labs
parent
fa9ffff2
Changes
16
Hide whitespace changes
Inline
Side-by-side
schedules/Gemfile
View file @
b32595db
...
...
@@ -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
...
...
schedules/app/assets/javascripts/application.js
View file @
b32595db
...
...
@@ -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();
// });
schedules/app/assets/javascripts/cart.js
View file @
b32595db
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
}
§ion_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
;
}
}
f
et
ch
(
`/sessions/update?section_ids=
${
this
.
ids
.
join
(
'
,
'
)}
`
,
{
cache
:
'
no-store
'
})
;
r
et
urn
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);
// };
schedules/app/assets/javascripts/schedules.js
View file @
b32595db
...
...
@@ -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
:
e
vents
,
events
:
renderE
vents
,
});
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
]);
};
schedules/app/assets/javascripts/search.js
View file @
b32595db
...
...
@@ -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
{
...
...
schedules/app/controllers/schedules_controller.rb
View file @
b32595db
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
schedules/app/controllers/sessions_controller.rb
View file @
b32595db
...
...
@@ -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
)
}