From 34f039d94bc4ec63ca1d696c20f3b6030d4a5c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Sun, 9 May 2021 11:19:36 +0200 Subject: [PATCH 01/17] Move scheduling admin view urls to separate file Additionally fix wrong app installed check for ak detail link (should be AKSubmission, not AKScheduling) --- AKModel/admin.py | 20 +++++--------------- AKScheduling/urls.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 15 deletions(-) create mode 100644 AKScheduling/urls.py diff --git a/AKModel/admin.py b/AKModel/admin.py index 9427710f..886e353b 100644 --- a/AKModel/admin.py +++ b/AKModel/admin.py @@ -117,12 +117,8 @@ class AKTrackAdmin(admin.ModelAdmin): urls = super().get_urls() custom_urls = [] if apps.is_installed("AKScheduling"): - from AKScheduling.views import TrackAdminView - - custom_urls.extend([ - path('<slug:event_slug>/manage/', self.admin_site.admin_view(TrackAdminView.as_view()), - name="tracks_manage"), - ]) + from AKScheduling.urls import get_admin_urls_track + custom_urls.extend(get_admin_urls_track(self.admin_site)) return custom_urls + urls @@ -282,14 +278,8 @@ class AKSlotAdmin(admin.ModelAdmin): urls = super().get_urls() custom_urls = [] if apps.is_installed("AKScheduling"): - from AKScheduling.views import SchedulingAdminView, UnscheduledSlotsAdminView - - custom_urls.extend([ - path('<slug:event_slug>/schedule/', self.admin_site.admin_view(SchedulingAdminView.as_view()), - name="schedule"), - path('<slug:event_slug>/unscheduled/', self.admin_site.admin_view(UnscheduledSlotsAdminView.as_view()), - name="slots_unscheduled"), - ]) + from AKScheduling.urls import get_admin_urls_slot + custom_urls.extend(get_admin_urls_slot(self.admin_site)) return custom_urls + urls def get_form(self, request, obj=None, change=False, **kwargs): @@ -307,7 +297,7 @@ class AKSlotAdmin(admin.ModelAdmin): return super(AKSlotAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) def ak_details_link(self, akslot): - if apps.is_installed("AKScheduling") and akslot.ak is not None: + if apps.is_installed("AKSubmission") and akslot.ak is not None: link = f"<a href={reverse('submit:ak_detail', args=[akslot.event.slug, akslot.ak.pk])}>{str(akslot.ak)}</a>" return mark_safe(link) return "-" diff --git a/AKScheduling/urls.py b/AKScheduling/urls.py new file mode 100644 index 00000000..4fb483a1 --- /dev/null +++ b/AKScheduling/urls.py @@ -0,0 +1,19 @@ +from django.urls import path + +from AKScheduling.views import SchedulingAdminView, UnscheduledSlotsAdminView, TrackAdminView + + +def get_admin_urls_slot(admin_site): + return [ + path('<slug:event_slug>/schedule/', admin_site.admin_view(SchedulingAdminView.as_view()), + name="schedule"), + path('<slug:event_slug>/unscheduled/', admin_site.admin_view(UnscheduledSlotsAdminView.as_view()), + name="slots_unscheduled"), + ] + + +def get_admin_urls_track(admin_site): + return [ + path('<slug:event_slug>/manage/', admin_site.admin_view(TrackAdminView.as_view()), + name="tracks_manage"), + ] -- GitLab From 806415d03092470da6a3804a47baab54f8909416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Sun, 9 May 2021 11:44:41 +0200 Subject: [PATCH 02/17] Move model admin view urls to separate file --- AKModel/admin.py | 41 +++++++++-------------------------------- AKModel/urls.py | 48 +++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/AKModel/admin.py b/AKModel/admin.py index 886e353b..2d832ad3 100644 --- a/AKModel/admin.py +++ b/AKModel/admin.py @@ -1,10 +1,10 @@ +from django import forms from django.apps import apps from django.contrib import admin from django.contrib.admin import SimpleListFilter from django.db.models import Count, F -from django import forms from django.shortcuts import render, redirect -from django.urls import path, reverse_lazy +from django.urls import reverse_lazy from django.utils import timezone from django.utils.html import format_html from django.utils.safestring import mark_safe @@ -16,9 +16,7 @@ from AKModel.availability.forms import AvailabilitiesFormMixin from AKModel.availability.models import Availability from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKTag, AKRequirement, AK, AKSlot, Room, AKOrgaMessage, \ ConstraintViolation -from AKModel.views import EventStatusView, AKCSVExportView, AKWikiExportView, AKMessageDeleteView, AKRequirementOverview, \ - NewEventWizardStartView, NewEventWizardSettingsView, NewEventWizardPrepareImportView, NewEventWizardFinishView, \ - NewEventWizardImportView, NewEventWizardActivateView +from AKModel.urls import get_admin_urls_event_wizard, get_admin_urls_event @admin.register(Event) @@ -30,36 +28,15 @@ class EventAdmin(admin.ModelAdmin): ordering = ['-start'] def add_view(self, request, form_url='', extra_context=None): - # Always use wizard to create new events - # (the built-in form wouldn't work anyways since the timezone cannot be specified before starting to fill the form) + # Always use wizard to create new events (the built-in form wouldn't work anyways since the timezone cannot + # be specified before starting to fill the form) return redirect("admin:new_event_wizard_start") def get_urls(self): - urls = super().get_urls() - custom_urls = [ - path('add/wizard/start/', self.admin_site.admin_view(NewEventWizardStartView.as_view()), - name="new_event_wizard_start"), - path('add/wizard/settings/', self.admin_site.admin_view(NewEventWizardSettingsView.as_view()), - name="new_event_wizard_settings"), - path('add/wizard/created/<slug:event_slug>/', self.admin_site.admin_view(NewEventWizardPrepareImportView.as_view()), - name="new_event_wizard_prepare_import"), - path('add/wizard/import/<slug:event_slug>/from/<slug:import_slug>/', - self.admin_site.admin_view(NewEventWizardImportView.as_view()), - name="new_event_wizard_import"), - path('add/wizard/activate/<slug:slug>/', - self.admin_site.admin_view(NewEventWizardActivateView.as_view()), - name="new_event_wizard_activate"), - path('add/wizard/finish/<slug:slug>/', - self.admin_site.admin_view(NewEventWizardFinishView.as_view()), - name="new_event_wizard_finish"), - path('<slug:slug>/status/', self.admin_site.admin_view(EventStatusView.as_view()), name="event_status"), - path('<slug:event_slug>/requirements/', self.admin_site.admin_view(AKRequirementOverview.as_view()), name="event_requirement_overview"), - path('<slug:event_slug>/ak-csv-export/', self.admin_site.admin_view(AKCSVExportView.as_view()), name="ak_csv_export"), - path('<slug:event_slug>/ak-wiki-export/', self.admin_site.admin_view(AKWikiExportView.as_view()), name="ak_wiki_export"), - path('<slug:slug>/delete-orga-messages/', self.admin_site.admin_view(AKMessageDeleteView.as_view()), - name="ak_delete_orga_messages"), - ] - return custom_urls + urls + urls = get_admin_urls_event_wizard(self.admin_site) + urls.extend(get_admin_urls_event(self.admin_site)) + urls.extend(super().get_urls()) + return urls def status_url(self, obj): return format_html("<a href='{url}'>{text}</a>", diff --git a/AKModel/urls.py b/AKModel/urls.py index 9b1f7591..03239837 100644 --- a/AKModel/urls.py +++ b/AKModel/urls.py @@ -3,6 +3,9 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter from AKModel import views +from AKModel.views import NewEventWizardStartView, NewEventWizardSettingsView, NewEventWizardPrepareImportView, \ + NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, EventStatusView, \ + AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView api_router = DefaultRouter() api_router.register('akowner', views.AKOwnerViewSet, basename='AKOwner') @@ -12,7 +15,6 @@ api_router.register('ak', views.AKViewSet, basename='AK') api_router.register('room', views.RoomViewSet, basename='Room') api_router.register('akslot', views.AKSlotViewSet, basename='AKSlot') - extra_paths = [] if apps.is_installed("AKScheduling"): from AKScheduling.api import ResourcesViewSet, RoomAvailabilitiesView, EventsView, EventsViewSet @@ -22,19 +24,17 @@ if apps.is_installed("AKScheduling"): extra_paths = [ path('api/scheduling-events/', EventsView.as_view(), name='scheduling-events'), - path('api/scheduling-room-availabilities/', RoomAvailabilitiesView.as_view(), name='scheduling-room-availabilities'), + path('api/scheduling-room-availabilities/', RoomAvailabilitiesView.as_view(), + name='scheduling-room-availabilities'), ] - event_specific_paths = [ - path('api/', include(api_router.urls), name='api'), - ] + path('api/', include(api_router.urls), name='api'), +] event_specific_paths.extend(extra_paths) - app_name = 'model' - urlpatterns = [ path( '<slug:event_slug>/', @@ -42,3 +42,37 @@ urlpatterns = [ ), path('user/', views.UserView.as_view(), name="user"), ] + + +def get_admin_urls_event_wizard(admin_site): + return [ + path('add/wizard/start/', admin_site.admin_view(NewEventWizardStartView.as_view()), + name="new_event_wizard_start"), + path('add/wizard/settings/', admin_site.admin_view(NewEventWizardSettingsView.as_view()), + name="new_event_wizard_settings"), + path('add/wizard/created/<slug:event_slug>/', admin_site.admin_view(NewEventWizardPrepareImportView.as_view()), + name="new_event_wizard_prepare_import"), + path('add/wizard/import/<slug:event_slug>/from/<slug:import_slug>/', + admin_site.admin_view(NewEventWizardImportView.as_view()), + name="new_event_wizard_import"), + path('add/wizard/activate/<slug:slug>/', + admin_site.admin_view(NewEventWizardActivateView.as_view()), + name="new_event_wizard_activate"), + path('add/wizard/finish/<slug:slug>/', + admin_site.admin_view(NewEventWizardFinishView.as_view()), + name="new_event_wizard_finish"), + ] + + +def get_admin_urls_event(admin_site): + return [ + path('<slug:slug>/status/', admin_site.admin_view(EventStatusView.as_view()), name="event_status"), + path('<slug:event_slug>/requirements/', admin_site.admin_view(AKRequirementOverview.as_view()), + name="event_requirement_overview"), + path('<slug:event_slug>/ak-csv-export/', admin_site.admin_view(AKCSVExportView.as_view()), + name="ak_csv_export"), + path('<slug:event_slug>/ak-wiki-export/', admin_site.admin_view(AKWikiExportView.as_view()), + name="ak_wiki_export"), + path('<slug:slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()), + name="ak_delete_orga_messages"), + ] -- GitLab From 0a2794b5b209b6eb545ba70e2522313b0d93f7b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Sun, 9 May 2021 11:51:40 +0200 Subject: [PATCH 03/17] Unify scheduling admin url structure (now all admin/AKModel/event/slug/action) --- AKModel/admin.py | 19 +++---------------- AKScheduling/urls.py | 9 ++------- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/AKModel/admin.py b/AKModel/admin.py index 2d832ad3..4a28ed70 100644 --- a/AKModel/admin.py +++ b/AKModel/admin.py @@ -35,6 +35,9 @@ class EventAdmin(admin.ModelAdmin): def get_urls(self): urls = get_admin_urls_event_wizard(self.admin_site) urls.extend(get_admin_urls_event(self.admin_site)) + if apps.is_installed("AKScheduling"): + from AKScheduling.urls import get_admin_urls_scheduling + urls.extend(get_admin_urls_scheduling(self.admin_site)) urls.extend(super().get_urls()) return urls @@ -90,14 +93,6 @@ class AKTrackAdmin(admin.ModelAdmin): kwargs['initial'] = Event.get_next_active() return super(AKTrackAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) - def get_urls(self): - urls = super().get_urls() - custom_urls = [] - if apps.is_installed("AKScheduling"): - from AKScheduling.urls import get_admin_urls_track - custom_urls.extend(get_admin_urls_track(self.admin_site)) - return custom_urls + urls - @admin.register(AKTag) class AKTagAdmin(admin.ModelAdmin): @@ -251,14 +246,6 @@ class AKSlotAdmin(admin.ModelAdmin): readonly_fields = ['ak_details_link', 'updated'] form = AKSlotAdminForm - def get_urls(self): - urls = super().get_urls() - custom_urls = [] - if apps.is_installed("AKScheduling"): - from AKScheduling.urls import get_admin_urls_slot - custom_urls.extend(get_admin_urls_slot(self.admin_site)) - return custom_urls + urls - def get_form(self, request, obj=None, change=False, **kwargs): # Use timezone of associated event if obj is not None and obj.event.timezone: diff --git a/AKScheduling/urls.py b/AKScheduling/urls.py index 4fb483a1..171f3a1a 100644 --- a/AKScheduling/urls.py +++ b/AKScheduling/urls.py @@ -3,17 +3,12 @@ from django.urls import path from AKScheduling.views import SchedulingAdminView, UnscheduledSlotsAdminView, TrackAdminView -def get_admin_urls_slot(admin_site): +def get_admin_urls_scheduling(admin_site): return [ path('<slug:event_slug>/schedule/', admin_site.admin_view(SchedulingAdminView.as_view()), name="schedule"), path('<slug:event_slug>/unscheduled/', admin_site.admin_view(UnscheduledSlotsAdminView.as_view()), name="slots_unscheduled"), - ] - - -def get_admin_urls_track(admin_site): - return [ - path('<slug:event_slug>/manage/', admin_site.admin_view(TrackAdminView.as_view()), + path('<slug:event_slug>/tracks/', admin_site.admin_view(TrackAdminView.as_view()), name="tracks_manage"), ] -- GitLab From 3b1bbb9138e71c0c9cdd969c2f1604476c549fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Sun, 9 May 2021 19:51:34 +0200 Subject: [PATCH 04/17] Introduce API for constraint violations (protected and currently readonly) This also introduces some additional properties for human readable details of the violations to the model --- AKModel/models.py | 16 ++++++++++++++++ AKModel/urls.py | 4 +++- AKScheduling/api.py | 19 ++++++++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index b3719536..a49e87f4 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -480,5 +480,21 @@ class ConstraintViolation(models.Model): get_details.short_description = _('Details') + @property + def details(self): + return self.get_details() + + @property + def level_display(self): + return self.get_level_display() + + @property + def type_display(self): + return self.get_type_display() + + @property + def timestamp_display(self): + return self.timestamp.astimezone(self.event.timezone).strftime('%d.%m.%y %H:%M') + def __str__(self): return f"{self.get_level_display()}: {self.get_type_display()} [{self.get_details()}]" diff --git a/AKModel/urls.py b/AKModel/urls.py index 03239837..184ab594 100644 --- a/AKModel/urls.py +++ b/AKModel/urls.py @@ -17,10 +17,12 @@ api_router.register('akslot', views.AKSlotViewSet, basename='AKSlot') extra_paths = [] if apps.is_installed("AKScheduling"): - from AKScheduling.api import ResourcesViewSet, RoomAvailabilitiesView, EventsView, EventsViewSet + from AKScheduling.api import ResourcesViewSet, RoomAvailabilitiesView, EventsView, EventsViewSet, \ + ConstraintViolationsViewSet api_router.register('scheduling-resources', ResourcesViewSet, basename='scheduling-resources') api_router.register('scheduling-event', EventsViewSet, basename='scheduling-event') + api_router.register('scheduling-constraint-violations', ConstraintViolationsViewSet, basename='scheduling-constraint-violations') extra_paths = [ path('api/scheduling-events/', EventsView.as_view(), name='scheduling-events'), diff --git a/AKScheduling/api.py b/AKScheduling/api.py index 27b4252e..af73c791 100644 --- a/AKScheduling/api.py +++ b/AKScheduling/api.py @@ -7,7 +7,7 @@ from django.views.generic import ListView from rest_framework import viewsets, mixins, serializers, permissions from AKModel.availability.models import Availability -from AKModel.models import Room, AKSlot +from AKModel.models import Room, AKSlot, ConstraintViolation from AKModel.views import EventSlugMixin @@ -109,3 +109,20 @@ class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet): def get_queryset(self): return AKSlot.objects.filter(event=self.event) + + +class ConstraintViolationSerializer(serializers.ModelSerializer): + class Meta: + model = ConstraintViolation + fields = ['pk', 'type_display', 'aks', 'ak_slots', 'ak_owner', 'room', 'requirement', 'category', 'comment', 'timestamp_display', 'manually_resolved', 'level_display', 'details'] + + +class ConstraintViolationsViewSet(EventSlugMixin, viewsets.ModelViewSet): + permission_classes = (permissions.DjangoModelPermissions,) + serializer_class = ConstraintViolationSerializer + + def get_object(self): + return get_object_or_404(ConstraintViolation, pk=self.kwargs["pk"]) + + def get_queryset(self): + return ConstraintViolation.objects.filter(event=self.event) -- GitLab From 20e28d03e0f8bc378e4e985a6a0422e9a01688e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Sun, 9 May 2021 20:27:33 +0200 Subject: [PATCH 05/17] Introduce basic constraint violation admin view showing current violations (using async reloading) --- AKModel/templates/admin/AKModel/status.html | 4 + .../locale/de_DE/LC_MESSAGES/django.po | 73 ++++++++-- .../AKScheduling/constraint_violations.html | 137 ++++++++++++++++++ AKScheduling/urls.py | 5 +- AKScheduling/views.py | 15 +- 5 files changed, 216 insertions(+), 18 deletions(-) create mode 100644 AKScheduling/templates/admin/AKScheduling/constraint_violations.html diff --git a/AKModel/templates/admin/AKModel/status.html b/AKModel/templates/admin/AKModel/status.html index ee84a94a..db22d935 100644 --- a/AKModel/templates/admin/AKModel/status.html +++ b/AKModel/templates/admin/AKModel/status.html @@ -74,6 +74,10 @@ <a class="btn btn-success" href="{% url 'admin:schedule' event_slug=event.slug %}">{% trans "Scheduling" %}</a> + {% if "AKScheduling | is_installed" %} + <a class="btn btn-success" + href="{% url 'admin:constraint-violations' slug=event.slug %}">{% trans "Constraint Violations" %} <span class="badge badge-secondary">{{ event.constraintviolation_set.count }}</span></a> + {% endif %} <a class="btn btn-success" href="{% url 'admin:tracks_manage' event_slug=event.slug %}">{% trans "Manage ak tracks" %}</a> <a class="btn btn-success" diff --git a/AKScheduling/locale/de_DE/LC_MESSAGES/django.po b/AKScheduling/locale/de_DE/LC_MESSAGES/django.po index f25c002d..5ed003e8 100644 --- a/AKScheduling/locale/de_DE/LC_MESSAGES/django.po +++ b/AKScheduling/locale/de_DE/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-04-29 22:48+0000\n" +"POT-Creation-Date: 2021-05-09 18:23+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -17,49 +17,92 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:11 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:11 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:10 msgid "Scheduling for" msgstr "Scheduling für" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:126 +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:74 +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:128 +msgid "No violations" +msgstr "Keine Verletzungen" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:81 +msgid "Cannot load current violations from server" +msgstr "Kann die aktuellen Verletzungen nicht vom Server laden" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:106 +msgid "Violation(s)" +msgstr "Verletzung(en)" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:109 +msgid "Auto reload?" +msgstr "Automatisch neu laden?" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:113 +msgid "Reload now" +msgstr "Jetzt neu laden" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:118 +msgid "Violation" +msgstr "Verletzung" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:119 +msgid "Problem" +msgstr "Problem" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:120 +msgid "Details" +msgstr "Details" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:121 +msgid "Since" +msgstr "Seit" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:134 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:243 +#: AKScheduling/templates/admin/AKScheduling/scheduling.html:197 +#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34 +msgid "Event Status" +msgstr "Event-Status" + +#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:136 +msgid "Scheduling" +msgstr "Scheduling" + +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:129 msgid "Name of new ak track" msgstr "Name des neuen AK-Tracks" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:142 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:145 msgid "Could not create ak track" msgstr "Konnte neuen AK-Track nicht anlegen" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:168 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:171 msgid "Could not update ak track name" msgstr "Konnte Namen des AK-Tracks nicht ändern" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:174 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:177 msgid "Do you really want to delete this ak track?" msgstr "Soll dieser AK-Track wirklich gelöscht werden?" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:188 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:191 msgid "Could not delete ak track" msgstr "AK-Track konnte nicht gelöscht werden" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:200 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:203 msgid "Manage AK Tracks" msgstr "AK-Tracks verwalten" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:201 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:204 msgid "Add ak track" msgstr "AK-Track hinzufügen" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:206 +#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:209 msgid "AKs without track" msgstr "AKs ohne Track" -#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:240 -#: AKScheduling/templates/admin/AKScheduling/scheduling.html:197 -#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34 -msgid "Event Status" -msgstr "Event-Status" - #: AKScheduling/templates/admin/AKScheduling/scheduling.html:87 msgid "Day (Horizontal)" msgstr "Tag (horizontal)" diff --git a/AKScheduling/templates/admin/AKScheduling/constraint_violations.html b/AKScheduling/templates/admin/AKScheduling/constraint_violations.html new file mode 100644 index 00000000..8645c192 --- /dev/null +++ b/AKScheduling/templates/admin/AKScheduling/constraint_violations.html @@ -0,0 +1,137 @@ +{% extends "admin/base_site.html" %} +{% load tags_AKModel %} + +{% load i18n %} +{% load l10n %} +{% load tz %} +{% load static %} +{% load tags_AKPlan %} +{% load fontawesome_5 %} + +{% block title %}{% trans "Scheduling for" %} {{event}}{% endblock %} + +{% block extrahead %} + {{ block.super }} + + <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); + } + } + }); + + // (Re-)Load constraint violations using AJAX and visualize using violation count badge and violation table + function reload() { + $.ajax({ + url: "{% url "model:scheduling-constraint-violations-list" event_slug=event.slug %}", + type: 'GET', + success: function (response) { + console.log(response); + + let table_html = ''; + + if(response.length > 0) { + // Update violation count badge + $('#violationCountBadge').html(response.length).removeClass('badge-success').addClass('badge-warning'); + + // Update violations table + for(let i=0;i<response.length;i++) { + table_html += "<tr><td>" + response[i].level_display + "</td><td>" + response[i].type_display + "</td><td>" + response[i].details + "</td><td>" + response[i].timestamp_display + "</td><td></td></tr>"; + } + } + else { + // Update violation count badge + $('#violationCountBadge').html(0).removeClass('badge-warning').addClass('badge-success'); + + // Update violations table + table_html ='<tr class="text-muted"><td colspan="5" class="text-center">{% trans "No violations" %}</td></tr>' + } + + // Show violation list (potentially empty) in violations table + $('#violationsTableBody').html(table_html); + }, + error: function (response) { + alert("{% trans 'Cannot load current violations from server' %}"); + } + }); + } + reload(); + + // Bind reload button + $('#btnReloadNow').click(reload); + + // Toggle automatic reloading (every 30 s) based on checkbox + let autoReloadInterval = undefined; + $('#cbxAutoReload').change(function () { + if(this.checked) { + autoReloadInterval = setInterval(reload, 30*1000); + } + else { + if(autoReloadInterval !== undefined) + clearInterval(autoReloadInterval); + } + }); + }); + </script> +{% endblock extrahead %} + +{% block content %} + <h4 class="mt-4 mb-4"><span id="violationCountBadge" class="badge badge-success">0</span> {% trans "Violation(s)" %}</h4> + + <input type="checkbox" id="cbxAutoReload"> + <label for="cbxAutoReload">{% trans "Auto reload?" %}</label> + + <br> + + <a href="#" id="btnReloadNow" class="btn btn-info">{% fa5_icon "sync-alt" "fas" %} {% trans "Reload now" %}</a> + + <table class="table table-striped mt-4 mb-4"> + <thead> + <tr> + <th>{% trans "Violation" %}</th> + <th>{% trans "Problem" %}</th> + <th>{% trans "Details" %}</th> + <th>{% trans "Since" %}</th> + <th></th> + </tr> + </thead> + <tbody id="violationsTableBody"> + <tr class="text-muted"> + <td colspan="5" class="text-center"> + {% trans "No violations" %} + </td> + </tr> + </tbody> + </table> + + <a href="{% url 'admin:event_status' event.slug %}">{% trans "Event Status" %}</a> + · + <a href="{% url 'admin:schedule' event.slug %}">{% trans "Scheduling" %}</a> +{% endblock %} diff --git a/AKScheduling/urls.py b/AKScheduling/urls.py index 171f3a1a..93f49095 100644 --- a/AKScheduling/urls.py +++ b/AKScheduling/urls.py @@ -1,6 +1,7 @@ from django.urls import path -from AKScheduling.views import SchedulingAdminView, UnscheduledSlotsAdminView, TrackAdminView +from AKScheduling.views import SchedulingAdminView, UnscheduledSlotsAdminView, TrackAdminView, \ + ConstraintViolationsAdminView def get_admin_urls_scheduling(admin_site): @@ -9,6 +10,8 @@ def get_admin_urls_scheduling(admin_site): name="schedule"), path('<slug:event_slug>/unscheduled/', admin_site.admin_view(UnscheduledSlotsAdminView.as_view()), name="slots_unscheduled"), + path('<slug:slug>/constraint-violations/', admin_site.admin_view(ConstraintViolationsAdminView.as_view()), + name="constraint-violations"), path('<slug:event_slug>/tracks/', admin_site.admin_view(TrackAdminView.as_view()), name="tracks_manage"), ] diff --git a/AKScheduling/views.py b/AKScheduling/views.py index 73719bd8..9caadf5a 100644 --- a/AKScheduling/views.py +++ b/AKScheduling/views.py @@ -1,7 +1,7 @@ -from django.views.generic import ListView +from django.views.generic import ListView, DetailView from django.utils.translation import gettext_lazy as _ -from AKModel.models import AKSlot, AKTrack +from AKModel.models import AKSlot, AKTrack, Event from AKModel.views import AdminViewMixin, FilterByEventSlugMixin @@ -47,3 +47,14 @@ class TrackAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView): context = super().get_context_data(object_list=object_list, **kwargs) context["aks_without_track"] = self.event.ak_set.filter(track=None) return context + + +class ConstraintViolationsAdminView(AdminViewMixin, DetailView): + template_name = "admin/AKScheduling/constraint_violations.html" + model = Event + context_object_name = "event" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = f"{_('Constraint violations for')} {context['event']}" + return context -- GitLab From 675503a228ef08e06c396ea05fd142c98b32ed7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Wed, 4 Nov 2020 14:23:39 +0100 Subject: [PATCH 06/17] Prepare automatic constraint checking Add helper fields and methods to ConstraintViolation model Introduce helper method do determine whether two AKSlots overlap Add receivers to AKScheduling Implement stub for OWNER_TWO_SLOTS violation --- AKModel/models.py | 46 +++++++++++++++++++++++- AKScheduling/models.py | 81 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index a49e87f4..972953a9 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -325,7 +325,8 @@ class AKSlot(models.Model): duration = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Duration'), help_text=_('Length in hours')) - fixed = models.BooleanField(default=False, verbose_name=_('Scheduling fixed'), help_text=_('Length and time of this AK should not be changed')) + fixed = models.BooleanField(default=False, verbose_name=_('Scheduling fixed'), + help_text=_('Length and time of this AK should not be changed')) event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'), help_text=_('Associated event')) @@ -388,6 +389,9 @@ class AKSlot(models.Model): """ return (timezone.now() - self.updated).total_seconds() + def overlaps(self, other: "AKSlot"): + return self.start <= other.end <= self.end or self.start <= other.start <= self.end + class AKOrgaMessage(models.Model): ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'), @@ -496,5 +500,45 @@ class ConstraintViolation(models.Model): def timestamp_display(self): return self.timestamp.astimezone(self.event.timezone).strftime('%d.%m.%y %H:%M') + # TODO Automatically save this + aks_tmp = set() + + @property + def _aks(self): + """ + Get all AKs belonging to this constraint violation + + The distinction between real and tmp relationships is needed since many to many + relations only work for objects already persisted in the database + + :return: set of all AKs belonging to this constraint violation + :rtype: set(AK) + """ + if self.pk and self.pk > 0: + return set(self.aks.all()) + return self.aks_tmp + + # TODO Automatically save this + ak_slots_tmp = set() + + @property + def _ak_slots(self): + """ + Get all AK Slots belonging to this constraint violation + + The distinction between real and tmp relationships is needed since many to many + relations only work for objects already persisted in the database + + :return: set of all AK Slots belonging to this constraint violation + :rtype: set(AKSlot) + """ + if self.pk and self.pk > 0: + return set(self.ak_slots.all()) + return self.ak_slots_tmp + def __str__(self): return f"{self.get_level_display()}: {self.get_type_display()} [{self.get_details()}]" + + def __eq__(self, other): + # TODO Check if FIELDS and FIELDS_MM are equal + return super().__eq__(other) diff --git a/AKScheduling/models.py b/AKScheduling/models.py index 6b202199..990bcd4a 100644 --- a/AKScheduling/models.py +++ b/AKScheduling/models.py @@ -1 +1,80 @@ -# Create your models here. +from django.db.models.signals import post_save +from django.dispatch import receiver + +from AKModel.availability.models import Availability +from AKModel.models import AK, AKSlot, Room, Event, AKOwner, ConstraintViolation + + +@receiver(post_save, sender=AK) +def ak_changed_handler(sender, instance: AK, **kwargs): + # Changes might affect: Owner(s), Requirements, Conflicts, Prerequisites, Category, Interest + print(f"{instance} changed") + + event = instance.event + + # Owner might have changed: Might affect multiple AKs by the same owner at the same time + conflicts = [] + type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS + # For all owners... + for owner in instance.owners.all(): + # ...find overlapping AKs... + slots_by_owner : [AKSlot] = [] + slots_by_owner_this_ak : [AKSlot] = [] + aks_by_owner = owner.ak_set.all() + for ak in aks_by_owner: + if ak != instance: + slots_by_owner.extend(ak.akslot_set.filter(start__isnull=False)) + else: + # ToDo Fill this outside of loop? + slots_by_owner_this_ak.extend(ak.akslot_set.filter(start__isnull=False)) + for slot in slots_by_owner_this_ak: + for other_slot in slots_by_owner: + if slot.overlaps(other_slot): + # TODO Create ConstraintViolation here + c = ConstraintViolation( + type=type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ak_owner=owner + ) + c.aks_tmp.add(instance) + c.aks_tmp.add(other_slot.ak) + c.ak_slots_tmp.add(slot) + c.ak_slots_tmp.add(other_slot) + conflicts.append(c) + print(f"{owner} has the following conflicts: {conflicts}") + # ... and compare to/update list of existing violations of this type: + current_violations = instance.constraintviolation_set.filter(type=type) + for conflict in conflicts: + pass + # TODO Remove from list of current_violations if an equal new one is found + # TODO Otherwise, store this conflict in db + # TODO Remove all violations still in current_violations + + +@receiver(post_save, sender=AKSlot) +def akslot_changed_handler(sender, instance, **kwargs): + # Changes might affect: Duplicate parallel, Two in room + print(f"{sender} changed") + # TODO Replace with real handling + + +@receiver(post_save, sender=Room) +def room_changed_handler(sender, **kwargs): + # Changes might affect: Room size, Requirement + print(f"{sender} changed") + # TODO Replace with real handling + + +@receiver(post_save, sender=Availability) +def availability_changed_handler(sender, **kwargs): + # Changes might affect: category availability, AK availability, Room availability + print(f"{sender} changed") + # TODO Replace with real handling + + +@receiver(post_save, sender=Event) +def room_changed_handler(sender, **kwargs): + # Changes might affect: Reso-Deadline + print(f"{sender} changed") + # TODO Replace with real handling -- GitLab From 3b2164e8420033434bf7738623c092b8b0260337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Thu, 5 Nov 2020 00:03:30 +0100 Subject: [PATCH 07/17] Implement first version for OWNER_TWO_SLOTS violation check caused by AK owner change Improve constraint violation string representation to prevent endless recursion Create violation check for changed owner of AK (bound to new m2m_changed signal) Add doc comments --- AKModel/models.py | 65 ++++++++++++++++++++++---- AKScheduling/models.py | 104 +++++++++++++++++++++++++---------------- 2 files changed, 120 insertions(+), 49 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 972953a9..1cd08ac8 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -463,7 +463,7 @@ class ConstraintViolation(models.Model): help_text=_('Mark this violation manually as resolved')) fields = ['ak_owner', 'room', 'requirement', 'category'] - fields_mm = ['aks', 'ak_slots'] + fields_mm = ['_aks', '_ak_slots'] def get_details(self): """ @@ -471,10 +471,10 @@ class ConstraintViolation(models.Model): :return: string of details :rtype: str """ - output = [] - # Stringify all ManyToMany fields - for field_mm in self.fields_mm: - output.append(f"{field_mm}: {', '.join(str(a) for a in getattr(self, field_mm).all())}") + # Stringify aks and ak slots fields (m2m) + output = [f"{_('AKs')}: {self._aks_str}", + f"{_('AK Slots')}: {self._ak_slots_str}"] + # Stringify all other fields for field in self.fields: a = getattr(self, field, None) @@ -500,7 +500,6 @@ class ConstraintViolation(models.Model): def timestamp_display(self): return self.timestamp.astimezone(self.event.timezone).strftime('%d.%m.%y %H:%M') - # TODO Automatically save this aks_tmp = set() @property @@ -518,9 +517,14 @@ class ConstraintViolation(models.Model): return set(self.aks.all()) return self.aks_tmp - # TODO Automatically save this ak_slots_tmp = set() + @property + def _aks_str(self): + if self.pk and self.pk > 0: + return ', '.join(str(a) for a in self.aks.all()) + return ', '.join(str(a) for a in self.aks_tmp) + @property def _ak_slots(self): """ @@ -536,9 +540,50 @@ class ConstraintViolation(models.Model): return set(self.ak_slots.all()) return self.ak_slots_tmp + @property + def _ak_slots_str(self): + if self.pk and self.pk > 0: + return ', '.join(str(a) for a in self.ak_slots.all()) + return ', '.join(str(a) for a in self.ak_slots_tmp) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # Store temporary m2m-relations in db + for ak in self.aks_tmp: + self.aks.add(ak) + for ak_slot in self.ak_slots_tmp: + self.ak_slots.add(ak_slot) + def __str__(self): return f"{self.get_level_display()}: {self.get_type_display()} [{self.get_details()}]" - def __eq__(self, other): - # TODO Check if FIELDS and FIELDS_MM are equal - return super().__eq__(other) + def matches(self, other): + """ + Check whether one constraint violation instance matches another, + this means has the same type, room, requirement, owner, category + as well as the same lists of aks and ak slots. + PK, timestamp, comments and manual resolving are ignored. + + :param other: second instance to compare to + :type other: ConstraintViolation + :return: true if both instances are similar in the way described, false if not + :rtype: bool + """ + if not isinstance(other, ConstraintViolation): + return False + # Check type + if self.type != other.type: + return False + # Make sure both have the same aks and ak slots + for field_mm in self.fields_mm: + s: set = getattr(self, field_mm) + o: set = getattr(other, field_mm) + if len(s) != len(o): + return False + if len(s.intersection(o)) != len(s): + return False + # Check other "defining" fields + for field in self.fields: + if getattr(self, field) != getattr(other, field): + return False + return True diff --git a/AKScheduling/models.py b/AKScheduling/models.py index 990bcd4a..100266fd 100644 --- a/AKScheduling/models.py +++ b/AKScheduling/models.py @@ -1,4 +1,4 @@ -from django.db.models.signals import post_save +from django.db.models.signals import post_save, m2m_changed from django.dispatch import receiver from AKModel.availability.models import Availability @@ -8,53 +8,79 @@ from AKModel.models import AK, AKSlot, Room, Event, AKOwner, ConstraintViolation @receiver(post_save, sender=AK) def ak_changed_handler(sender, instance: AK, **kwargs): # Changes might affect: Owner(s), Requirements, Conflicts, Prerequisites, Category, Interest - print(f"{instance} changed") + pass + + +@receiver(m2m_changed, sender=AK.owners.through) +def ak_changed_handler(sender, instance: AK, action: str, **kwargs): + """ + Owners of AK changed + """ + # Only signal after change (post_add, post_delete, post_clear) are relevant + if not action.startswith("post"): + return + + # print(f"{instance} changed") event = instance.event - # Owner might have changed: Might affect multiple AKs by the same owner at the same time - conflicts = [] - type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS - # For all owners... + # Owner(s) changed: Might affect multiple AKs by the same owner(s) at the same time + violation_type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS + new_violations = [] + + slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False) + + # For all owners (after recent change)... for owner in instance.owners.all(): - # ...find overlapping AKs... - slots_by_owner : [AKSlot] = [] - slots_by_owner_this_ak : [AKSlot] = [] - aks_by_owner = owner.ak_set.all() - for ak in aks_by_owner: + # ...find other slots that might be overlapping... + + for ak in owner.ak_set.all(): + # ...find overlapping slots... if ak != instance: - slots_by_owner.extend(ak.akslot_set.filter(start__isnull=False)) - else: - # ToDo Fill this outside of loop? - slots_by_owner_this_ak.extend(ak.akslot_set.filter(start__isnull=False)) - for slot in slots_by_owner_this_ak: - for other_slot in slots_by_owner: - if slot.overlaps(other_slot): - # TODO Create ConstraintViolation here - c = ConstraintViolation( - type=type, - level=ConstraintViolation.ViolationLevel.VIOLATION, - event=event, - ak_owner=owner - ) - c.aks_tmp.add(instance) - c.aks_tmp.add(other_slot.ak) - c.ak_slots_tmp.add(slot) - c.ak_slots_tmp.add(other_slot) - conflicts.append(c) - print(f"{owner} has the following conflicts: {conflicts}") - # ... and compare to/update list of existing violations of this type: - current_violations = instance.constraintviolation_set.filter(type=type) - for conflict in conflicts: - pass - # TODO Remove from list of current_violations if an equal new one is found - # TODO Otherwise, store this conflict in db - # TODO Remove all violations still in current_violations + for slot in slots_of_this_ak: + for other_slot in ak.akslot_set.filter(start__isnull=False): + if slot.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ak_owner=owner + ) + c.aks_tmp.add(instance) + c.aks_tmp.add(other_slot.ak) + c.ak_slots_tmp.add(slot) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + # print(f"{owner} has the following conflicts: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + # print(existing_violations_to_check) + + for new_violation in new_violations: + found_match = False + for existing_violation in existing_violations_to_check: + if existing_violation.matches(new_violation): + # Remove from existing violations set since it should stay in db + existing_violations_to_check.remove(existing_violation) + found_match = True + break + + # Only save new violation if no match was found + if not found_match: + new_violation.save() + + # Cleanup obsolete violations (ones without matches computed under current conditions) + for outdated_violation in existing_violations_to_check: + outdated_violation.delete() @receiver(post_save, sender=AKSlot) def akslot_changed_handler(sender, instance, **kwargs): - # Changes might affect: Duplicate parallel, Two in room + # Changes might affect: Duplicate parallel, Two in room, Resodeadline print(f"{sender} changed") # TODO Replace with real handling -- GitLab From a43b02076cb1b59ff463bb56418349db18dcc422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Wed, 12 May 2021 01:16:50 +0200 Subject: [PATCH 08/17] Fix temporary m2m relations of constraint violation Previously, ak and ak slot sets were modelled as class attributes, which could cause wrong AKs to show up in a relation when multiple constraints were updated in the same function call --- AKModel/models.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 1cd08ac8..e1435c3e 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -465,6 +465,11 @@ class ConstraintViolation(models.Model): fields = ['ak_owner', 'room', 'requirement', 'category'] fields_mm = ['_aks', '_ak_slots'] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.aks_tmp = set() + self.ak_slots_tmp = set() + def get_details(self): """ Get details of this constraint (all fields connected to it) @@ -500,8 +505,6 @@ class ConstraintViolation(models.Model): def timestamp_display(self): return self.timestamp.astimezone(self.event.timezone).strftime('%d.%m.%y %H:%M') - aks_tmp = set() - @property def _aks(self): """ @@ -517,8 +520,6 @@ class ConstraintViolation(models.Model): return set(self.aks.all()) return self.aks_tmp - ak_slots_tmp = set() - @property def _aks_str(self): if self.pk and self.pk > 0: -- GitLab From b85b61087edde7c6480b2d2b11e9b66f9df4e185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Wed, 12 May 2021 01:18:35 +0200 Subject: [PATCH 09/17] Implement more violation checks and extract common code into a function Implement check for overlapping AK slots in the same room Implement check for overlapping AK slots by the same owner triggered by changing the slot --- AKScheduling/models.py | 115 ++++++++++++++++++++++++++++++++++------- 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/AKScheduling/models.py b/AKScheduling/models.py index 100266fd..4aa0fb22 100644 --- a/AKScheduling/models.py +++ b/AKScheduling/models.py @@ -5,6 +5,35 @@ from AKModel.availability.models import Availability from AKModel.models import AK, AKSlot, Room, Event, AKOwner, ConstraintViolation +def update_constraint_violations(new_violations, existing_violations_to_check): + """ + Update existing constraint violations (subset for which new violations were computed) based on these new violations. + This will add all new violations without a match, preserve the matching ones + and delete the obsolete ones (those without a match from the newly calculated violations). + + :param new_violations: list of new (not yet saved) violations that exist after the last change + :type new_violations: list[ConstraintViolation] + :param existing_violations_to_check: list of related violations currently in the db + :type existing_violations_to_check: list[ConstraintViolation] + """ + for new_violation in new_violations: + found_match = False + for existing_violation in existing_violations_to_check: + if existing_violation.matches(new_violation): + # Remove from existing violations set since it should stay in db + existing_violations_to_check.remove(existing_violation) + found_match = True + break + + # Only save new violation if no match was found + if not found_match: + new_violation.save() + + # Cleanup obsolete violations (ones without matches computed under current conditions) + for outdated_violation in existing_violations_to_check: + outdated_violation.delete() + + @receiver(post_save, sender=AK) def ak_changed_handler(sender, instance: AK, **kwargs): # Changes might affect: Owner(s), Requirements, Conflicts, Prerequisites, Category, Interest @@ -59,30 +88,78 @@ def ak_changed_handler(sender, instance: AK, action: str, **kwargs): # belonging to the AK that was recently changed (important!) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) # print(existing_violations_to_check) - - for new_violation in new_violations: - found_match = False - for existing_violation in existing_violations_to_check: - if existing_violation.matches(new_violation): - # Remove from existing violations set since it should stay in db - existing_violations_to_check.remove(existing_violation) - found_match = True - break - - # Only save new violation if no match was found - if not found_match: - new_violation.save() - - # Cleanup obsolete violations (ones without matches computed under current conditions) - for outdated_violation in existing_violations_to_check: - outdated_violation.delete() + update_constraint_violations(new_violations, existing_violations_to_check) @receiver(post_save, sender=AKSlot) -def akslot_changed_handler(sender, instance, **kwargs): +def akslot_changed_handler(sender, instance: AKSlot, **kwargs): # Changes might affect: Duplicate parallel, Two in room, Resodeadline print(f"{sender} changed") - # TODO Replace with real handling + event = instance.event + + # == Check for two parallel slots by one of the owners == + + violation_type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS + new_violations = [] + + # For all owners (after recent change)... + for owner in instance.ak.owners.all(): + # ...find other slots that might be overlapping... + + for ak in owner.ak_set.all(): + # ...find overlapping slots... + if ak != instance.ak: + for other_slot in ak.akslot_set.filter(start__isnull=False): + if instance.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ak_owner=owner + ) + c.aks_tmp.add(instance.ak) + c.aks_tmp.add(other_slot.ak) + c.ak_slots_tmp.add(instance) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + print(f"{owner} has the following conflicts: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) + + # == Check for two aks in the same room at the same time == + + violation_type = ConstraintViolation.ViolationType.ROOM_TWO_SLOTS + new_violations = [] + + # For all slots in this room... + for other_slot in instance.room.akslot_set.all(): + if other_slot != instance: + # ... find overlapping slots... + if instance.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.WARNING, + event=event, + room=instance.room + ) + c.ak_slots_tmp.add(instance) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + print(f"Multiple slots in room {instance.room}: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.room.constraintviolation_set.filter(type=violation_type)) + print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) @receiver(post_save, sender=Room) -- GitLab From e34b6280c2823e3182b828ee3cc222bb50e7191d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Wed, 12 May 2021 01:41:42 +0200 Subject: [PATCH 10/17] Implement reso deadline violation check (both for slot and event changes) Additionally, make sure that ak fields are always filled (was not the case for multiple aks in one room check) --- AKScheduling/models.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/AKScheduling/models.py b/AKScheduling/models.py index 4aa0fb22..29624910 100644 --- a/AKScheduling/models.py +++ b/AKScheduling/models.py @@ -34,6 +34,28 @@ def update_constraint_violations(new_violations, existing_violations_to_check): outdated_violation.delete() +def update_cv_reso_deadline_for_slot(slot): + """ + Update constraint violation AK_AFTER_RESODEADLINE for given slot + + :param slot: slot to check/update + :type slot: AKSlot + """ + event = slot.event + if slot.ak.reso and slot.event.reso_deadline: + violation_type = ConstraintViolation.ViolationType.AK_AFTER_RESODEADLINE + new_violations = [] + if slot.end > event.reso_deadline: + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ) + c.aks_tmp.add(slot.ak) + c.ak_slots_tmp.add(slot) + new_violations.append(c) + update_constraint_violations(new_violations, list(slot.constraintviolation_set.filter(type=violation_type))) + @receiver(post_save, sender=AK) def ak_changed_handler(sender, instance: AK, **kwargs): # Changes might affect: Owner(s), Requirements, Conflicts, Prerequisites, Category, Interest @@ -149,6 +171,8 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): event=event, room=instance.room ) + c.aks_tmp.add(instance.ak) + c.aks_tmp.add(other_slot.ak) c.ak_slots_tmp.add(instance) c.ak_slots_tmp.add(other_slot) new_violations.append(c) @@ -161,6 +185,10 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): print(existing_violations_to_check) update_constraint_violations(new_violations, existing_violations_to_check) + # == Check for reso ak after reso deadline == + + update_cv_reso_deadline_for_slot(instance) + @receiver(post_save, sender=Room) def room_changed_handler(sender, **kwargs): @@ -177,7 +205,8 @@ def availability_changed_handler(sender, **kwargs): @receiver(post_save, sender=Event) -def room_changed_handler(sender, **kwargs): - # Changes might affect: Reso-Deadline - print(f"{sender} changed") - # TODO Replace with real handling +def room_changed_handler(sender, instance, **kwargs): + # == Check for reso ak after reso deadline (which might have changed) == + if instance.reso_deadline: + for slot in instance.akslot_set.filter(start__isnull=False, ak__reso=True): + update_cv_reso_deadline_for_slot(slot) -- GitLab From 9664edc4625c44eb9a374f441959d972a2bcc609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Wed, 12 May 2021 01:48:35 +0200 Subject: [PATCH 11/17] Implement violation check for multiple slots of the same AK at the same time --- AKScheduling/models.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/AKScheduling/models.py b/AKScheduling/models.py index 29624910..3104d90e 100644 --- a/AKScheduling/models.py +++ b/AKScheduling/models.py @@ -180,7 +180,7 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): print(f"Multiple slots in room {instance.room}: {new_violations}") # ... and compare to/update list of existing violations of this type - # belonging to the AK that was recently changed (important!) + # belonging to the slot that was recently changed (important!) existing_violations_to_check = list(instance.room.constraintviolation_set.filter(type=violation_type)) print(existing_violations_to_check) update_constraint_violations(new_violations, existing_violations_to_check) @@ -189,6 +189,32 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): update_cv_reso_deadline_for_slot(instance) + # == Check for two slots of the same AK at the same time (warning) == + + violation_type = ConstraintViolation.ViolationType.AK_SLOT_COLLISION + new_violations = [] + + # For all other slots of this ak... + for other_slot in instance.ak.akslot_set.filter(start__isnull=False): + if other_slot != instance: + # ... find overlapping slots... + if instance.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.WARNING, + event=event, + ) + c.aks_tmp.add(instance.ak) + c.ak_slots_tmp.add(instance) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + # ... and compare to/update list of existing violations of this type + # belonging to the slot that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + update_constraint_violations(new_violations, existing_violations_to_check) + @receiver(post_save, sender=Room) def room_changed_handler(sender, **kwargs): -- GitLab From 17ae90884b1e8fa912d8670d55439b14a9e3edc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Wed, 4 Nov 2020 14:23:39 +0100 Subject: [PATCH 12/17] Prepare automatic constraint checking Add helper fields and methods to ConstraintViolation model Introduce helper method do determine whether two AKSlots overlap Add receivers to AKScheduling Implement stub for OWNER_TWO_SLOTS violation --- AKModel/models.py | 46 +++++++++++++++++++++++- AKScheduling/models.py | 81 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 125 insertions(+), 2 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index a49e87f4..972953a9 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -325,7 +325,8 @@ class AKSlot(models.Model): duration = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Duration'), help_text=_('Length in hours')) - fixed = models.BooleanField(default=False, verbose_name=_('Scheduling fixed'), help_text=_('Length and time of this AK should not be changed')) + fixed = models.BooleanField(default=False, verbose_name=_('Scheduling fixed'), + help_text=_('Length and time of this AK should not be changed')) event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'), help_text=_('Associated event')) @@ -388,6 +389,9 @@ class AKSlot(models.Model): """ return (timezone.now() - self.updated).total_seconds() + def overlaps(self, other: "AKSlot"): + return self.start <= other.end <= self.end or self.start <= other.start <= self.end + class AKOrgaMessage(models.Model): ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'), @@ -496,5 +500,45 @@ class ConstraintViolation(models.Model): def timestamp_display(self): return self.timestamp.astimezone(self.event.timezone).strftime('%d.%m.%y %H:%M') + # TODO Automatically save this + aks_tmp = set() + + @property + def _aks(self): + """ + Get all AKs belonging to this constraint violation + + The distinction between real and tmp relationships is needed since many to many + relations only work for objects already persisted in the database + + :return: set of all AKs belonging to this constraint violation + :rtype: set(AK) + """ + if self.pk and self.pk > 0: + return set(self.aks.all()) + return self.aks_tmp + + # TODO Automatically save this + ak_slots_tmp = set() + + @property + def _ak_slots(self): + """ + Get all AK Slots belonging to this constraint violation + + The distinction between real and tmp relationships is needed since many to many + relations only work for objects already persisted in the database + + :return: set of all AK Slots belonging to this constraint violation + :rtype: set(AKSlot) + """ + if self.pk and self.pk > 0: + return set(self.ak_slots.all()) + return self.ak_slots_tmp + def __str__(self): return f"{self.get_level_display()}: {self.get_type_display()} [{self.get_details()}]" + + def __eq__(self, other): + # TODO Check if FIELDS and FIELDS_MM are equal + return super().__eq__(other) diff --git a/AKScheduling/models.py b/AKScheduling/models.py index 6b202199..990bcd4a 100644 --- a/AKScheduling/models.py +++ b/AKScheduling/models.py @@ -1 +1,80 @@ -# Create your models here. +from django.db.models.signals import post_save +from django.dispatch import receiver + +from AKModel.availability.models import Availability +from AKModel.models import AK, AKSlot, Room, Event, AKOwner, ConstraintViolation + + +@receiver(post_save, sender=AK) +def ak_changed_handler(sender, instance: AK, **kwargs): + # Changes might affect: Owner(s), Requirements, Conflicts, Prerequisites, Category, Interest + print(f"{instance} changed") + + event = instance.event + + # Owner might have changed: Might affect multiple AKs by the same owner at the same time + conflicts = [] + type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS + # For all owners... + for owner in instance.owners.all(): + # ...find overlapping AKs... + slots_by_owner : [AKSlot] = [] + slots_by_owner_this_ak : [AKSlot] = [] + aks_by_owner = owner.ak_set.all() + for ak in aks_by_owner: + if ak != instance: + slots_by_owner.extend(ak.akslot_set.filter(start__isnull=False)) + else: + # ToDo Fill this outside of loop? + slots_by_owner_this_ak.extend(ak.akslot_set.filter(start__isnull=False)) + for slot in slots_by_owner_this_ak: + for other_slot in slots_by_owner: + if slot.overlaps(other_slot): + # TODO Create ConstraintViolation here + c = ConstraintViolation( + type=type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ak_owner=owner + ) + c.aks_tmp.add(instance) + c.aks_tmp.add(other_slot.ak) + c.ak_slots_tmp.add(slot) + c.ak_slots_tmp.add(other_slot) + conflicts.append(c) + print(f"{owner} has the following conflicts: {conflicts}") + # ... and compare to/update list of existing violations of this type: + current_violations = instance.constraintviolation_set.filter(type=type) + for conflict in conflicts: + pass + # TODO Remove from list of current_violations if an equal new one is found + # TODO Otherwise, store this conflict in db + # TODO Remove all violations still in current_violations + + +@receiver(post_save, sender=AKSlot) +def akslot_changed_handler(sender, instance, **kwargs): + # Changes might affect: Duplicate parallel, Two in room + print(f"{sender} changed") + # TODO Replace with real handling + + +@receiver(post_save, sender=Room) +def room_changed_handler(sender, **kwargs): + # Changes might affect: Room size, Requirement + print(f"{sender} changed") + # TODO Replace with real handling + + +@receiver(post_save, sender=Availability) +def availability_changed_handler(sender, **kwargs): + # Changes might affect: category availability, AK availability, Room availability + print(f"{sender} changed") + # TODO Replace with real handling + + +@receiver(post_save, sender=Event) +def room_changed_handler(sender, **kwargs): + # Changes might affect: Reso-Deadline + print(f"{sender} changed") + # TODO Replace with real handling -- GitLab From 06b50a56812834cff8ef910ede0ba311eea4d7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Thu, 5 Nov 2020 00:03:30 +0100 Subject: [PATCH 13/17] Implement first version for OWNER_TWO_SLOTS violation check caused by AK owner change Improve constraint violation string representation to prevent endless recursion Create violation check for changed owner of AK (bound to new m2m_changed signal) Add doc comments --- AKModel/models.py | 65 ++++++++++++++++++++++---- AKScheduling/models.py | 104 +++++++++++++++++++++++++---------------- 2 files changed, 120 insertions(+), 49 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 972953a9..1cd08ac8 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -463,7 +463,7 @@ class ConstraintViolation(models.Model): help_text=_('Mark this violation manually as resolved')) fields = ['ak_owner', 'room', 'requirement', 'category'] - fields_mm = ['aks', 'ak_slots'] + fields_mm = ['_aks', '_ak_slots'] def get_details(self): """ @@ -471,10 +471,10 @@ class ConstraintViolation(models.Model): :return: string of details :rtype: str """ - output = [] - # Stringify all ManyToMany fields - for field_mm in self.fields_mm: - output.append(f"{field_mm}: {', '.join(str(a) for a in getattr(self, field_mm).all())}") + # Stringify aks and ak slots fields (m2m) + output = [f"{_('AKs')}: {self._aks_str}", + f"{_('AK Slots')}: {self._ak_slots_str}"] + # Stringify all other fields for field in self.fields: a = getattr(self, field, None) @@ -500,7 +500,6 @@ class ConstraintViolation(models.Model): def timestamp_display(self): return self.timestamp.astimezone(self.event.timezone).strftime('%d.%m.%y %H:%M') - # TODO Automatically save this aks_tmp = set() @property @@ -518,9 +517,14 @@ class ConstraintViolation(models.Model): return set(self.aks.all()) return self.aks_tmp - # TODO Automatically save this ak_slots_tmp = set() + @property + def _aks_str(self): + if self.pk and self.pk > 0: + return ', '.join(str(a) for a in self.aks.all()) + return ', '.join(str(a) for a in self.aks_tmp) + @property def _ak_slots(self): """ @@ -536,9 +540,50 @@ class ConstraintViolation(models.Model): return set(self.ak_slots.all()) return self.ak_slots_tmp + @property + def _ak_slots_str(self): + if self.pk and self.pk > 0: + return ', '.join(str(a) for a in self.ak_slots.all()) + return ', '.join(str(a) for a in self.ak_slots_tmp) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # Store temporary m2m-relations in db + for ak in self.aks_tmp: + self.aks.add(ak) + for ak_slot in self.ak_slots_tmp: + self.ak_slots.add(ak_slot) + def __str__(self): return f"{self.get_level_display()}: {self.get_type_display()} [{self.get_details()}]" - def __eq__(self, other): - # TODO Check if FIELDS and FIELDS_MM are equal - return super().__eq__(other) + def matches(self, other): + """ + Check whether one constraint violation instance matches another, + this means has the same type, room, requirement, owner, category + as well as the same lists of aks and ak slots. + PK, timestamp, comments and manual resolving are ignored. + + :param other: second instance to compare to + :type other: ConstraintViolation + :return: true if both instances are similar in the way described, false if not + :rtype: bool + """ + if not isinstance(other, ConstraintViolation): + return False + # Check type + if self.type != other.type: + return False + # Make sure both have the same aks and ak slots + for field_mm in self.fields_mm: + s: set = getattr(self, field_mm) + o: set = getattr(other, field_mm) + if len(s) != len(o): + return False + if len(s.intersection(o)) != len(s): + return False + # Check other "defining" fields + for field in self.fields: + if getattr(self, field) != getattr(other, field): + return False + return True diff --git a/AKScheduling/models.py b/AKScheduling/models.py index 990bcd4a..100266fd 100644 --- a/AKScheduling/models.py +++ b/AKScheduling/models.py @@ -1,4 +1,4 @@ -from django.db.models.signals import post_save +from django.db.models.signals import post_save, m2m_changed from django.dispatch import receiver from AKModel.availability.models import Availability @@ -8,53 +8,79 @@ from AKModel.models import AK, AKSlot, Room, Event, AKOwner, ConstraintViolation @receiver(post_save, sender=AK) def ak_changed_handler(sender, instance: AK, **kwargs): # Changes might affect: Owner(s), Requirements, Conflicts, Prerequisites, Category, Interest - print(f"{instance} changed") + pass + + +@receiver(m2m_changed, sender=AK.owners.through) +def ak_changed_handler(sender, instance: AK, action: str, **kwargs): + """ + Owners of AK changed + """ + # Only signal after change (post_add, post_delete, post_clear) are relevant + if not action.startswith("post"): + return + + # print(f"{instance} changed") event = instance.event - # Owner might have changed: Might affect multiple AKs by the same owner at the same time - conflicts = [] - type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS - # For all owners... + # Owner(s) changed: Might affect multiple AKs by the same owner(s) at the same time + violation_type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS + new_violations = [] + + slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False) + + # For all owners (after recent change)... for owner in instance.owners.all(): - # ...find overlapping AKs... - slots_by_owner : [AKSlot] = [] - slots_by_owner_this_ak : [AKSlot] = [] - aks_by_owner = owner.ak_set.all() - for ak in aks_by_owner: + # ...find other slots that might be overlapping... + + for ak in owner.ak_set.all(): + # ...find overlapping slots... if ak != instance: - slots_by_owner.extend(ak.akslot_set.filter(start__isnull=False)) - else: - # ToDo Fill this outside of loop? - slots_by_owner_this_ak.extend(ak.akslot_set.filter(start__isnull=False)) - for slot in slots_by_owner_this_ak: - for other_slot in slots_by_owner: - if slot.overlaps(other_slot): - # TODO Create ConstraintViolation here - c = ConstraintViolation( - type=type, - level=ConstraintViolation.ViolationLevel.VIOLATION, - event=event, - ak_owner=owner - ) - c.aks_tmp.add(instance) - c.aks_tmp.add(other_slot.ak) - c.ak_slots_tmp.add(slot) - c.ak_slots_tmp.add(other_slot) - conflicts.append(c) - print(f"{owner} has the following conflicts: {conflicts}") - # ... and compare to/update list of existing violations of this type: - current_violations = instance.constraintviolation_set.filter(type=type) - for conflict in conflicts: - pass - # TODO Remove from list of current_violations if an equal new one is found - # TODO Otherwise, store this conflict in db - # TODO Remove all violations still in current_violations + for slot in slots_of_this_ak: + for other_slot in ak.akslot_set.filter(start__isnull=False): + if slot.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ak_owner=owner + ) + c.aks_tmp.add(instance) + c.aks_tmp.add(other_slot.ak) + c.ak_slots_tmp.add(slot) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + # print(f"{owner} has the following conflicts: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + # print(existing_violations_to_check) + + for new_violation in new_violations: + found_match = False + for existing_violation in existing_violations_to_check: + if existing_violation.matches(new_violation): + # Remove from existing violations set since it should stay in db + existing_violations_to_check.remove(existing_violation) + found_match = True + break + + # Only save new violation if no match was found + if not found_match: + new_violation.save() + + # Cleanup obsolete violations (ones without matches computed under current conditions) + for outdated_violation in existing_violations_to_check: + outdated_violation.delete() @receiver(post_save, sender=AKSlot) def akslot_changed_handler(sender, instance, **kwargs): - # Changes might affect: Duplicate parallel, Two in room + # Changes might affect: Duplicate parallel, Two in room, Resodeadline print(f"{sender} changed") # TODO Replace with real handling -- GitLab From cbbd0475f9d1c2414e7bec5443353fc771b9978f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Wed, 12 May 2021 01:16:50 +0200 Subject: [PATCH 14/17] Fix temporary m2m relations of constraint violation Previously, ak and ak slot sets were modelled as class attributes, which could cause wrong AKs to show up in a relation when multiple constraints were updated in the same function call --- AKModel/models.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/AKModel/models.py b/AKModel/models.py index 1cd08ac8..e1435c3e 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -465,6 +465,11 @@ class ConstraintViolation(models.Model): fields = ['ak_owner', 'room', 'requirement', 'category'] fields_mm = ['_aks', '_ak_slots'] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.aks_tmp = set() + self.ak_slots_tmp = set() + def get_details(self): """ Get details of this constraint (all fields connected to it) @@ -500,8 +505,6 @@ class ConstraintViolation(models.Model): def timestamp_display(self): return self.timestamp.astimezone(self.event.timezone).strftime('%d.%m.%y %H:%M') - aks_tmp = set() - @property def _aks(self): """ @@ -517,8 +520,6 @@ class ConstraintViolation(models.Model): return set(self.aks.all()) return self.aks_tmp - ak_slots_tmp = set() - @property def _aks_str(self): if self.pk and self.pk > 0: -- GitLab From 24182767c04d1e709b890dc63f5428428867ac14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Wed, 12 May 2021 01:18:35 +0200 Subject: [PATCH 15/17] Implement more violation checks and extract common code into a function Implement check for overlapping AK slots in the same room Implement check for overlapping AK slots by the same owner triggered by changing the slot --- AKScheduling/models.py | 115 ++++++++++++++++++++++++++++++++++------- 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/AKScheduling/models.py b/AKScheduling/models.py index 100266fd..4aa0fb22 100644 --- a/AKScheduling/models.py +++ b/AKScheduling/models.py @@ -5,6 +5,35 @@ from AKModel.availability.models import Availability from AKModel.models import AK, AKSlot, Room, Event, AKOwner, ConstraintViolation +def update_constraint_violations(new_violations, existing_violations_to_check): + """ + Update existing constraint violations (subset for which new violations were computed) based on these new violations. + This will add all new violations without a match, preserve the matching ones + and delete the obsolete ones (those without a match from the newly calculated violations). + + :param new_violations: list of new (not yet saved) violations that exist after the last change + :type new_violations: list[ConstraintViolation] + :param existing_violations_to_check: list of related violations currently in the db + :type existing_violations_to_check: list[ConstraintViolation] + """ + for new_violation in new_violations: + found_match = False + for existing_violation in existing_violations_to_check: + if existing_violation.matches(new_violation): + # Remove from existing violations set since it should stay in db + existing_violations_to_check.remove(existing_violation) + found_match = True + break + + # Only save new violation if no match was found + if not found_match: + new_violation.save() + + # Cleanup obsolete violations (ones without matches computed under current conditions) + for outdated_violation in existing_violations_to_check: + outdated_violation.delete() + + @receiver(post_save, sender=AK) def ak_changed_handler(sender, instance: AK, **kwargs): # Changes might affect: Owner(s), Requirements, Conflicts, Prerequisites, Category, Interest @@ -59,30 +88,78 @@ def ak_changed_handler(sender, instance: AK, action: str, **kwargs): # belonging to the AK that was recently changed (important!) existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) # print(existing_violations_to_check) - - for new_violation in new_violations: - found_match = False - for existing_violation in existing_violations_to_check: - if existing_violation.matches(new_violation): - # Remove from existing violations set since it should stay in db - existing_violations_to_check.remove(existing_violation) - found_match = True - break - - # Only save new violation if no match was found - if not found_match: - new_violation.save() - - # Cleanup obsolete violations (ones without matches computed under current conditions) - for outdated_violation in existing_violations_to_check: - outdated_violation.delete() + update_constraint_violations(new_violations, existing_violations_to_check) @receiver(post_save, sender=AKSlot) -def akslot_changed_handler(sender, instance, **kwargs): +def akslot_changed_handler(sender, instance: AKSlot, **kwargs): # Changes might affect: Duplicate parallel, Two in room, Resodeadline print(f"{sender} changed") - # TODO Replace with real handling + event = instance.event + + # == Check for two parallel slots by one of the owners == + + violation_type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS + new_violations = [] + + # For all owners (after recent change)... + for owner in instance.ak.owners.all(): + # ...find other slots that might be overlapping... + + for ak in owner.ak_set.all(): + # ...find overlapping slots... + if ak != instance.ak: + for other_slot in ak.akslot_set.filter(start__isnull=False): + if instance.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ak_owner=owner + ) + c.aks_tmp.add(instance.ak) + c.aks_tmp.add(other_slot.ak) + c.ak_slots_tmp.add(instance) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + print(f"{owner} has the following conflicts: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) + + # == Check for two aks in the same room at the same time == + + violation_type = ConstraintViolation.ViolationType.ROOM_TWO_SLOTS + new_violations = [] + + # For all slots in this room... + for other_slot in instance.room.akslot_set.all(): + if other_slot != instance: + # ... find overlapping slots... + if instance.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.WARNING, + event=event, + room=instance.room + ) + c.ak_slots_tmp.add(instance) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + print(f"Multiple slots in room {instance.room}: {new_violations}") + + # ... and compare to/update list of existing violations of this type + # belonging to the AK that was recently changed (important!) + existing_violations_to_check = list(instance.room.constraintviolation_set.filter(type=violation_type)) + print(existing_violations_to_check) + update_constraint_violations(new_violations, existing_violations_to_check) @receiver(post_save, sender=Room) -- GitLab From f2bca5495f1e877c5d1576d4b81fbd8fe3ea5c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Wed, 12 May 2021 01:41:42 +0200 Subject: [PATCH 16/17] Implement reso deadline violation check (both for slot and event changes) Additionally, make sure that ak fields are always filled (was not the case for multiple aks in one room check) --- AKScheduling/models.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/AKScheduling/models.py b/AKScheduling/models.py index 4aa0fb22..29624910 100644 --- a/AKScheduling/models.py +++ b/AKScheduling/models.py @@ -34,6 +34,28 @@ def update_constraint_violations(new_violations, existing_violations_to_check): outdated_violation.delete() +def update_cv_reso_deadline_for_slot(slot): + """ + Update constraint violation AK_AFTER_RESODEADLINE for given slot + + :param slot: slot to check/update + :type slot: AKSlot + """ + event = slot.event + if slot.ak.reso and slot.event.reso_deadline: + violation_type = ConstraintViolation.ViolationType.AK_AFTER_RESODEADLINE + new_violations = [] + if slot.end > event.reso_deadline: + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.VIOLATION, + event=event, + ) + c.aks_tmp.add(slot.ak) + c.ak_slots_tmp.add(slot) + new_violations.append(c) + update_constraint_violations(new_violations, list(slot.constraintviolation_set.filter(type=violation_type))) + @receiver(post_save, sender=AK) def ak_changed_handler(sender, instance: AK, **kwargs): # Changes might affect: Owner(s), Requirements, Conflicts, Prerequisites, Category, Interest @@ -149,6 +171,8 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): event=event, room=instance.room ) + c.aks_tmp.add(instance.ak) + c.aks_tmp.add(other_slot.ak) c.ak_slots_tmp.add(instance) c.ak_slots_tmp.add(other_slot) new_violations.append(c) @@ -161,6 +185,10 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): print(existing_violations_to_check) update_constraint_violations(new_violations, existing_violations_to_check) + # == Check for reso ak after reso deadline == + + update_cv_reso_deadline_for_slot(instance) + @receiver(post_save, sender=Room) def room_changed_handler(sender, **kwargs): @@ -177,7 +205,8 @@ def availability_changed_handler(sender, **kwargs): @receiver(post_save, sender=Event) -def room_changed_handler(sender, **kwargs): - # Changes might affect: Reso-Deadline - print(f"{sender} changed") - # TODO Replace with real handling +def room_changed_handler(sender, instance, **kwargs): + # == Check for reso ak after reso deadline (which might have changed) == + if instance.reso_deadline: + for slot in instance.akslot_set.filter(start__isnull=False, ak__reso=True): + update_cv_reso_deadline_for_slot(slot) -- GitLab From ff9a7e7b189da024ec1f157ee8ef554baa3b5469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Wed, 12 May 2021 01:48:35 +0200 Subject: [PATCH 17/17] Implement violation check for multiple slots of the same AK at the same time --- AKScheduling/models.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/AKScheduling/models.py b/AKScheduling/models.py index 29624910..3104d90e 100644 --- a/AKScheduling/models.py +++ b/AKScheduling/models.py @@ -180,7 +180,7 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): print(f"Multiple slots in room {instance.room}: {new_violations}") # ... and compare to/update list of existing violations of this type - # belonging to the AK that was recently changed (important!) + # belonging to the slot that was recently changed (important!) existing_violations_to_check = list(instance.room.constraintviolation_set.filter(type=violation_type)) print(existing_violations_to_check) update_constraint_violations(new_violations, existing_violations_to_check) @@ -189,6 +189,32 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs): update_cv_reso_deadline_for_slot(instance) + # == Check for two slots of the same AK at the same time (warning) == + + violation_type = ConstraintViolation.ViolationType.AK_SLOT_COLLISION + new_violations = [] + + # For all other slots of this ak... + for other_slot in instance.ak.akslot_set.filter(start__isnull=False): + if other_slot != instance: + # ... find overlapping slots... + if instance.overlaps(other_slot): + # ...and create a temporary violation if necessary... + c = ConstraintViolation( + type=violation_type, + level=ConstraintViolation.ViolationLevel.WARNING, + event=event, + ) + c.aks_tmp.add(instance.ak) + c.ak_slots_tmp.add(instance) + c.ak_slots_tmp.add(other_slot) + new_violations.append(c) + + # ... and compare to/update list of existing violations of this type + # belonging to the slot that was recently changed (important!) + existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type)) + update_constraint_violations(new_violations, existing_violations_to_check) + @receiver(post_save, sender=Room) def room_changed_handler(sender, **kwargs): -- GitLab