diff --git a/README.md b/README.md index 86377ed..393c157 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,15 @@ to an actual account. NB: for local development, with either `apt` or `brew`, `xmlsec1` must also be installed manually! +# Set up local development + +clone the repo + +1. `cd website` +2. `poetry shell` +3. `poetry install` + (if any errors come up here you can try to install it yourself with pip install XX) +4. `python manage.py migrate` +5. `python manage.py compilescss` +6. `python manage.py python manage.py collectstatic --no-input -v0 --ignore="*.scss"` +7. `python manage.py runserver` \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 8099b3f..a4deb96 100644 --- a/poetry.lock +++ b/poetry.lock @@ -517,7 +517,7 @@ python-versions = "*" [[package]] name = "xmlsec" -version = "1.3.9" +version = "1.3.12" description = "Python bindings for the XML Security Library" category = "main" optional = false diff --git a/website/room_reservation/static/js/calendar-init.js b/website/room_reservation/static/js/calendar-init.js index a241cc2..9cc61c8 100644 --- a/website/room_reservation/static/js/calendar-init.js +++ b/website/room_reservation/static/js/calendar-init.js @@ -1,205 +1,242 @@ const csrfToken = document.getElementById('calendar').getAttribute('data-csrf'); const notifications = document.getElementById('notifications'); let mouseHovering = false; +let roomFilterSelection = -1; notifications.addEventListener('mouseenter', () => mouseHovering = true); notifications.addEventListener('mouseleave', () => mouseHovering = false); function el(name, attrs, ...els) { - const newEl = document.createElement(name); - Object.keys(attrs).forEach(key => newEl.setAttribute(key, attrs[key])); - els.forEach(el => newEl.appendChild(el)); - return newEl; + const newEl = document.createElement(name); + Object.keys(attrs).forEach(key => newEl.setAttribute(key, attrs[key])); + els.forEach(el => newEl.appendChild(el)); + return newEl; } function text(content) { - return document.createTextNode(content); + return document.createTextNode(content); } function addNotification(textContent, undoCallback) { - const closeBtn = el('button', {type: 'button', class: 'btn btn-outline-light justify-content-end'}, el('i', {class: 'fas fa-times'})); - const undoBtn = el('button', {type: 'button', class: 'btn btn-outline-light justify-content-end mr-2'}, el('i', {class: 'fas fa-undo-alt'}), text(' UNDO')); - - const notif = el('div', {class: 'notification-collapsed'}, - el('p', {}, text(textContent)), - el('ul', {class: 'nav justify-content-end'}, - el('li', {class: 'nav-item'}, undoBtn), - el('li', {class: 'nav-item'}, closeBtn))); - notifications.prepend(notif); - - closeBtn.addEventListener('click', event => { - notif.remove(); - }); - undoBtn.addEventListener('click', () => { - undoCallback(); - notif.remove(); - }); - // This makes sure the css transition plays - // 40ms seems arbitrary but when testing it seemed like this worked the best in Firefox - // Any lower and the transition wouldn't reliably play. - window.setTimeout(() => notif.className = "notification", 40); - window.setTimeout(() => { - if (!mouseHovering) { - notif.remove(); - } - }, 10000); + const closeBtn = el('button', { + type: 'button', + class: 'btn btn-outline-light justify-content-end' + }, el('i', {class: 'fas fa-times'})); + const undoBtn = el('button', { + type: 'button', + class: 'btn btn-outline-light justify-content-end mr-2' + }, el('i', {class: 'fas fa-undo-alt'}), text(' UNDO')); + + const notif = el('div', {class: 'notification-collapsed'}, + el('p', {}, text(textContent)), + el('ul', {class: 'nav justify-content-end'}, + el('li', {class: 'nav-item'}, undoBtn), + el('li', {class: 'nav-item'}, closeBtn))); + notifications.prepend(notif); + + closeBtn.addEventListener('click', event => { + notif.remove(); + }); + undoBtn.addEventListener('click', () => { + undoCallback(); + notif.remove(); + }); + // This makes sure the css transition plays + // 40ms seems arbitrary but when testing it seemed like this worked the best in Firefox + // Any lower and the transition wouldn't reliably play. + window.setTimeout(() => notif.className = "notification", 40); + window.setTimeout(() => { + if (!mouseHovering) { + notif.remove(); + } + }, 10000); } async function addEvent(event) { - const body = JSON.stringify({ - room: event.extendedProps.room, - start_time: event.start, - end_time: event.end - }); - const resp = await fetch(new Request('/reservations/create', { - method: 'POST', - credentials: 'include', - headers: { 'X-CSRFToken': csrfToken }, - body: body, - })); - if (resp.status !== 200) { - alert("An unknown error occurred."); - event.remove(); - return; - } - const text = await resp.text(); - return JSON.parse(text); + const body = JSON.stringify({ + room: event.extendedProps.room, + start_time: event.start, + end_time: event.end + }); + const resp = await fetch(new Request('/reservations/create', { + method: 'POST', + credentials: 'include', + headers: {'X-CSRFToken': csrfToken}, + body: body, + })); + if (resp.status !== 200) { + alert("An unknown error occurred."); + event.remove(); + return; + } + const text = await resp.text(); + return JSON.parse(text); } -async function changeEvent(info) { - const event = info.event; - const pk = event.extendedProps.pk, - start_time = event.start, - end_time = event.end; +function filterEvents(json) { + if (roomFilterSelection !== -1) { + json = json.filter(function (n, _i) { + return n.room === roomFilterSelection; + }) + } - const body = JSON.stringify({ - room: event.extendedProps.room, - start_time: start_time, - end_time: end_time, - }); - const resp = await fetch(new Request(`/reservations/${pk}/update`, { - method: 'POST', - credentials: 'include', - headers: { 'X-CSRFToken': csrfToken }, - body: body, - })); - - if (resp.status !== 200) { - info.revert(); - alert("An unknown error occurred."); - return; - } - const text = await resp.text(); - const message = JSON.parse(text); - if (!message.ok) { - info.revert(); - alert(message.message); - } + return json; } -document.addEventListener('DOMContentLoaded', function() { - const calendarEl = document.getElementById('calendar'); - const Draggable = FullCalendarInteraction.Draggable; +async function changeEvent(info) { + const event = info.event; + const pk = event.extendedProps.pk, + start_time = event.start, + end_time = event.end; - const containerEl = document.getElementById('external-events-list'); - if (containerEl !== null) { - new Draggable(containerEl, { - itemSelector: '.fc-event.draggable', + const body = JSON.stringify({ + room: event.extendedProps.room, + start_time: start_time, + end_time: end_time, }); - } - const calendar = new FullCalendar.Calendar(calendarEl, { - plugins: [ 'dayGrid', 'timeGrid', 'bootstrap', 'interaction' ], - themeSystem: 'bootstrap', - header: { - right: 'timeGridDay,dayGridWeek today,prev,next', - }, - initialView: 'timeGridDay', - defaultView: 'timeGridDay', - weekNumbers: true, - weekNumbersWithinDays: true, - weekends: false, - firstDay: 1, - timeFormat: "HH:mm", - slotLabelFormat: { - hour: '2-digit', - minute: '2-digit', - hour12: false, - }, - slotDuration: '00:15:00', - eventTimeFormat: { - hour: '2-digit', - minute: '2-digit', - hour12: false, - }, - slotEventOverlap: false, - eventLimit: true, - minTime: '8:00', - maxTime: '18:00', - height: 800, - timeZone: 'local', - allDaySlot: false, - nowIndicator: true, - editable: false, - droppable: true, - displayEventEnd: true, - eventReceive: async function({event}) { - const message = await addEvent(event); - if (!message.ok) { - alert(message.message); - event.remove(); - return; - } - event.setProp('title', event.title + ' (you)'); - event.setExtendedProp('pk', message.pk); - }, - eventClick: async function({event}) { - if (!event.durationEditable) { - return; - } - const pk = event.extendedProps.pk; - const resp = await fetch(new Request(`/reservations/${pk}/delete`, { - method: 'POST', - credentials: 'include', - headers: { 'X-CSRFToken': csrfToken }, - })); - if (resp.status !== 200) { + const resp = await fetch(new Request(`/reservations/${pk}/update`, { + method: 'POST', + credentials: 'include', + headers: {'X-CSRFToken': csrfToken}, + body: body, + })); + + if (resp.status !== 200) { + info.revert(); alert("An unknown error occurred."); return; - } - const text = await resp.text(); - const message = JSON.parse(text); - if (!message.ok) { + } + const text = await resp.text(); + const message = JSON.parse(text); + if (!message.ok) { + info.revert(); alert(message.message); - return; - } - event.remove(); - let name = "your"; - if (event.extendedProps.reservee) { - name = event.extendedProps.reservee + "s"; - } - const date = FullCalendar.formatDate(event.start, {month: 'short', day: 'numeric'}); - addNotification(`Deleted ${name} reservation from ${date}`, async () => { - const message = await addEvent(event); - if (!message.ok) { - alert(message.message); - event.remove(); - return; - } - calendar.addEvent({ - pk: message.pk, - title: event.title, - reservee: event.extendedProps.reservee, - room: event.extendedProps.room, - start: event.start, - end: event.end, - editable: true, + } +} + +document.addEventListener('DOMContentLoaded', function () { + const calendarEl = document.getElementById('calendar'); + const Draggable = FullCalendarInteraction.Draggable; + + const containerEl = document.getElementById('external-events-list'); + if (containerEl !== null) { + new Draggable(containerEl, { + itemSelector: '.fc-event.draggable', }); - }) - }, - eventDrop: changeEvent, - eventResize: changeEvent, - events: JSON.parse(document.getElementById('calendar').getAttribute('data-events')) - }); - - calendar.render(); + } + const origData = JSON.parse(document.getElementById('calendar').getAttribute('data-events')); + let calenderData = origData; + const calendar = new FullCalendar.Calendar(calendarEl, { + plugins: ['dayGrid', 'timeGrid', 'bootstrap', 'interaction'], + themeSystem: 'bootstrap', + header: { + right: 'timeGridDay,dayGridWeek today,prev,next', + }, + initialView: 'timeGridDay', + defaultView: 'timeGridDay', + weekNumbers: true, + weekNumbersWithinDays: true, + weekends: false, + firstDay: 1, + timeFormat: "HH:mm", + slotLabelFormat: { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }, + slotDuration: '00:15:00', + eventTimeFormat: { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }, + slotEventOverlap: false, + eventLimit: true, + minTime: '8:00', + maxTime: '18:00', + height: 800, + timeZone: 'local', + allDaySlot: false, + nowIndicator: true, + editable: false, + droppable: true, + displayEventEnd: true, + eventReceive: async function ({event}) { + console.log(event) + const message = await addEvent(event); + if (!message.ok) { + alert(message.message); + event.remove(); + return; + } + event.setProp('title', event.title + ' (you)'); + event.setExtendedProp('pk', message.pk); + }, + eventClick: async function ({event}) { + if (!event.durationEditable) { + return; + } + const pk = event.extendedProps.pk; + const resp = await fetch(new Request(`/reservations/${pk}/delete`, { + method: 'POST', + credentials: 'include', + headers: {'X-CSRFToken': csrfToken}, + })); + if (resp.status !== 200) { + alert("An unknown error occurred."); + return; + } + const text = await resp.text(); + const message = JSON.parse(text); + if (!message.ok) { + alert(message.message); + return; + } + event.remove(); + let name = "your"; + if (event.extendedProps.reservee) { + name = event.extendedProps.reservee + "s"; + } + const date = FullCalendar.formatDate(event.start, {month: 'short', day: 'numeric'}); + addNotification(`Deleted ${name} reservation from ${date}`, async () => { + const message = await addEvent(event); + if (!message.ok) { + alert(message.message); + event.remove(); + return; + } + calendar.addEvent({ + pk: message.pk, + title: event.title, + reservee: event.extendedProps.reservee, + room: event.extendedProps.room, + start: event.start, + end: event.end, + editable: true, + }); + }) + }, + eventDrop: changeEvent, + eventResize: changeEvent, + events: function (info, successCallback, failureCallback) { + successCallback(calenderData); + }, + }); + + const roomFilter = document.getElementById("room-filter"); + if (roomFilter !== null) { + // Reset on page update + roomFilter.value = -1; + + roomFilter.onchange = (_event) => { + let res = parseInt(roomFilter.value); + if (res !== roomFilterSelection) { + roomFilterSelection = res; + calenderData = filterEvents(origData); + calendar.refetchEvents(); + } + } + } + + calendar.render(); }); \ No newline at end of file diff --git a/website/room_reservation/static/scss/index.scss b/website/room_reservation/static/scss/index.scss index 0672fd2..a1bda43 100644 --- a/website/room_reservation/static/scss/index.scss +++ b/website/room_reservation/static/scss/index.scss @@ -12,6 +12,15 @@ padding: 10px 8px; } +#filter-rooms { + margin: 10px 0; +} + +#filter-rooms > label { + padding-right: 10px; + margin-bottom: 0; +} + #notifications { position: absolute; z-index: 100; diff --git a/website/room_reservation/templates/room_reservation/index.html b/website/room_reservation/templates/room_reservation/index.html index 8b3443b..a54fb08 100644 --- a/website/room_reservation/templates/room_reservation/index.html +++ b/website/room_reservation/templates/room_reservation/index.html @@ -4,13 +4,15 @@ {% block content %}

Mercator reservations

- {# This is where calendar-init.js will place notifications for deleted events. #} + {# This is where calendar-init.js will place notifications for deleted events. #}
-

If you are logged in, you can drag a room to the calendar to make a reservation and click your events to remove them.

+

If you are logged in, you can drag a room to the calendar to make a reservation and click + your events to remove them.

-

You can make reservations at most 1 week in advance. Without a reservation, you cannot come to Mercator.
+

You can make reservations at most 1 week in advance. Without a reservation, you cannot come + to Mercator.
Please only make a reservation for a place to study.

@@ -20,9 +22,19 @@

Mercator reservations

{% for room in rooms %} {# The `draggable` attribute is used in calendar-init.js to make the rooms draggable. #} -
{{ room }}
+
{{ room }}
{% endfor %}
+
+ + +