diff --git a/AKModel/urls.py b/AKModel/urls.py index 19e0083d3242a4bb6899579b9695cb1df3f1fed4..9b1f7591ee877043baca3cb63aac843dcb64ac4d 100644 --- a/AKModel/urls.py +++ b/AKModel/urls.py @@ -15,9 +15,10 @@ api_router.register('akslot', views.AKSlotViewSet, basename='AKSlot') extra_paths = [] if apps.is_installed("AKScheduling"): - from AKScheduling.api import ResourcesViewSet, RoomAvailabilitiesView, EventsView + from AKScheduling.api import ResourcesViewSet, RoomAvailabilitiesView, EventsView, EventsViewSet api_router.register('scheduling-resources', ResourcesViewSet, basename='scheduling-resources') + api_router.register('scheduling-event', EventsViewSet, basename='scheduling-event') extra_paths = [ path('api/scheduling-events/', EventsView.as_view(), name='scheduling-events'), diff --git a/AKScheduling/api.py b/AKScheduling/api.py index 110b50fa250c52cffb118f8085aed74b659393d9..e54e75fd0ea36eb07f2dbe581f2761d5dcae7278 100644 --- a/AKScheduling/api.py +++ b/AKScheduling/api.py @@ -1,6 +1,13 @@ -from rest_framework import viewsets, permissions, mixins, serializers +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import JsonResponse +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.utils import timezone +from django.views.generic import ListView +from rest_framework import viewsets, mixins, serializers, permissions -from AKModel.models import Room +from AKModel.availability.models import Availability +from AKModel.models import Room, AKSlot from AKModel.views import EventSlugMixin @@ -23,3 +30,83 @@ class ResourcesViewSet(EventSlugMixin, mixins.RetrieveModelMixin, mixins.ListMod def get_queryset(self): return Room.objects.filter(event=self.event) + + +class EventsView(LoginRequiredMixin, EventSlugMixin, ListView): + model = AKSlot + + def get_queryset(self): + return super().get_queryset().filter(event=self.event, room__isnull=False) + + def render_to_response(self, context, **response_kwargs): + return JsonResponse( + [{ + "slotID": slot.pk, + "title": slot.ak.short_name, + "description": slot.ak.name, + "resourceId": slot.room.id, + "start": timezone.localtime(slot.start, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"), + "end": timezone.localtime(slot.end, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"), + "backgroundColor": slot.ak.category.color, + # TODO Mark conflicts here? + "borderColor": slot.ak.category.color, + "constraint": 'roomAvailable', + 'url': str(reverse('submit:ak_detail', kwargs={"event_slug": self.event.slug, "pk": slot.ak.pk})), + } for slot in context["object_list"]], + safe=False, + **response_kwargs + ) + + +class RoomAvailabilitiesView(LoginRequiredMixin, EventSlugMixin, ListView): + model = Availability + context_object_name = "availabilities" + + def get_queryset(self): + return super().get_queryset().filter(event=self.event, room__isnull=False) + + def render_to_response(self, context, **response_kwargs): + return JsonResponse( + [{ + "title": "", + "resourceId": a.room.id, + "start": timezone.localtime(a.start, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"), + "end": timezone.localtime(a.end, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"), + "backgroundColor": "#28B62C", + "display": 'background', + "groupId": 'roomAvailable', + } for a in context["availabilities"]], + safe=False, + **response_kwargs + ) + + +class EventSerializer(serializers.ModelSerializer): + class Meta: + model = AKSlot + fields = ['id', 'start', 'end', 'roomId'] + + start = serializers.DateTimeField() + end = serializers.DateTimeField() + roomId = serializers.IntegerField(source='room.pk') + + def update(self, instance, validated_data): + start = timezone.make_aware(timezone.make_naive(validated_data.get('start', instance.start)), instance.event.timezone) + end = timezone.make_aware(timezone.make_naive(validated_data.get('end', instance.end)), instance.event.timezone) + instance.start = start + instance.room = get_object_or_404(Room, pk=validated_data.get('room')["pk"]) + diff = end - start + instance.duration = round(diff.days * 24 + (diff.seconds / 3600), 2) + instance.save() + return instance + + +class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet): + permission_classes = (permissions.DjangoModelPermissions,) + serializer_class = EventSerializer + + def get_object(self): + return get_object_or_404(AKSlot, pk=self.kwargs["pk"]) + + def get_queryset(self): + return AKSlot.objects.filter(event=self.event) diff --git a/AKScheduling/templates/admin/AKScheduling/scheduling.html b/AKScheduling/templates/admin/AKScheduling/scheduling.html index e87aab7aa987a80d65fe0e36c2910cea61ba2ee2..a17cb88b4c9910a3753b384230d60112f28c730b 100644 --- a/AKScheduling/templates/admin/AKScheduling/scheduling.html +++ b/AKScheduling/templates/admin/AKScheduling/scheduling.html @@ -21,6 +21,45 @@ <script> document.addEventListener('DOMContentLoaded', function () { + // CSRF Protection/Authentication + function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + const csrftoken = getCookie('csrftoken'); + + function csrfSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); + } + $.ajaxSetup({ + beforeSend: function(xhr, settings) { + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", csrftoken); + } + } + }); + + + // Place slots by dropping placeholders on calendar + var containerEl = document.getElementById('unscheduled-slots'); + new FullCalendar.Draggable(containerEl, { + itemSelector: '.unscheduled-slot', + }); + + + // Calendar var planEl = document.getElementById('planCalendar'); var plan = new FullCalendar.Calendar(planEl, { @@ -67,6 +106,7 @@ slotDuration: '00:30', } }, + // Show full AK title as tooltip for each AK (needs to be removed and newly placed when AK is moved) eventDidMount : function(info) { if(info.event.extendedProps.description !== undefined) { $(info.el).tooltip({title: info.event.extendedProps.description, trigger: 'hover'}); @@ -76,6 +116,8 @@ $(info.el).tooltip('dispose'); }, + // React to event changes (moving or change of duration) + eventChange: updateEvent, editable: true, allDaySlot: false, nowIndicator: true, @@ -94,6 +136,24 @@ plan.setOption('contentHeight', $(window).height() - $('#header').height() * 11); plan.render(); + + function updateEvent(changeInfo) { + room = changeInfo.event.getResources()[0]; + $.ajax({ + url: '{% url "model:scheduling-event-list" event_slug=event.slug %}' + changeInfo.event.extendedProps.slotID + "/", + type: 'PUT', + data: { + start: plan.formatIso(changeInfo.event.start), + end: plan.formatIso(changeInfo.event.end), + roomId: room.id, + }, + success: function(response) {}, + error: function(response) { + changeInfo.revert(); + alert("ERROR. Did not update "+changeInfo.event.title) + } + }); + } }); </script> {% endblock extrahead %} diff --git a/templates/admin_base.html b/templates/admin_base.html index cecf50f3694855670212591a35db6791be08c152..1022bb45582e4d5b8d201d7d0a60991ee6fb2887 100644 --- a/templates/admin_base.html +++ b/templates/admin_base.html @@ -5,7 +5,7 @@ {% block extrahead %} {% bootstrap_css %} - {% bootstrap_javascript jquery='slim' %} + {% bootstrap_javascript jquery='full' %} {% fontawesome_5_static %} <style>