Skip to content
Snippets Groups Projects
Commit 64d6fe64 authored by Benjamin Hättasch's avatar Benjamin Hättasch
Browse files

Port availabilities editor to fullcalendar v5 and adjust usage

This used the common fullcalendar v5 instance instead of a special v3 version only for this editor
Improve visualization of availability deletion
Improve (event specific) timezone handling and remove dependency to moment-timezones
parent b2dee786
No related branches found
No related tags found
No related merge requests found
...@@ -21,7 +21,7 @@ zero_time = datetime.time(0, 0) ...@@ -21,7 +21,7 @@ zero_time = datetime.time(0, 0)
# remove serialization as requirements are not covered # remove serialization as requirements are not covered
# add translation # add translation
# add meta class # add meta class
# enable availabilites for AKs and AKCategories # enable availabilities for AKs and AKCategories
# add verbose names and help texts to model attributes # add verbose names and help texts to model attributes
class Availability(models.Model): class Availability(models.Model):
"""The Availability class models when people or rooms are available for. """The Availability class models when people or rooms are available for.
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
# Copyright 2017-2019, Tobias Kunze # Copyright 2017-2019, Tobias Kunze
# Original Copyrights licensed under the Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 # Original Copyrights licensed under the Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
# Changes are marked in the code # Changes are marked in the code
from django.utils import timezone
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from rest_framework.serializers import ModelSerializer from rest_framework.serializers import ModelSerializer
...@@ -10,10 +11,20 @@ from AKModel.availability.models import Availability ...@@ -10,10 +11,20 @@ from AKModel.availability.models import Availability
class AvailabilitySerializer(ModelSerializer): class AvailabilitySerializer(ModelSerializer):
allDay = SerializerMethodField() allDay = SerializerMethodField()
start = SerializerMethodField()
end = SerializerMethodField()
def get_allDay(self, obj): def get_allDay(self, obj):
return obj.all_day return obj.all_day
# Use already localized strings in serialized field
# (default would be UTC, but that would require heavy timezone calculation on client side)
def get_start(self, obj):
return timezone.localtime(obj.start, obj.event.timezone).strftime("%Y-%m-%dT%H:%M:%S")
def get_end(self, obj):
return timezone.localtime(obj.end, obj.event.timezone).strftime("%Y-%m-%dT%H:%M:%S")
class Meta: class Meta:
model = Availability model = Availability
fields = ('id', 'start', 'end', 'allDay') fields = ('id', 'start', 'end', 'allDay')
...@@ -2,15 +2,23 @@ ...@@ -2,15 +2,23 @@
{% load i18n admin_urls %} {% load i18n admin_urls %}
{% load static %} {% load static %}
{% load bootstrap4 %} {% load bootstrap4 %}
{% load tz %}
{% block extrahead %} {% block extrahead %}
{{ block.super }} {{ block.super }}
{% bootstrap_javascript jquery='slim' %} {% bootstrap_javascript jquery='slim' %}
<link href='{% static 'AKSubmission/vendor/fullcalendar3/fullcalendar.min.css' %}' rel='stylesheet'/> {% include "AKPlan/load_fullcalendar.html" %}
<link href='{% static 'AKSubmission/css/availabilities.css' %}' rel='stylesheet'/>
<script src="{% static "AKSubmission/vendor/moment/moment-with-locales.js" %}"></script>
<script src="{% static "AKSubmission/vendor/moment-timezone/moment-timezone-with-data-10-year-range.js" %}"></script>
<script src='{% static 'AKSubmission/vendor/fullcalendar3/fullcalendar.min.js' %}'></script>
<script src="{% static "common/js/availabilities.js" %}"></script> <script src="{% static "common/js/availabilities.js" %}"></script>
<script>
{% get_current_language as LANGUAGE_CODE %}
document.addEventListener('DOMContentLoaded', function () {
createAvailabilityEditors(
'{{ original.event.timezone }}',
'{{ LANGUAGE_CODE }}',
'{{ original.event.start | timezone:original.event.timezone | date:"Y-m-d H:i:s" }}',
'{{ original.event.end | timezone:original.event.timezone | date:"Y-m-d H:i:s" }}'
);
});
</script>
{% endblock %} {% endblock %}
...@@ -4,19 +4,30 @@ ...@@ -4,19 +4,30 @@
{% load bootstrap4 %} {% load bootstrap4 %}
{% load fontawesome_5 %} {% load fontawesome_5 %}
{% load static %} {% load static %}
{% load tz %}
{% block title %}{% trans "AKs" %}: {{ event.name }} - {% trans "New AK" %}{% endblock %} {% block title %}{% trans "AKs" %}: {{ event.name }} - {% trans "New AK" %}{% endblock %}
{% block imports %} {% block imports %}
<link rel="stylesheet" href="{% static 'common/vendor/chosen-js/chosen.css' %}"> <link rel="stylesheet" href="{% static 'common/vendor/chosen-js/chosen.css' %}">
<link rel="stylesheet" href="{% static 'common/css/bootstrap-chosen.css' %}"> <link rel="stylesheet" href="{% static 'common/css/bootstrap-chosen.css' %}">
<link href='{% static 'AKSubmission/vendor/fullcalendar3/fullcalendar.min.css' %}' rel='stylesheet'/>
<link href='{% static 'AKSubmission/css/availabilities.css' %}' rel='stylesheet'/>
{% include "AKPlan/load_fullcalendar.html" %}
<script src="{% static "AKSubmission/vendor/moment/moment-with-locales.js" %}"></script> <script src="{% static "AKSubmission/vendor/moment/moment-with-locales.js" %}"></script>
<script src="{% static "AKSubmission/vendor/moment-timezone/moment-timezone-with-data-10-year-range.js" %}"></script>
<script src='{% static 'AKSubmission/vendor/fullcalendar3/fullcalendar.min.js' %}'></script>
<script src="{% static "common/js/availabilities.js" %}"></script> <script src="{% static "common/js/availabilities.js" %}"></script>
<script>
{% get_current_language as LANGUAGE_CODE %}
document.addEventListener('DOMContentLoaded', function () {
createAvailabilityEditors(
'{{ event.timezone }}',
'{{ LANGUAGE_CODE }}',
'{{ event.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
'{{ event.end | timezone:event.timezone | date:"Y-m-d H:i:s" }}'
);
});
</script>
{% endblock %} {% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
......
...@@ -6,17 +6,6 @@ ...@@ -6,17 +6,6 @@
{% block title %}{% trans "AKs" %}: {{ event.name }} - {% trans "New AK Wish" %}{% endblock %} {% block title %}{% trans "AKs" %}: {{ event.name }} - {% trans "New AK Wish" %}{% endblock %}
{% block imports %}
<link rel="stylesheet" href="{% static 'common/vendor/chosen-js/chosen.css' %}">
<link rel="stylesheet" href="{% static 'common/css/bootstrap-chosen.css' %}">
<link href='{% static 'AKSubmission/vendor/fullcalendar3/fullcalendar.min.css' %}' rel='stylesheet'/>
<link href='{% static 'AKSubmission/css/availabilities.css' %}' rel='stylesheet'/>
<script src="{% static "AKSubmission/vendor/moment/moment-with-locales.js" %}"></script>
<script src="{% static "AKSubmission/vendor/moment-timezone/moment-timezone-with-data-10-year-range.js" %}"></script>
<script src='{% static 'AKSubmission/vendor/fullcalendar3/fullcalendar.min.js' %}'></script>
<script src="{% static "common/js/availabilities.js" %}"></script>
{% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
{% include "AKSubmission/submission_breadcrumbs.html" %} {% include "AKSubmission/submission_breadcrumbs.html" %}
...@@ -27,4 +16,4 @@ ...@@ -27,4 +16,4 @@
{% block headline %} {% block headline %}
<h2>{% trans 'New AK Wish' %}</h2> <h2>{% trans 'New AK Wish' %}</h2>
{% endblock %} {% endblock %}
\ No newline at end of file
...@@ -30,3 +30,12 @@ ...@@ -30,3 +30,12 @@
word-break: normal; word-break: normal;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.deleteEvent {
background-color: #6a6a6a !important;
}
.deleteEvent .fc-event-title {
font-size: 5vw;
text-align: center;
}
// This part of the code was adapted from pretalx (https://github.com/pretalx/pretalx) // Availability editor using fullcalendar v5
// This code was initially based on the availability editor from pretalx (https://github.com/pretalx/pretalx)
// Copyright 2017-2019, Tobias Kunze // Copyright 2017-2019, Tobias Kunze
// Original Copyrights licensed under the Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 // Original Copyrights licensed under the Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
// Changes are marked in the code // It was significantly changed to deal with the newer fullcalendar version, event specific timezones,
document.addEventListener("DOMContentLoaded", function () { // to remove the dependency to moments timezone and improve the visualization of deletion
"use strict"
function createAvailabilityEditors(timezone, language, startDate, endDate) {
$("input.availabilities-editor-data").each(function () { $("input.availabilities-editor-data").each(function () {
var data_field = $(this) const eventColor = '#28B62C';
var editor = $('<div class="availabilities-editor">')
editor.attr("data-name", data_field.attr("name"))
data_field.after(editor)
function save_events() { let data_field = $(this);
data = { let editor = $('<div class="availabilities-editor">');
availabilities: editor.fullCalendar("clientEvents").map(function (e) { editor.attr("data-name", data_field.attr("name"));
if (e.allDay) { data_field.after(editor);
return { data_field.hide();
start: e.start.format("YYYY-MM-DD HH:mm:ss"),
end: e.end.format("YYYY-MM-DD HH:mm:ss"),
}
} else {
return {
start: e.start.toISOString(),
end: e.end.toISOString(),
}
}
}),
}
data_field.attr("value", JSON.stringify(data))
}
var editable = !Boolean(data_field.attr("disabled")) let editable = !Boolean(data_field.attr("disabled"));
let data = JSON.parse(data_field.attr("value"));
let events = data.availabilities.map(function (e) {
start = moment(e.start);
end = moment(e.end);
allDay = start.format("HHmmss") === 0 && end.format("HHmmss") === 0;
var data = JSON.parse(data_field.attr("value")) return {
var events = data.availabilities.map(function (e) { id: e.id,
e.start = moment(e.start)//.tz(data.event.timezone) start: start.format(),
e.end = moment(e.end)//.tz(data.event.timezone) end: end.format(),
allDay: allDay,
title: ""
};
});
if (e.start.format("HHmmss") == 0 && e.end.format("HHmmss") == 0) { let eventMarkedForDeletion = undefined;
e.allDay = true let eventMarkedForDeletionEl = undefined;
} let newEventsCounter = 0;
return e let plan = new FullCalendar.Calendar(editor[0], {
}) timeZone: timezone,
editor.fullCalendar({ themeSystem: 'bootstrap',
locale: language,
schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
editable: editable,
selectable: editable,
headerToolbar: false,
initialView: 'timeGridWholeEvent',
views: { views: {
agendaVariableDays: { timeGridWholeEvent: {
type: "agenda", type: 'timeGrid',
duration: { visibleRange: {
days: start: startDate,
moment(data.event.date_to).diff( end: endDate,
moment(data.event.date_from),
"days"
) + 1,
}, },
}, }
},
defaultView: "agendaVariableDays",
defaultDate: data.event.date_from,
visibleRange: {
start: data.event.date_from,
end: data.event.date_to,
}, },
events: events, allDaySlot: true,
nowIndicator: false, events: data.availabilities,
navLinks: false, eventBackgroundColor: eventColor,
header: false, select: function (info) {
timeFormat: "H:mm", resetDeletionCandidate();
slotLabelFormat: "H:mm", plan.addEvent({
scrollTime: "09:00:00", title: "",
selectable: editable, start: info.start,
selectHelper: true, end: info.end,
select: function (start, end) { id: 'new' + newEventsCounter
var wasInDeleteMode = false
editor.fullCalendar("clientEvents").forEach(function (e) {
if (e.className.indexOf("delete") >= 0) {
wasInDeleteMode = true
}
e.className = ""
editor.fullCalendar("updateEvent", e)
}) })
newEventsCounter++;
if (wasInDeleteMode) { save_events();
editor.fullCalendar("unselect") },
return eventClick: function (info) {
} if (eventMarkedForDeletion !== undefined && (eventMarkedForDeletion.id === info.event.id)) {
info.event.remove();
var eventData = { eventMarkedForDeletion = undefined;
start: start, eventMarkedForDeletionEl = undefined;
end: end, save_events();
} else {
resetDeletionCandidate();
makeDeletionCandidate(info.el);
eventMarkedForDeletion = info.event;
eventMarkedForDeletionEl = info.el;
} }
editor.fullCalendar("renderEvent", eventData, true)
editor.fullCalendar("unselect")
save_events()
}, },
eventResize: save_events,
eventDrop: save_events,
editable: editable,
selectOverlap: false, selectOverlap: false,
eventOverlap: false, eventOverlap: false,
eventColor: "#00DD00", eventChange: save_events,
eventClick: function (calEvent, jsEvent, view) { });
if (!editable) { plan.render();
return
}
if (calEvent.className.indexOf("delete") >= 0) { function makeDeletionCandidate(el) {
editor.fullCalendar("removeEvents", function (searchEvent) { el.classList.add("deleteEvent");
return searchEvent._id === calEvent._id $(el).find(".fc-event-title").html("<i class='fas fa-trash'></i> <i class='fas fa-question'></i>");
}) }
save_events()
} else { function resetDeletionCandidate() {
editor.fullCalendar("clientEvents").forEach(function (e) { if (eventMarkedForDeletionEl !== undefined) {
if (e._id == calEvent._id) { eventMarkedForDeletionEl.classList.remove("deleteEvent");
e.className = "delete" $(eventMarkedForDeletionEl).find(".fc-event-title").html("");
} else { }
e.className = "" eventMarkedForDeletionEl = undefined;
} eventMarkedForDeletion = undefined;
editor.fullCalendar("updateEvent", e) }
})
} function save_events() {
}, data = {
}) availabilities: plan.getEvents().map(function (e) {
}) let id = e.id;
}) if(e.id.startsWith("new"))
id = "";
return {
id: id,
// Make sure these timestamps are correctly interpreted as localized ones
// by removing the UTC-signaler ("Z" at the end)
// A bit dirty, but still more elegant than creating a timestamp with the
// required format manually
start: e.start.toISOString().replace("Z", ""),
end: e.end.toISOString().replace("Z", ""),
allDay: e.allDay,
}
}),
}
data_field.attr("value", JSON.stringify(data));
}
});
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment