Skip to content
Snippets Groups Projects
Commit 3f220b81 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 72e8c3e7
No related branches found
No related tags found
No related merge requests found
......@@ -21,7 +21,7 @@ zero_time = datetime.time(0, 0)
# remove serialization as requirements are not covered
# add translation
# add meta class
# enable availabilites for AKs and AKCategories
# enable availabilities for AKs and AKCategories
# add verbose names and help texts to model attributes
class Availability(models.Model):
"""The Availability class models when people or rooms are available for.
......
......@@ -2,6 +2,7 @@
# Copyright 2017-2019, Tobias Kunze
# Original Copyrights licensed under the Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
# Changes are marked in the code
from django.utils import timezone
from rest_framework.fields import SerializerMethodField
from rest_framework.serializers import ModelSerializer
......@@ -10,10 +11,20 @@ from AKModel.availability.models import Availability
class AvailabilitySerializer(ModelSerializer):
allDay = SerializerMethodField()
start = SerializerMethodField()
end = SerializerMethodField()
def get_allDay(self, obj):
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:
model = Availability
fields = ('id', 'start', 'end', 'allDay')
......@@ -2,15 +2,23 @@
{% load i18n admin_urls %}
{% load static %}
{% load bootstrap4 %}
{% load tz %}
{% block extrahead %}
{{ block.super }}
{% bootstrap_javascript jquery='slim' %}
<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>
{% include "AKPlan/load_fullcalendar.html" %}
<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 %}
......@@ -4,19 +4,30 @@
{% load bootstrap4 %}
{% load fontawesome_5 %}
{% load static %}
{% load tz %}
{% block title %}{% trans "AKs" %}: {{ event.name }} - {% trans "New AK" %}{% 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'/>
{% include "AKPlan/load_fullcalendar.html" %}
<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>
{% 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 %}
{% block breadcrumbs %}
......
......@@ -6,17 +6,6 @@
{% 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 %}
{% include "AKSubmission/submission_breadcrumbs.html" %}
......@@ -27,4 +16,4 @@
{% block headline %}
<h2>{% trans 'New AK Wish' %}</h2>
{% endblock %}
\ No newline at end of file
{% endblock %}
......@@ -30,3 +30,12 @@
word-break: normal;
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
// Original Copyrights licensed under the Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
// Changes are marked in the code
document.addEventListener("DOMContentLoaded", function () {
"use strict"
// It was significantly changed to deal with the newer fullcalendar version, event specific timezones,
// to remove the dependency to moments timezone and improve the visualization of deletion
function createAvailabilityEditors(timezone, language, startDate, endDate) {
$("input.availabilities-editor-data").each(function () {
var data_field = $(this)
var editor = $('<div class="availabilities-editor">')
editor.attr("data-name", data_field.attr("name"))
data_field.after(editor)
const eventColor = '#28B62C';
function save_events() {
data = {
availabilities: editor.fullCalendar("clientEvents").map(function (e) {
if (e.allDay) {
return {
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))
}
let data_field = $(this);
let editor = $('<div class="availabilities-editor">');
editor.attr("data-name", data_field.attr("name"));
data_field.after(editor);
data_field.hide();
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"))
var events = data.availabilities.map(function (e) {
e.start = moment(e.start)//.tz(data.event.timezone)
e.end = moment(e.end)//.tz(data.event.timezone)
return {
id: e.id,
start: start.format(),
end: end.format(),
allDay: allDay,
title: ""
};
});
if (e.start.format("HHmmss") == 0 && e.end.format("HHmmss") == 0) {
e.allDay = true
}
let eventMarkedForDeletion = undefined;
let eventMarkedForDeletionEl = undefined;
let newEventsCounter = 0;
return e
})
editor.fullCalendar({
let plan = new FullCalendar.Calendar(editor[0], {
timeZone: timezone,
themeSystem: 'bootstrap',
locale: language,
schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
editable: editable,
selectable: editable,
headerToolbar: false,
initialView: 'timeGridWholeEvent',
views: {
agendaVariableDays: {
type: "agenda",
duration: {
days:
moment(data.event.date_to).diff(
moment(data.event.date_from),
"days"
) + 1,
timeGridWholeEvent: {
type: 'timeGrid',
visibleRange: {
start: startDate,
end: endDate,
},
},
},
defaultView: "agendaVariableDays",
defaultDate: data.event.date_from,
visibleRange: {
start: data.event.date_from,
end: data.event.date_to,
}
},
events: events,
nowIndicator: false,
navLinks: false,
header: false,
timeFormat: "H:mm",
slotLabelFormat: "H:mm",
scrollTime: "09:00:00",
selectable: editable,
selectHelper: true,
select: function (start, end) {
var wasInDeleteMode = false
editor.fullCalendar("clientEvents").forEach(function (e) {
if (e.className.indexOf("delete") >= 0) {
wasInDeleteMode = true
}
e.className = ""
editor.fullCalendar("updateEvent", e)
allDaySlot: true,
events: data.availabilities,
eventBackgroundColor: eventColor,
select: function (info) {
resetDeletionCandidate();
plan.addEvent({
title: "",
start: info.start,
end: info.end,
id: 'new' + newEventsCounter
})
if (wasInDeleteMode) {
editor.fullCalendar("unselect")
return
}
var eventData = {
start: start,
end: end,
newEventsCounter++;
save_events();
},
eventClick: function (info) {
if (eventMarkedForDeletion !== undefined && (eventMarkedForDeletion.id === info.event.id)) {
info.event.remove();
eventMarkedForDeletion = undefined;
eventMarkedForDeletionEl = undefined;
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,
eventOverlap: false,
eventColor: "#00DD00",
eventClick: function (calEvent, jsEvent, view) {
if (!editable) {
return
}
eventChange: save_events,
});
plan.render();
if (calEvent.className.indexOf("delete") >= 0) {
editor.fullCalendar("removeEvents", function (searchEvent) {
return searchEvent._id === calEvent._id
})
save_events()
} else {
editor.fullCalendar("clientEvents").forEach(function (e) {
if (e._id == calEvent._id) {
e.className = "delete"
} else {
e.className = ""
}
editor.fullCalendar("updateEvent", e)
})
}
},
})
})
})
function makeDeletionCandidate(el) {
el.classList.add("deleteEvent");
$(el).find(".fc-event-title").html("<i class='fas fa-trash'></i> <i class='fas fa-question'></i>");
}
function resetDeletionCandidate() {
if (eventMarkedForDeletionEl !== undefined) {
eventMarkedForDeletionEl.classList.remove("deleteEvent");
$(eventMarkedForDeletionEl).find(".fc-event-title").html("");
}
eventMarkedForDeletionEl = undefined;
eventMarkedForDeletion = undefined;
}
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.
Please register or to comment