Commit 257bea38 authored by Zac Wood's avatar Zac Wood
Browse files

Add share to calendar, record time of each update, better closures

parent b7bac51a
Pipeline #4339 failed with stage
in 3 minutes and 27 seconds
......@@ -32,3 +32,22 @@ 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);
}
callback(new Blob([arr], { type: type || 'image/png' }));
});
},
});
}
import React from 'react';
import ReactDOM from 'react-dom';
import Cart from 'src/Cart';
import { saveAs } from 'file-saver';
import html2canvas from 'html2canvas';
import $ from 'jquery';
import CalendarPage from 'src/CalendarPage';
document.addEventListener('DOMContentLoaded', () => {
ReactDOM.render(<CalendarPage />, document.getElementById('root'));
});
const downloadIcs = async () => {
const response = await fetch(`${window.location.protocol}//${window.location.hostname}/api/schedules?crns=${Cart._courses.join(',')}`);
const text = await response.text();
const blob = new Blob([text], { type: 'text/calendar;charset=utf-8' });
saveAs(blob, 'GMU Schedule.ics');
};
const addToSystemCalendar = () => {
window.open(`webcal://${window.location.hostname}/api/schedules?crns=${Cart._courses.join(',')}`);
};
const saveImage = () => {
html2canvas(document.querySelector('#calendar')).then(canvas => {
canvas.toBlob(blob => {
saveAs(blob, 'GMU Schedule.png');
});
});
};
const initListeners = () => {
const items = Array.from(document.querySelectorAll('.section-item'));
items.forEach(item => (item.onclick = () => remove(item)));
// document.getElementById('open-modal-btn').onclick = setUrlInModal;
document.getElementById('download-ics').onclick = downloadIcs;
document.getElementById('add-to-system').onclick = addToSystemCalendar;
document.getElementById('save-image').onclick = saveImage;
document.getElementById('share-url').innerText = `${window.location.protocol}//${window.location.hostname}/schedule/view?crns=${Cart._courses.join(',')}`;
document.getElementById('share-url').href = `${window.location.protocol}//${window.location.hostname}/schedule/view?crns=${Cart._courses.join(',')}`;
};
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);
}
callback(new Blob([arr], { type: type || 'image/png' }));
});
},
});
}
import Cart from 'src/Cart';
import { saveAs } from 'file-saver';
import html2canvas from 'html2canvas';
import $ from 'jquery';
import 'fullcalendar';
import 'moment';
import 'url-polyfill';
const params = new URLSearchParams(document.location.search);
const crns = params.get('crns');
import React from 'react';
import ReactDOM from 'react-dom';
import ViewCalendarPage from 'src/ViewCalendarPage';
document.addEventListener('DOMContentLoaded', () => {
const eventsTemplate = document.querySelector('#events');
if (eventsTemplate) {
const eventsJSON = eventsTemplate.dataset.events;
const events = JSON.parse(eventsJSON);
window.events = events;
$('#calendar').fullCalendar({
defaultDate: new Date(2019, 0, 14),
defaultView: 'agendaWeek',
header: false,
events: renderEvents,
columnHeaderFormat: 'dddd',
allDaySlot: false,
});
}
initListeners();
ReactDOM.render(<ViewCalendarPage />, document.getElementById('root'));
});
const renderEvents = (start, end, timezone, callback) => {
callback(window.events);
};
/**
* Generates a URL for the current sections in the schedule
* and sets the link in the modal to it.
*/
const setUrlInModal = () => {
document.getElementById('calendar-link').innerText = `${window.location.protocol}//${window.location.hostname}/api/schedules?crns=${crns}`;
};
const downloadIcs = async () => {
const response = await fetch(`${window.location.protocol}//${window.location.hostname}/api/schedules?crns=${crns}`);
const text = await response.text();
const blob = new Blob([text], { type: 'text/calendar;charset=utf-8' });
saveAs(blob, 'GMU Schedule.ics');
};
const addToSystemCalendar = () => {
window.open(`webcal://${window.location.hostname}/api/schedules?crns=${crns}`);
};
const saveImage = () => {
html2canvas(document.querySelector('#calendar')).then(canvas => {
canvas.toBlob(blob => {
saveAs(blob, 'GMU Schedule.png');
});
});
};
const initListeners = () => {
// document.getElementById('open-modal-btn').onclick = setUrlInModal;
document.getElementById('download-ics').onclick = downloadIcs;
document.getElementById('add-to-system').onclick = addToSystemCalendar;
document.getElementById('save-image').onclick = saveImage;
};
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);
}
callback(new Blob([arr], { type: type || 'image/png' }));
});
},
});
}
......@@ -3,6 +3,7 @@ import Calendar from 'src/Calendar';
import Cart from 'src/Cart';
import SectionList from 'src/SectionList';
import ExportModal from 'src/ExportModal';
import ShareModal from 'src/ShareModal';
import QuickAdd from 'src/QuickAdd';
import moment from 'moment';
......@@ -51,7 +52,7 @@ export default class CalendarPage extends React.Component {
</div>
<div className="col-4">
<div className="d-flex justify-content-center">
<button className="btn btn-lg btn-secondary">
<button className="btn btn-lg btn-secondary" data-toggle="modal" data-target="#shareModal">
<i className="fas fa-share-square mr-2" />
Share
</button>
......@@ -70,6 +71,9 @@ export default class CalendarPage extends React.Component {
<SectionList onClick={this.toggleSection} sections={this.state.sections} expanded={true} />
<QuickAdd loadCalendar={this.loadEvents} />
<ExportModal />
<ShareModal
link={`${window.location.protocol}//${window.location.hostname}${window.location.port === '3000' ? ':3000' : ''}/schedule/view?crns=${Cart.crns.join(',')}`}
/>
</div>
);
}
......
import React from 'react';
import Cart from 'src/Cart';
export default class extends React.Component {
render() {
return (
<div className="modal fade" id="exportModal" tabindex="-1" role="dialog" aria-labelledby="exportModalLabel" aria-hidden="true">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title" id="exportModalLabel">
Your calendar has been generated! <br /> (Click on the options below to see further instructions)
</h5>
<button type="button" className="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div className="modal-body">
<button type="button" className="btn-variant" data-toggle="collapse" data-target="#apple-info">
{' '}
<h5> Apple Calendar </h5>{' '}
</button>
<div id="apple-info" className="collapse">
To add your schedule to Apple Calendar, click the "Add to calendar" button below. If you are on a device running macOS or iOS, this will open a
dialogue which will walk you through adding the calendar.
</div>
<hr />
<button type="button" className="btn-variant" data-toggle="collapse" data-target="#google-info">
{' '}
<h5>Google Calendar</h5>{' '}
</button>
<div id="google-info" className="collapse">
<strong>On desktop:</strong>
<br />
First, download the calendar file using the "Download calendar file" below. Open your{' '}
<a href="https://calendar.google.com/" target="_blank">
Google Calendar
</a>
. Click the "Settings" button in the top right, and then click the Settings tab. In the menu on the left, click "Import & export" and "Import". Now,
upload the calendar file you downloaded and click "Import".
<br />
<strong>On mobile (Android only):</strong>
<br />
Click the "Download calendar file" button. This will download the calendar file which you may then open and add to your calendar.
<br />
</div>
<hr />
<button type="button" className="btn-variant" data-toggle="collapse" data-target="#outlook-info">
{' '}
<h5>Outlook Calendar</h5>{' '}
</button>
<div id="outlook-info" className="collapse">
<button type="button" className="btn-variant" data-toggle="collapse" data-target="#outlook-desktop">
{' '}
<strong>On desktop (Windows):</strong>{' '}
</button>
<br />
<div id="outlook-desktop" className="collapse">
First, download the calendar file using the “Download calendar file” button below. In Outlook, choose File, then Open and Export, and then
Import/Export. In the Import and Export Wizard Box, choose “Import and iCalendar (.ics) or vCalendar file (.vcs)” and the “next” button. Search
for the button you downloaded in the beginning. Click “Okay” and then “Import.”
</div>
<br />
<button type="button" className="btn-variant" data-toggle="collapse" data-target="#outlook-mac">
{' '}
<strong>On desktop (Mac):</strong>{' '}
</button>
<br />
<div id="outlook-mac" className="collapse">
First, download the calendar file using the “Download calendar file” button below. Open Outlook and make sure the calendar in which you want to
import the file into has a checkmark next to it. Alternatively, you can add it into a new calendar by clicking the “Organize” tab and then the
“New Calendar” button. Double click the new Calendar to rename it. Open the Finder application and search for the file you downloaded in the
beginning. Then, drag and drop the file into the desired Calendar area.
</div>
<br />
<button type="button" className="btn-variant" data-toggle="collapse" data-target="#outlook-classNameic">
{' '}
<strong>Outlook Online (ClassNameic Layout)</strong>{' '}
</button>
<br />
<div id="outlook-classNameic" className="collapse">
To check if you are using the ClassNameic Layout, look in the top right and see if “The new Outlook” bar is slid to the left. If it is not, you
may consider reading “The New Outlook Layout” instructions or clicking the bar to slide it to the left. First, download the calendar file using
the “Download calendar file” button below. Login onto your{' '}
<a href="https://outlook.live.com/owa/" target="blank">
Outlook
</a>{' '}
and click the calendar icon on the bottom left. On the menu bar, located above the Calendar, choose the “Add Calendar” menu. From the drop down
menu, click import from file and browse for the calendar file you downloaded in the beginning. Click the save icon, then the calendar will be
imported.
</div>
<br />
<button type="button" className="btn-variant" data-toggle="collapse" data-target="#outlook-new">
{' '}
<strong>Outlook Online (New Outlook Layout)</strong>{' '}
</button>
<br />
<div id="outlook-new" className="collapse">
To check if you are using the New Outlook Layout, look in the top right and see if “The new Outlook” bar is slid to the right. If it is not, you
may consider reading the “ClassNameic Layout” instructions or clicking the bar to slide it to the right. First download the calendar file using
the “Download calendar file” button below. Login onto your{' '}
<a href="https://outlook.live.com/owa/" target="blank">
Outlook
</a>{' '}
and click the calendar icon on the bottom left. On the left side bar, under “Calendars”, click the “Discover calendars” button. Choose on the
“From File” menu under the “Import” Section. Then click the browse button and search for the file you downloaded in the beginning. Lastly,
choose “Import” and your calendar will be displayed.
</div>
</div>
<br />
<hr />
<h5>.ics file</h5>
To download a .ics file containing your schedule, click the "Download calendar file" button below.
</div>
<div className="modal-footer flex">
<button id="download-ics" type="button" className="btn btn-secondary" onClick={this.downloadIcs}>
Download calendar file
</button>
<button id="add-to-system" type="button" className="btn btn-primary" onClick={this.addToSystemCalendar}>
Add to system calendar
</button>
</div>
</div>
</div>
</div>
);
}
downloadIcs = async () => {
const response = await fetch(
`${window.location.protocol}//${window.location.hostname}${window.location.port === '3000' ? ':3000' : ''}/api/schedules?crns=${Cart.crns.join(',')}`
);
const text = await response.text();
const blob = new Blob([text], { type: 'text/calendar;charset=utf-8' });
saveAs(blob, 'GMU Schedule.ics');
};
addToSystemCalendar = () => {
window.open(`webcal://${window.location.hostname}${window.location.port === '3000' ? ':3000' : ''}/api/schedules?crns=${Cart.crns.join(',')}`);
};
}
......@@ -20,7 +20,7 @@ export default class Section extends React.Component {
};
render() {
const { name, title, crn, instructor_name, instructor_url, teaching_rating, location, days, start_time, end_time } = this.props;
const { name, title, crn, readOnly, instructor_name, instructor_url, teaching_rating, location, days, start_time, end_time } = this.props;
const { inCart } = this.state;
const percent = teaching_rating ? <Stars percent={(teaching_rating[0] / 5) * 100} /> : null;
......@@ -49,8 +49,12 @@ export default class Section extends React.Component {
{crn})
</em>
</p>
{!readOnly && (
<div>
{remove}
{add}
</div>
)}
<i className="fas fa-chalkboard-teacher" />{' '}
{instructor_name !== 'TBA' ? (
<span>
......
......@@ -14,7 +14,7 @@ export default class SectionList extends React.Component {
{this.props.expanded ? (
<div className="d-flex list-group list-group-flush sections">
{this.props.sections.map(section => (
<Section key={section.id} onClick={this.props.onClick} {...section} />
<Section key={section.id} onClick={this.props.onClick} readOnly={this.props.readOnly} {...section} />
))}
</div>
) : (
......
import React from 'react';
const ShareModal = props => {
return (
<div className="modal fade" id="shareModal" tabindex="-1" role="dialog" aria-labelledby="shareModalLabel" aria-hidden="true">
<div className="modal-dialog" role="document">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Share your schedule!</h5>
</div>
<div className="modal-body">
The following link contains a copy of your schedule: <br />
<a id="shareLink" href={props.link}>
{props.link}
</a>
</div>
</div>
</div>
</div>
);
};
export default ShareModal;
import React from 'react';
import Calendar from 'src/Calendar';
import Cart from 'src/Cart';
import SectionList from 'src/SectionList';
import ExportModal from 'src/ExportModal';
import QuickAdd from 'src/QuickAdd';
import moment from 'moment';
import 'url-polyfill';
const params = new URLSearchParams(document.location.search);
const crns = params.get('crns');
export default class CalendarPage extends React.Component {
state = { events: [], sections: [] };
constructor(props) {
super(props);
this.loadEvents();
}
loadEvents = async () => {
const response = await fetch(`/schedule/events?crns=${crns}`);
const json = await response.json();
this.setState({ ...json });
};
events = () => {
return this.state.events.map(e => ({ ...e, start: moment(e.start).toDate(), end: moment(e.end).toDate() }));
};
render() {
return (
<div className="container">
<Calendar events={this.events()} />
{this.state.sections.length > 0 ? (
<div className="d-flex justify-content-between align-items-end">
<h2 className="mt-4">Schedule</h2>{' '}
</div>
) : null}
<SectionList readOnly={true} sections={this.state.sections} expanded={true} />
</div>
);
}
}
......@@ -10,7 +10,7 @@ class Course < ApplicationRecord
"#{subject} #{course_number}"
end
def rating(question = 1, sections = self.course_sections)
def rating(question = 1, sections = course_sections)
total = 0
resp = 0
sections.each do |s|
......
module Update
FILE_NAME = "db/data/last_update.txt".freeze
def self.last_update_date
begin
File.open(FILE_NAME).first
rescue
"Data has not yet been loaded."
end
end
def self.new_update
File.write(FILE_NAME, Time.now.strftime("%Y-%m-%d %k:%M:%S"))
end
end
......@@ -2,7 +2,7 @@
<h1><i class="fas fa-calendar-alt"></i>&nbsp;SRCT Schedules</h1>
<p class="lead">Version 3.0</p>
<hr />
Last updated: 2:00am, 4/14/19
Last updated: <%= Update::last_update_date %>
</div>
<h3>Thank you to our contributors who make Schedules possible!</h3>
......
<%= javascript_pack_tag 'schedules_view' %>
<%= stylesheet_link_tag 'schedules' %>
<%= stylesheet_link_tag 'fullcalendar.min' %>
<button id="open-modal-btn" type="button" class="btn btn-primary" data-toggle="modal" data-target="#exportModal">
Export Schedule
</button>
<button id="save-image" class="btn btn-secondary" onclick="saveImage()">Save Image</button>
<div id="calendar"></div>
<template id="events" data-events="<%= @events.to_json %>"></template>
<hr />
<h2>Selected Courses</h2>
<%= render partial: 'shared/section', collection: @all, locals: { editable: false } %>
<!-- Export Modal -->
<div class="modal fade" id="exportModal" tabindex="-1" role="dialog" aria-labelledby="exportModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exportModalLabel">Your calendar has been generated!</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<h5>Apple Calendar</h5>
To add your schedule to Apple Calendar, click the "Add to calendar" button below. If you are on a device
running macOS or iOS, this will open a dialogue which will walk you through adding the calendar.
<hr />
<h5>Google Calendar</h5>
<strong>On desktop:</strong>
<br />
First, download the calendar file using the "Download calendar file" below. Open your <a href="https://calendar.google.com/" target="_blank">Google Calendar</a>. Click the "Settings" button in the top
right, and then click the Settings tab. In the menu on the left, click "Import & export" and "Import". Now, upload the calendar file you downloaded and click "Import".
<br />
<strong>On mobile (Android only):</strong>
<br />
Click the "Download calendar file" button. This will download the calendar file which you may then open and
add to your calendar.
<hr />
<h5>.ics file</h5>
To download a .ics file containing your schedule, click the "Download calendar file" button below.
</div>
<div class="modal-footer flex">
<button id="download-ics" type="button" class="btn btn-secondary">Download calendar file</button>
<button id="add-to-system" type="button" class="btn btn-primary">Add to system calendar</button>
</div>
</div>
</div>
</div>
<div id="root"></div>
......@@ -98,13 +98,13 @@ def wipe_db
end
def load_closures
semesters = YAML.load_file("db/closures.yaml")
semesters = YAML.load_file("db/data/closures.yaml")
semesters.each do |semester, dates|
season, year = semester.split
s = Semester.find_by(season: season, year: year)
next if s.nil?
dates.each do |date|
Closure.create!(date: Date.strptime(date, "%Y-%m-%d"), semester: s)
Closure.find_or_create_by!(date: Date.strptime(date, "%Y-%m-%d"), semester: s)
end
end
end
......@@ -118,7 +118,7 @@ def main
[parser.parse_semesters.first]
else
# expand to include however many semesters you want
parser.parse_semesters[1..7]
parser.parse_semesters[0..7]
end
puts "\tParsing subjects..."
......@@ -145,6 +145,8 @@ def main
end
load_closures