From d237c85f81beae4491cc75c15fc93fcb48d7e20b Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 May 2024 04:38:46 +0200
Subject: [PATCH 01/77] JSON import and export code, migrated from mmarx fork

---
 AKModel/forms.py                              |  9 ++
 AKModel/models.py                             | 77 +++++++++++++++++
 .../admin/AKModel/ak_json_export.html         | 19 +++++
 AKModel/urls.py                               |  9 +-
 AKModel/views/ak.py                           | 85 +++++++++++++++++++
 AKModel/views/manage.py                       | 12 ++-
 AKModel/views/status.py                       |  8 ++
 7 files changed, 216 insertions(+), 3 deletions(-)
 create mode 100644 AKModel/templates/admin/AKModel/ak_json_export.html

diff --git a/AKModel/forms.py b/AKModel/forms.py
index 4d1fe7ef..f74dc39e 100644
--- a/AKModel/forms.py
+++ b/AKModel/forms.py
@@ -272,3 +272,12 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm):
         # Filter possible values for m2m when event is specified
         if hasattr(self.instance, "event") and self.instance.event is not None:
             self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event)
+
+
+class JSONImportForm(AdminIntermediateForm):
+    json_data = forms.CharField(
+        required=True,
+        widget=forms.Textarea,
+        label=_("JSON data"),
+        help_text=_("JSON data from the scheduling solver"),
+    )
diff --git a/AKModel/models.py b/AKModel/models.py
index 22586a57..c4c6f804 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -1,4 +1,5 @@
 import itertools
+import json
 from datetime import timedelta
 
 from django.db import models
@@ -162,6 +163,66 @@ class Event(models.Model):
                 .filter(availabilities__count=0, owners__count__gt=0)
                 )
 
+    def time_slots(self, *, slots_in_an_hour=1.0):
+        from AKModel.availability.models import Availability
+
+        rooms = Room.objects.filter(event=self)
+        slot_duration = timedelta(hours=(1.0 / slots_in_an_hour))
+        slot_index = 0
+        current_slot = self.start
+        current_block = []
+        previous_slot = None
+
+        room_availabilities = list({availability
+                               for room in rooms
+                               for availability in room.availabilities.all()})
+
+        while current_slot < self.end:
+            slot = Availability(event=self,
+                                start=current_slot,
+                                end=current_slot + slot_duration)
+
+            if any((availability.contains(slot)
+                    for availability in room_availabilities)):
+                if previous_slot is not None and previous_slot + slot_duration < current_slot:
+                    yield current_block
+                    current_block = []
+
+                current_block.append(slot_index)
+                previous_slot = current_slot
+
+            slot_index += 1
+            current_slot += slot_duration
+
+        yield current_block
+
+    def time_slot(self, *, time_slot_index, slots_in_an_hour=1.0):
+        from AKModel.availability.models import Availability
+        slot_duration = timedelta(hours=(1.0 / slots_in_an_hour))
+
+        start = self.start + time_slot_index * slot_duration
+
+        return Availability(event=self,
+                            start=start,
+                            end=start + slot_duration)
+
+    def schedule_from_json(self, schedule):
+        schedule = json.loads(schedule)
+
+        slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"]
+
+        for scheduled_slot in schedule["scheduled_aks"]:
+            slot = AKSlot.objects.get(scheduled_slot["ak_id"])
+            slot.room = scheduled_slot["room_id"]
+
+            start = min(scheduled_slot["time_slot_ids"])
+            end = max(scheduled_slot["time_slot_ids"])
+
+            slot.start = self.time_slot(time_slot_index=start,
+                                        slots_in_an_hour=slots_in_an_hour)
+            slot.end = self.time_slot(time_slot_index=end + 1,
+                                      slots_in_an_hour=slots_in_an_hour)
+
 
 class AKOwner(models.Model):
     """ An AKOwner describes the person organizing/holding an AK.
@@ -236,6 +297,22 @@ class AKOwner(models.Model):
         """
         return AKOwner.objects.get(event=event, slug=slug)
 
+    def as_json(self) -> str:
+        data = {
+            "id": self.pk,
+            "info": {
+                "name": self.name,
+            },
+            "capacity": self.capacity,
+            "fulfilled_room_constraints": [constraint.name
+                                           for constraint in self.properties.all()],
+            "time_constraints": [f"availability-room-{self.pk}"]
+        }
+
+        return json.dumps(data)
+
+
+
 
 class AKCategory(models.Model):
     """ An AKCategory describes the characteristics of an AK, e.g. content vs. recreational.
diff --git a/AKModel/templates/admin/AKModel/ak_json_export.html b/AKModel/templates/admin/AKModel/ak_json_export.html
new file mode 100644
index 00000000..da582504
--- /dev/null
+++ b/AKModel/templates/admin/AKModel/ak_json_export.html
@@ -0,0 +1,19 @@
+{% extends "admin/base_site.html" %}
+
+{% load tz %}
+
+{% block content %}
+<pre>
+  {"aks": [
+      {% for slot in slots %}{{ slot.as_json }}{% if not forloop.last %},
+      {% endif %}{% endfor %}
+    ],
+  "rooms": [
+      {% for room in rooms %}{{ room.as_json }}{% if not forloop.last %},
+      {% endif %}{% endfor %}
+    ],
+  "participants": {{ participants }},
+  "timeslots": {{ timeslots }}
+  }
+</pre>
+{% endblock %}
diff --git a/AKModel/urls.py b/AKModel/urls.py
index 3abf6460..9871b411 100644
--- a/AKModel/urls.py
+++ b/AKModel/urls.py
@@ -5,8 +5,9 @@ from rest_framework.routers import DefaultRouter
 
 import AKModel.views.api
 from AKModel.views.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, \
-    AKsByUserView
-from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView
+    AKsByUserView, AKJSONImportView
+from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKJSONExportView, AKWikiExportView, \
+     AKMessageDeleteView
 from AKModel.views.event_wizard import NewEventWizardStartView, NewEventWizardPrepareImportView, \
     NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, NewEventWizardSettingsView
 from AKModel.views.room import RoomBatchCreationView
@@ -96,6 +97,10 @@ def get_admin_urls_event(admin_site):
              name="aks_by_owner"),
         path('<slug:event_slug>/ak-csv-export/', admin_site.admin_view(AKCSVExportView.as_view()),
              name="ak_csv_export"),
+        path('<slug:event_slug>/ak-json-export/', admin_site.admin_view(AKJSONExportView.as_view()),
+             name="ak_json_export"),
+        path('<slug:event_slug>/ak-json-import/', admin_site.admin_view(AKJSONImportView.as_view()),
+             name="ak_json_import"),
         path('<slug:slug>/ak-wiki-export/', admin_site.admin_view(AKWikiExportView.as_view()),
              name="ak_wiki_export"),
         path('<slug:event_slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()),
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 3afec5ac..547f3606 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -1,3 +1,6 @@
+import json
+from datetime import timedelta
+
 from django.contrib import messages
 from django.urls import reverse_lazy
 from django.utils.translation import gettext_lazy as _
@@ -37,6 +40,88 @@ class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         return super().get_queryset().order_by("ak__track")
 
 
+class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
+    """
+    View: Export all AK slots of this event in JSON format ordered by tracks
+    """
+    template_name = "admin/AKModel/ak_json_export.html"
+    model = AKSlot
+    context_object_name = "slots"
+    title = _("AK JSON Export")
+
+    def get_queryset(self):
+        return super().get_queryset().order_by("ak__track")
+
+    def get_context_data(self, **kwargs):
+        from AKModel.availability.models import Availability
+
+        SLOTS_IN_AN_HOUR = 1
+
+        rooms = Room.objects.filter(event=self.event)
+        participants = []
+        timeslots = {
+            "info": {"duration": (1.0 / SLOTS_IN_AN_HOUR), },
+            "blocks": [],
+            }
+
+        context = super().get_context_data(**kwargs)
+        context["rooms"] = rooms
+        context["participants"] = json.dumps(participants)
+
+        for slot in context["slots"]:
+            slot.slots_in_an_hour = SLOTS_IN_AN_HOUR
+
+        ak_availabilities = {slot.ak.pk: availability
+                             for slot in context["slots"]
+                             for availability in slot.ak.availabilities.all()}
+        room_availabilities = {room.pk: availability
+                                    for room in rooms
+                                    for availability in room.availabilities.all()}
+        person_availabilities = {person.pk: availability
+                                      for person in AKOwner.objects.filter(event=self.event)
+                                      for availability in person.availabilities.all()}
+
+
+
+        for block in self.event.time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR):
+            current_block = []
+
+            for slot_index in block:
+                slot = self.event.time_slot(time_slot_index=slot_index,
+                                            slots_in_an_hour=SLOTS_IN_AN_HOUR)
+                constraints = []
+
+                if slot.end < self.event.reso_deadline:
+                    constraints.append("resolution")
+
+                for (ak, availability) in ak_availabilities.items():
+                    if availability.contains(slot):
+                        constraints.append(f"availability-ak-{ak}")
+
+                for (person, availability) in person_availabilities.items():
+                    if availability.contains(slot):
+                        constraints.append(f"availability-person-{person}")
+
+                for (room, availability) in room_availabilities.items():
+                    if availability.contains(slot):
+                        constraints.append(f"availability-room-{room}")
+
+                current_block.append({
+                    "id": slot_index,
+                    "info": {
+                        "start": slot.simplified,
+                    },
+                    "fulfilled_time_constraints": constraints,
+                    })
+
+            timeslots["blocks"].append(current_block)
+
+        context["timeslots"] = json.dumps(timeslots)
+
+        return context
+
+
+
 class AKWikiExportView(AdminViewMixin, DetailView):
     """
     View: Export AKs of this event in wiki syntax
diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py
index 64443cb8..0ed33044 100644
--- a/AKModel/views/manage.py
+++ b/AKModel/views/manage.py
@@ -12,7 +12,7 @@ from django.views.generic import TemplateView, DetailView
 from django_tex.core import render_template_with_context, run_tex_in_directory
 from django_tex.response import PDFResponse
 
-from AKModel.forms import SlideExportForm, DefaultSlotEditorForm
+from AKModel.forms import SlideExportForm, DefaultSlotEditorForm, JSONImportForm
 from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin
 from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner
 
@@ -245,3 +245,13 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView):
     model = AKOwner
     context_object_name = 'owner'
     template_name = "admin/AKModel/aks_by_user.html"
+
+
+class AKJSONImportView(EventSlugMixin, IntermediateAdminView):
+    form_class = JSONImportForm
+    title = _("AK JSON Import")
+
+    def form_valid(self, form):
+        self.event.schedule_from_json(form.data["json_data"])
+
+        return redirect("admin:event_status", self.event.slug)
diff --git a/AKModel/views/status.py b/AKModel/views/status.py
index e14ce2fb..0c12b303 100644
--- a/AKModel/views/status.py
+++ b/AKModel/views/status.py
@@ -133,10 +133,18 @@ class EventAKsWidget(TemplateStatusWidget):
                     "text": _("Manage ak tracks"),
                     "url": reverse_lazy("admin:tracks_manage", kwargs={"event_slug": context["event"].slug}),
                 },
+                {
+                    "text": _("Import AK schedule from JSON"),
+                    "url": reverse_lazy("admin:ak_json_import", kwargs={"event_slug": context["event"].slug}),
+                },
                 {
                     "text": _("Export AKs as CSV"),
                     "url": reverse_lazy("admin:ak_csv_export", kwargs={"event_slug": context["event"].slug}),
                 },
+                {
+                    "text": _("Export AKs as JSON"),
+                    "url": reverse_lazy("admin:ak_json_export", kwargs={"event_slug": context["event"].slug}),
+                },
                 {
                     "text": _("Export AKs for Wiki"),
                     "url": reverse_lazy("admin:ak_wiki_export", kwargs={"slug": context["event"].slug}),
-- 
GitLab


From 1db999b14dd2ce7b2fd32b4481ca268a4515a969 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 May 2024 04:40:18 +0200
Subject: [PATCH 02/77] Update language files

---
 AKModel/locale/de_DE/LC_MESSAGES/django.po    | 452 +++++++++---------
 AKPlan/locale/de_DE/LC_MESSAGES/django.po     |  10 +-
 AKPlanning/locale/de_DE/LC_MESSAGES/django.po |   6 +-
 .../locale/de_DE/LC_MESSAGES/django.po        |   8 +-
 4 files changed, 249 insertions(+), 227 deletions(-)

diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index 261c5b2c..1026edf9 100644
--- a/AKModel/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-04-25 01:29+0200\n"
+"POT-Creation-Date: 2024-05-27 02:31+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"
@@ -33,10 +33,9 @@ msgstr "Plan veröffentlichen"
 msgid "Unpublish plan"
 msgstr "Plan verbergen"
 
-#: AKModel/admin.py:168 AKModel/models.py:360 AKModel/models.py:682
-#: AKModel/templates/admin/AKModel/aks_by_user.html:12
+#: AKModel/admin.py:168 AKModel/models.py:437 AKModel/models.py:759
 #: AKModel/templates/admin/AKModel/status/event_aks.html:10
-#: AKModel/views/manage.py:73 AKModel/views/status.py:98
+#: AKModel/views/manage.py:73 AKModel/views/status.py:97
 msgid "AKs"
 msgstr "AKs"
 
@@ -60,11 +59,11 @@ msgstr "In Wiki-Syntax exportieren"
 msgid "Cannot export AKs from more than one event at the same time."
 msgstr "Kann nicht AKs von mehreren Events zur selben Zeit exportieren."
 
-#: AKModel/admin.py:320 AKModel/views/ak.py:99
+#: AKModel/admin.py:320 AKModel/views/ak.py:184
 msgid "Reset interest in AKs"
 msgstr "Interesse an AKs zurücksetzen"
 
-#: AKModel/admin.py:330 AKModel/views/ak.py:114
+#: AKModel/admin.py:330 AKModel/views/ak.py:199
 msgid "Reset AKs' interest counters"
 msgstr "Interessenszähler der AKs zurücksetzen"
 
@@ -109,17 +108,17 @@ msgstr "Die eingegebene Verfügbarkeit enthält ein ungültiges Datum."
 msgid "Please fill in your availabilities!"
 msgstr "Bitte Verfügbarkeiten eintragen!"
 
-#: AKModel/availability/models.py:43 AKModel/models.py:60 AKModel/models.py:174
-#: AKModel/models.py:251 AKModel/models.py:270 AKModel/models.py:296
-#: AKModel/models.py:350 AKModel/models.py:492 AKModel/models.py:531
-#: AKModel/models.py:621 AKModel/models.py:678 AKModel/models.py:869
+#: AKModel/availability/models.py:43 AKModel/models.py:61 AKModel/models.py:235
+#: AKModel/models.py:328 AKModel/models.py:347 AKModel/models.py:373
+#: AKModel/models.py:427 AKModel/models.py:569 AKModel/models.py:608
+#: AKModel/models.py:698 AKModel/models.py:755 AKModel/models.py:946
 msgid "Event"
 msgstr "Event"
 
-#: AKModel/availability/models.py:44 AKModel/models.py:175
-#: AKModel/models.py:252 AKModel/models.py:271 AKModel/models.py:297
-#: AKModel/models.py:351 AKModel/models.py:493 AKModel/models.py:532
-#: AKModel/models.py:622 AKModel/models.py:679 AKModel/models.py:870
+#: AKModel/availability/models.py:44 AKModel/models.py:236
+#: AKModel/models.py:329 AKModel/models.py:348 AKModel/models.py:374
+#: AKModel/models.py:428 AKModel/models.py:570 AKModel/models.py:609
+#: AKModel/models.py:699 AKModel/models.py:756 AKModel/models.py:947
 msgid "Associated event"
 msgstr "Zugehöriges Event"
 
@@ -131,8 +130,8 @@ msgstr "Person"
 msgid "Person whose availability this is"
 msgstr "Person deren Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:61 AKModel/models.py:496
-#: AKModel/models.py:521 AKModel/models.py:688
+#: AKModel/availability/models.py:61 AKModel/models.py:573
+#: AKModel/models.py:598 AKModel/models.py:765
 msgid "Room"
 msgstr "Raum"
 
@@ -140,8 +139,8 @@ msgstr "Raum"
 msgid "Room whose availability this is"
 msgstr "Raum dessen Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:70 AKModel/models.py:359
-#: AKModel/models.py:520 AKModel/models.py:616
+#: AKModel/availability/models.py:70 AKModel/models.py:436
+#: AKModel/models.py:597 AKModel/models.py:693
 msgid "AK"
 msgstr "AK"
 
@@ -149,8 +148,8 @@ msgstr "AK"
 msgid "AK whose availability this is"
 msgstr "Verfügbarkeiten"
 
-#: AKModel/availability/models.py:79 AKModel/models.py:255
-#: AKModel/models.py:694
+#: AKModel/availability/models.py:79 AKModel/models.py:332
+#: AKModel/models.py:771
 msgid "AK Category"
 msgstr "AK-Kategorie"
 
@@ -220,7 +219,7 @@ msgstr ""
 "fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen "
 "ausgefüllt zu werden?"
 
-#: AKModel/forms.py:189 AKModel/models.py:863
+#: AKModel/forms.py:189 AKModel/models.py:940
 msgid "Default Slots"
 msgstr "Standardslots"
 
@@ -259,7 +258,15 @@ msgstr "Standardverfügbarkeiten für alle Räume anlegen?"
 msgid "CSV must contain a name column"
 msgstr "CSV muss eine name-Spalte enthalten"
 
-#: AKModel/metaviews/admin.py:156 AKModel/models.py:29
+#: AKModel/forms.py:281
+msgid "JSON data"
+msgstr "JSON-Daten"
+
+#: AKModel/forms.py:282
+msgid "JSON data from the scheduling solver"
+msgstr "JSON-Daten, die der scheduling-solver produziert hat"
+
+#: AKModel/metaviews/admin.py:156 AKModel/models.py:30
 msgid "Start"
 msgstr "Start"
 
@@ -284,66 +291,66 @@ msgstr "Aktivieren?"
 msgid "Finish"
 msgstr "Abschluss"
 
-#: AKModel/models.py:20 AKModel/models.py:243 AKModel/models.py:267
-#: AKModel/models.py:294 AKModel/models.py:312 AKModel/models.py:484
+#: AKModel/models.py:21 AKModel/models.py:320 AKModel/models.py:344
+#: AKModel/models.py:371 AKModel/models.py:389 AKModel/models.py:561
 msgid "Name"
 msgstr "Name"
 
-#: AKModel/models.py:21
+#: AKModel/models.py:22
 msgid "Name or iteration of the event"
 msgstr "Name oder Iteration des Events"
 
-#: AKModel/models.py:22
+#: AKModel/models.py:23
 msgid "Short Form"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:23
+#: AKModel/models.py:24
 msgid "Short name of letters/numbers/dots/dashes/underscores used in URLs."
 msgstr ""
 "Kurzname bestehend aus Buchstaben, Nummern, Punkten und Unterstrichen zur "
 "Nutzung in URLs"
 
-#: AKModel/models.py:25
+#: AKModel/models.py:26
 msgid "Place"
 msgstr "Ort"
 
-#: AKModel/models.py:26
+#: AKModel/models.py:27
 msgid "City etc. the event takes place in"
 msgstr "Stadt o.ä. in der das Event stattfindet"
 
-#: AKModel/models.py:28
+#: AKModel/models.py:29
 msgid "Time Zone"
 msgstr "Zeitzone"
 
-#: AKModel/models.py:28
+#: AKModel/models.py:29
 msgid "Time Zone where this event takes place in"
 msgstr "Zeitzone in der das Event stattfindet"
 
-#: AKModel/models.py:29
+#: AKModel/models.py:30
 msgid "Time the event begins"
 msgstr "Zeit zu der das Event beginnt"
 
-#: AKModel/models.py:30
+#: AKModel/models.py:31
 msgid "End"
 msgstr "Ende"
 
-#: AKModel/models.py:30
+#: AKModel/models.py:31
 msgid "Time the event ends"
 msgstr "Zeit zu der das Event endet"
 
-#: AKModel/models.py:31
+#: AKModel/models.py:32
 msgid "Resolution Deadline"
 msgstr "Resolutionsdeadline"
 
-#: AKModel/models.py:32
+#: AKModel/models.py:33
 msgid "When should AKs with intention to submit a resolution be done?"
 msgstr "Wann sollen AKs mit Resolutionsabsicht stattgefunden haben?"
 
-#: AKModel/models.py:34
+#: AKModel/models.py:35
 msgid "Interest Window Start"
 msgstr "Beginn Interessensbekundung"
 
-#: AKModel/models.py:36
+#: AKModel/models.py:37
 msgid ""
 "Opening time for expression of interest. When left blank, no interest "
 "indication will be possible."
@@ -351,71 +358,71 @@ msgstr ""
 "Öffnungszeitpunkt für die Angabe von Interesse an AKs.Wenn das Feld leer "
 "bleibt, wird keine Abgabe von Interesse möglich sein."
 
-#: AKModel/models.py:38
+#: AKModel/models.py:39
 msgid "Interest Window End"
 msgstr "Ende Interessensbekundung"
 
-#: AKModel/models.py:39
+#: AKModel/models.py:40
 msgid "Closing time for expression of interest."
 msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs."
 
-#: AKModel/models.py:41
+#: AKModel/models.py:42
 msgid "Public event"
 msgstr "Öffentliches Event"
 
-#: AKModel/models.py:42
+#: AKModel/models.py:43
 msgid "Show this event on overview page."
 msgstr "Zeige dieses Event auf der Übersichtseite an"
 
-#: AKModel/models.py:44
+#: AKModel/models.py:45
 msgid "Active State"
 msgstr "Aktiver Status"
 
-#: AKModel/models.py:44
+#: AKModel/models.py:45
 msgid "Marks currently active events"
 msgstr "Markiert aktuell aktive Events"
 
-#: AKModel/models.py:45
+#: AKModel/models.py:46
 msgid "Plan Hidden"
 msgstr "Plan verborgen"
 
-#: AKModel/models.py:45
+#: AKModel/models.py:46
 msgid "Hides plan for non-staff users"
 msgstr "Verbirgt den Plan für Nutzer*innen ohne erweiterte Rechte"
 
-#: AKModel/models.py:47
+#: AKModel/models.py:48
 msgid "Plan published at"
 msgstr "Plan veröffentlicht am/um"
 
-#: AKModel/models.py:48
+#: AKModel/models.py:49
 msgid "Timestamp at which the plan was published"
 msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde"
 
-#: AKModel/models.py:50
+#: AKModel/models.py:51
 msgid "Base URL"
 msgstr "URL-Prefix"
 
-#: AKModel/models.py:50
+#: AKModel/models.py:51
 msgid "Prefix for wiki link construction"
 msgstr "Prefix für die automatische Generierung von Wiki-Links"
 
-#: AKModel/models.py:51
+#: AKModel/models.py:52
 msgid "Wiki Export Template Name"
 msgstr "Wiki-Export Templatename"
 
-#: AKModel/models.py:52
+#: AKModel/models.py:53
 msgid "Default Slot Length"
 msgstr "Standardslotlänge"
 
-#: AKModel/models.py:53
+#: AKModel/models.py:54
 msgid "Default length in hours that is assumed for AKs in this event."
 msgstr "Standardlänge von Slots (in Stunden) für dieses Event"
 
-#: AKModel/models.py:55
+#: AKModel/models.py:56
 msgid "Contact email address"
 msgstr "E-Mail Kontaktadresse"
 
-#: AKModel/models.py:56
+#: AKModel/models.py:57
 msgid ""
 "An email address that is displayed on every page and can be used for all "
 "kinds of questions"
@@ -423,75 +430,75 @@ msgstr ""
 "Eine Mailadresse die auf jeder Seite angezeigt wird und für alle Arten von "
 "Fragen genutzt werden kann"
 
-#: AKModel/models.py:61
+#: AKModel/models.py:62
 msgid "Events"
 msgstr "Events"
 
-#: AKModel/models.py:169
+#: AKModel/models.py:230
 msgid "Nickname"
 msgstr "Spitzname"
 
-#: AKModel/models.py:169
+#: AKModel/models.py:230
 msgid "Name to identify an AK owner by"
 msgstr "Name, durch den eine AK-Leitung identifiziert wird"
 
-#: AKModel/models.py:170
+#: AKModel/models.py:231
 msgid "Slug"
 msgstr "Slug"
 
-#: AKModel/models.py:170
+#: AKModel/models.py:231
 msgid "Slug for URL generation"
 msgstr "Slug für URL-Generierung"
 
-#: AKModel/models.py:171
+#: AKModel/models.py:232
 msgid "Institution"
 msgstr "Instutution"
 
-#: AKModel/models.py:171
+#: AKModel/models.py:232
 msgid "Uni etc."
 msgstr "Universität o.ä."
 
-#: AKModel/models.py:172 AKModel/models.py:321
+#: AKModel/models.py:233 AKModel/models.py:398
 msgid "Web Link"
 msgstr "Internet Link"
 
-#: AKModel/models.py:172
+#: AKModel/models.py:233
 msgid "Link to Homepage"
 msgstr "Link zu Homepage oder Webseite"
 
-#: AKModel/models.py:178 AKModel/models.py:687
+#: AKModel/models.py:239 AKModel/models.py:764
 msgid "AK Owner"
 msgstr "AK-Leitung"
 
-#: AKModel/models.py:179
+#: AKModel/models.py:240
 msgid "AK Owners"
 msgstr "AK-Leitungen"
 
-#: AKModel/models.py:243
+#: AKModel/models.py:320
 msgid "Name of the AK Category"
 msgstr "Name der AK-Kategorie"
 
-#: AKModel/models.py:244 AKModel/models.py:268
+#: AKModel/models.py:321 AKModel/models.py:345
 msgid "Color"
 msgstr "Farbe"
 
-#: AKModel/models.py:244 AKModel/models.py:268
+#: AKModel/models.py:321 AKModel/models.py:345
 msgid "Color for displaying"
 msgstr "Farbe für die Anzeige"
 
-#: AKModel/models.py:245 AKModel/models.py:315
+#: AKModel/models.py:322 AKModel/models.py:392
 msgid "Description"
 msgstr "Beschreibung"
 
-#: AKModel/models.py:246
+#: AKModel/models.py:323
 msgid "Short description of this AK Category"
 msgstr "Beschreibung der AK-Kategorie"
 
-#: AKModel/models.py:247
+#: AKModel/models.py:324
 msgid "Present by default"
 msgstr "Defaultmäßig präsentieren"
 
-#: AKModel/models.py:248
+#: AKModel/models.py:325
 msgid ""
 "Present AKs of this category by default if AK owner did not specify whether "
 "this AK should be presented?"
@@ -499,132 +506,132 @@ msgstr ""
 "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für "
 "ihren AK nicht explizit spezifiziert haben?"
 
-#: AKModel/models.py:256
+#: AKModel/models.py:333
 msgid "AK Categories"
 msgstr "AK-Kategorien"
 
-#: AKModel/models.py:267
+#: AKModel/models.py:344
 msgid "Name of the AK Track"
 msgstr "Name des AK-Tracks"
 
-#: AKModel/models.py:274
+#: AKModel/models.py:351
 msgid "AK Track"
 msgstr "AK-Track"
 
-#: AKModel/models.py:275
+#: AKModel/models.py:352
 msgid "AK Tracks"
 msgstr "AK-Tracks"
 
-#: AKModel/models.py:294
+#: AKModel/models.py:371
 msgid "Name of the Requirement"
 msgstr "Name der Anforderung"
 
-#: AKModel/models.py:300 AKModel/models.py:691
+#: AKModel/models.py:377 AKModel/models.py:768
 msgid "AK Requirement"
 msgstr "AK-Anforderung"
 
-#: AKModel/models.py:301
+#: AKModel/models.py:378
 msgid "AK Requirements"
 msgstr "AK-Anforderungen"
 
-#: AKModel/models.py:312
+#: AKModel/models.py:389
 msgid "Name of the AK"
 msgstr "Name des AKs"
 
-#: AKModel/models.py:313
+#: AKModel/models.py:390
 msgid "Short Name"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:314
+#: AKModel/models.py:391
 msgid "Name displayed in the schedule"
 msgstr "Name zur Anzeige im AK-Plan"
 
-#: AKModel/models.py:315
+#: AKModel/models.py:392
 msgid "Description of the AK"
 msgstr "Beschreibung des AKs"
 
-#: AKModel/models.py:317
+#: AKModel/models.py:394
 msgid "Owners"
 msgstr "Leitungen"
 
-#: AKModel/models.py:318
+#: AKModel/models.py:395
 msgid "Those organizing the AK"
 msgstr "Menschen, die den AK organisieren und halten"
 
-#: AKModel/models.py:321
+#: AKModel/models.py:398
 msgid "Link to wiki page"
 msgstr "Link zur Wiki Seite"
 
-#: AKModel/models.py:322
+#: AKModel/models.py:399
 msgid "Protocol Link"
 msgstr "Protokolllink"
 
-#: AKModel/models.py:322
+#: AKModel/models.py:399
 msgid "Link to protocol"
 msgstr "Link zum Protokoll"
 
-#: AKModel/models.py:324
+#: AKModel/models.py:401
 msgid "Category"
 msgstr "Kategorie"
 
-#: AKModel/models.py:325
+#: AKModel/models.py:402
 msgid "Category of the AK"
 msgstr "Kategorie des AKs"
 
-#: AKModel/models.py:326
+#: AKModel/models.py:403
 msgid "Track"
 msgstr "Track"
 
-#: AKModel/models.py:327
+#: AKModel/models.py:404
 msgid "Track the AK belongs to"
 msgstr "Track zu dem der AK gehört"
 
-#: AKModel/models.py:329
+#: AKModel/models.py:406
 msgid "Resolution Intention"
 msgstr "Resolutionsabsicht"
 
-#: AKModel/models.py:330
+#: AKModel/models.py:407
 msgid "Intends to submit a resolution"
 msgstr "Beabsichtigt eine Resolution einzureichen"
 
-#: AKModel/models.py:331
+#: AKModel/models.py:408
 msgid "Present this AK"
 msgstr "AK präsentieren"
 
-#: AKModel/models.py:332
+#: AKModel/models.py:409
 msgid "Present results of this AK"
 msgstr "Die Ergebnisse dieses AKs vorstellen"
 
-#: AKModel/models.py:334 AKModel/views/status.py:163
+#: AKModel/models.py:411 AKModel/views/status.py:170
 msgid "Requirements"
 msgstr "Anforderungen"
 
-#: AKModel/models.py:335
+#: AKModel/models.py:412
 msgid "AK's Requirements"
 msgstr "Anforderungen des AKs"
 
-#: AKModel/models.py:337
+#: AKModel/models.py:414
 msgid "Conflicting AKs"
 msgstr "AK-Konflikte"
 
-#: AKModel/models.py:338
+#: AKModel/models.py:415
 msgid "AKs that conflict and thus must not take place at the same time"
 msgstr ""
 "AKs, die Konflikte haben und deshalb nicht gleichzeitig stattfinden dürfen"
 
-#: AKModel/models.py:339
+#: AKModel/models.py:416
 msgid "Prerequisite AKs"
 msgstr "Vorausgesetzte AKs"
 
-#: AKModel/models.py:340
+#: AKModel/models.py:417
 msgid "AKs that should precede this AK in the schedule"
 msgstr "AKs die im AK-Plan vor diesem AK stattfinden müssen"
 
-#: AKModel/models.py:342
+#: AKModel/models.py:419
 msgid "Organizational Notes"
 msgstr "Notizen zur Organisation"
 
-#: AKModel/models.py:343
+#: AKModel/models.py:420
 msgid ""
 "Notes to organizers. These are public. For private notes, please use the "
 "button for private messages on the detail page of this AK (after creation/"
@@ -634,289 +641,291 @@ msgstr ""
 "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem "
 "Anlegen/Bearbeiten)."
 
-#: AKModel/models.py:346
+#: AKModel/models.py:423
 msgid "Interest"
 msgstr "Interesse"
 
-#: AKModel/models.py:346
+#: AKModel/models.py:423
 msgid "Expected number of people"
 msgstr "Erwartete Personenzahl"
 
-#: AKModel/models.py:347
+#: AKModel/models.py:424
 msgid "Interest Counter"
 msgstr "Interessenszähler"
 
-#: AKModel/models.py:348
+#: AKModel/models.py:425
 msgid "People who have indicated interest online"
 msgstr "Anzahl Personen, die online Interesse bekundet haben"
 
-#: AKModel/models.py:353
+#: AKModel/models.py:430
 msgid "Export?"
 msgstr "Export?"
 
-#: AKModel/models.py:354
+#: AKModel/models.py:431
 msgid "Include AK in wiki export?"
 msgstr "AK bei Wiki-Export berücksichtigen?"
 
-#: AKModel/models.py:484
+#: AKModel/models.py:561
 msgid "Name or number of the room"
 msgstr "Name oder Nummer des Raums"
 
-#: AKModel/models.py:485
+#: AKModel/models.py:562
 msgid "Location"
 msgstr "Ort"
 
-#: AKModel/models.py:486
+#: AKModel/models.py:563
 msgid "Name or number of the location"
 msgstr "Name oder Nummer des Ortes"
 
-#: AKModel/models.py:487
+#: AKModel/models.py:564
 msgid "Capacity"
 msgstr "Kapazität"
 
-#: AKModel/models.py:488
+#: AKModel/models.py:565
 msgid "Maximum number of people (-1 for unlimited)."
 msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)."
 
-#: AKModel/models.py:489
+#: AKModel/models.py:566
 msgid "Properties"
 msgstr "Eigenschaften"
 
-#: AKModel/models.py:490
+#: AKModel/models.py:567
 msgid "AK requirements fulfilled by the room"
 msgstr "AK-Anforderungen, die dieser Raum erfüllt"
 
-#: AKModel/models.py:497 AKModel/views/status.py:60
+#: AKModel/models.py:574 AKModel/views/status.py:59
 msgid "Rooms"
 msgstr "Räume"
 
-#: AKModel/models.py:520
+#: AKModel/models.py:597
 msgid "AK being mapped"
 msgstr "AK, der zugeordnet wird"
 
-#: AKModel/models.py:522
+#: AKModel/models.py:599
 msgid "Room the AK will take place in"
 msgstr "Raum in dem der AK stattfindet"
 
-#: AKModel/models.py:523 AKModel/models.py:866
+#: AKModel/models.py:600 AKModel/models.py:943
 msgid "Slot Begin"
 msgstr "Beginn des Slots"
 
-#: AKModel/models.py:523 AKModel/models.py:866
+#: AKModel/models.py:600 AKModel/models.py:943
 msgid "Time and date the slot begins"
 msgstr "Zeit und Datum zu der der AK beginnt"
 
-#: AKModel/models.py:525
+#: AKModel/models.py:602
 msgid "Duration"
 msgstr "Dauer"
 
-#: AKModel/models.py:526
+#: AKModel/models.py:603
 msgid "Length in hours"
 msgstr "Länge in Stunden"
 
-#: AKModel/models.py:528
+#: AKModel/models.py:605
 msgid "Scheduling fixed"
 msgstr "Planung fix"
 
-#: AKModel/models.py:529
+#: AKModel/models.py:606
 msgid "Length and time of this AK should not be changed"
 msgstr "Dauer und Zeit dieses AKs sollten nicht verändert werden"
 
-#: AKModel/models.py:534
+#: AKModel/models.py:611
 msgid "Last update"
 msgstr "Letzte Aktualisierung"
 
-#: AKModel/models.py:537
+#: AKModel/models.py:614
 msgid "AK Slot"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:538 AKModel/models.py:684
+#: AKModel/models.py:615 AKModel/models.py:761
 msgid "AK Slots"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:560 AKModel/models.py:569
+#: AKModel/models.py:637 AKModel/models.py:646
 msgid "Not scheduled yet"
 msgstr "Noch nicht geplant"
 
-#: AKModel/models.py:617
+#: AKModel/models.py:694
 msgid "AK this message belongs to"
 msgstr "AK zu dem die Nachricht gehört"
 
-#: AKModel/models.py:618
+#: AKModel/models.py:695
 msgid "Message text"
 msgstr "Nachrichtentext"
 
-#: AKModel/models.py:619
+#: AKModel/models.py:696
 msgid "Message to the organizers. This is not publicly visible."
 msgstr ""
 "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar."
 
-#: AKModel/models.py:623
+#: AKModel/models.py:700
 msgid "Resolved"
 msgstr "Erledigt"
 
-#: AKModel/models.py:624
+#: AKModel/models.py:701
 msgid "This message has been resolved (no further action needed)"
-msgstr "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen notwendig)"
+msgstr ""
+"Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen "
+"notwendig)"
 
-#: AKModel/models.py:627
+#: AKModel/models.py:704
 msgid "AK Orga Message"
 msgstr "AK-Organachricht"
 
-#: AKModel/models.py:628
+#: AKModel/models.py:705
 msgid "AK Orga Messages"
 msgstr "AK-Organachrichten"
 
-#: AKModel/models.py:645
+#: AKModel/models.py:722
 msgid "Constraint Violation"
 msgstr "Constraintverletzung"
 
-#: AKModel/models.py:646
+#: AKModel/models.py:723
 msgid "Constraint Violations"
 msgstr "Constraintverletzungen"
 
-#: AKModel/models.py:653
+#: AKModel/models.py:730
 msgid "Owner has two parallel slots"
 msgstr "Leitung hat zwei Slots parallel"
 
-#: AKModel/models.py:654
+#: AKModel/models.py:731
 msgid "AK Slot was scheduled outside the AK's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des AKs platziert"
 
-#: AKModel/models.py:655
+#: AKModel/models.py:732
 msgid "Room has two AK slots scheduled at the same time"
 msgstr "Raum hat zwei AK Slots gleichzeitig"
 
-#: AKModel/models.py:656
+#: AKModel/models.py:733
 msgid "Room does not satisfy the requirement of the scheduled AK"
 msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht"
 
-#: AKModel/models.py:657
+#: AKModel/models.py:734
 msgid "AK Slot is scheduled at the same time as an AK listed as a conflict"
 msgstr ""
 "AK Slot wurde wurde zur gleichen Zeit wie ein Konflikt des AKs platziert"
 
-#: AKModel/models.py:658
+#: AKModel/models.py:735
 msgid "AK Slot is scheduled before an AK listed as a prerequisite"
 msgstr "AK Slot wurde vor einem als Voraussetzung gelisteten AK platziert"
 
-#: AKModel/models.py:660
+#: AKModel/models.py:737
 msgid ""
 "AK Slot for AK with intention to submit a resolution is scheduled after "
 "resolution deadline"
 msgstr ""
 "AK Slot eines AKs mit Resoabsicht wurde nach der Resodeadline platziert"
 
-#: AKModel/models.py:661
+#: AKModel/models.py:738
 msgid "AK Slot in a category is outside that categories availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeiten seiner Kategorie"
 
-#: AKModel/models.py:662
+#: AKModel/models.py:739
 msgid "Two AK Slots for the same AK scheduled at the same time"
 msgstr "Zwei AK Slots eines AKs wurden zur selben Zeit platziert"
 
-#: AKModel/models.py:663
+#: AKModel/models.py:740
 msgid "Room does not have enough space for interest in scheduled AK Slot"
 msgstr "Room hat nicht genug Platz für das Interesse am geplanten AK-Slot"
 
-#: AKModel/models.py:664
+#: AKModel/models.py:741
 msgid "AK Slot is scheduled outside the event's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des Events platziert"
 
-#: AKModel/models.py:670
+#: AKModel/models.py:747
 msgid "Warning"
 msgstr "Warnung"
 
-#: AKModel/models.py:671
+#: AKModel/models.py:748
 msgid "Violation"
 msgstr "Verletzung"
 
-#: AKModel/models.py:673
+#: AKModel/models.py:750
 msgid "Type"
 msgstr "Art"
 
-#: AKModel/models.py:674
+#: AKModel/models.py:751
 msgid "Type of violation, i.e. what kind of constraint was violated"
 msgstr "Art der Verletzung, gibt an welche Art Constraint verletzt wurde"
 
-#: AKModel/models.py:675
+#: AKModel/models.py:752
 msgid "Level"
 msgstr "Level"
 
-#: AKModel/models.py:676
+#: AKModel/models.py:753
 msgid "Severity level of the violation"
 msgstr "Schweregrad der Verletzung"
 
-#: AKModel/models.py:683
+#: AKModel/models.py:760
 msgid "AK(s) belonging to this constraint"
 msgstr "AK(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:685
+#: AKModel/models.py:762
 msgid "AK Slot(s) belonging to this constraint"
 msgstr "AK Slot(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:687
+#: AKModel/models.py:764
 msgid "AK Owner belonging to this constraint"
 msgstr "AK Leitung(en), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:689
+#: AKModel/models.py:766
 msgid "Room belonging to this constraint"
 msgstr "Raum, der zu diesem Constraint gehört"
 
-#: AKModel/models.py:692
+#: AKModel/models.py:769
 msgid "AK Requirement belonging to this constraint"
 msgstr "AK Anforderung, die zu diesem Constraint gehört"
 
-#: AKModel/models.py:694
+#: AKModel/models.py:771
 msgid "AK Category belonging to this constraint"
 msgstr "AK Kategorie, di zu diesem Constraint gehört"
 
-#: AKModel/models.py:696
+#: AKModel/models.py:773
 msgid "Comment"
 msgstr "Kommentar"
 
-#: AKModel/models.py:696
+#: AKModel/models.py:773
 msgid "Comment or further details for this violation"
 msgstr "Kommentar oder weitere Details zu dieser Vereletzung"
 
-#: AKModel/models.py:699
+#: AKModel/models.py:776
 msgid "Timestamp"
 msgstr "Timestamp"
 
-#: AKModel/models.py:699
+#: AKModel/models.py:776
 msgid "Time of creation"
 msgstr "Zeitpunkt der ERstellung"
 
-#: AKModel/models.py:700
+#: AKModel/models.py:777
 msgid "Manually Resolved"
 msgstr "Manuell behoben"
 
-#: AKModel/models.py:701
+#: AKModel/models.py:778
 msgid "Mark this violation manually as resolved"
 msgstr "Markiere diese Verletzung manuell als behoben"
 
-#: AKModel/models.py:728 AKModel/templates/admin/AKModel/aks_by_user.html:22
+#: AKModel/models.py:805
 #: AKModel/templates/admin/AKModel/requirements_overview.html:27
 msgid "Details"
 msgstr "Details"
 
-#: AKModel/models.py:862
+#: AKModel/models.py:939
 msgid "Default Slot"
 msgstr "Standardslot"
 
-#: AKModel/models.py:867
+#: AKModel/models.py:944
 msgid "Slot End"
 msgstr "Ende des Slots"
 
-#: AKModel/models.py:867
+#: AKModel/models.py:944
 msgid "Time and date the slot ends"
 msgstr "Zeit und Datum zu der der Slot endet"
 
-#: AKModel/models.py:872
+#: AKModel/models.py:949
 msgid "Primary categories"
 msgstr "Primäre Kategorien"
 
-#: AKModel/models.py:873
+#: AKModel/models.py:950
 msgid "Categories that should be assigned to this slot primarily"
 msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen"
 
@@ -953,19 +962,6 @@ msgstr "Bestätigen"
 msgid "Cancel"
 msgstr "Abbrechen"
 
-#: AKModel/templates/admin/AKModel/aks_by_user.html:8
-msgid "AKs by Owner"
-msgstr "AKs der Leitung"
-
-#: AKModel/templates/admin/AKModel/aks_by_user.html:26
-#: AKModel/templates/admin/AKModel/requirements_overview.html:31
-msgid "Edit"
-msgstr "Bearbeiten"
-
-#: AKModel/templates/admin/AKModel/aks_by_user.html:33
-msgid "This user does not have any AKs currently"
-msgstr "Diese Leitung hat aktuell keine AKs"
-
 #: AKModel/templates/admin/AKModel/event_wizard/activate.html:9
 #: AKModel/templates/admin/AKModel/event_wizard/created_prepare_import.html:9
 #: AKModel/templates/admin/AKModel/event_wizard/finish.html:9
@@ -1032,12 +1028,16 @@ msgstr ""
 msgid "Requirements Overview"
 msgstr "Übersicht Anforderungen"
 
+#: AKModel/templates/admin/AKModel/requirements_overview.html:31
+msgid "Edit"
+msgstr "Bearbeiten"
+
 #: AKModel/templates/admin/AKModel/requirements_overview.html:38
 msgid "No AKs with this requirement"
 msgstr "Kein AK mit dieser Anforderung"
 
 #: AKModel/templates/admin/AKModel/requirements_overview.html:45
-#: AKModel/views/status.py:179
+#: AKModel/views/status.py:186
 msgid "Add Requirement"
 msgstr "Anforderung hinzufügen"
 
@@ -1090,7 +1090,7 @@ msgstr "Bisher keine Räume"
 msgid "Active Events"
 msgstr "Aktive Events"
 
-#: AKModel/templates/admin/ak_index.html:16 AKModel/views/status.py:109
+#: AKModel/templates/admin/ak_index.html:16 AKModel/views/status.py:108
 msgid "Scheduling"
 msgstr "Scheduling"
 
@@ -1123,43 +1123,47 @@ msgstr "Login"
 msgid "Register"
 msgstr "Registrieren"
 
-#: AKModel/views/ak.py:17
+#: AKModel/views/ak.py:20
 msgid "Requirements for Event"
 msgstr "Anforderungen für das Event"
 
-#: AKModel/views/ak.py:34
+#: AKModel/views/ak.py:37
 msgid "AK CSV Export"
 msgstr "AK-CSV-Export"
 
-#: AKModel/views/ak.py:48
+#: AKModel/views/ak.py:50
+msgid "AK JSON Export"
+msgstr "AK-JSON-Export"
+
+#: AKModel/views/ak.py:133
 msgid "AK Wiki Export"
 msgstr "AK-Wiki-Export"
 
-#: AKModel/views/ak.py:59 AKModel/views/manage.py:53
+#: AKModel/views/ak.py:144 AKModel/views/manage.py:53
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKModel/views/ak.py:71
+#: AKModel/views/ak.py:156
 msgid "Delete AK Orga Messages"
 msgstr "AK-Organachrichten löschen"
 
-#: AKModel/views/ak.py:89
+#: AKModel/views/ak.py:174
 msgid "AK Orga Messages successfully deleted"
 msgstr "AK-Organachrichten erfolgreich gelöscht"
 
-#: AKModel/views/ak.py:101
+#: AKModel/views/ak.py:186
 msgid "Interest of the following AKs will be set to not filled (-1):"
 msgstr "Interesse an den folgenden AKs wird auf nicht ausgefüllt (-1) gesetzt:"
 
-#: AKModel/views/ak.py:102
+#: AKModel/views/ak.py:187
 msgid "Reset of interest in AKs successful."
 msgstr "Interesse an AKs erfolgreich zurückgesetzt."
 
-#: AKModel/views/ak.py:116
+#: AKModel/views/ak.py:201
 msgid "Interest counter of the following AKs will be set to 0:"
 msgstr "Interessensbekundungszähler der folgenden AKs wird auf 0 gesetzt:"
 
-#: AKModel/views/ak.py:117
+#: AKModel/views/ak.py:202
 msgid "AKs' interest counters set back to 0."
 msgstr "Interessenszähler der AKs zurückgesetzt"
 
@@ -1173,7 +1177,7 @@ msgstr "'%(obj)s' kopiert"
 msgid "Could not copy '%(obj)s' (%(error)s)"
 msgstr "'%(obj)s' konnte nicht kopiert werden (%(error)s)"
 
-#: AKModel/views/manage.py:35 AKModel/views/status.py:146
+#: AKModel/views/manage.py:35 AKModel/views/status.py:153
 msgid "Export AK Slides"
 msgstr "AK-Folien exportieren"
 
@@ -1241,7 +1245,7 @@ msgstr "Den Plan/die Pläne verbergen von:"
 msgid "Plan unpublished"
 msgstr "Plan verborgen"
 
-#: AKModel/views/manage.py:166 AKModel/views/status.py:130
+#: AKModel/views/manage.py:166 AKModel/views/status.py:129
 msgid "Edit Default Slots"
 msgstr "Standardslots bearbeiten"
 
@@ -1257,12 +1261,16 @@ msgid "Updated {u} slot(s). created {c} new slot(s) and deleted {d} slot(s)"
 msgstr ""
 "{u} Slot(s) aktualisiert, {c} Slot(s) hinzugefügt und {d} Slot(s) gelöscht"
 
+#: AKModel/views/manage.py:252
+msgid "AK JSON Import"
+msgstr "AK-JSON-Import"
+
 #: AKModel/views/room.py:37
 #, python-format
 msgid "Created Room '%(room)s'"
 msgstr "Raum '%(room)s' angelegt"
 
-#: AKModel/views/room.py:51 AKModel/views/status.py:82
+#: AKModel/views/room.py:51 AKModel/views/status.py:81
 msgid "Import Rooms from CSV"
 msgstr "Räume aus CSV importieren"
 
@@ -1280,50 +1288,64 @@ msgstr "{count} Raum/Räume importiert"
 msgid "No rooms imported"
 msgstr "Keine Räume importiert"
 
-#: AKModel/views/status.py:17
+#: AKModel/views/status.py:16
 msgid "Overview"
 msgstr "Überblick"
 
-#: AKModel/views/status.py:33
+#: AKModel/views/status.py:32
 msgid "Categories"
 msgstr "Kategorien"
 
-#: AKModel/views/status.py:37
+#: AKModel/views/status.py:36
 msgid "Add category"
 msgstr "Kategorie hinzufügen"
 
-#: AKModel/views/status.py:64
+#: AKModel/views/status.py:63
 msgid "Add Room"
 msgstr "Raum hinzufügen"
 
-#: AKModel/views/status.py:116
+#: AKModel/views/status.py:115
 msgid "AKs requiring special attention"
 msgstr "AKs, die besondere Aufmerksamkeit benötigen"
 
-#: AKModel/views/status.py:122
+#: AKModel/views/status.py:121
 msgid "Enter Interest"
 msgstr "Interesse erfassen"
 
-#: AKModel/views/status.py:134
+#: AKModel/views/status.py:133
 msgid "Manage ak tracks"
 msgstr "AK-Tracks verwalten"
 
-#: AKModel/views/status.py:138
+#: AKModel/views/status.py:137
+msgid "Import AK schedule from JSON"
+msgstr "AK-Plan aus JSON importieren"
+
+#: AKModel/views/status.py:141
 msgid "Export AKs as CSV"
 msgstr "AKs als CSV exportieren"
 
-#: AKModel/views/status.py:142
+#: AKModel/views/status.py:145
+msgid "Export AKs as JSON"
+msgstr "AKs als JSON exportieren"
+
+#: AKModel/views/status.py:149
 msgid "Export AKs for Wiki"
 msgstr "AKs im Wiki-Format exportieren"
 
-#: AKModel/views/status.py:175
+#: AKModel/views/status.py:182
 msgid "Show AKs for requirements"
 msgstr "Zu Anforderungen gehörige AKs anzeigen"
 
-#: AKModel/views/status.py:189
+#: AKModel/views/status.py:196
 msgid "Event Status"
 msgstr "Eventstatus"
 
+#~ msgid "AKs by Owner"
+#~ msgstr "AKs der Leitung"
+
+#~ msgid "This user does not have any AKs currently"
+#~ msgstr "Diese Leitung hat aktuell keine AKs"
+
 #~ msgid "Opening time for expression of interest."
 #~ msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs."
 
diff --git a/AKPlan/locale/de_DE/LC_MESSAGES/django.po b/AKPlan/locale/de_DE/LC_MESSAGES/django.po
index 90ee78c1..1bbfe725 100644
--- a/AKPlan/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKPlan/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: 2023-05-15 20:03+0200\n"
+"POT-Creation-Date: 2024-05-27 01:57+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"
@@ -38,7 +38,7 @@ msgstr "Veranstaltung"
 #: AKPlan/templates/AKPlan/plan_index.html:59
 #: AKPlan/templates/AKPlan/plan_room.html:13
 #: AKPlan/templates/AKPlan/plan_room.html:59
-#: AKPlan/templates/AKPlan/plan_wall.html:65
+#: AKPlan/templates/AKPlan/plan_wall.html:67
 msgid "Room"
 msgstr "Raum"
 
@@ -63,12 +63,12 @@ msgid "AK Wall"
 msgstr "AK-Wall"
 
 #: AKPlan/templates/AKPlan/plan_index.html:130
-#: AKPlan/templates/AKPlan/plan_wall.html:130
+#: AKPlan/templates/AKPlan/plan_wall.html:132
 msgid "Current AKs"
 msgstr "Aktuelle AKs"
 
 #: AKPlan/templates/AKPlan/plan_index.html:137
-#: AKPlan/templates/AKPlan/plan_wall.html:135
+#: AKPlan/templates/AKPlan/plan_wall.html:137
 msgid "Next AKs"
 msgstr "Nächste AKs"
 
@@ -99,7 +99,7 @@ msgstr "Eigenschaften"
 msgid "Track"
 msgstr "Track"
 
-#: AKPlan/templates/AKPlan/plan_wall.html:145
+#: AKPlan/templates/AKPlan/plan_wall.html:147
 msgid "Reload page automatically?"
 msgstr "Seite automatisch neu laden?"
 
diff --git a/AKPlanning/locale/de_DE/LC_MESSAGES/django.po b/AKPlanning/locale/de_DE/LC_MESSAGES/django.po
index 3f47223f..68529ec4 100644
--- a/AKPlanning/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKPlanning/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: 2023-08-16 16:30+0200\n"
+"POT-Creation-Date: 2024-05-27 01:57+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,10 +17,10 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: AKPlanning/settings.py:148
+#: AKPlanning/settings.py:147
 msgid "German"
 msgstr "Deutsch"
 
-#: AKPlanning/settings.py:149
+#: AKPlanning/settings.py:148
 msgid "English"
 msgstr "Englisch"
diff --git a/AKSubmission/locale/de_DE/LC_MESSAGES/django.po b/AKSubmission/locale/de_DE/LC_MESSAGES/django.po
index b25db760..e8add44b 100644
--- a/AKSubmission/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKSubmission/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: 2023-08-16 16:30+0200\n"
+"POT-Creation-Date: 2024-05-27 01:57+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,16 +17,16 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
-#: AKSubmission/forms.py:93
+#: AKSubmission/forms.py:95
 #, python-format
 msgid "\"%(duration)s\" is not a valid duration"
 msgstr "\"%(duration)s\" ist keine gültige Dauer"
 
-#: AKSubmission/forms.py:159
+#: AKSubmission/forms.py:155
 msgid "Duration(s)"
 msgstr "Dauer(n)"
 
-#: AKSubmission/forms.py:161
+#: AKSubmission/forms.py:157
 msgid ""
 "Enter at least one planned duration (in hours). If your AK should have "
 "multiple slots, use multiple lines"
-- 
GitLab


From 6884a49bc4349fb2c3ff9866b2e701bca64a6788 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 May 2024 05:14:56 +0200
Subject: [PATCH 03/77] Fix migration erros

---
 AKModel/models.py   | 52 +++++++++++++++++++++++++++++++--------------
 AKModel/views/ak.py |  4 +---
 2 files changed, 37 insertions(+), 19 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index c4c6f804..7049fca3 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -297,22 +297,6 @@ class AKOwner(models.Model):
         """
         return AKOwner.objects.get(event=event, slug=slug)
 
-    def as_json(self) -> str:
-        data = {
-            "id": self.pk,
-            "info": {
-                "name": self.name,
-            },
-            "capacity": self.capacity,
-            "fulfilled_room_constraints": [constraint.name
-                                           for constraint in self.properties.all()],
-            "time_constraints": [f"availability-room-{self.pk}"]
-        }
-
-        return json.dumps(data)
-
-
-
 
 class AKCategory(models.Model):
     """ An AKCategory describes the characteristics of an AK, e.g. content vs. recreational.
@@ -590,6 +574,20 @@ class Room(models.Model):
     def __str__(self):
         return self.title
 
+    def as_json(self) -> str:
+        data = {
+            "id": self.pk,
+            "info": {
+                "name": self.name,
+            },
+            "capacity": self.capacity,
+            "fulfilled_room_constraints": [constraint.name
+                                           for constraint in self.properties.all()],
+            "time_constraints": [f"availability-room-{self.pk}"]
+        }
+
+        return json.dumps(data)
+
 
 class AKSlot(models.Model):
     """ An AK Mapping matches an AK to a room during a certain time.
@@ -685,6 +683,28 @@ class AKSlot(models.Model):
             self.duration = min(self.duration, event_duration_hours)
         super().save(force_insert, force_update, using, update_fields)
 
+    def as_json(self) -> str:
+        data = {
+            "id": self.pk,
+            "duration": int(self.duration * self.slots_in_an_hour),
+            "properties": {},
+            "room_constraints": [constraint.name
+                                 for constraint in self.ak.requirements.all()],
+            "time_constraints": ["resolution"] if self.ak.reso else [],
+            "info": {
+                "name": self.ak.name,
+                "head": ", ".join([str(owner)
+                                   for owner in self.ak.owners.all()]),
+                "description": self.ak.description,
+                "reso": self.ak.reso,
+                },
+            }
+
+        data["time_constraints"].append(f"availability-ak-{self.pk}")
+        data["time_constraints"] += [f"availability-person-{owner.pk}"
+                                     for owner in self.ak.owners.all()]
+
+        return json.dumps(data)
 
 class AKOrgaMessage(models.Model):
     """
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 547f3606..88fc6bf4 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -8,7 +8,7 @@ from django.views.generic import ListView, DetailView
 
 from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \
     IntermediateAdminActionView
-from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK
+from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner
 
 
 class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
@@ -81,8 +81,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                                       for person in AKOwner.objects.filter(event=self.event)
                                       for availability in person.availabilities.all()}
 
-
-
         for block in self.event.time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR):
             current_block = []
 
-- 
GitLab


From ce126f7050ce34c33b24e137f3710a343a41db77 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 May 2024 05:15:09 +0200
Subject: [PATCH 04/77] Handle case of unset reso_deadline

---
 AKModel/views/ak.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 88fc6bf4..0539898e 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -89,7 +89,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                                             slots_in_an_hour=SLOTS_IN_AN_HOUR)
                 constraints = []
 
-                if slot.end < self.event.reso_deadline:
+                if self.event.reso_deadline is None or slot.end < self.event.reso_deadline:
                     constraints.append("resolution")
 
                 for (ak, availability) in ak_availabilities.items():
-- 
GitLab


From ae5cfae376066b3753446ec040085e2d171a2b5e Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 May 2024 06:52:44 +0200
Subject: [PATCH 05/77] Do not introduce constraints if available for whole
 event

---
 AKModel/models.py | 38 ++++++++++++++++++++++++++++++++++----
 1 file changed, 34 insertions(+), 4 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 7049fca3..55060197 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -575,6 +575,17 @@ class Room(models.Model):
         return self.title
 
     def as_json(self) -> str:
+        from AKModel.availability.models import Availability
+
+        # check if room is available for the whole event
+        # -> no time constraint needs to be introduced
+        full_event = Availability(event=self.event, start=self.event.start, end=self.event.end)
+        avail_union = Availability.union(self.availabilities.all())
+        if len(avail_union) == 1 and avail_union[0].contains(full_event):
+            time_constraints = []
+        else:
+            time_constraints = [f"availability-room-{self.pk}"]
+
         data = {
             "id": self.pk,
             "info": {
@@ -583,7 +594,7 @@ class Room(models.Model):
             "capacity": self.capacity,
             "fulfilled_room_constraints": [constraint.name
                                            for constraint in self.properties.all()],
-            "time_constraints": [f"availability-room-{self.pk}"]
+            "time_constraints": time_constraints
         }
 
         return json.dumps(data)
@@ -684,6 +695,25 @@ class AKSlot(models.Model):
         super().save(force_insert, force_update, using, update_fields)
 
     def as_json(self) -> str:
+        from AKModel.availability.models import Availability
+
+        # check if ak resp. owner is available for the whole event
+        # -> no time constraint needs to be introduced
+        full_event = Availability(event=self.event, start=self.event.start, end=self.event.end)
+
+        ak_avail_union = Availability.union(self.ak.availabilities.all())
+        if not ak_avail_union or ak_avail_union[0].contains(full_event):
+            ak_time_constraints = []
+        else:
+            ak_time_constraints = [f"availability-ak-{self.ak.pk}"]
+
+        def _owner_time_constraints(owner: AKOwner):
+            owner_avail_union = Availability.union(owner.availabilities.all())
+            if not owner_avail_union or owner_avail_union[0].contains(full_event):
+                return []
+            else:
+                return [f"availability-person-{owner.pk}"]
+
         data = {
             "id": self.pk,
             "duration": int(self.duration * self.slots_in_an_hour),
@@ -700,9 +730,9 @@ class AKSlot(models.Model):
                 },
             }
 
-        data["time_constraints"].append(f"availability-ak-{self.pk}")
-        data["time_constraints"] += [f"availability-person-{owner.pk}"
-                                     for owner in self.ak.owners.all()]
+        data["time_constraints"].extend(ak_time_constraints)
+        for owner in self.ak.owners.all():
+            data["time_constraints"].extend(_owner_time_constraints(owner))
 
         return json.dumps(data)
 
-- 
GitLab


From 9db6b5b7d3e31c00588f5a5ab5cf363cc09f64ff Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 May 2024 07:36:57 +0200
Subject: [PATCH 06/77] Add note for possible bug or unexpected behaviour

---
 AKModel/models.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/AKModel/models.py b/AKModel/models.py
index 55060197..68158954 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -579,6 +579,9 @@ class Room(models.Model):
 
         # check if room is available for the whole event
         # -> no time constraint needs to be introduced
+
+        # NOTE: Cannot use `Availability.with_event_length` as its end is the
+        #       event end + 1 day
         full_event = Availability(event=self.event, start=self.event.start, end=self.event.end)
         avail_union = Availability.union(self.availabilities.all())
         if len(avail_union) == 1 and avail_union[0].contains(full_event):
@@ -699,6 +702,9 @@ class AKSlot(models.Model):
 
         # check if ak resp. owner is available for the whole event
         # -> no time constraint needs to be introduced
+
+        # NOTE: Cannot use `Availability.with_event_length` as its end is the
+        #       event end + 1 day
         full_event = Availability(event=self.event, start=self.event.start, end=self.event.end)
 
         ak_avail_union = Availability.union(self.ak.availabilities.all())
-- 
GitLab


From e2884a5911f48baf89486f3b32c589351ce34149 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 May 2024 07:37:17 +0200
Subject: [PATCH 07/77] Fix check

---
 AKModel/models.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 68158954..c0078013 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -584,7 +584,7 @@ class Room(models.Model):
         #       event end + 1 day
         full_event = Availability(event=self.event, start=self.event.start, end=self.event.end)
         avail_union = Availability.union(self.availabilities.all())
-        if len(avail_union) == 1 and avail_union[0].contains(full_event):
+        if not avail_union or avail_union[0].contains(full_event):
             time_constraints = []
         else:
             time_constraints = [f"availability-room-{self.pk}"]
-- 
GitLab


From 440709c1891512885031e45c073817e3a8f1b37c Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 May 2024 07:46:06 +0200
Subject: [PATCH 08/77] Introduce availabilits class method is_event_covered

---
 AKModel/availability/models.py |  8 ++++++++
 AKModel/models.py              | 17 +++--------------
 2 files changed, 11 insertions(+), 14 deletions(-)

diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py
index 7ce794dc..de51e438 100644
--- a/AKModel/availability/models.py
+++ b/AKModel/availability/models.py
@@ -267,6 +267,14 @@ class Availability(models.Model):
         return Availability(start=timeframe_start, end=timeframe_end, event=event, person=person,
                                     room=room, ak=ak, ak_category=ak_category)
 
+    @classmethod
+    def is_event_covered(cls, event, availabilities: List['Availability']) -> bool:
+        # NOTE: Cannot use `Availability.with_event_length` as its end is the
+        #       event end + 1 day
+        full_event = Availability(event=event, start=event.start, end=event.end)
+        avail_union = Availability.union(availabilities)
+        return not avail_union or avail_union[0].contains(full_event)
+
     class Meta:
         verbose_name = _('Availability')
         verbose_name_plural = _('Availabilities')
diff --git a/AKModel/models.py b/AKModel/models.py
index c0078013..2052a5cd 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -579,12 +579,7 @@ class Room(models.Model):
 
         # check if room is available for the whole event
         # -> no time constraint needs to be introduced
-
-        # NOTE: Cannot use `Availability.with_event_length` as its end is the
-        #       event end + 1 day
-        full_event = Availability(event=self.event, start=self.event.start, end=self.event.end)
-        avail_union = Availability.union(self.availabilities.all())
-        if not avail_union or avail_union[0].contains(full_event):
+        if Availability.is_event_covered(self.event, self.availabilities.all()):
             time_constraints = []
         else:
             time_constraints = [f"availability-room-{self.pk}"]
@@ -703,19 +698,13 @@ class AKSlot(models.Model):
         # check if ak resp. owner is available for the whole event
         # -> no time constraint needs to be introduced
 
-        # NOTE: Cannot use `Availability.with_event_length` as its end is the
-        #       event end + 1 day
-        full_event = Availability(event=self.event, start=self.event.start, end=self.event.end)
-
-        ak_avail_union = Availability.union(self.ak.availabilities.all())
-        if not ak_avail_union or ak_avail_union[0].contains(full_event):
+        if Availability.is_event_covered(self.event, self.ak.availabilities.all()):
             ak_time_constraints = []
         else:
             ak_time_constraints = [f"availability-ak-{self.ak.pk}"]
 
         def _owner_time_constraints(owner: AKOwner):
-            owner_avail_union = Availability.union(owner.availabilities.all())
-            if not owner_avail_union or owner_avail_union[0].contains(full_event):
+            if Availability.is_event_covered(self.event, owner.availabilities.all()):
                 return []
             else:
                 return [f"availability-person-{owner.pk}"]
-- 
GitLab


From 32e7ee941e55202981a95ed0c05540188449ed15 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 May 2024 07:54:03 +0200
Subject: [PATCH 09/77] Filter unnecessary fulfilled constraints

---
 AKModel/views/ak.py | 40 +++++++++++++++++++++++++---------------
 1 file changed, 25 insertions(+), 15 deletions(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 0539898e..2c09ca08 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -1,5 +1,6 @@
 import json
 from datetime import timedelta
+from typing import List
 
 from django.contrib import messages
 from django.urls import reverse_lazy
@@ -71,15 +72,24 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         for slot in context["slots"]:
             slot.slots_in_an_hour = SLOTS_IN_AN_HOUR
 
-        ak_availabilities = {slot.ak.pk: availability
-                             for slot in context["slots"]
-                             for availability in slot.ak.availabilities.all()}
-        room_availabilities = {room.pk: availability
-                                    for room in rooms
-                                    for availability in room.availabilities.all()}
-        person_availabilities = {person.pk: availability
-                                      for person in AKOwner.objects.filter(event=self.event)
-                                      for availability in person.availabilities.all()}
+        ak_availabilities = {
+            slot.ak.pk: Availability.union(slot.ak.availabilities.all())
+            for slot in context["slots"]
+        }
+        room_availabilities = {
+            room.pk: Availability.union(room.availabilities.all())
+            for room in rooms
+        }
+        person_availabilities = {
+            person.pk: Availability.union(person.availabilities.all())
+            for person in AKOwner.objects.filter(event=self.event)
+        }
+
+        def _test_add_constraint(slot: Availability, availabilities: List[Availability]) -> bool:
+            return (
+                (not Availability.is_event_covered(self.event, availabilities))
+                and any(availability.contains(slot) for availability in availabilities)
+            )
 
         for block in self.event.time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR):
             current_block = []
@@ -92,16 +102,16 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                 if self.event.reso_deadline is None or slot.end < self.event.reso_deadline:
                     constraints.append("resolution")
 
-                for (ak, availability) in ak_availabilities.items():
-                    if availability.contains(slot):
+                for ak, availabilities in ak_availabilities.items():
+                    if _test_add_constraint(slot, availabilities):
                         constraints.append(f"availability-ak-{ak}")
 
-                for (person, availability) in person_availabilities.items():
-                    if availability.contains(slot):
+                for person, availabilities in person_availabilities.items():
+                    if _test_add_constraint(slot, availabilities):
                         constraints.append(f"availability-person-{person}")
 
-                for (room, availability) in room_availabilities.items():
-                    if availability.contains(slot):
+                for person, availabilities in room_availabilities.items():
+                    if _test_add_constraint(slot, availabilities):
                         constraints.append(f"availability-room-{room}")
 
                 current_block.append({
-- 
GitLab


From 20ab6461f233093004b458c64acec602a51e8de6 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 May 2024 09:03:20 +0200
Subject: [PATCH 10/77] Handle fixed AKs

---
 AKModel/models.py   |  2 +-
 AKModel/views/ak.py | 26 +++++++++++++++++++++-----
 2 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 2052a5cd..bfa31082 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -698,7 +698,7 @@ class AKSlot(models.Model):
         # check if ak resp. owner is available for the whole event
         # -> no time constraint needs to be introduced
 
-        if Availability.is_event_covered(self.event, self.ak.availabilities.all()):
+        if not self.fixed and Availability.is_event_covered(self.event, self.ak.availabilities.all()):
             ak_time_constraints = []
         else:
             ak_time_constraints = [f"availability-ak-{self.ak.pk}"]
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 2c09ca08..d3b0e93c 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -85,11 +85,27 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             for person in AKOwner.objects.filter(event=self.event)
         }
 
+        ak_fixed = {
+            ak: values.get()
+            for ak in ak_availabilities.keys()
+            if (values := AKSlot.objects.select_related().filter(ak__pk=ak, fixed=True)).exists()
+        }
+
+        def _test_slot_contained(slot: Availability, availabilities: List[Availability]) -> bool:
+            return any(availability.contains(slot) for availability in availabilities)
+
+        def _test_event_covered(slot: Availability, availabilities: List[Availability]) -> bool:
+            return not Availability.is_event_covered(self.event, availabilities)
+
+        def _test_fixed_ak(ak, slot) -> bool:
+            if not ak in ak_fixed:
+                return False
+
+            fixed_slot = Availability(self.event, start=ak_fixed[ak].start, end=ak_fixed[ak].end)
+            return fixed_slot.overlaps(slot, strict=True)
+
         def _test_add_constraint(slot: Availability, availabilities: List[Availability]) -> bool:
-            return (
-                (not Availability.is_event_covered(self.event, availabilities))
-                and any(availability.contains(slot) for availability in availabilities)
-            )
+            return _test_event_covered(slot, availabilities) and _test_slot_contained(slot, availabilities)
 
         for block in self.event.time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR):
             current_block = []
@@ -103,7 +119,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                     constraints.append("resolution")
 
                 for ak, availabilities in ak_availabilities.items():
-                    if _test_add_constraint(slot, availabilities):
+                    if _test_add_constraint(slot, availabilities) or _test_fixed_ak(ak, slot):
                         constraints.append(f"availability-ak-{ak}")
 
                 for person, availabilities in person_availabilities.items():
-- 
GitLab


From e6b791af0ba8a5a7dd5ec97536e68c8e0faedb13 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 May 2024 09:09:11 +0200
Subject: [PATCH 11/77] Handle fixed rooms

---
 AKModel/models.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/AKModel/models.py b/AKModel/models.py
index bfa31082..fe8bb843 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -595,6 +595,8 @@ class Room(models.Model):
             "time_constraints": time_constraints
         }
 
+        data["fulfilled_room_constraints"].append(f"availability-room-{self.pk}")
+
         return json.dumps(data)
 
 
@@ -729,6 +731,9 @@ class AKSlot(models.Model):
         for owner in self.ak.owners.all():
             data["time_constraints"].extend(_owner_time_constraints(owner))
 
+        if self.room is not None:
+            data["room_constraints"].append(f"availability-room-{self.room.pk}")
+
         return json.dumps(data)
 
 class AKOrgaMessage(models.Model):
-- 
GitLab


From 2b9419cebd54f3862f0015c8699ee646c527d4be Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 May 2024 09:16:35 +0200
Subject: [PATCH 12/77] type hints

---
 AKModel/models.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index fe8bb843..4cc9343d 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -196,7 +196,7 @@ class Event(models.Model):
 
         yield current_block
 
-    def time_slot(self, *, time_slot_index, slots_in_an_hour=1.0):
+    def time_slot(self, *, time_slot_index: int, slots_in_an_hour: float = 1.0) -> "Availability":
         from AKModel.availability.models import Availability
         slot_duration = timedelta(hours=(1.0 / slots_in_an_hour))
 
@@ -206,7 +206,7 @@ class Event(models.Model):
                             start=start,
                             end=start + slot_duration)
 
-    def schedule_from_json(self, schedule):
+    def schedule_from_json(self, schedule: str) -> None:
         schedule = json.loads(schedule)
 
         slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"]
-- 
GitLab


From 034748fecf5dca5f2bde21a4583fe1b29bb7ba92 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 May 2024 09:28:49 +0200
Subject: [PATCH 13/77] Fix bug that assigned time but not assigned room
 crashes

---
 AKScheduling/models.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/AKScheduling/models.py b/AKScheduling/models.py
index 1495f311..aa6be3d0 100644
--- a/AKScheduling/models.py
+++ b/AKScheduling/models.py
@@ -288,6 +288,8 @@ def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs)
     for slot in slots_of_this_ak:
 
         room = slot.room
+        if room is None:
+            continue
         room_requirements = room.properties.all()
 
         for requirement in instance.requirements.all():
-- 
GitLab


From dabd1769bc9ea3c54c1a4194da85896a1fcb743d Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Tue, 28 May 2024 21:30:27 +0200
Subject: [PATCH 14/77] Fix import code

---
 AKModel/models.py | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 4cc9343d..5337bb4f 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -212,17 +212,19 @@ class Event(models.Model):
         slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"]
 
         for scheduled_slot in schedule["scheduled_aks"]:
-            slot = AKSlot.objects.get(scheduled_slot["ak_id"])
-            slot.room = scheduled_slot["room_id"]
+            slot = AKSlot.objects.get(id=int(scheduled_slot["ak_id"]))
+            slot.room = Room.objects.get(id=int(scheduled_slot["room_id"]))
 
-            start = min(scheduled_slot["time_slot_ids"])
-            end = max(scheduled_slot["time_slot_ids"])
+            scheduled_slot["timeslot_ids"] = list(map(int, scheduled_slot["timeslot_ids"]))
+
+            start = min(scheduled_slot["timeslot_ids"])
+            end = max(scheduled_slot["timeslot_ids"])
 
             slot.start = self.time_slot(time_slot_index=start,
-                                        slots_in_an_hour=slots_in_an_hour)
-            slot.end = self.time_slot(time_slot_index=end + 1,
-                                      slots_in_an_hour=slots_in_an_hour)
+                                        slots_in_an_hour=slots_in_an_hour).start
 
+            slot.duration = (end - start + 1) * timedelta(hours=(1.0 / slots_in_an_hour)).total_seconds() / 3600.0
+            slot.save()
 
 class AKOwner(models.Model):
     """ An AKOwner describes the person organizing/holding an AK.
-- 
GitLab


From cebd106214a660d411de2fcbf775174e13448d95 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Tue, 28 May 2024 21:30:37 +0200
Subject: [PATCH 15/77] Add missing import

---
 AKModel/views/manage.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py
index 0ed33044..f4564f09 100644
--- a/AKModel/views/manage.py
+++ b/AKModel/views/manage.py
@@ -4,8 +4,10 @@ import os
 import tempfile
 from itertools import zip_longest
 
+
 from django.contrib import messages
 from django.db.models.functions import Now
+from django.shortcuts import redirect
 from django.utils.dateparse import parse_datetime
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import TemplateView, DetailView
-- 
GitLab


From 6df70c288f04c52187298b94b0ed20a3e8a5d55b Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Wed, 29 May 2024 01:14:59 +0200
Subject: [PATCH 16/77] Export ids as str

---
 AKModel/models.py   | 4 ++--
 AKModel/views/ak.py | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 5337bb4f..8e221a3a 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -587,7 +587,7 @@ class Room(models.Model):
             time_constraints = [f"availability-room-{self.pk}"]
 
         data = {
-            "id": self.pk,
+            "id": str(self.pk),
             "info": {
                 "name": self.name,
             },
@@ -714,7 +714,7 @@ class AKSlot(models.Model):
                 return [f"availability-person-{owner.pk}"]
 
         data = {
-            "id": self.pk,
+            "id": str(self.pk),
             "duration": int(self.duration * self.slots_in_an_hour),
             "properties": {},
             "room_constraints": [constraint.name
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index d3b0e93c..b8b55ee8 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -131,7 +131,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                         constraints.append(f"availability-room-{room}")
 
                 current_block.append({
-                    "id": slot_index,
+                    "id": str(slot_index),
                     "info": {
                         "start": slot.simplified,
                     },
-- 
GitLab


From b6c5952e618973bf2a4be22cc6d869696915e3a8 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Wed, 29 May 2024 18:34:11 +0200
Subject: [PATCH 17/77] Refactor

---
 AKModel/views/ak.py | 26 +++++++++++++-------------
 1 file changed, 13 insertions(+), 13 deletions(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index b8b55ee8..611cd619 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -86,9 +86,9 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         }
 
         ak_fixed = {
-            ak: values.get()
-            for ak in ak_availabilities.keys()
-            if (values := AKSlot.objects.select_related().filter(ak__pk=ak, fixed=True)).exists()
+            ak_id: values.get()
+            for ak_id in ak_availabilities.keys()
+            if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists()
         }
 
         def _test_slot_contained(slot: Availability, availabilities: List[Availability]) -> bool:
@@ -97,11 +97,11 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         def _test_event_covered(slot: Availability, availabilities: List[Availability]) -> bool:
             return not Availability.is_event_covered(self.event, availabilities)
 
-        def _test_fixed_ak(ak, slot) -> bool:
-            if not ak in ak_fixed:
+        def _test_fixed_ak(ak_id, slot: Availability) -> bool:
+            if not ak_id in ak_fixed:
                 return False
 
-            fixed_slot = Availability(self.event, start=ak_fixed[ak].start, end=ak_fixed[ak].end)
+            fixed_slot = Availability(self.event, start=ak_fixed[ak_id].start, end=ak_fixed[ak_id].end)
             return fixed_slot.overlaps(slot, strict=True)
 
         def _test_add_constraint(slot: Availability, availabilities: List[Availability]) -> bool:
@@ -118,17 +118,17 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                 if self.event.reso_deadline is None or slot.end < self.event.reso_deadline:
                     constraints.append("resolution")
 
-                for ak, availabilities in ak_availabilities.items():
-                    if _test_add_constraint(slot, availabilities) or _test_fixed_ak(ak, slot):
-                        constraints.append(f"availability-ak-{ak}")
+                for ak_id, availabilities in ak_availabilities.items():
+                    if _test_add_constraint(slot, availabilities) or _test_fixed_ak(ak_id, slot):
+                        constraints.append(f"availability-ak-{ak_id}")
 
-                for person, availabilities in person_availabilities.items():
+                for person_id, availabilities in person_availabilities.items():
                     if _test_add_constraint(slot, availabilities):
-                        constraints.append(f"availability-person-{person}")
+                        constraints.append(f"availability-person-{person_id}")
 
-                for person, availabilities in room_availabilities.items():
+                for room_id, availabilities in room_availabilities.items():
                     if _test_add_constraint(slot, availabilities):
-                        constraints.append(f"availability-room-{room}")
+                        constraints.append(f"availability-room-{room_id}")
 
                 current_block.append({
                     "id": str(slot_index),
-- 
GitLab


From d2f88dd144b85620f2a6bb34af4479965e383734 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Wed, 29 May 2024 18:53:32 +0200
Subject: [PATCH 18/77] More refactoring

---
 AKModel/views/ak.py | 34 +++++++++++++++++++---------------
 1 file changed, 19 insertions(+), 15 deletions(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 611cd619..a8e07d26 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -113,29 +113,33 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             for slot_index in block:
                 slot = self.event.time_slot(time_slot_index=slot_index,
                                             slots_in_an_hour=SLOTS_IN_AN_HOUR)
-                constraints = []
+                time_constraints = []
 
                 if self.event.reso_deadline is None or slot.end < self.event.reso_deadline:
-                    constraints.append("resolution")
-
-                for ak_id, availabilities in ak_availabilities.items():
-                    if _test_add_constraint(slot, availabilities) or _test_fixed_ak(ak_id, slot):
-                        constraints.append(f"availability-ak-{ak_id}")
-
-                for person_id, availabilities in person_availabilities.items():
-                    if _test_add_constraint(slot, availabilities):
-                        constraints.append(f"availability-person-{person_id}")
-
-                for room_id, availabilities in room_availabilities.items():
-                    if _test_add_constraint(slot, availabilities):
-                        constraints.append(f"availability-room-{room_id}")
+                    time_constraints.append("resolution")
+
+                time_constraints.extend([
+                    f"availability-ak-{ak_id}"
+                    for ak_id, availabilities in ak_availabilities.items()
+                    if _test_add_constraint(slot, availabilities) or _test_fixed_ak(ak_id, slot)
+                ])
+                time_constraints.extend([
+                    f"availability-person-{person_id}"
+                    for person_id, availabilities in person_availabilities.items()
+                    if _test_add_constraint(slot, availabilities)
+                ])
+                time_constraints.extend([
+                    f"availability-room-{room_id}"
+                    for room_id, availabilities in room_availabilities.items()
+                    if _test_add_constraint(slot, availabilities)
+                ])
 
                 current_block.append({
                     "id": str(slot_index),
                     "info": {
                         "start": slot.simplified,
                     },
-                    "fulfilled_time_constraints": constraints,
+                    "fulfilled_time_constraints": time_constraints,
                     })
 
             timeslots["blocks"].append(current_block)
-- 
GitLab


From 14c3690a7b99af8156974daf06d6118c9f7bd5f0 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Wed, 29 May 2024 18:54:15 +0200
Subject: [PATCH 19/77] Add no-proxy constraints

---
 AKModel/models.py | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/AKModel/models.py b/AKModel/models.py
index 8e221a3a..3f5a0f4c 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -599,6 +599,9 @@ class Room(models.Model):
 
         data["fulfilled_room_constraints"].append(f"availability-room-{self.pk}")
 
+        if not any(constr.startswith("proxy") for constr in data["fulfilled_room_constraints"]):
+            data["fulfilled_room_constraints"].append("no-proxy")
+
         return json.dumps(data)
 
 
@@ -736,6 +739,9 @@ class AKSlot(models.Model):
         if self.room is not None:
             data["room_constraints"].append(f"availability-room-{self.room.pk}")
 
+        if not any(constr.startswith("proxy") for constr in data["room_constraints"]):
+            data["room_constraints"].append("no-proxy")
+
         return json.dumps(data)
 
 class AKOrgaMessage(models.Model):
-- 
GitLab


From d92c87e1e4d0d6fa74f6941c559fec047499fd5c Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Wed, 29 May 2024 19:14:06 +0200
Subject: [PATCH 20/77] Add empty info dict to json export

---
 AKModel/templates/admin/AKModel/ak_json_export.html | 3 ++-
 AKModel/views/ak.py                                 | 2 ++
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/AKModel/templates/admin/AKModel/ak_json_export.html b/AKModel/templates/admin/AKModel/ak_json_export.html
index da582504..38e5526e 100644
--- a/AKModel/templates/admin/AKModel/ak_json_export.html
+++ b/AKModel/templates/admin/AKModel/ak_json_export.html
@@ -13,7 +13,8 @@
       {% endif %}{% endfor %}
     ],
   "participants": {{ participants }},
-  "timeslots": {{ timeslots }}
+  "timeslots": {{ timeslots }},
+  "info": {{ info_dict }}
   }
 </pre>
 {% endblock %}
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index a8e07d26..90c3ef6e 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -146,6 +146,8 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
 
         context["timeslots"] = json.dumps(timeslots)
 
+        context["info_dict"] = {}
+
         return context
 
 
-- 
GitLab


From aea39c3c8739151b228b56c0d20ecf7f77c4779c Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Wed, 29 May 2024 19:51:19 +0200
Subject: [PATCH 21/77] Only export preset room if ak is fixed

---
 AKModel/models.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 3f5a0f4c..86fa8e15 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -736,7 +736,7 @@ class AKSlot(models.Model):
         for owner in self.ak.owners.all():
             data["time_constraints"].extend(_owner_time_constraints(owner))
 
-        if self.room is not None:
+        if self.room is not None and self.fixed:
             data["room_constraints"].append(f"availability-room-{self.room.pk}")
 
         if not any(constr.startswith("proxy") for constr in data["room_constraints"]):
-- 
GitLab


From 15f238b039f72054a1d68c921b4e7827b44800fc Mon Sep 17 00:00:00 2001
From: Felix Blanke <s6feblan@uni-bonn.de>
Date: Wed, 29 May 2024 21:51:29 +0000
Subject: [PATCH 22/77] Generate timeslots based on default slots

---
 AKModel/models.py   | 97 ++++++++++++++++++++++++++++-----------------
 AKModel/views/ak.py |  6 +--
 2 files changed, 63 insertions(+), 40 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 86fa8e15..3a91facb 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -1,6 +1,7 @@
 import itertools
 import json
-from datetime import timedelta
+from datetime import datetime, timedelta
+from typing import Iterable
 
 from django.db import models
 from django.apps import apps
@@ -163,67 +164,91 @@ class Event(models.Model):
                 .filter(availabilities__count=0, owners__count__gt=0)
                 )
 
-    def time_slots(self, *, slots_in_an_hour=1.0):
+    def _generate_slots_from_block(
+        self, start: datetime, end: datetime, slot_duration: timedelta, slot_index: int = 0
+    ) -> Iterable[list[int, "Availability"]]:
         from AKModel.availability.models import Availability
 
-        rooms = Room.objects.filter(event=self)
-        slot_duration = timedelta(hours=(1.0 / slots_in_an_hour))
-        slot_index = 0
-        current_slot = self.start
-        current_block = []
-        previous_slot = None
-
-        room_availabilities = list({availability
-                               for room in rooms
-                               for availability in room.availabilities.all()})
+        current_slot_start = start
+        previous_slot_start: datetime | None = None
 
-        while current_slot < self.end:
-            slot = Availability(event=self,
-                                start=current_slot,
-                                end=current_slot + slot_duration)
+        current_block = []
 
-            if any((availability.contains(slot)
-                    for availability in room_availabilities)):
-                if previous_slot is not None and previous_slot + slot_duration < current_slot:
+        room_availabilities = list({
+            availability
+            for room in Room.objects.filter(event=self)
+            for availability in room.availabilities.all()
+        })
+
+        while current_slot_start + slot_duration <= end:
+            slot = Availability(
+                event=self,
+                start=current_slot_start,
+                end=current_slot_start + slot_duration,
+            )
+
+            if any((availability.contains(slot) for availability in room_availabilities)):
+                # no gap in a block
+                if (
+                    previous_slot_start is not None
+                    and previous_slot_start + slot_duration < current_slot_start
+                ):
                     yield current_block
                     current_block = []
 
-                current_block.append(slot_index)
-                previous_slot = current_slot
+                current_block.append((slot_index, slot))
+                previous_slot_start = current_slot_start
 
             slot_index += 1
-            current_slot += slot_duration
+            current_slot_start += slot_duration
 
-        yield current_block
+        if current_block:
+            yield current_block
 
-    def time_slot(self, *, time_slot_index: int, slots_in_an_hour: float = 1.0) -> "Availability":
-        from AKModel.availability.models import Availability
-        slot_duration = timedelta(hours=(1.0 / slots_in_an_hour))
+        return slot_index
 
-        start = self.start + time_slot_index * slot_duration
+    def uniform_time_slots(self, *, slots_in_an_hour=1.0) -> Iterable[list[int, "Availability"]]:
+        yield from self._generate_slots_from_block(
+            start=self.start,
+            end=self.end,
+            slot_duration=timedelta(hours=(1.0 / slots_in_an_hour)),
+        )
 
-        return Availability(event=self,
-                            start=start,
-                            end=start + slot_duration)
+    def default_time_slots(self, *, slots_in_an_hour=1.0) -> Iterable[list[int, "Availability"]]:
+        slot_duration = timedelta(hours=(1.0 / slots_in_an_hour))
+        slot_index = 0
+
+        for block_slot in DefaultSlot.objects.filter(event=self).order_by("start", "end"):
+            # NOTE: We do not differentiate between different primary categories
+            slot_index = yield from self._generate_slots_from_block(
+                start=block_slot.start,
+                end=block_slot.end,
+                slot_duration=slot_duration,
+                slot_index=slot_index,
+            )
 
     def schedule_from_json(self, schedule: str) -> None:
         schedule = json.loads(schedule)
 
         slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"]
 
+        timeslot_dict = {
+            slot_idx: slot
+            for block in self.default_time_slots(slots_in_an_hour=slots_in_an_hour)
+            for slot_idx, slot in block
+        }
+
         for scheduled_slot in schedule["scheduled_aks"]:
             slot = AKSlot.objects.get(id=int(scheduled_slot["ak_id"]))
             slot.room = Room.objects.get(id=int(scheduled_slot["room_id"]))
 
             scheduled_slot["timeslot_ids"] = list(map(int, scheduled_slot["timeslot_ids"]))
 
-            start = min(scheduled_slot["timeslot_ids"])
-            end = max(scheduled_slot["timeslot_ids"])
-
-            slot.start = self.time_slot(time_slot_index=start,
-                                        slots_in_an_hour=slots_in_an_hour).start
+            start_timeslot = timeslot_dict[min(scheduled_slot["timeslot_ids"])]
+            end_timeslot = timeslot_dict[max(scheduled_slot["timeslot_ids"])]
 
-            slot.duration = (end - start + 1) * timedelta(hours=(1.0 / slots_in_an_hour)).total_seconds() / 3600.0
+            slot.start = start_timeslot.start
+            slot.duration = (end_timeslot.end - start_timeslot.start).total_seconds() / 3600.0
             slot.save()
 
 class AKOwner(models.Model):
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 90c3ef6e..c65ae266 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -107,12 +107,10 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         def _test_add_constraint(slot: Availability, availabilities: List[Availability]) -> bool:
             return _test_event_covered(slot, availabilities) and _test_slot_contained(slot, availabilities)
 
-        for block in self.event.time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR):
+        for block in self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR):
             current_block = []
 
-            for slot_index in block:
-                slot = self.event.time_slot(time_slot_index=slot_index,
-                                            slots_in_an_hour=SLOTS_IN_AN_HOUR)
+            for slot_index, slot in block:
                 time_constraints = []
 
                 if self.event.reso_deadline is None or slot.end < self.event.reso_deadline:
-- 
GitLab


From 77f4dac93f4f8f84417ad16c311f37c8eebcce4f Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Thu, 30 May 2024 21:27:27 +0200
Subject: [PATCH 23/77] Add more event info to json export

---
 AKModel/views/ak.py | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index c65ae266..21a6fd72 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -144,7 +144,15 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
 
         context["timeslots"] = json.dumps(timeslots)
 
-        context["info_dict"] = {}
+        info_dict = {
+            "title": self.event.name,
+            "slug": self.event.slug
+        }
+        for attr in ["contact_email", "place"]:
+            if hasattr(self.event, attr) and getattr(self.event, attr):
+                info_dict[attr] = getattr(self.event, attr)
+
+        context["info_dict"] = json.dumps(info_dict)
 
         return context
 
-- 
GitLab


From e5fa470d49c2b2fe60210a510b72a3292d2b13bc Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Fri, 31 May 2024 00:50:21 +0200
Subject: [PATCH 24/77] Adress pylint warnings

---
 AKModel/availability/models.py | 18 ++++++++-
 AKModel/forms.py               |  1 +
 AKModel/models.py              | 70 +++++++++++++++++++++++++++++++---
 AKModel/views/ak.py            | 60 ++++++++++++++++-------------
 AKModel/views/manage.py        |  5 ++-
 5 files changed, 119 insertions(+), 35 deletions(-)

diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py
index de51e438..80546543 100644
--- a/AKModel/availability/models.py
+++ b/AKModel/availability/models.py
@@ -247,7 +247,14 @@ class Availability(models.Model):
                 f'{self.end.astimezone(self.event.timezone).strftime("%a %H:%M")}')
 
     @classmethod
-    def with_event_length(cls, event, person=None, room=None, ak=None, ak_category=None):
+    def with_event_length(
+        cls,
+        event: Event,
+        person: AKOwner | None = None,
+        room: Room | None = None,
+        ak: AK | None = None,
+        ak_category: AKCategory | None = None,
+    ) -> "Availability":
         """
         Create an availability covering exactly the time between event start and event end.
         Can e.g., be used to create default availabilities.
@@ -268,7 +275,14 @@ class Availability(models.Model):
                                     room=room, ak=ak, ak_category=ak_category)
 
     @classmethod
-    def is_event_covered(cls, event, availabilities: List['Availability']) -> bool:
+    def is_event_covered(cls, event: Event, availabilities: List['Availability']) -> bool:
+        """Check if list of availibilities cover whole event.
+
+        :param event: event to check.
+        :param availabilities: availabilities to check.
+        :return: whether the availabilities cover full event.
+        :rtype: bool
+        """
         # NOTE: Cannot use `Availability.with_event_length` as its end is the
         #       event end + 1 day
         full_event = Availability(event=event, start=event.start, end=event.end)
diff --git a/AKModel/forms.py b/AKModel/forms.py
index f74dc39e..74ca1b68 100644
--- a/AKModel/forms.py
+++ b/AKModel/forms.py
@@ -275,6 +275,7 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm):
 
 
 class JSONImportForm(AdminIntermediateForm):
+    """Form to import an AK schedule from a json file."""
     json_data = forms.CharField(
         required=True,
         widget=forms.Textarea,
diff --git a/AKModel/models.py b/AKModel/models.py
index 3a91facb..49159427 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -8,7 +8,6 @@ from django.apps import apps
 from django.db.models import Count
 from django.urls import reverse_lazy
 from django.utils import timezone
-from django.utils.datetime_safe import datetime
 from django.utils.text import slugify
 from django.utils.translation import gettext_lazy as _
 from simple_history.models import HistoricalRecords
@@ -167,6 +166,24 @@ class Event(models.Model):
     def _generate_slots_from_block(
         self, start: datetime, end: datetime, slot_duration: timedelta, slot_index: int = 0
     ) -> Iterable[list[int, "Availability"]]:
+        """Discretize a time range into timeslots.
+
+        Uses a uniform discretization into blocks of length `slot_duration`,
+        starting at `start`. No incomplete timeslots are generated, i.e.
+        if (`end` - `start`) is not a whole number multiple of `slot_duration`
+        then the last incomplete timeslot is dropped.
+
+        :param start: Start of the time range.
+        :param end: Start of the time range.
+        :param slot_duration: Duration of a single timeslot in the discretization.
+        :param slot_index: index of the first timeslot. Defaults to 0.
+
+        :yield: Block of optimizer timeslots as the discretization result.
+        :ytype: list of tuples, each consisisting of the timeslot id
+            and its availability to indicate its start and duration.
+        """
+        # local import to prevent cyclic import
+        # pylint: disable=import-outside-toplevel
         from AKModel.availability.models import Availability
 
         current_slot_start = start
@@ -208,14 +225,32 @@ class Event(models.Model):
         return slot_index
 
     def uniform_time_slots(self, *, slots_in_an_hour=1.0) -> Iterable[list[int, "Availability"]]:
+        """Uniformly discretize the entire event into a single block of timeslots.
+
+        :param slots_in_an_hour: The percentage of an hour covered by a single slot.
+            Determines the discretization granularity.
+        :yield: Block of optimizer timeslots as the discretization result.
+        :ytype: a single list of tuples, each consisisting of the timeslot id
+            and its availability to indicate its start and duration.
+        """
         yield from self._generate_slots_from_block(
             start=self.start,
             end=self.end,
-            slot_duration=timedelta(hours=(1.0 / slots_in_an_hour)),
+            slot_duration=timedelta(hours=1.0 / slots_in_an_hour),
         )
 
     def default_time_slots(self, *, slots_in_an_hour=1.0) -> Iterable[list[int, "Availability"]]:
-        slot_duration = timedelta(hours=(1.0 / slots_in_an_hour))
+        """Discretize the all default slots into a blocks of timeslots.
+
+        In the discretization each default slot corresponds to one block.
+
+        :param slots_in_an_hour: The percentage of an hour covered by a single slot.
+            Determines the discretization granularity.
+        :yield: Block of optimizer timeslots as the discretization result.
+        :ytype: list of tuples, each consisisting of the timeslot id
+            and its availability to indicate its start and duration.
+        """
+        slot_duration = timedelta(hours=1.0 / slots_in_an_hour)
         slot_index = 0
 
         for block_slot in DefaultSlot.objects.filter(event=self).order_by("start", "end"):
@@ -228,6 +263,13 @@ class Event(models.Model):
             )
 
     def schedule_from_json(self, schedule: str) -> None:
+        """Load AK schedule from a json string.
+
+        :param schedule: A string that can be decoded to json, describing
+            the AK schedule. The json data is assumed to be constructed
+            following the output specification of the KoMa conference optimizer, cf.
+            https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
+        """
         schedule = json.loads(schedule)
 
         slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"]
@@ -602,6 +644,15 @@ class Room(models.Model):
         return self.title
 
     def as_json(self) -> str:
+        """Return a json string representation of this room object.
+
+        :return: The json string representation is constructed
+            following the input specification of the KoMa conference optimizer, cf.
+            https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
+        :rtype: str
+        """
+        # local import to prevent cyclic import
+        # pylint: disable=import-outside-toplevel
         from AKModel.availability.models import Availability
 
         # check if room is available for the whole event
@@ -725,6 +776,15 @@ class AKSlot(models.Model):
         super().save(force_insert, force_update, using, update_fields)
 
     def as_json(self) -> str:
+        """Return a json string representation of the AK object of this slot.
+
+        :return: The json string representation is constructed
+            following the input specification of the KoMa conference optimizer, cf.
+            https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
+        :rtype: str
+        """
+        # local import to prevent cyclic import
+        # pylint: disable=import-outside-toplevel
         from AKModel.availability.models import Availability
 
         # check if ak resp. owner is available for the whole event
@@ -738,9 +798,9 @@ class AKSlot(models.Model):
         def _owner_time_constraints(owner: AKOwner):
             if Availability.is_event_covered(self.event, owner.availabilities.all()):
                 return []
-            else:
-                return [f"availability-person-{owner.pk}"]
+            return [f"availability-person-{owner.pk}"]
 
+        # self.slots_in_an_hour is set in AKJSONExportView
         data = {
             "id": str(self.pk),
             "duration": int(self.duration * self.slots_in_an_hour),
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 21a6fd72..818f15d9 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -1,5 +1,4 @@
 import json
-from datetime import timedelta
 from typing import List
 
 from django.contrib import messages
@@ -7,6 +6,7 @@ from django.urls import reverse_lazy
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import ListView, DetailView
 
+from AKModel.availability.models import Availability
 from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \
     IntermediateAdminActionView
 from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner
@@ -50,25 +50,44 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
     context_object_name = "slots"
     title = _("AK JSON Export")
 
+    def _test_slot_contained(self, slot: Availability, availabilities: List[Availability]) -> bool:
+        return any(availability.contains(slot) for availability in availabilities)
+
+    def _test_event_covered(self, availabilities: List[Availability]) -> bool:
+        return not Availability.is_event_covered(self.event, availabilities)
+
+    def _test_fixed_ak(self, ak_id, slot: Availability, ak_fixed: dict) -> bool:
+        if not ak_id in ak_fixed:
+            return False
+
+        fixed_slot = Availability(self.event, start=ak_fixed[ak_id].start, end=ak_fixed[ak_id].end)
+        return fixed_slot.overlaps(slot, strict=True)
+
+    def _test_add_constraint(self, slot: Availability, availabilities: List[Availability]) -> bool:
+        return (
+            self._test_event_covered(availabilities)
+            and self._test_slot_contained(slot, availabilities)
+        )
+
+
     def get_queryset(self):
         return super().get_queryset().order_by("ak__track")
 
     def get_context_data(self, **kwargs):
-        from AKModel.availability.models import Availability
+        context = super().get_context_data(**kwargs)
+        context["participants"] = json.dumps([])
+
+        rooms = Room.objects.filter(event=self.event)
+        context["rooms"] = rooms
 
+        # TODO: Configure magic number in event
         SLOTS_IN_AN_HOUR = 1
 
-        rooms = Room.objects.filter(event=self.event)
-        participants = []
         timeslots = {
             "info": {"duration": (1.0 / SLOTS_IN_AN_HOUR), },
             "blocks": [],
             }
 
-        context = super().get_context_data(**kwargs)
-        context["rooms"] = rooms
-        context["participants"] = json.dumps(participants)
-
         for slot in context["slots"]:
             slot.slots_in_an_hour = SLOTS_IN_AN_HOUR
 
@@ -91,22 +110,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists()
         }
 
-        def _test_slot_contained(slot: Availability, availabilities: List[Availability]) -> bool:
-            return any(availability.contains(slot) for availability in availabilities)
-
-        def _test_event_covered(slot: Availability, availabilities: List[Availability]) -> bool:
-            return not Availability.is_event_covered(self.event, availabilities)
-
-        def _test_fixed_ak(ak_id, slot: Availability) -> bool:
-            if not ak_id in ak_fixed:
-                return False
-
-            fixed_slot = Availability(self.event, start=ak_fixed[ak_id].start, end=ak_fixed[ak_id].end)
-            return fixed_slot.overlaps(slot, strict=True)
-
-        def _test_add_constraint(slot: Availability, availabilities: List[Availability]) -> bool:
-            return _test_event_covered(slot, availabilities) and _test_slot_contained(slot, availabilities)
-
         for block in self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR):
             current_block = []
 
@@ -119,17 +122,20 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                 time_constraints.extend([
                     f"availability-ak-{ak_id}"
                     for ak_id, availabilities in ak_availabilities.items()
-                    if _test_add_constraint(slot, availabilities) or _test_fixed_ak(ak_id, slot)
+                    if (
+                        self._test_add_constraint(slot, availabilities)
+                        or self._test_fixed_ak(ak_id, slot, ak_fixed)
+                    )
                 ])
                 time_constraints.extend([
                     f"availability-person-{person_id}"
                     for person_id, availabilities in person_availabilities.items()
-                    if _test_add_constraint(slot, availabilities)
+                    if self._test_add_constraint(slot, availabilities)
                 ])
                 time_constraints.extend([
                     f"availability-room-{room_id}"
                     for room_id, availabilities in room_availabilities.items()
-                    if _test_add_constraint(slot, availabilities)
+                    if self._test_add_constraint(slot, availabilities)
                 ])
 
                 current_block.append({
diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py
index f4564f09..ec5076fb 100644
--- a/AKModel/views/manage.py
+++ b/AKModel/views/manage.py
@@ -60,7 +60,7 @@ class ExportSlidesView(EventSlugMixin, IntermediateAdminView):
             Create a list of tuples cosisting of an AK and a list of upcoming AKs (list length depending on setting)
             """
             next_aks_list = zip_longest(*[ak_list[i + 1:] for i in range(NEXT_AK_LIST_LENGTH)], fillvalue=None)
-            return [(ak, next_aks) for ak, next_aks in zip_longest(ak_list, next_aks_list, fillvalue=[])]
+            return list(zip_longest(ak_list, next_aks_list, fillvalue=[]))
 
         # Get all relevant AKs (wishes separately, and either all AKs or only those who should directly or indirectly
         # be presented when restriction setting was chosen)
@@ -250,6 +250,9 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView):
 
 
 class AKJSONImportView(EventSlugMixin, IntermediateAdminView):
+    """
+    View: Import an AK schedule from a json file that can be pasted into this view.
+    """
     form_class = JSONImportForm
     title = _("AK JSON Import")
 
-- 
GitLab


From 216eecff41372f2ec9375491b680ff87c393ecbd Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Fri, 31 May 2024 01:36:46 +0200
Subject: [PATCH 25/77] Bump python version to 3.10

---
 .gitlab-ci.yml | 2 +-
 INSTALL.md     | 6 +++---
 Utils/setup.sh | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index fe4d4446..55ef5901 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: python:3.9
+image: python:3.10
 
 services:
   - mysql
diff --git a/INSTALL.md b/INSTALL.md
index c887af92..5344a998 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -10,7 +10,7 @@ setup.
 
 ### System Requirements
 
-* Python 3.8+ incl. development tools
+* Python 3.10+ incl. development tools
 * Virtualenv
 * pdflatex & beamer
   class (`texlive-latex-base texlive-latex-recommended texlive-latex-extra texlive-fonts-extra texlive-luatex`)
@@ -37,7 +37,7 @@ Python requirements are listed in ``requirements.txt``. They can be installed wi
 
 ### Manual Setup
 
-1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.7``
+1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.10``
 1. activate virtualenv ``source venv/bin/activate``
 1. install python requirements ``pip install -r requirements.txt``
 1. setup necessary database tables etc. ``python manage.py migrate``
@@ -68,7 +68,7 @@ is not stored in any repository or similar, and disable DEBUG mode (``settings.p
 1. create a folder, e.g. ``mkdir /srv/AKPlanning/``
 1. change to the new directory ``cd /srv/AKPlanning/``
 1. clone this repository ``git clone URL .``
-1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.7``
+1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.10``
 1. activate virtualenv ``source venv/bin/activate``
 1. update tools ``pip install --upgrade setuptools pip wheel``
 1. install python requirements ``pip install -r requirements.txt``
diff --git a/Utils/setup.sh b/Utils/setup.sh
index 1c951824..6a93207d 100755
--- a/Utils/setup.sh
+++ b/Utils/setup.sh
@@ -10,7 +10,7 @@ rm -rf venv/
 
 # Setup Python Environment
 # Requires: Virtualenv, appropriate Python installation
-virtualenv venv -p python3.9
+virtualenv venv -p python3.10
 source venv/bin/activate
 pip install --upgrade setuptools pip wheel
 pip install -r requirements.txt
-- 
GitLab


From 58bbe6a95f069a0b4e2bf1c6ccfc33a40bfc80c7 Mon Sep 17 00:00:00 2001
From: Felix Blanke <s6feblan@uni-bonn.de>
Date: Fri, 31 May 2024 14:18:53 +0000
Subject: [PATCH 26/77] Merge AK category slots

---
 AKModel/availability/models.py |  10 +-
 AKModel/models.py              | 162 +++++++++++++++++++++++++++++----
 AKModel/views/ak.py            |  22 ++---
 3 files changed, 163 insertions(+), 31 deletions(-)

diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py
index 80546543..35814ee0 100644
--- a/AKModel/availability/models.py
+++ b/AKModel/availability/models.py
@@ -151,9 +151,12 @@ class Availability(models.Model):
         if not other.overlaps(self, strict=False):
             raise Exception('Only overlapping Availabilities can be merged.')
 
-        return Availability(
+        avail = Availability(
             start=min(self.start, other.start), end=max(self.end, other.end)
         )
+        if self.event == other.event:
+            avail.event = self.event
+        return avail
 
     def __or__(self, other: 'Availability') -> 'Availability':
         """Performs the merge operation: ``availability1 | availability2``"""
@@ -168,9 +171,12 @@ class Availability(models.Model):
         if not other.overlaps(self, False):
             raise Exception('Only overlapping Availabilities can be intersected.')
 
-        return Availability(
+        avail = Availability(
             start=max(self.start, other.start), end=min(self.end, other.end)
         )
+        if self.event == other.event:
+            avail.event = self.event
+        return avail
 
     def __and__(self, other: 'Availability') -> 'Availability':
         """Performs the intersect operation: ``availability1 &
diff --git a/AKModel/models.py b/AKModel/models.py
index 49159427..2d58a456 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -1,5 +1,6 @@
 import itertools
 import json
+from dataclasses import dataclass
 from datetime import datetime, timedelta
 from typing import Iterable
 
@@ -14,6 +15,35 @@ from simple_history.models import HistoricalRecords
 from timezone_field import TimeZoneField
 
 
+@dataclass
+class OptimizerTimeslot:
+    """Class describing a timeslot. Used to interface with an optimizer."""
+
+    avail: "Availability"
+    idx: int
+    constraints: set[str]
+
+    def merge(self, other: "OptimizerTimeslot") -> "OptimizerTimeslot":
+        """Merge with other OptimizerTimeslot.
+
+        Creates a new OptimizerTimeslot object.
+        Its availability is constructed by merging the availabilities of self and other,
+        its constraints by taking the union of both constraint sets.
+        As an index, the index of self is used.
+        """
+        avail = self.avail.merge_with(other.avail)
+        constraints = self.constraints.union(other.constraints)
+        # we simply use the index of result[-1]
+        return OptimizerTimeslot(
+            avail=avail, idx=self.idx, constraints=constraints
+        )
+
+    def __repr__(self) -> str:
+        return f"({self.avail.simplified}, {self.idx}, {self.constraints})"
+
+TimeslotBlock = list[OptimizerTimeslot]
+
+
 class Event(models.Model):
     """
     An event supplies the frame for all Aks.
@@ -164,8 +194,13 @@ class Event(models.Model):
                 )
 
     def _generate_slots_from_block(
-        self, start: datetime, end: datetime, slot_duration: timedelta, slot_index: int = 0
-    ) -> Iterable[list[int, "Availability"]]:
+        self,
+        start: datetime,
+        end: datetime,
+        slot_duration: timedelta,
+        slot_index: int = 0,
+        constraints: set[str] | None = None,
+    ) -> Iterable[TimeslotBlock]:
         """Discretize a time range into timeslots.
 
         Uses a uniform discretization into blocks of length `slot_duration`,
@@ -179,8 +214,7 @@ class Event(models.Model):
         :param slot_index: index of the first timeslot. Defaults to 0.
 
         :yield: Block of optimizer timeslots as the discretization result.
-        :ytype: list of tuples, each consisisting of the timeslot id
-            and its availability to indicate its start and duration.
+        :ytype: list of TimeslotBlock
         """
         # local import to prevent cyclic import
         # pylint: disable=import-outside-toplevel
@@ -189,6 +223,9 @@ class Event(models.Model):
         current_slot_start = start
         previous_slot_start: datetime | None = None
 
+        if constraints is None:
+            constraints = set()
+
         current_block = []
 
         room_availabilities = list({
@@ -213,7 +250,9 @@ class Event(models.Model):
                     yield current_block
                     current_block = []
 
-                current_block.append((slot_index, slot))
+                current_block.append(
+                    OptimizerTimeslot(avail=slot, idx=slot_index, constraints=constraints)
+                )
                 previous_slot_start = current_slot_start
 
             slot_index += 1
@@ -224,44 +263,113 @@ class Event(models.Model):
 
         return slot_index
 
-    def uniform_time_slots(self, *, slots_in_an_hour=1.0) -> Iterable[list[int, "Availability"]]:
+    def uniform_time_slots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]:
         """Uniformly discretize the entire event into a single block of timeslots.
 
         :param slots_in_an_hour: The percentage of an hour covered by a single slot.
             Determines the discretization granularity.
         :yield: Block of optimizer timeslots as the discretization result.
-        :ytype: a single list of tuples, each consisisting of the timeslot id
-            and its availability to indicate its start and duration.
+        :ytype: a single list of TimeslotBlock
         """
+        all_category_constraints = AKCategory.create_category_constraints(
+            AKCategory.objects.filter(event=self).all()
+        )
+
         yield from self._generate_slots_from_block(
             start=self.start,
             end=self.end,
             slot_duration=timedelta(hours=1.0 / slots_in_an_hour),
+            constraints=all_category_constraints,
         )
 
-    def default_time_slots(self, *, slots_in_an_hour=1.0) -> Iterable[list[int, "Availability"]]:
-        """Discretize the all default slots into a blocks of timeslots.
+    def default_time_slots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]:
+        """Discretize all default slots into blocks of timeslots.
 
         In the discretization each default slot corresponds to one block.
 
         :param slots_in_an_hour: The percentage of an hour covered by a single slot.
             Determines the discretization granularity.
         :yield: Block of optimizer timeslots as the discretization result.
-        :ytype: list of tuples, each consisisting of the timeslot id
-            and its availability to indicate its start and duration.
+        :ytype: list of TimeslotBlock
         """
         slot_duration = timedelta(hours=1.0 / slots_in_an_hour)
         slot_index = 0
 
         for block_slot in DefaultSlot.objects.filter(event=self).order_by("start", "end"):
-            # NOTE: We do not differentiate between different primary categories
+            category_constraints = AKCategory.create_category_constraints(
+                block_slot.primary_categories.all()
+            )
+
             slot_index = yield from self._generate_slots_from_block(
                 start=block_slot.start,
                 end=block_slot.end,
                 slot_duration=slot_duration,
                 slot_index=slot_index,
+                constraints=category_constraints,
             )
 
+    def merge_blocks(
+        self, blocks: Iterable[TimeslotBlock]
+    ) -> Iterable[TimeslotBlock]:
+        """Merge iterable of blocks together.
+
+        The timeslots of all blocks are grouped into maximal blocks.
+        Timeslots with the same start and end are identified with each other
+        and merged (cf `OptimizerTimeslot.merge`).
+        Throws a ValueError if any timeslots are overlapping but do not
+        share the same start and end, i.e. partial overlap is not allowed.
+
+        :param blocks: iterable of blocks to merge.
+        :return: iterable of merged blocks.
+        :rtype: iterable over lists of OptimizerTimeslot objects
+        """
+        if not blocks:
+            return []
+
+        # flatten timeslot iterables to single chain
+        timeslot_chain = itertools.chain.from_iterable(blocks)
+
+        # sort timeslots according to start
+        timeslots = sorted(
+            timeslot_chain,
+            key=lambda slot: slot.avail.start
+        )
+
+        if not timeslots:
+            return []
+
+        all_blocks = []
+        current_block = [timeslots[0]]
+        timeslots = timeslots[1:]
+
+        for slot in timeslots:
+            if current_block and slot.avail.overlaps(current_block[-1].avail, strict=True):
+                if (
+                    slot.avail.start == current_block[-1].avail.start
+                    and slot.avail.end == current_block[-1].avail.end
+                ):
+                    # the same timeslot -> merge
+                    current_block[-1] = current_block[-1].merge(slot)
+                else:
+                    # partial overlap of interiors -> not supported
+                    # TODO: Show comprehensive message in production
+                    raise ValueError(
+                        "Partially overlapping timeslots are not supported!"
+                        f" ({current_block[-1].avail.simplified}, {slot.avail.simplified})"
+                    )
+            elif not current_block or slot.avail.overlaps(current_block[-1].avail, strict=False):
+                # only endpoints in intersection -> same block
+                current_block.append(slot)
+            else:
+                # no overlap at all -> new block
+                all_blocks.append(current_block)
+                current_block = [slot]
+
+        if current_block:
+            all_blocks.append(current_block)
+
+        return all_blocks
+
     def schedule_from_json(self, schedule: str) -> None:
         """Load AK schedule from a json string.
 
@@ -275,9 +383,9 @@ class Event(models.Model):
         slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"]
 
         timeslot_dict = {
-            slot_idx: slot
-            for block in self.default_time_slots(slots_in_an_hour=slots_in_an_hour)
-            for slot_idx, slot in block
+            timeslot.idx: timeslot
+            for block in self.merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour))
+            for timeslot in block
         }
 
         for scheduled_slot in schedule["scheduled_aks"]:
@@ -286,8 +394,8 @@ class Event(models.Model):
 
             scheduled_slot["timeslot_ids"] = list(map(int, scheduled_slot["timeslot_ids"]))
 
-            start_timeslot = timeslot_dict[min(scheduled_slot["timeslot_ids"])]
-            end_timeslot = timeslot_dict[max(scheduled_slot["timeslot_ids"])]
+            start_timeslot = timeslot_dict[min(scheduled_slot["timeslot_ids"])].avail
+            end_timeslot = timeslot_dict[max(scheduled_slot["timeslot_ids"])].avail
 
             slot.start = start_timeslot.start
             slot.duration = (end_timeslot.end - start_timeslot.start).total_seconds() / 3600.0
@@ -390,6 +498,20 @@ class AKCategory(models.Model):
     def __str__(self):
         return self.name
 
+    @staticmethod
+    def create_category_constraints(categories: Iterable["AKCategory"]) -> set[str]:
+        """Create a set of constraint strings from an AKCategory iterable.
+
+        :param categories: The iterable of categories to derive the constraint strings from.
+        :return: A set of category constraint strings, i.e. strings of the form
+            'availability-cat-<cat.name>'.
+        :rtype: set of strings.
+        """
+        return {
+            f"availability-cat-{cat.name}"
+            for cat in categories
+        }
+
 
 class AKTrack(models.Model):
     """ An AKTrack describes a set of semantically related AKs.
@@ -821,6 +943,10 @@ class AKSlot(models.Model):
         for owner in self.ak.owners.all():
             data["time_constraints"].extend(_owner_time_constraints(owner))
 
+        if self.ak.category:
+            category_constraints = AKCategory.create_category_constraints([self.ak.category])
+            data["time_constraints"].extend(category_constraints)
+
         if self.room is not None and self.fixed:
             data["room_constraints"].append(f"availability-room-{self.room.pk}")
 
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 818f15d9..76bfdb28 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -50,6 +50,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
     context_object_name = "slots"
     title = _("AK JSON Export")
 
+
     def _test_slot_contained(self, slot: Availability, availabilities: List[Availability]) -> bool:
         return any(availability.contains(slot) for availability in availabilities)
 
@@ -69,7 +70,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             and self._test_slot_contained(slot, availabilities)
         )
 
-
     def get_queryset(self):
         return super().get_queryset().order_by("ak__track")
 
@@ -110,38 +110,38 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists()
         }
 
-        for block in self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR):
+        for block in self.event.merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)):
             current_block = []
 
-            for slot_index, slot in block:
+            for timeslot in block:
                 time_constraints = []
-
-                if self.event.reso_deadline is None or slot.end < self.event.reso_deadline:
+                if self.event.reso_deadline is None or timeslot.avail.end < self.event.reso_deadline:
                     time_constraints.append("resolution")
 
                 time_constraints.extend([
                     f"availability-ak-{ak_id}"
                     for ak_id, availabilities in ak_availabilities.items()
                     if (
-                        self._test_add_constraint(slot, availabilities)
-                        or self._test_fixed_ak(ak_id, slot, ak_fixed)
+                        self._test_add_constraint(timeslot.avail, availabilities)
+                        or self._test_fixed_ak(ak_id, timeslot.avail, ak_fixed)
                     )
                 ])
                 time_constraints.extend([
                     f"availability-person-{person_id}"
                     for person_id, availabilities in person_availabilities.items()
-                    if self._test_add_constraint(slot, availabilities)
+                    if self._test_add_constraint(timeslot.avail, availabilities)
                 ])
                 time_constraints.extend([
                     f"availability-room-{room_id}"
                     for room_id, availabilities in room_availabilities.items()
-                    if self._test_add_constraint(slot, availabilities)
+                    if self._test_add_constraint(timeslot.avail, availabilities)
                 ])
+                time_constraints.extend(timeslot.constraints)
 
                 current_block.append({
-                    "id": str(slot_index),
+                    "id": str(timeslot.idx),
                     "info": {
-                        "start": slot.simplified,
+                        "start": timeslot.avail.simplified,
                     },
                     "fulfilled_time_constraints": time_constraints,
                     })
-- 
GitLab


From 326da97b7d4bbe7b896c08ace44d3cc1315d210b Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Fri, 31 May 2024 16:23:17 +0200
Subject: [PATCH 27/77] Update language files

---
 AKModel/locale/de_DE/LC_MESSAGES/django.po | 428 ++++++++++-----------
 1 file changed, 214 insertions(+), 214 deletions(-)

diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index 1026edf9..4cc752ae 100644
--- a/AKModel/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-05-27 02:31+0000\n"
+"POT-Creation-Date: 2024-05-31 14:22+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"
@@ -25,17 +25,17 @@ msgstr "Status"
 msgid "Toggle plan visibility"
 msgstr "Plansichtbarkeit ändern"
 
-#: AKModel/admin.py:110 AKModel/admin.py:121 AKModel/views/manage.py:138
+#: AKModel/admin.py:110 AKModel/admin.py:121 AKModel/views/manage.py:140
 msgid "Publish plan"
 msgstr "Plan veröffentlichen"
 
-#: AKModel/admin.py:113 AKModel/admin.py:129 AKModel/views/manage.py:151
+#: AKModel/admin.py:113 AKModel/admin.py:129 AKModel/views/manage.py:153
 msgid "Unpublish plan"
 msgstr "Plan verbergen"
 
-#: AKModel/admin.py:168 AKModel/models.py:437 AKModel/models.py:759
+#: AKModel/admin.py:168 AKModel/models.py:612 AKModel/models.py:1028
 #: AKModel/templates/admin/AKModel/status/event_aks.html:10
-#: AKModel/views/manage.py:73 AKModel/views/status.py:97
+#: AKModel/views/manage.py:75 AKModel/views/status.py:97
 msgid "AKs"
 msgstr "AKs"
 
@@ -59,11 +59,11 @@ msgstr "In Wiki-Syntax exportieren"
 msgid "Cannot export AKs from more than one event at the same time."
 msgstr "Kann nicht AKs von mehreren Events zur selben Zeit exportieren."
 
-#: AKModel/admin.py:320 AKModel/views/ak.py:184
+#: AKModel/admin.py:320 AKModel/views/ak.py:226
 msgid "Reset interest in AKs"
 msgstr "Interesse an AKs zurücksetzen"
 
-#: AKModel/admin.py:330 AKModel/views/ak.py:199
+#: AKModel/admin.py:330 AKModel/views/ak.py:241
 msgid "Reset AKs' interest counters"
 msgstr "Interessenszähler der AKs zurücksetzen"
 
@@ -71,19 +71,19 @@ msgstr "Interessenszähler der AKs zurücksetzen"
 msgid "AK Details"
 msgstr "AK-Details"
 
-#: AKModel/admin.py:505 AKModel/views/manage.py:99
+#: AKModel/admin.py:505 AKModel/views/manage.py:101
 msgid "Mark Constraint Violations as manually resolved"
 msgstr "Markiere Constraintverletzungen als manuell behoben"
 
-#: AKModel/admin.py:514 AKModel/views/manage.py:112
+#: AKModel/admin.py:514 AKModel/views/manage.py:114
 msgid "Set Constraint Violations to level \"violation\""
 msgstr "Constraintverletzungen auf Level \"Violation\" setzen"
 
-#: AKModel/admin.py:523 AKModel/views/manage.py:125
+#: AKModel/admin.py:523 AKModel/views/manage.py:127
 msgid "Set Constraint Violations to level \"warning\""
 msgstr "Constraintverletzungen auf Level \"Warning\" setzen"
 
-#: AKModel/availability/forms.py:25 AKModel/availability/models.py:271
+#: AKModel/availability/forms.py:25 AKModel/availability/models.py:299
 msgid "Availability"
 msgstr "Verfügbarkeit"
 
@@ -108,17 +108,17 @@ msgstr "Die eingegebene Verfügbarkeit enthält ein ungültiges Datum."
 msgid "Please fill in your availabilities!"
 msgstr "Bitte Verfügbarkeiten eintragen!"
 
-#: AKModel/availability/models.py:43 AKModel/models.py:61 AKModel/models.py:235
-#: AKModel/models.py:328 AKModel/models.py:347 AKModel/models.py:373
-#: AKModel/models.py:427 AKModel/models.py:569 AKModel/models.py:608
-#: AKModel/models.py:698 AKModel/models.py:755 AKModel/models.py:946
+#: AKModel/availability/models.py:43 AKModel/models.py:91 AKModel/models.py:412
+#: AKModel/models.py:489 AKModel/models.py:522 AKModel/models.py:548
+#: AKModel/models.py:602 AKModel/models.py:744 AKModel/models.py:820
+#: AKModel/models.py:967 AKModel/models.py:1024 AKModel/models.py:1215
 msgid "Event"
 msgstr "Event"
 
-#: AKModel/availability/models.py:44 AKModel/models.py:236
-#: AKModel/models.py:329 AKModel/models.py:348 AKModel/models.py:374
-#: AKModel/models.py:428 AKModel/models.py:570 AKModel/models.py:609
-#: AKModel/models.py:699 AKModel/models.py:756 AKModel/models.py:947
+#: AKModel/availability/models.py:44 AKModel/models.py:413
+#: AKModel/models.py:490 AKModel/models.py:523 AKModel/models.py:549
+#: AKModel/models.py:603 AKModel/models.py:745 AKModel/models.py:821
+#: AKModel/models.py:968 AKModel/models.py:1025 AKModel/models.py:1216
 msgid "Associated event"
 msgstr "Zugehöriges Event"
 
@@ -130,8 +130,8 @@ msgstr "Person"
 msgid "Person whose availability this is"
 msgstr "Person deren Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:61 AKModel/models.py:573
-#: AKModel/models.py:598 AKModel/models.py:765
+#: AKModel/availability/models.py:61 AKModel/models.py:748
+#: AKModel/models.py:810 AKModel/models.py:1034
 msgid "Room"
 msgstr "Raum"
 
@@ -139,8 +139,8 @@ msgstr "Raum"
 msgid "Room whose availability this is"
 msgstr "Raum dessen Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:70 AKModel/models.py:436
-#: AKModel/models.py:597 AKModel/models.py:693
+#: AKModel/availability/models.py:70 AKModel/models.py:611
+#: AKModel/models.py:809 AKModel/models.py:962
 msgid "AK"
 msgstr "AK"
 
@@ -148,8 +148,8 @@ msgstr "AK"
 msgid "AK whose availability this is"
 msgstr "Verfügbarkeiten"
 
-#: AKModel/availability/models.py:79 AKModel/models.py:332
-#: AKModel/models.py:771
+#: AKModel/availability/models.py:79 AKModel/models.py:493
+#: AKModel/models.py:1040
 msgid "AK Category"
 msgstr "AK-Kategorie"
 
@@ -157,7 +157,7 @@ msgstr "AK-Kategorie"
 msgid "AK Category whose availability this is"
 msgstr "AK-Kategorie, deren Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:272
+#: AKModel/availability/models.py:300
 msgid "Availabilities"
 msgstr "Verfügbarkeiten"
 
@@ -219,7 +219,7 @@ msgstr ""
 "fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen "
 "ausgefüllt zu werden?"
 
-#: AKModel/forms.py:189 AKModel/models.py:940
+#: AKModel/forms.py:189 AKModel/models.py:1209
 msgid "Default Slots"
 msgstr "Standardslots"
 
@@ -258,15 +258,15 @@ msgstr "Standardverfügbarkeiten für alle Räume anlegen?"
 msgid "CSV must contain a name column"
 msgstr "CSV muss eine name-Spalte enthalten"
 
-#: AKModel/forms.py:281
+#: AKModel/forms.py:282
 msgid "JSON data"
 msgstr "JSON-Daten"
 
-#: AKModel/forms.py:282
+#: AKModel/forms.py:283
 msgid "JSON data from the scheduling solver"
 msgstr "JSON-Daten, die der scheduling-solver produziert hat"
 
-#: AKModel/metaviews/admin.py:156 AKModel/models.py:30
+#: AKModel/metaviews/admin.py:156 AKModel/models.py:60
 msgid "Start"
 msgstr "Start"
 
@@ -291,66 +291,66 @@ msgstr "Aktivieren?"
 msgid "Finish"
 msgstr "Abschluss"
 
-#: AKModel/models.py:21 AKModel/models.py:320 AKModel/models.py:344
-#: AKModel/models.py:371 AKModel/models.py:389 AKModel/models.py:561
+#: AKModel/models.py:51 AKModel/models.py:481 AKModel/models.py:519
+#: AKModel/models.py:546 AKModel/models.py:564 AKModel/models.py:736
 msgid "Name"
 msgstr "Name"
 
-#: AKModel/models.py:22
+#: AKModel/models.py:52
 msgid "Name or iteration of the event"
 msgstr "Name oder Iteration des Events"
 
-#: AKModel/models.py:23
+#: AKModel/models.py:53
 msgid "Short Form"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:24
+#: AKModel/models.py:54
 msgid "Short name of letters/numbers/dots/dashes/underscores used in URLs."
 msgstr ""
 "Kurzname bestehend aus Buchstaben, Nummern, Punkten und Unterstrichen zur "
 "Nutzung in URLs"
 
-#: AKModel/models.py:26
+#: AKModel/models.py:56
 msgid "Place"
 msgstr "Ort"
 
-#: AKModel/models.py:27
+#: AKModel/models.py:57
 msgid "City etc. the event takes place in"
 msgstr "Stadt o.ä. in der das Event stattfindet"
 
-#: AKModel/models.py:29
+#: AKModel/models.py:59
 msgid "Time Zone"
 msgstr "Zeitzone"
 
-#: AKModel/models.py:29
+#: AKModel/models.py:59
 msgid "Time Zone where this event takes place in"
 msgstr "Zeitzone in der das Event stattfindet"
 
-#: AKModel/models.py:30
+#: AKModel/models.py:60
 msgid "Time the event begins"
 msgstr "Zeit zu der das Event beginnt"
 
-#: AKModel/models.py:31
+#: AKModel/models.py:61
 msgid "End"
 msgstr "Ende"
 
-#: AKModel/models.py:31
+#: AKModel/models.py:61
 msgid "Time the event ends"
 msgstr "Zeit zu der das Event endet"
 
-#: AKModel/models.py:32
+#: AKModel/models.py:62
 msgid "Resolution Deadline"
 msgstr "Resolutionsdeadline"
 
-#: AKModel/models.py:33
+#: AKModel/models.py:63
 msgid "When should AKs with intention to submit a resolution be done?"
 msgstr "Wann sollen AKs mit Resolutionsabsicht stattgefunden haben?"
 
-#: AKModel/models.py:35
+#: AKModel/models.py:65
 msgid "Interest Window Start"
 msgstr "Beginn Interessensbekundung"
 
-#: AKModel/models.py:37
+#: AKModel/models.py:67
 msgid ""
 "Opening time for expression of interest. When left blank, no interest "
 "indication will be possible."
@@ -358,71 +358,71 @@ msgstr ""
 "Öffnungszeitpunkt für die Angabe von Interesse an AKs.Wenn das Feld leer "
 "bleibt, wird keine Abgabe von Interesse möglich sein."
 
-#: AKModel/models.py:39
+#: AKModel/models.py:69
 msgid "Interest Window End"
 msgstr "Ende Interessensbekundung"
 
-#: AKModel/models.py:40
+#: AKModel/models.py:70
 msgid "Closing time for expression of interest."
 msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs."
 
-#: AKModel/models.py:42
+#: AKModel/models.py:72
 msgid "Public event"
 msgstr "Öffentliches Event"
 
-#: AKModel/models.py:43
+#: AKModel/models.py:73
 msgid "Show this event on overview page."
 msgstr "Zeige dieses Event auf der Übersichtseite an"
 
-#: AKModel/models.py:45
+#: AKModel/models.py:75
 msgid "Active State"
 msgstr "Aktiver Status"
 
-#: AKModel/models.py:45
+#: AKModel/models.py:75
 msgid "Marks currently active events"
 msgstr "Markiert aktuell aktive Events"
 
-#: AKModel/models.py:46
+#: AKModel/models.py:76
 msgid "Plan Hidden"
 msgstr "Plan verborgen"
 
-#: AKModel/models.py:46
+#: AKModel/models.py:76
 msgid "Hides plan for non-staff users"
 msgstr "Verbirgt den Plan für Nutzer*innen ohne erweiterte Rechte"
 
-#: AKModel/models.py:48
+#: AKModel/models.py:78
 msgid "Plan published at"
 msgstr "Plan veröffentlicht am/um"
 
-#: AKModel/models.py:49
+#: AKModel/models.py:79
 msgid "Timestamp at which the plan was published"
 msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde"
 
-#: AKModel/models.py:51
+#: AKModel/models.py:81
 msgid "Base URL"
 msgstr "URL-Prefix"
 
-#: AKModel/models.py:51
+#: AKModel/models.py:81
 msgid "Prefix for wiki link construction"
 msgstr "Prefix für die automatische Generierung von Wiki-Links"
 
-#: AKModel/models.py:52
+#: AKModel/models.py:82
 msgid "Wiki Export Template Name"
 msgstr "Wiki-Export Templatename"
 
-#: AKModel/models.py:53
+#: AKModel/models.py:83
 msgid "Default Slot Length"
 msgstr "Standardslotlänge"
 
-#: AKModel/models.py:54
+#: AKModel/models.py:84
 msgid "Default length in hours that is assumed for AKs in this event."
 msgstr "Standardlänge von Slots (in Stunden) für dieses Event"
 
-#: AKModel/models.py:56
+#: AKModel/models.py:86
 msgid "Contact email address"
 msgstr "E-Mail Kontaktadresse"
 
-#: AKModel/models.py:57
+#: AKModel/models.py:87
 msgid ""
 "An email address that is displayed on every page and can be used for all "
 "kinds of questions"
@@ -430,75 +430,75 @@ msgstr ""
 "Eine Mailadresse die auf jeder Seite angezeigt wird und für alle Arten von "
 "Fragen genutzt werden kann"
 
-#: AKModel/models.py:62
+#: AKModel/models.py:92
 msgid "Events"
 msgstr "Events"
 
-#: AKModel/models.py:230
+#: AKModel/models.py:407
 msgid "Nickname"
 msgstr "Spitzname"
 
-#: AKModel/models.py:230
+#: AKModel/models.py:407
 msgid "Name to identify an AK owner by"
 msgstr "Name, durch den eine AK-Leitung identifiziert wird"
 
-#: AKModel/models.py:231
+#: AKModel/models.py:408
 msgid "Slug"
 msgstr "Slug"
 
-#: AKModel/models.py:231
+#: AKModel/models.py:408
 msgid "Slug for URL generation"
 msgstr "Slug für URL-Generierung"
 
-#: AKModel/models.py:232
+#: AKModel/models.py:409
 msgid "Institution"
 msgstr "Instutution"
 
-#: AKModel/models.py:232
+#: AKModel/models.py:409
 msgid "Uni etc."
 msgstr "Universität o.ä."
 
-#: AKModel/models.py:233 AKModel/models.py:398
+#: AKModel/models.py:410 AKModel/models.py:573
 msgid "Web Link"
 msgstr "Internet Link"
 
-#: AKModel/models.py:233
+#: AKModel/models.py:410
 msgid "Link to Homepage"
 msgstr "Link zu Homepage oder Webseite"
 
-#: AKModel/models.py:239 AKModel/models.py:764
+#: AKModel/models.py:416 AKModel/models.py:1033
 msgid "AK Owner"
 msgstr "AK-Leitung"
 
-#: AKModel/models.py:240
+#: AKModel/models.py:417
 msgid "AK Owners"
 msgstr "AK-Leitungen"
 
-#: AKModel/models.py:320
+#: AKModel/models.py:481
 msgid "Name of the AK Category"
 msgstr "Name der AK-Kategorie"
 
-#: AKModel/models.py:321 AKModel/models.py:345
+#: AKModel/models.py:482 AKModel/models.py:520
 msgid "Color"
 msgstr "Farbe"
 
-#: AKModel/models.py:321 AKModel/models.py:345
+#: AKModel/models.py:482 AKModel/models.py:520
 msgid "Color for displaying"
 msgstr "Farbe für die Anzeige"
 
-#: AKModel/models.py:322 AKModel/models.py:392
+#: AKModel/models.py:483 AKModel/models.py:567
 msgid "Description"
 msgstr "Beschreibung"
 
-#: AKModel/models.py:323
+#: AKModel/models.py:484
 msgid "Short description of this AK Category"
 msgstr "Beschreibung der AK-Kategorie"
 
-#: AKModel/models.py:324
+#: AKModel/models.py:485
 msgid "Present by default"
 msgstr "Defaultmäßig präsentieren"
 
-#: AKModel/models.py:325
+#: AKModel/models.py:486
 msgid ""
 "Present AKs of this category by default if AK owner did not specify whether "
 "this AK should be presented?"
@@ -506,132 +506,132 @@ msgstr ""
 "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für "
 "ihren AK nicht explizit spezifiziert haben?"
 
-#: AKModel/models.py:333
+#: AKModel/models.py:494
 msgid "AK Categories"
 msgstr "AK-Kategorien"
 
-#: AKModel/models.py:344
+#: AKModel/models.py:519
 msgid "Name of the AK Track"
 msgstr "Name des AK-Tracks"
 
-#: AKModel/models.py:351
+#: AKModel/models.py:526
 msgid "AK Track"
 msgstr "AK-Track"
 
-#: AKModel/models.py:352
+#: AKModel/models.py:527
 msgid "AK Tracks"
 msgstr "AK-Tracks"
 
-#: AKModel/models.py:371
+#: AKModel/models.py:546
 msgid "Name of the Requirement"
 msgstr "Name der Anforderung"
 
-#: AKModel/models.py:377 AKModel/models.py:768
+#: AKModel/models.py:552 AKModel/models.py:1037
 msgid "AK Requirement"
 msgstr "AK-Anforderung"
 
-#: AKModel/models.py:378
+#: AKModel/models.py:553
 msgid "AK Requirements"
 msgstr "AK-Anforderungen"
 
-#: AKModel/models.py:389
+#: AKModel/models.py:564
 msgid "Name of the AK"
 msgstr "Name des AKs"
 
-#: AKModel/models.py:390
+#: AKModel/models.py:565
 msgid "Short Name"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:391
+#: AKModel/models.py:566
 msgid "Name displayed in the schedule"
 msgstr "Name zur Anzeige im AK-Plan"
 
-#: AKModel/models.py:392
+#: AKModel/models.py:567
 msgid "Description of the AK"
 msgstr "Beschreibung des AKs"
 
-#: AKModel/models.py:394
+#: AKModel/models.py:569
 msgid "Owners"
 msgstr "Leitungen"
 
-#: AKModel/models.py:395
+#: AKModel/models.py:570
 msgid "Those organizing the AK"
 msgstr "Menschen, die den AK organisieren und halten"
 
-#: AKModel/models.py:398
+#: AKModel/models.py:573
 msgid "Link to wiki page"
 msgstr "Link zur Wiki Seite"
 
-#: AKModel/models.py:399
+#: AKModel/models.py:574
 msgid "Protocol Link"
 msgstr "Protokolllink"
 
-#: AKModel/models.py:399
+#: AKModel/models.py:574
 msgid "Link to protocol"
 msgstr "Link zum Protokoll"
 
-#: AKModel/models.py:401
+#: AKModel/models.py:576
 msgid "Category"
 msgstr "Kategorie"
 
-#: AKModel/models.py:402
+#: AKModel/models.py:577
 msgid "Category of the AK"
 msgstr "Kategorie des AKs"
 
-#: AKModel/models.py:403
+#: AKModel/models.py:578
 msgid "Track"
 msgstr "Track"
 
-#: AKModel/models.py:404
+#: AKModel/models.py:579
 msgid "Track the AK belongs to"
 msgstr "Track zu dem der AK gehört"
 
-#: AKModel/models.py:406
+#: AKModel/models.py:581
 msgid "Resolution Intention"
 msgstr "Resolutionsabsicht"
 
-#: AKModel/models.py:407
+#: AKModel/models.py:582
 msgid "Intends to submit a resolution"
 msgstr "Beabsichtigt eine Resolution einzureichen"
 
-#: AKModel/models.py:408
+#: AKModel/models.py:583
 msgid "Present this AK"
 msgstr "AK präsentieren"
 
-#: AKModel/models.py:409
+#: AKModel/models.py:584
 msgid "Present results of this AK"
 msgstr "Die Ergebnisse dieses AKs vorstellen"
 
-#: AKModel/models.py:411 AKModel/views/status.py:170
+#: AKModel/models.py:586 AKModel/views/status.py:170
 msgid "Requirements"
 msgstr "Anforderungen"
 
-#: AKModel/models.py:412
+#: AKModel/models.py:587
 msgid "AK's Requirements"
 msgstr "Anforderungen des AKs"
 
-#: AKModel/models.py:414
+#: AKModel/models.py:589
 msgid "Conflicting AKs"
 msgstr "AK-Konflikte"
 
-#: AKModel/models.py:415
+#: AKModel/models.py:590
 msgid "AKs that conflict and thus must not take place at the same time"
 msgstr ""
 "AKs, die Konflikte haben und deshalb nicht gleichzeitig stattfinden dürfen"
 
-#: AKModel/models.py:416
+#: AKModel/models.py:591
 msgid "Prerequisite AKs"
 msgstr "Vorausgesetzte AKs"
 
-#: AKModel/models.py:417
+#: AKModel/models.py:592
 msgid "AKs that should precede this AK in the schedule"
 msgstr "AKs die im AK-Plan vor diesem AK stattfinden müssen"
 
-#: AKModel/models.py:419
+#: AKModel/models.py:594
 msgid "Organizational Notes"
 msgstr "Notizen zur Organisation"
 
-#: AKModel/models.py:420
+#: AKModel/models.py:595
 msgid ""
 "Notes to organizers. These are public. For private notes, please use the "
 "button for private messages on the detail page of this AK (after creation/"
@@ -641,291 +641,291 @@ msgstr ""
 "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem "
 "Anlegen/Bearbeiten)."
 
-#: AKModel/models.py:423
+#: AKModel/models.py:598
 msgid "Interest"
 msgstr "Interesse"
 
-#: AKModel/models.py:423
+#: AKModel/models.py:598
 msgid "Expected number of people"
 msgstr "Erwartete Personenzahl"
 
-#: AKModel/models.py:424
+#: AKModel/models.py:599
 msgid "Interest Counter"
 msgstr "Interessenszähler"
 
-#: AKModel/models.py:425
+#: AKModel/models.py:600
 msgid "People who have indicated interest online"
 msgstr "Anzahl Personen, die online Interesse bekundet haben"
 
-#: AKModel/models.py:430
+#: AKModel/models.py:605
 msgid "Export?"
 msgstr "Export?"
 
-#: AKModel/models.py:431
+#: AKModel/models.py:606
 msgid "Include AK in wiki export?"
 msgstr "AK bei Wiki-Export berücksichtigen?"
 
-#: AKModel/models.py:561
+#: AKModel/models.py:736
 msgid "Name or number of the room"
 msgstr "Name oder Nummer des Raums"
 
-#: AKModel/models.py:562
+#: AKModel/models.py:737
 msgid "Location"
 msgstr "Ort"
 
-#: AKModel/models.py:563
+#: AKModel/models.py:738
 msgid "Name or number of the location"
 msgstr "Name oder Nummer des Ortes"
 
-#: AKModel/models.py:564
+#: AKModel/models.py:739
 msgid "Capacity"
 msgstr "Kapazität"
 
-#: AKModel/models.py:565
+#: AKModel/models.py:740
 msgid "Maximum number of people (-1 for unlimited)."
 msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)."
 
-#: AKModel/models.py:566
+#: AKModel/models.py:741
 msgid "Properties"
 msgstr "Eigenschaften"
 
-#: AKModel/models.py:567
+#: AKModel/models.py:742
 msgid "AK requirements fulfilled by the room"
 msgstr "AK-Anforderungen, die dieser Raum erfüllt"
 
-#: AKModel/models.py:574 AKModel/views/status.py:59
+#: AKModel/models.py:749 AKModel/views/status.py:59
 msgid "Rooms"
 msgstr "Räume"
 
-#: AKModel/models.py:597
+#: AKModel/models.py:809
 msgid "AK being mapped"
 msgstr "AK, der zugeordnet wird"
 
-#: AKModel/models.py:599
+#: AKModel/models.py:811
 msgid "Room the AK will take place in"
 msgstr "Raum in dem der AK stattfindet"
 
-#: AKModel/models.py:600 AKModel/models.py:943
+#: AKModel/models.py:812 AKModel/models.py:1212
 msgid "Slot Begin"
 msgstr "Beginn des Slots"
 
-#: AKModel/models.py:600 AKModel/models.py:943
+#: AKModel/models.py:812 AKModel/models.py:1212
 msgid "Time and date the slot begins"
 msgstr "Zeit und Datum zu der der AK beginnt"
 
-#: AKModel/models.py:602
+#: AKModel/models.py:814
 msgid "Duration"
 msgstr "Dauer"
 
-#: AKModel/models.py:603
+#: AKModel/models.py:815
 msgid "Length in hours"
 msgstr "Länge in Stunden"
 
-#: AKModel/models.py:605
+#: AKModel/models.py:817
 msgid "Scheduling fixed"
 msgstr "Planung fix"
 
-#: AKModel/models.py:606
+#: AKModel/models.py:818
 msgid "Length and time of this AK should not be changed"
 msgstr "Dauer und Zeit dieses AKs sollten nicht verändert werden"
 
-#: AKModel/models.py:611
+#: AKModel/models.py:823
 msgid "Last update"
 msgstr "Letzte Aktualisierung"
 
-#: AKModel/models.py:614
+#: AKModel/models.py:826
 msgid "AK Slot"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:615 AKModel/models.py:761
+#: AKModel/models.py:827 AKModel/models.py:1030
 msgid "AK Slots"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:637 AKModel/models.py:646
+#: AKModel/models.py:849 AKModel/models.py:858
 msgid "Not scheduled yet"
 msgstr "Noch nicht geplant"
 
-#: AKModel/models.py:694
+#: AKModel/models.py:963
 msgid "AK this message belongs to"
 msgstr "AK zu dem die Nachricht gehört"
 
-#: AKModel/models.py:695
+#: AKModel/models.py:964
 msgid "Message text"
 msgstr "Nachrichtentext"
 
-#: AKModel/models.py:696
+#: AKModel/models.py:965
 msgid "Message to the organizers. This is not publicly visible."
 msgstr ""
 "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar."
 
-#: AKModel/models.py:700
+#: AKModel/models.py:969
 msgid "Resolved"
 msgstr "Erledigt"
 
-#: AKModel/models.py:701
+#: AKModel/models.py:970
 msgid "This message has been resolved (no further action needed)"
 msgstr ""
 "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen "
 "notwendig)"
 
-#: AKModel/models.py:704
+#: AKModel/models.py:973
 msgid "AK Orga Message"
 msgstr "AK-Organachricht"
 
-#: AKModel/models.py:705
+#: AKModel/models.py:974
 msgid "AK Orga Messages"
 msgstr "AK-Organachrichten"
 
-#: AKModel/models.py:722
+#: AKModel/models.py:991
 msgid "Constraint Violation"
 msgstr "Constraintverletzung"
 
-#: AKModel/models.py:723
+#: AKModel/models.py:992
 msgid "Constraint Violations"
 msgstr "Constraintverletzungen"
 
-#: AKModel/models.py:730
+#: AKModel/models.py:999
 msgid "Owner has two parallel slots"
 msgstr "Leitung hat zwei Slots parallel"
 
-#: AKModel/models.py:731
+#: AKModel/models.py:1000
 msgid "AK Slot was scheduled outside the AK's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des AKs platziert"
 
-#: AKModel/models.py:732
+#: AKModel/models.py:1001
 msgid "Room has two AK slots scheduled at the same time"
 msgstr "Raum hat zwei AK Slots gleichzeitig"
 
-#: AKModel/models.py:733
+#: AKModel/models.py:1002
 msgid "Room does not satisfy the requirement of the scheduled AK"
 msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht"
 
-#: AKModel/models.py:734
+#: AKModel/models.py:1003
 msgid "AK Slot is scheduled at the same time as an AK listed as a conflict"
 msgstr ""
 "AK Slot wurde wurde zur gleichen Zeit wie ein Konflikt des AKs platziert"
 
-#: AKModel/models.py:735
+#: AKModel/models.py:1004
 msgid "AK Slot is scheduled before an AK listed as a prerequisite"
 msgstr "AK Slot wurde vor einem als Voraussetzung gelisteten AK platziert"
 
-#: AKModel/models.py:737
+#: AKModel/models.py:1006
 msgid ""
 "AK Slot for AK with intention to submit a resolution is scheduled after "
 "resolution deadline"
 msgstr ""
 "AK Slot eines AKs mit Resoabsicht wurde nach der Resodeadline platziert"
 
-#: AKModel/models.py:738
+#: AKModel/models.py:1007
 msgid "AK Slot in a category is outside that categories availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeiten seiner Kategorie"
 
-#: AKModel/models.py:739
+#: AKModel/models.py:1008
 msgid "Two AK Slots for the same AK scheduled at the same time"
 msgstr "Zwei AK Slots eines AKs wurden zur selben Zeit platziert"
 
-#: AKModel/models.py:740
+#: AKModel/models.py:1009
 msgid "Room does not have enough space for interest in scheduled AK Slot"
 msgstr "Room hat nicht genug Platz für das Interesse am geplanten AK-Slot"
 
-#: AKModel/models.py:741
+#: AKModel/models.py:1010
 msgid "AK Slot is scheduled outside the event's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des Events platziert"
 
-#: AKModel/models.py:747
+#: AKModel/models.py:1016
 msgid "Warning"
 msgstr "Warnung"
 
-#: AKModel/models.py:748
+#: AKModel/models.py:1017
 msgid "Violation"
 msgstr "Verletzung"
 
-#: AKModel/models.py:750
+#: AKModel/models.py:1019
 msgid "Type"
 msgstr "Art"
 
-#: AKModel/models.py:751
+#: AKModel/models.py:1020
 msgid "Type of violation, i.e. what kind of constraint was violated"
 msgstr "Art der Verletzung, gibt an welche Art Constraint verletzt wurde"
 
-#: AKModel/models.py:752
+#: AKModel/models.py:1021
 msgid "Level"
 msgstr "Level"
 
-#: AKModel/models.py:753
+#: AKModel/models.py:1022
 msgid "Severity level of the violation"
 msgstr "Schweregrad der Verletzung"
 
-#: AKModel/models.py:760
+#: AKModel/models.py:1029
 msgid "AK(s) belonging to this constraint"
 msgstr "AK(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:762
+#: AKModel/models.py:1031
 msgid "AK Slot(s) belonging to this constraint"
 msgstr "AK Slot(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:764
+#: AKModel/models.py:1033
 msgid "AK Owner belonging to this constraint"
 msgstr "AK Leitung(en), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:766
+#: AKModel/models.py:1035
 msgid "Room belonging to this constraint"
 msgstr "Raum, der zu diesem Constraint gehört"
 
-#: AKModel/models.py:769
+#: AKModel/models.py:1038
 msgid "AK Requirement belonging to this constraint"
 msgstr "AK Anforderung, die zu diesem Constraint gehört"
 
-#: AKModel/models.py:771
+#: AKModel/models.py:1040
 msgid "AK Category belonging to this constraint"
 msgstr "AK Kategorie, di zu diesem Constraint gehört"
 
-#: AKModel/models.py:773
+#: AKModel/models.py:1042
 msgid "Comment"
 msgstr "Kommentar"
 
-#: AKModel/models.py:773
+#: AKModel/models.py:1042
 msgid "Comment or further details for this violation"
 msgstr "Kommentar oder weitere Details zu dieser Vereletzung"
 
-#: AKModel/models.py:776
+#: AKModel/models.py:1045
 msgid "Timestamp"
 msgstr "Timestamp"
 
-#: AKModel/models.py:776
+#: AKModel/models.py:1045
 msgid "Time of creation"
 msgstr "Zeitpunkt der ERstellung"
 
-#: AKModel/models.py:777
+#: AKModel/models.py:1046
 msgid "Manually Resolved"
 msgstr "Manuell behoben"
 
-#: AKModel/models.py:778
+#: AKModel/models.py:1047
 msgid "Mark this violation manually as resolved"
 msgstr "Markiere diese Verletzung manuell als behoben"
 
-#: AKModel/models.py:805
+#: AKModel/models.py:1074
 #: AKModel/templates/admin/AKModel/requirements_overview.html:27
 msgid "Details"
 msgstr "Details"
 
-#: AKModel/models.py:939
+#: AKModel/models.py:1208
 msgid "Default Slot"
 msgstr "Standardslot"
 
-#: AKModel/models.py:944
+#: AKModel/models.py:1213
 msgid "Slot End"
 msgstr "Ende des Slots"
 
-#: AKModel/models.py:944
+#: AKModel/models.py:1213
 msgid "Time and date the slot ends"
 msgstr "Zeit und Datum zu der der Slot endet"
 
-#: AKModel/models.py:949
+#: AKModel/models.py:1218
 msgid "Primary categories"
 msgstr "Primäre Kategorien"
 
-#: AKModel/models.py:950
+#: AKModel/models.py:1219
 msgid "Categories that should be assigned to this slot primarily"
 msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen"
 
@@ -1123,47 +1123,47 @@ msgstr "Login"
 msgid "Register"
 msgstr "Registrieren"
 
-#: AKModel/views/ak.py:20
+#: AKModel/views/ak.py:21
 msgid "Requirements for Event"
 msgstr "Anforderungen für das Event"
 
-#: AKModel/views/ak.py:37
+#: AKModel/views/ak.py:38
 msgid "AK CSV Export"
 msgstr "AK-CSV-Export"
 
-#: AKModel/views/ak.py:50
+#: AKModel/views/ak.py:51
 msgid "AK JSON Export"
 msgstr "AK-JSON-Export"
 
-#: AKModel/views/ak.py:133
+#: AKModel/views/ak.py:175
 msgid "AK Wiki Export"
 msgstr "AK-Wiki-Export"
 
-#: AKModel/views/ak.py:144 AKModel/views/manage.py:53
+#: AKModel/views/ak.py:186 AKModel/views/manage.py:55
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKModel/views/ak.py:156
+#: AKModel/views/ak.py:198
 msgid "Delete AK Orga Messages"
 msgstr "AK-Organachrichten löschen"
 
-#: AKModel/views/ak.py:174
+#: AKModel/views/ak.py:216
 msgid "AK Orga Messages successfully deleted"
 msgstr "AK-Organachrichten erfolgreich gelöscht"
 
-#: AKModel/views/ak.py:186
+#: AKModel/views/ak.py:228
 msgid "Interest of the following AKs will be set to not filled (-1):"
 msgstr "Interesse an den folgenden AKs wird auf nicht ausgefüllt (-1) gesetzt:"
 
-#: AKModel/views/ak.py:187
+#: AKModel/views/ak.py:229
 msgid "Reset of interest in AKs successful."
 msgstr "Interesse an AKs erfolgreich zurückgesetzt."
 
-#: AKModel/views/ak.py:201
+#: AKModel/views/ak.py:243
 msgid "Interest counter of the following AKs will be set to 0:"
 msgstr "Interessensbekundungszähler der folgenden AKs wird auf 0 gesetzt:"
 
-#: AKModel/views/ak.py:202
+#: AKModel/views/ak.py:244
 msgid "AKs' interest counters set back to 0."
 msgstr "Interessenszähler der AKs zurückgesetzt"
 
@@ -1177,91 +1177,91 @@ msgstr "'%(obj)s' kopiert"
 msgid "Could not copy '%(obj)s' (%(error)s)"
 msgstr "'%(obj)s' konnte nicht kopiert werden (%(error)s)"
 
-#: AKModel/views/manage.py:35 AKModel/views/status.py:153
+#: AKModel/views/manage.py:37 AKModel/views/status.py:153
 msgid "Export AK Slides"
 msgstr "AK-Folien exportieren"
 
-#: AKModel/views/manage.py:48
+#: AKModel/views/manage.py:50
 msgid "Symbols"
 msgstr "Symbole"
 
-#: AKModel/views/manage.py:49
+#: AKModel/views/manage.py:51
 msgid "Who?"
 msgstr "Wer?"
 
-#: AKModel/views/manage.py:50
+#: AKModel/views/manage.py:52
 msgid "Duration(s)"
 msgstr "Dauer(n)"
 
-#: AKModel/views/manage.py:51
+#: AKModel/views/manage.py:53
 msgid "Reso intention?"
 msgstr "Resolutionsabsicht?"
 
-#: AKModel/views/manage.py:52
+#: AKModel/views/manage.py:54
 msgid "Category (for Wishes)"
 msgstr "Kategorie (für Wünsche)"
 
-#: AKModel/views/manage.py:101
+#: AKModel/views/manage.py:103
 msgid "The following Constraint Violations will be marked as manually resolved"
 msgstr ""
 "Die folgenden Constraintverletzungen werden als manuell behoben markiert."
 
-#: AKModel/views/manage.py:102
+#: AKModel/views/manage.py:104
 msgid "Constraint Violations marked as resolved"
 msgstr "Constraintverletzungen als manuell behoben markiert"
 
-#: AKModel/views/manage.py:114
+#: AKModel/views/manage.py:116
 msgid "The following Constraint Violations will be set to level 'violation'"
 msgstr ""
 "Die folgenden Constraintverletzungen werden auf das Level \"Violation\" "
 "gesetzt."
 
-#: AKModel/views/manage.py:115
+#: AKModel/views/manage.py:117
 msgid "Constraint Violations set to level 'violation'"
 msgstr "Constraintverletzungen auf Level \"Violation\" gesetzt"
 
-#: AKModel/views/manage.py:127
+#: AKModel/views/manage.py:129
 msgid "The following Constraint Violations will be set to level 'warning'"
 msgstr ""
 "Die folgenden Constraintverletzungen werden auf das Level 'warning' gesetzt."
 
-#: AKModel/views/manage.py:128
+#: AKModel/views/manage.py:130
 msgid "Constraint Violations set to level 'warning'"
 msgstr "Constraintverletzungen auf Level \"Warning\" gesetzt"
 
-#: AKModel/views/manage.py:140
+#: AKModel/views/manage.py:142
 msgid "Publish the plan(s) of:"
 msgstr "Den Plan/die Pläne veröffentlichen von:"
 
-#: AKModel/views/manage.py:141
+#: AKModel/views/manage.py:143
 msgid "Plan published"
 msgstr "Plan veröffentlicht"
 
-#: AKModel/views/manage.py:153
+#: AKModel/views/manage.py:155
 msgid "Unpublish the plan(s) of:"
 msgstr "Den Plan/die Pläne verbergen von:"
 
-#: AKModel/views/manage.py:154
+#: AKModel/views/manage.py:156
 msgid "Plan unpublished"
 msgstr "Plan verborgen"
 
-#: AKModel/views/manage.py:166 AKModel/views/status.py:129
+#: AKModel/views/manage.py:168 AKModel/views/status.py:129
 msgid "Edit Default Slots"
 msgstr "Standardslots bearbeiten"
 
-#: AKModel/views/manage.py:204
+#: AKModel/views/manage.py:206
 #, python-brace-format
 msgid "Could not update slot {id} since it does not belong to {event}"
 msgstr ""
 "Konnte  Slot {id} nicht bearbeiten, da er nicht zum Event {event} gehört"
 
-#: AKModel/views/manage.py:235
+#: AKModel/views/manage.py:237
 #, python-brace-format
 msgid "Updated {u} slot(s). created {c} new slot(s) and deleted {d} slot(s)"
 msgstr ""
 "{u} Slot(s) aktualisiert, {c} Slot(s) hinzugefügt und {d} Slot(s) gelöscht"
 
-#: AKModel/views/manage.py:252
+#: AKModel/views/manage.py:257
 msgid "AK JSON Import"
 msgstr "AK-JSON-Import"
 
-- 
GitLab


From 25a3b3f9c322b1862ef444e7b753f68f143284ed Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Thu, 26 Dec 2024 18:51:18 +0100
Subject: [PATCH 28/77] Rename json import to make clear that a schedule is
 imported

---
 AKModel/forms.py                           | 2 +-
 AKModel/locale/de_DE/LC_MESSAGES/django.po | 4 ++--
 AKModel/models.py                          | 2 +-
 AKModel/urls.py                            | 6 +++---
 AKModel/views/manage.py                    | 8 ++++----
 AKModel/views/status.py                    | 2 +-
 6 files changed, 12 insertions(+), 12 deletions(-)

diff --git a/AKModel/forms.py b/AKModel/forms.py
index 74ca1b68..bf77085c 100644
--- a/AKModel/forms.py
+++ b/AKModel/forms.py
@@ -274,7 +274,7 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm):
             self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event)
 
 
-class JSONImportForm(AdminIntermediateForm):
+class JSONScheduleImportForm(AdminIntermediateForm):
     """Form to import an AK schedule from a json file."""
     json_data = forms.CharField(
         required=True,
diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index 4cc752ae..4fcbd8a8 100644
--- a/AKModel/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po
@@ -1262,8 +1262,8 @@ msgstr ""
 "{u} Slot(s) aktualisiert, {c} Slot(s) hinzugefügt und {d} Slot(s) gelöscht"
 
 #: AKModel/views/manage.py:257
-msgid "AK JSON Import"
-msgstr "AK-JSON-Import"
+msgid "AK Schedule JSON Import"
+msgstr "AK-Plan JSON-Import"
 
 #: AKModel/views/room.py:37
 #, python-format
diff --git a/AKModel/models.py b/AKModel/models.py
index 2d58a456..fdb03256 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -631,7 +631,7 @@ class AK(models.Model):
         availabilities = ', \n'.join(f'{a.simplified}' for a in Availability.objects.select_related('event')
                                      .filter(ak=self))
         detail_string = f"""{self.name}{" (R)" if self.reso else ""}:
-        
+
         {self.owners_list}
 
         {_('Interest')}: {self.interest}"""
diff --git a/AKModel/urls.py b/AKModel/urls.py
index 9871b411..9c103405 100644
--- a/AKModel/urls.py
+++ b/AKModel/urls.py
@@ -5,7 +5,7 @@ from rest_framework.routers import DefaultRouter
 
 import AKModel.views.api
 from AKModel.views.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, \
-    AKsByUserView, AKJSONImportView
+    AKsByUserView, AKScheduleJSONImportView
 from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKJSONExportView, AKWikiExportView, \
      AKMessageDeleteView
 from AKModel.views.event_wizard import NewEventWizardStartView, NewEventWizardPrepareImportView, \
@@ -99,8 +99,8 @@ def get_admin_urls_event(admin_site):
              name="ak_csv_export"),
         path('<slug:event_slug>/ak-json-export/', admin_site.admin_view(AKJSONExportView.as_view()),
              name="ak_json_export"),
-        path('<slug:event_slug>/ak-json-import/', admin_site.admin_view(AKJSONImportView.as_view()),
-             name="ak_json_import"),
+        path('<slug:event_slug>/ak-schedule-json-import/', admin_site.admin_view(AKScheduleJSONImportView.as_view()),
+             name="ak_schedule_json_import"),
         path('<slug:slug>/ak-wiki-export/', admin_site.admin_view(AKWikiExportView.as_view()),
              name="ak_wiki_export"),
         path('<slug:event_slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()),
diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py
index ec5076fb..1bad9534 100644
--- a/AKModel/views/manage.py
+++ b/AKModel/views/manage.py
@@ -14,7 +14,7 @@ from django.views.generic import TemplateView, DetailView
 from django_tex.core import render_template_with_context, run_tex_in_directory
 from django_tex.response import PDFResponse
 
-from AKModel.forms import SlideExportForm, DefaultSlotEditorForm, JSONImportForm
+from AKModel.forms import SlideExportForm, DefaultSlotEditorForm, JSONScheduleImportForm
 from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin
 from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner
 
@@ -249,12 +249,12 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView):
     template_name = "admin/AKModel/aks_by_user.html"
 
 
-class AKJSONImportView(EventSlugMixin, IntermediateAdminView):
+class AKScheduleJSONImportView(EventSlugMixin, IntermediateAdminView):
     """
     View: Import an AK schedule from a json file that can be pasted into this view.
     """
-    form_class = JSONImportForm
-    title = _("AK JSON Import")
+    form_class = JSONScheduleImportForm
+    title = _("AK Schedule JSON Import")
 
     def form_valid(self, form):
         self.event.schedule_from_json(form.data["json_data"])
diff --git a/AKModel/views/status.py b/AKModel/views/status.py
index 0c12b303..f7baa0da 100644
--- a/AKModel/views/status.py
+++ b/AKModel/views/status.py
@@ -135,7 +135,7 @@ class EventAKsWidget(TemplateStatusWidget):
                 },
                 {
                     "text": _("Import AK schedule from JSON"),
-                    "url": reverse_lazy("admin:ak_json_import", kwargs={"event_slug": context["event"].slug}),
+                    "url": reverse_lazy("admin:ak_schedule_json_import", kwargs={"event_slug": context["event"].slug}),
                 },
                 {
                     "text": _("Export AKs as CSV"),
-- 
GitLab


From 1b1b56fda4864489dba7e8d6991154e26b4c042b Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Thu, 26 Dec 2024 19:02:38 +0100
Subject: [PATCH 29/77] Check all blocks of union if event is covered

---
 AKModel/availability/models.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py
index 35814ee0..27a6c228 100644
--- a/AKModel/availability/models.py
+++ b/AKModel/availability/models.py
@@ -293,7 +293,7 @@ class Availability(models.Model):
         #       event end + 1 day
         full_event = Availability(event=event, start=event.start, end=event.end)
         avail_union = Availability.union(availabilities)
-        return not avail_union or avail_union[0].contains(full_event)
+        return any(avail.contains(full_event) for avail in avail_union)
 
     class Meta:
         verbose_name = _('Availability')
-- 
GitLab


From cf082b61b7e1a52cd118ad2abe5173b5d7b9159f Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Thu, 26 Dec 2024 19:28:42 +0100
Subject: [PATCH 30/77] Improve docstring and type hints for optimizer slots

---
 AKModel/models.py | 27 +++++++++++++++++++--------
 1 file changed, 19 insertions(+), 8 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index fdb03256..08e3d371 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -2,7 +2,7 @@ import itertools
 import json
 from dataclasses import dataclass
 from datetime import datetime, timedelta
-from typing import Iterable
+from typing import Iterable, Generator
 
 from django.db import models
 from django.apps import apps
@@ -17,10 +17,15 @@ from timezone_field import TimeZoneField
 
 @dataclass
 class OptimizerTimeslot:
-    """Class describing a timeslot. Used to interface with an optimizer."""
+    """Class describing a discrete timeslot. Used to interface with an optimizer."""
 
+    """The availability object corresponding to this timeslot."""
     avail: "Availability"
+
+    """The unique index of this optimizer timeslot."""
     idx: int
+
+    """The set of time constraints fulfilled by this object."""
     constraints: set[str]
 
     def merge(self, other: "OptimizerTimeslot") -> "OptimizerTimeslot":
@@ -33,7 +38,6 @@ class OptimizerTimeslot:
         """
         avail = self.avail.merge_with(other.avail)
         constraints = self.constraints.union(other.constraints)
-        # we simply use the index of result[-1]
         return OptimizerTimeslot(
             avail=avail, idx=self.idx, constraints=constraints
         )
@@ -200,10 +204,10 @@ class Event(models.Model):
         slot_duration: timedelta,
         slot_index: int = 0,
         constraints: set[str] | None = None,
-    ) -> Iterable[TimeslotBlock]:
+    ) -> Generator[TimeslotBlock, None, int]:
         """Discretize a time range into timeslots.
 
-        Uses a uniform discretization into blocks of length `slot_duration`,
+        Uses a uniform discretization into discrete slots of length `slot_duration`,
         starting at `start`. No incomplete timeslots are generated, i.e.
         if (`end` - `start`) is not a whole number multiple of `slot_duration`
         then the last incomplete timeslot is dropped.
@@ -214,7 +218,11 @@ class Event(models.Model):
         :param slot_index: index of the first timeslot. Defaults to 0.
 
         :yield: Block of optimizer timeslots as the discretization result.
-        :ytype: list of TimeslotBlock
+        :ytype: list of OptimizerTimeslot
+
+        :return: The first slot index after the yielded blocks, i.e.
+            `slot_index` + total # generated timeslots
+        :rtype: int
         """
         # local import to prevent cyclic import
         # pylint: disable=import-outside-toplevel
@@ -264,12 +272,15 @@ class Event(models.Model):
         return slot_index
 
     def uniform_time_slots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]:
-        """Uniformly discretize the entire event into a single block of timeslots.
+        """Uniformly discretize the entire event into blocks of timeslots.
+
+        Discretizes entire event uniformly. May not necessarily result in a single block
+        as slots with no room availability are dropped.
 
         :param slots_in_an_hour: The percentage of an hour covered by a single slot.
             Determines the discretization granularity.
         :yield: Block of optimizer timeslots as the discretization result.
-        :ytype: a single list of TimeslotBlock
+        :ytype: list of OptimizerTimeslot
         """
         all_category_constraints = AKCategory.create_category_constraints(
             AKCategory.objects.filter(event=self).all()
-- 
GitLab


From fd6adb0e83d9fece3feabbded3af9677700393f0 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Thu, 26 Dec 2024 19:29:59 +0100
Subject: [PATCH 31/77] Use round instead of int to calc duration

---
 AKModel/models.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 08e3d371..494e5911 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -936,7 +936,7 @@ class AKSlot(models.Model):
         # self.slots_in_an_hour is set in AKJSONExportView
         data = {
             "id": str(self.pk),
-            "duration": int(self.duration * self.slots_in_an_hour),
+            "duration": round(self.duration * self.slots_in_an_hour),
             "properties": {},
             "room_constraints": [constraint.name
                                  for constraint in self.ak.requirements.all()],
-- 
GitLab


From 61922712b361a51fe2979478b774ff74226136c6 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Thu, 26 Dec 2024 19:35:14 +0100
Subject: [PATCH 32/77] Detach merge_blocks from Event object

---
 AKModel/models.py   | 127 ++++++++++++++++++++++----------------------
 AKModel/views/ak.py |   4 +-
 2 files changed, 66 insertions(+), 65 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 494e5911..3b6eb600 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -48,6 +48,69 @@ class OptimizerTimeslot:
 TimeslotBlock = list[OptimizerTimeslot]
 
 
+def merge_blocks(
+    blocks: Iterable[TimeslotBlock]
+) -> Iterable[TimeslotBlock]:
+    """Merge iterable of blocks together.
+
+    The timeslots of all blocks are grouped into maximal blocks.
+    Timeslots with the same start and end are identified with each other
+    and merged (cf `OptimizerTimeslot.merge`).
+    Throws a ValueError if any timeslots are overlapping but do not
+    share the same start and end, i.e. partial overlap is not allowed.
+
+    :param blocks: iterable of blocks to merge.
+    :return: iterable of merged blocks.
+    :rtype: iterable over lists of OptimizerTimeslot objects
+    """
+    if not blocks:
+        return []
+
+    # flatten timeslot iterables to single chain
+    timeslot_chain = itertools.chain.from_iterable(blocks)
+
+    # sort timeslots according to start
+    timeslots = sorted(
+        timeslot_chain,
+        key=lambda slot: slot.avail.start
+    )
+
+    if not timeslots:
+        return []
+
+    all_blocks = []
+    current_block = [timeslots[0]]
+    timeslots = timeslots[1:]
+
+    for slot in timeslots:
+        if current_block and slot.avail.overlaps(current_block[-1].avail, strict=True):
+            if (
+                slot.avail.start == current_block[-1].avail.start
+                and slot.avail.end == current_block[-1].avail.end
+            ):
+                # the same timeslot -> merge
+                current_block[-1] = current_block[-1].merge(slot)
+            else:
+                # partial overlap of interiors -> not supported
+                # TODO: Show comprehensive message in production
+                raise ValueError(
+                    "Partially overlapping timeslots are not supported!"
+                    f" ({current_block[-1].avail.simplified}, {slot.avail.simplified})"
+                )
+        elif not current_block or slot.avail.overlaps(current_block[-1].avail, strict=False):
+            # only endpoints in intersection -> same block
+            current_block.append(slot)
+        else:
+            # no overlap at all -> new block
+            all_blocks.append(current_block)
+            current_block = [slot]
+
+    if current_block:
+        all_blocks.append(current_block)
+
+    return all_blocks
+
+
 class Event(models.Model):
     """
     An event supplies the frame for all Aks.
@@ -319,68 +382,6 @@ class Event(models.Model):
                 constraints=category_constraints,
             )
 
-    def merge_blocks(
-        self, blocks: Iterable[TimeslotBlock]
-    ) -> Iterable[TimeslotBlock]:
-        """Merge iterable of blocks together.
-
-        The timeslots of all blocks are grouped into maximal blocks.
-        Timeslots with the same start and end are identified with each other
-        and merged (cf `OptimizerTimeslot.merge`).
-        Throws a ValueError if any timeslots are overlapping but do not
-        share the same start and end, i.e. partial overlap is not allowed.
-
-        :param blocks: iterable of blocks to merge.
-        :return: iterable of merged blocks.
-        :rtype: iterable over lists of OptimizerTimeslot objects
-        """
-        if not blocks:
-            return []
-
-        # flatten timeslot iterables to single chain
-        timeslot_chain = itertools.chain.from_iterable(blocks)
-
-        # sort timeslots according to start
-        timeslots = sorted(
-            timeslot_chain,
-            key=lambda slot: slot.avail.start
-        )
-
-        if not timeslots:
-            return []
-
-        all_blocks = []
-        current_block = [timeslots[0]]
-        timeslots = timeslots[1:]
-
-        for slot in timeslots:
-            if current_block and slot.avail.overlaps(current_block[-1].avail, strict=True):
-                if (
-                    slot.avail.start == current_block[-1].avail.start
-                    and slot.avail.end == current_block[-1].avail.end
-                ):
-                    # the same timeslot -> merge
-                    current_block[-1] = current_block[-1].merge(slot)
-                else:
-                    # partial overlap of interiors -> not supported
-                    # TODO: Show comprehensive message in production
-                    raise ValueError(
-                        "Partially overlapping timeslots are not supported!"
-                        f" ({current_block[-1].avail.simplified}, {slot.avail.simplified})"
-                    )
-            elif not current_block or slot.avail.overlaps(current_block[-1].avail, strict=False):
-                # only endpoints in intersection -> same block
-                current_block.append(slot)
-            else:
-                # no overlap at all -> new block
-                all_blocks.append(current_block)
-                current_block = [slot]
-
-        if current_block:
-            all_blocks.append(current_block)
-
-        return all_blocks
-
     def schedule_from_json(self, schedule: str) -> None:
         """Load AK schedule from a json string.
 
@@ -395,7 +396,7 @@ class Event(models.Model):
 
         timeslot_dict = {
             timeslot.idx: timeslot
-            for block in self.merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour))
+            for block in merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour))
             for timeslot in block
         }
 
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 76bfdb28..0ee77537 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -9,7 +9,7 @@ from django.views.generic import ListView, DetailView
 from AKModel.availability.models import Availability
 from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \
     IntermediateAdminActionView
-from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner
+from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner, merge_blocks
 
 
 class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
@@ -110,7 +110,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists()
         }
 
-        for block in self.event.merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)):
+        for block in merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)):
             current_block = []
 
             for timeslot in block:
-- 
GitLab


From 8d049baab9c3dd23c9627b4feee1e60be71904de Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Fri, 27 Dec 2024 14:58:46 +0100
Subject: [PATCH 33/77] Make tests more descriptive

---
 AKModel/views/ak.py | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 0ee77537..44fbc096 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -52,12 +52,15 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
 
 
     def _test_slot_contained(self, slot: Availability, availabilities: List[Availability]) -> bool:
+        """Test if slot is contained in any member of availabilities."""
         return any(availability.contains(slot) for availability in availabilities)
 
-    def _test_event_covered(self, availabilities: List[Availability]) -> bool:
+    def _test_event_not_covered(self, availabilities: List[Availability]) -> bool:
+        """Test if event is not covered by availabilities."""
         return not Availability.is_event_covered(self.event, availabilities)
 
-    def _test_fixed_ak(self, ak_id, slot: Availability, ak_fixed: dict) -> bool:
+    def _test_ak_fixed_in_slot(self, ak_id, slot: Availability, ak_fixed: dict) -> bool:
+        """Test if AK defined by `ak_id` is fixed to happen during slot."""
         if not ak_id in ak_fixed:
             return False
 
@@ -65,8 +68,9 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         return fixed_slot.overlaps(slot, strict=True)
 
     def _test_add_constraint(self, slot: Availability, availabilities: List[Availability]) -> bool:
+        """Test if object is not available for whole event and may happen during slot."""
         return (
-            self._test_event_covered(availabilities)
+            self._test_event_not_covered(availabilities)
             and self._test_slot_contained(slot, availabilities)
         )
 
@@ -123,7 +127,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                     for ak_id, availabilities in ak_availabilities.items()
                     if (
                         self._test_add_constraint(timeslot.avail, availabilities)
-                        or self._test_fixed_ak(ak_id, timeslot.avail, ak_fixed)
+                        or self._test_ak_fixed_in_slot(ak_id, timeslot.avail, ak_fixed)
                     )
                 ])
                 time_constraints.extend([
-- 
GitLab


From c73020b00c9138e9bca6ede2809829fef1a01d32 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Fri, 27 Dec 2024 14:59:11 +0100
Subject: [PATCH 34/77] Use uniform discretization if no default slots exist

---
 AKModel/views/ak.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 44fbc096..93d790f8 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -114,7 +114,13 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists()
         }
 
-        for block in merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)):
+        if DefaultSlot.objects.filter(event=self).exists():
+            # discretize default slots if they exists
+            blocks = merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR))
+        else:
+            blocks = self.event.uniform_time_slots(slos_in_an_hour=SLOTS_IN_AN_HOUR)
+
+        for block in blocks:
             current_block = []
 
             for timeslot in block:
-- 
GitLab


From 1661b3594255e1ccfbd919c3540b2daf762c9202 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Fri, 27 Dec 2024 15:02:33 +0100
Subject: [PATCH 35/77] Add more comments

---
 AKModel/views/ak.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 93d790f8..74c54b9e 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -125,9 +125,12 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
 
             for timeslot in block:
                 time_constraints = []
+                # if reso_deadline is set and timeslot ends before it,
+                #   add fulfilled time constraint 'resolution'
                 if self.event.reso_deadline is None or timeslot.avail.end < self.event.reso_deadline:
                     time_constraints.append("resolution")
 
+                # add fulfilled time constraints for all AKs that cannot happen during full event
                 time_constraints.extend([
                     f"availability-ak-{ak_id}"
                     for ak_id, availabilities in ak_availabilities.items()
@@ -136,11 +139,13 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                         or self._test_ak_fixed_in_slot(ak_id, timeslot.avail, ak_fixed)
                     )
                 ])
+                # add fulfilled time constraints for all persons that are not available for full event
                 time_constraints.extend([
                     f"availability-person-{person_id}"
                     for person_id, availabilities in person_availabilities.items()
                     if self._test_add_constraint(timeslot.avail, availabilities)
                 ])
+                # add fulfilled time constraints for all rooms that are not available for full event
                 time_constraints.extend([
                     f"availability-room-{room_id}"
                     for room_id, availabilities in room_availabilities.items()
-- 
GitLab


From 53dfe886b832a20b06e994db3db2883631d5f44c Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Sat, 28 Dec 2024 01:32:39 +0100
Subject: [PATCH 36/77] Fix docstr placement

---
 AKModel/models.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 3b6eb600..d30912d3 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -19,14 +19,14 @@ from timezone_field import TimeZoneField
 class OptimizerTimeslot:
     """Class describing a discrete timeslot. Used to interface with an optimizer."""
 
-    """The availability object corresponding to this timeslot."""
     avail: "Availability"
+    """The availability object corresponding to this timeslot."""
 
-    """The unique index of this optimizer timeslot."""
     idx: int
+    """The unique index of this optimizer timeslot."""
 
-    """The set of time constraints fulfilled by this object."""
     constraints: set[str]
+    """The set of time constraints fulfilled by this object."""
 
     def merge(self, other: "OptimizerTimeslot") -> "OptimizerTimeslot":
         """Merge with other OptimizerTimeslot.
-- 
GitLab


From ec5c79e7b6a10908c337e18dc69e9d0dd78f8ef2 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Sat, 28 Dec 2024 01:32:54 +0100
Subject: [PATCH 37/77] Fix import

---
 AKModel/views/ak.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 74c54b9e..230ab247 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -9,7 +9,7 @@ from django.views.generic import ListView, DetailView
 from AKModel.availability.models import Availability
 from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \
     IntermediateAdminActionView
-from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner, merge_blocks
+from AKModel.models import AKRequirement, AKSlot, DefaultSlot, Event, AKOrgaMessage, AK, Room, AKOwner, merge_blocks
 
 
 class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
-- 
GitLab


From 05dbef0b4fe43fc163a834788ed0aeb2e4fc065c Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Sat, 28 Dec 2024 01:41:59 +0100
Subject: [PATCH 38/77] Add json import/export view to test for only internal
 access

---
 AKModel/tests.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/AKModel/tests.py b/AKModel/tests.py
index fb24dc08..6730315d 100644
--- a/AKModel/tests.py
+++ b/AKModel/tests.py
@@ -214,7 +214,9 @@ class ModelViewTests(BasicViewTests, TestCase):
         ('admin:event_status', {'event_slug': 'kif42'}),
         ('admin:event_requirement_overview', {'event_slug': 'kif42'}),
         ('admin:ak_csv_export', {'event_slug': 'kif42'}),
+        ('admin:ak_json_export', {'event_slug': 'kif42'}),
         ('admin:ak_wiki_export', {'slug': 'kif42'}),
+        ('admin:ak_schedule_json_import', {'event_slug': 'kif42'}),
         ('admin:ak_delete_orga_messages', {'event_slug': 'kif42'}),
         ('admin:ak_slide_export', {'event_slug': 'kif42'}),
         ('admin:default-slots-editor', {'event_slug': 'kif42'}),
-- 
GitLab


From 4505f33b8bc8c45d8f5034d1f9bd4be471ebb43f Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Sat, 28 Dec 2024 01:47:13 +0100
Subject: [PATCH 39/77] Fix event filter

---
 AKModel/views/ak.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 230ab247..ecb766ac 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -114,7 +114,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists()
         }
 
-        if DefaultSlot.objects.filter(event=self).exists():
+        if DefaultSlot.objects.filter(event=self.event).exists():
             # discretize default slots if they exists
             blocks = merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR))
         else:
-- 
GitLab


From 1ae6e91655c8343dee69269adf55c09cbd9fb60c Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Sat, 28 Dec 2024 01:50:21 +0100
Subject: [PATCH 40/77] Fix typo

---
 AKModel/views/ak.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index ecb766ac..90599a1b 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -118,7 +118,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             # discretize default slots if they exists
             blocks = merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR))
         else:
-            blocks = self.event.uniform_time_slots(slos_in_an_hour=SLOTS_IN_AN_HOUR)
+            blocks = self.event.uniform_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)
 
         for block in blocks:
             current_block = []
-- 
GitLab


From dafa0810a062eefe19482c1965b412cddb32ddfd Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Tue, 21 Jan 2025 16:16:39 +0100
Subject: [PATCH 41/77] Change rounding strategy to ceil

---
 AKModel/models.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index d30912d3..636f7a9f 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -1,5 +1,6 @@
 import itertools
 import json
+import math
 from dataclasses import dataclass
 from datetime import datetime, timedelta
 from typing import Iterable, Generator
@@ -934,10 +935,12 @@ class AKSlot(models.Model):
                 return []
             return [f"availability-person-{owner.pk}"]
 
+        ceil_offet_eps = 1e-4
+
         # self.slots_in_an_hour is set in AKJSONExportView
         data = {
             "id": str(self.pk),
-            "duration": round(self.duration * self.slots_in_an_hour),
+            "duration": math.ceil(self.duration * self.slots_in_an_hour - ceil_offet_eps),
             "properties": {},
             "room_constraints": [constraint.name
                                  for constraint in self.ak.requirements.all()],
-- 
GitLab


From 3f8f7ad79bed1d3265a90a0c7b0efbfb61a1c30d Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Tue, 21 Jan 2025 16:56:41 +0100
Subject: [PATCH 42/77] change offset from float to decimal

---
 AKModel/models.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 636f7a9f..0e995932 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -3,6 +3,7 @@ import json
 import math
 from dataclasses import dataclass
 from datetime import datetime, timedelta
+from decimal import Decimal
 from typing import Iterable, Generator
 
 from django.db import models
@@ -935,7 +936,7 @@ class AKSlot(models.Model):
                 return []
             return [f"availability-person-{owner.pk}"]
 
-        ceil_offet_eps = 1e-4
+        ceil_offet_eps = decimal.Decimal(1e-4)
 
         # self.slots_in_an_hour is set in AKJSONExportView
         data = {
-- 
GitLab


From 693fec82a0f222f59773a298c13f3bf70c4bdb77 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Tue, 21 Jan 2025 17:00:53 +0100
Subject: [PATCH 43/77] Fix import

---
 AKModel/models.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 0e995932..f25b04de 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -1,9 +1,9 @@
+import decimal
 import itertools
 import json
 import math
 from dataclasses import dataclass
 from datetime import datetime, timedelta
-from decimal import Decimal
 from typing import Iterable, Generator
 
 from django.db import models
-- 
GitLab


From 57ac0fe7a353a074d82d1d5be58055e914a4a53b Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Tue, 21 Jan 2025 19:50:18 +0100
Subject: [PATCH 44/77] Add new field to AK info block with exact duration in
 hours

---
 AKModel/models.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/AKModel/models.py b/AKModel/models.py
index f25b04de..08607e19 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -952,6 +952,7 @@ class AKSlot(models.Model):
                                    for owner in self.ak.owners.all()]),
                 "description": self.ak.description,
                 "reso": self.ak.reso,
+                "duration_in_hours": self.duration,
                 },
             }
 
-- 
GitLab


From 9301ef7a672f9bd5cf4c59ac1264658658bb6e2c Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Tue, 21 Jan 2025 19:57:51 +0100
Subject: [PATCH 45/77] Convert decimal to float first

---
 AKModel/models.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 08607e19..57e488fe 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -952,7 +952,7 @@ class AKSlot(models.Model):
                                    for owner in self.ak.owners.all()]),
                 "description": self.ak.description,
                 "reso": self.ak.reso,
-                "duration_in_hours": self.duration,
+                "duration_in_hours": float(self.duration),
                 },
             }
 
-- 
GitLab


From ba533efa2beb5d9c8b9cce7525a5bb99e782230c Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Wed, 22 Jan 2025 14:28:12 +0100
Subject: [PATCH 46/77] Export conflicts and dependencies as AK properties

---
 AKModel/models.py | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index d30912d3..4c6d6188 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -934,11 +934,17 @@ class AKSlot(models.Model):
                 return []
             return [f"availability-person-{owner.pk}"]
 
+        conflict_slots = AKSlot.objects.filter(ak__in=self.ak.conflicts.all())
+        dependency_slots = AKSlot.objects.filter(ak__in=self.ak.prerequisites.all())
+
         # self.slots_in_an_hour is set in AKJSONExportView
         data = {
             "id": str(self.pk),
             "duration": round(self.duration * self.slots_in_an_hour),
-            "properties": {},
+            "properties": {
+                "conflicts": [str(conflict.pk) for conflict in conflict_slots.all()],
+                "dependencies": [str(dep.pk) for dep in dependency_slots.all()],
+            },
             "room_constraints": [constraint.name
                                  for constraint in self.ak.requirements.all()],
             "time_constraints": ["resolution"] if self.ak.reso else [],
-- 
GitLab


From f1d4a9eeaa269d4839bfb672d518da7ceaaaf2e1 Mon Sep 17 00:00:00 2001
From: Felix Blanke <s6feblan@uni-bonn.de>
Date: Wed, 22 Jan 2025 13:45:40 +0000
Subject: [PATCH 47/77] Encapsulate discretization in
 Event::discretize_timeslots

---
 AKModel/models.py   | 19 ++++++++++++++++++-
 AKModel/views/ak.py |  8 ++------
 2 files changed, 20 insertions(+), 7 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index d30912d3..2a80cef7 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -382,6 +382,23 @@ class Event(models.Model):
                 constraints=category_constraints,
             )
 
+    def discretize_timeslots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]:
+        """"Choose discretization scheme.
+
+        Uses default_time_slots if the event has any DefaultSlot, otherwise uniform_time_slots.
+
+        :param slots_in_an_hour: The percentage of an hour covered by a single slot.
+            Determines the discretization granularity.
+        :yield: Block of optimizer timeslots as the discretization result.
+        :ytype: list of TimeslotBlock
+        """
+
+        if DefaultSlot.objects.filter(event=self).exists():
+            # discretize default slots if they exists
+            yield from merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour))
+        else:
+            yield from self.uniform_time_slots(slots_in_an_hour=slots_in_an_hour)
+
     def schedule_from_json(self, schedule: str) -> None:
         """Load AK schedule from a json string.
 
@@ -396,7 +413,7 @@ class Event(models.Model):
 
         timeslot_dict = {
             timeslot.idx: timeslot
-            for block in merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour))
+            for block in self.discretize_timeslots(slots_in_an_hour=slots_in_an_hour)
             for timeslot in block
         }
 
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 90599a1b..1d60717b 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -9,7 +9,7 @@ from django.views.generic import ListView, DetailView
 from AKModel.availability.models import Availability
 from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \
     IntermediateAdminActionView
-from AKModel.models import AKRequirement, AKSlot, DefaultSlot, Event, AKOrgaMessage, AK, Room, AKOwner, merge_blocks
+from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner
 
 
 class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
@@ -114,11 +114,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists()
         }
 
-        if DefaultSlot.objects.filter(event=self.event).exists():
-            # discretize default slots if they exists
-            blocks = merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR))
-        else:
-            blocks = self.event.uniform_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)
+        blocks = self.event.discretize_timeslots(slots_in_an_hour=SLOTS_IN_AN_HOUR)
 
         for block in blocks:
             current_block = []
-- 
GitLab


From c420f00ee20e109c4dd7b877e1cc041c98e5eb46 Mon Sep 17 00:00:00 2001
From: Lorax66 <lorenzo.conti@uni-bonn.de>
Date: Wed, 22 Jan 2025 14:57:11 +0100
Subject: [PATCH 48/77] fix

---
 AKModel/models.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index ef7da9a2..267f0bb0 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -966,7 +966,6 @@ class AKSlot(models.Model):
                 "conflicts": [str(conflict.pk) for conflict in conflict_slots.all()],
                 "dependencies": [str(dep.pk) for dep in dependency_slots.all()],
             },
-            "properties": {},
             "room_constraints": [constraint.name
                                  for constraint in self.ak.requirements.all()],
             "time_constraints": ["resolution"] if self.ak.reso else [],
-- 
GitLab


From 417e8151154d2848112fda9374fb2bb25b9c1857 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Thu, 23 Jan 2025 16:55:06 +0100
Subject: [PATCH 49/77] Add other slots of same AK to conflict list

---
 AKModel/models.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 267f0bb0..7cca95c1 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -955,6 +955,7 @@ class AKSlot(models.Model):
 
         conflict_slots = AKSlot.objects.filter(ak__in=self.ak.conflicts.all())
         dependency_slots = AKSlot.objects.filter(ak__in=self.ak.prerequisites.all())
+        other_ak_slots = AKSlot.objects.filter(ak=self.ak).exclude(pk=self.pk)
 
         ceil_offet_eps = decimal.Decimal(1e-4)
 
@@ -963,7 +964,9 @@ class AKSlot(models.Model):
             "id": str(self.pk),
             "duration": math.ceil(self.duration * self.slots_in_an_hour - ceil_offet_eps),
             "properties": {
-                "conflicts": [str(conflict.pk) for conflict in conflict_slots.all()],
+                "conflicts":
+                    [str(conflict.pk) for conflict in conflict_slots.all()]
+                  + [str(second_slot.pk) for second_slot in other_ak_slots.all()],
                 "dependencies": [str(dep.pk) for dep in dependency_slots.all()],
             },
             "room_constraints": [constraint.name
-- 
GitLab


From b0f0b9ae7e7082c0c057e99d3fe2f2f4120b3a71 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Thu, 23 Jan 2025 16:55:26 +0100
Subject: [PATCH 50/77] Add django AK id to info dict of slot

---
 AKModel/models.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/AKModel/models.py b/AKModel/models.py
index 7cca95c1..1d12a76d 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -979,6 +979,7 @@ class AKSlot(models.Model):
                 "description": self.ak.description,
                 "reso": self.ak.reso,
                 "duration_in_hours": float(self.duration),
+                "django_ak_id": str(self.ak.pk),
                 },
             }
 
-- 
GitLab


From 57c40a50b029a5b817e3ae46694dbaf48667ef9b Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Sun, 26 Jan 2025 23:10:53 +0100
Subject: [PATCH 51/77] Refactor

---
 AKModel/models.py   | 10 ++++----
 AKModel/views/ak.py | 57 ++++++++++++++++++++++++---------------------
 2 files changed, 36 insertions(+), 31 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 267f0bb0..b645c0c1 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -943,10 +943,12 @@ class AKSlot(models.Model):
         # check if ak resp. owner is available for the whole event
         # -> no time constraint needs to be introduced
 
-        if not self.fixed and Availability.is_event_covered(self.event, self.ak.availabilities.all()):
-            ak_time_constraints = []
-        else:
+        if self.fixed and self.start is not None:
+            ak_time_constraints = [f"fixed-akslot-{self.id}"]
+        elif Availability.is_event_covered(self.event, self.ak.availabilities.all()):
             ak_time_constraints = [f"availability-ak-{self.ak.pk}"]
+        else:
+            ak_time_constraints = []
 
         def _owner_time_constraints(owner: AKOwner):
             if Availability.is_event_covered(self.event, owner.availabilities.all()):
@@ -987,7 +989,7 @@ class AKSlot(models.Model):
             category_constraints = AKCategory.create_category_constraints([self.ak.category])
             data["time_constraints"].extend(category_constraints)
 
-        if self.room is not None and self.fixed:
+        if self.fixed and self.room is not None:
             data["room_constraints"].append(f"availability-room-{self.room.pk}")
 
         if not any(constr.startswith("proxy") for constr in data["room_constraints"]):
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 1d60717b..d266e9bc 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -59,13 +59,13 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         """Test if event is not covered by availabilities."""
         return not Availability.is_event_covered(self.event, availabilities)
 
-    def _test_ak_fixed_in_slot(self, ak_id, slot: Availability, ak_fixed: dict) -> bool:
-        """Test if AK defined by `ak_id` is fixed to happen during slot."""
-        if not ak_id in ak_fixed:
+    def _test_akslot_fixed_in_timeslot(self, ak_slot: AKSlot, timeslot: Availability) -> bool:
+        """Test if an AKSlot is fixed to overlap a timeslot slot."""
+        if not ak_slot.fixed or ak_slot.start is None:
             return False
 
-        fixed_slot = Availability(self.event, start=ak_fixed[ak_id].start, end=ak_fixed[ak_id].end)
-        return fixed_slot.overlaps(slot, strict=True)
+        fixed_avail = Availability(event=self.event, start=ak_slot.start, end=ak_slot.end)
+        return fixed_avail.overlaps(timeslot, strict=True)
 
     def _test_add_constraint(self, slot: Availability, availabilities: List[Availability]) -> bool:
         """Test if object is not available for whole event and may happen during slot."""
@@ -74,6 +74,19 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             and self._test_slot_contained(slot, availabilities)
         )
 
+    def _generate_time_constraints(
+        self,
+        avail_label: str,
+        avail_dict: dict,
+        timeslot_avail: Availability,
+        prefix: str = "availability",
+    ) -> list[str]:
+        return [
+            f"{prefix}-{avail_label}-{pk}"
+            for pk, availabilities in avail_dict.items()
+            if self._test_add_constraint(timeslot_avail, availabilities)
+        ]
+
     def get_queryset(self):
         return super().get_queryset().order_by("ak__track")
 
@@ -108,12 +121,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             for person in AKOwner.objects.filter(event=self.event)
         }
 
-        ak_fixed = {
-            ak_id: values.get()
-            for ak_id in ak_availabilities.keys()
-            if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists()
-        }
-
         blocks = self.event.discretize_timeslots(slots_in_an_hour=SLOTS_IN_AN_HOUR)
 
         for block in blocks:
@@ -127,26 +134,22 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                     time_constraints.append("resolution")
 
                 # add fulfilled time constraints for all AKs that cannot happen during full event
-                time_constraints.extend([
-                    f"availability-ak-{ak_id}"
-                    for ak_id, availabilities in ak_availabilities.items()
-                    if (
-                        self._test_add_constraint(timeslot.avail, availabilities)
-                        or self._test_ak_fixed_in_slot(ak_id, timeslot.avail, ak_fixed)
-                    )
-                ])
+                self._generate_time_constraints("ak", ak_availabilities, timeslot.avail)
+
                 # add fulfilled time constraints for all persons that are not available for full event
-                time_constraints.extend([
-                    f"availability-person-{person_id}"
-                    for person_id, availabilities in person_availabilities.items()
-                    if self._test_add_constraint(timeslot.avail, availabilities)
-                ])
+                self._generate_time_constraints("person", person_availabilities, timeslot.avail)
+
                 # add fulfilled time constraints for all rooms that are not available for full event
+                self._generate_time_constraints("room", room_availabilities, timeslot.avail)
+
+                # add fulfilled time constraints for all AKSlots fixed to happen during timeslot
                 time_constraints.extend([
-                    f"availability-room-{room_id}"
-                    for room_id, availabilities in room_availabilities.items()
-                    if self._test_add_constraint(timeslot.avail, availabilities)
+                    f"fixed-akslot-{slot.id}"
+                    for slot in AKSlot.objects.filter(event=self.event, fixed=True)
+                                              .exclude(start__isnull=True)
+                    if self._test_akslot_fixed_in_timeslot(slot, timeslot.avail)
                 ])
+
                 time_constraints.extend(timeslot.constraints)
 
                 current_block.append({
-- 
GitLab


From cf2637d26115e30ed2e2ca10c919f45f14620ade Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Sun, 26 Jan 2025 23:29:04 +0100
Subject: [PATCH 52/77] Rename constraint for fixed rooms

---
 AKModel/models.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index b645c0c1..895523fd 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -826,7 +826,7 @@ class Room(models.Model):
             "time_constraints": time_constraints
         }
 
-        data["fulfilled_room_constraints"].append(f"availability-room-{self.pk}")
+        data["fulfilled_room_constraints"].append(f"fixed-room-{self.pk}")
 
         if not any(constr.startswith("proxy") for constr in data["fulfilled_room_constraints"]):
             data["fulfilled_room_constraints"].append("no-proxy")
@@ -990,7 +990,7 @@ class AKSlot(models.Model):
             data["time_constraints"].extend(category_constraints)
 
         if self.fixed and self.room is not None:
-            data["room_constraints"].append(f"availability-room-{self.room.pk}")
+            data["room_constraints"].append(f"fixed-room-{self.room.pk}")
 
         if not any(constr.startswith("proxy") for constr in data["room_constraints"]):
             data["room_constraints"].append("no-proxy")
-- 
GitLab


From e3bdd34795a3c1b926bf568e17c05e8f21810082 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 Jan 2025 00:19:20 +0100
Subject: [PATCH 53/77] Only add constr if event is not covered

---
 AKModel/models.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 895523fd..ea5461bf 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -945,7 +945,7 @@ class AKSlot(models.Model):
 
         if self.fixed and self.start is not None:
             ak_time_constraints = [f"fixed-akslot-{self.id}"]
-        elif Availability.is_event_covered(self.event, self.ak.availabilities.all()):
+        elif not Availability.is_event_covered(self.event, self.ak.availabilities.all()):
             ak_time_constraints = [f"availability-ak-{self.ak.pk}"]
         else:
             ak_time_constraints = []
-- 
GitLab


From 205436f871305b7071729f33609be28137fe83cc Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 Jan 2025 00:34:48 +0100
Subject: [PATCH 54/77] Iterate over AKs instead of AKSlots

---
 AKModel/views/ak.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index d266e9bc..caee99b4 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -109,8 +109,8 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             slot.slots_in_an_hour = SLOTS_IN_AN_HOUR
 
         ak_availabilities = {
-            slot.ak.pk: Availability.union(slot.ak.availabilities.all())
-            for slot in context["slots"]
+            ak.pk: Availability.union(ak.availabilities.all())
+            for ak in AK.objects.filter(event=self.event).all()
         }
         room_availabilities = {
             room.pk: Availability.union(room.availabilities.all())
-- 
GitLab


From 26f432ad285d715fea550ac09b4b868a9d4deccb Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 27 Jan 2025 00:35:29 +0100
Subject: [PATCH 55/77] Add generated constraints to list

---
 AKModel/views/ak.py | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index caee99b4..5e642dd5 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -134,13 +134,19 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                     time_constraints.append("resolution")
 
                 # add fulfilled time constraints for all AKs that cannot happen during full event
-                self._generate_time_constraints("ak", ak_availabilities, timeslot.avail)
+                time_constraints.extend(
+                    self._generate_time_constraints("ak", ak_availabilities, timeslot.avail)
+                )
 
                 # add fulfilled time constraints for all persons that are not available for full event
-                self._generate_time_constraints("person", person_availabilities, timeslot.avail)
+                time_constraints.extend(
+                    self._generate_time_constraints("person", person_availabilities, timeslot.avail)
+                )
 
                 # add fulfilled time constraints for all rooms that are not available for full event
-                self._generate_time_constraints("room", room_availabilities, timeslot.avail)
+                time_constraints.extend(
+                    self._generate_time_constraints("room", room_availabilities, timeslot.avail)
+                )
 
                 # add fulfilled time constraints for all AKSlots fixed to happen during timeslot
                 time_constraints.extend([
-- 
GitLab


From e030e675181725f1d5600ca079f156e8962adcbf Mon Sep 17 00:00:00 2001
From: Felix Blanke <s6feblan@uni-bonn.de>
Date: Thu, 6 Feb 2025 12:00:27 +0000
Subject: [PATCH 56/77] Avoid setting the slot duration at import

---
 AKModel/locale/de_DE/LC_MESSAGES/django.po    | 417 ++++++++++--------
 AKModel/models.py                             |  60 ++-
 AKModel/views/manage.py                       |  14 +-
 .../locale/de_DE/LC_MESSAGES/django.po        |  18 +-
 .../locale/de_DE/LC_MESSAGES/django.po        |   6 +-
 5 files changed, 321 insertions(+), 194 deletions(-)

diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index 4fcbd8a8..42b38565 100644
--- a/AKModel/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-05-31 14:22+0000\n"
+"POT-Creation-Date: 2025-01-22 19:00+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"
@@ -33,7 +33,8 @@ msgstr "Plan veröffentlichen"
 msgid "Unpublish plan"
 msgstr "Plan verbergen"
 
-#: AKModel/admin.py:168 AKModel/models.py:612 AKModel/models.py:1028
+#: AKModel/admin.py:168 AKModel/models.py:689 AKModel/models.py:1114
+#: AKModel/models.py:1150
 #: AKModel/templates/admin/AKModel/status/event_aks.html:10
 #: AKModel/views/manage.py:75 AKModel/views/status.py:97
 msgid "AKs"
@@ -59,11 +60,11 @@ msgstr "In Wiki-Syntax exportieren"
 msgid "Cannot export AKs from more than one event at the same time."
 msgstr "Kann nicht AKs von mehreren Events zur selben Zeit exportieren."
 
-#: AKModel/admin.py:320 AKModel/views/ak.py:226
+#: AKModel/admin.py:320 AKModel/views/ak.py:237
 msgid "Reset interest in AKs"
 msgstr "Interesse an AKs zurücksetzen"
 
-#: AKModel/admin.py:330 AKModel/views/ak.py:241
+#: AKModel/admin.py:330 AKModel/views/ak.py:252
 msgid "Reset AKs' interest counters"
 msgstr "Interessenszähler der AKs zurücksetzen"
 
@@ -108,17 +109,18 @@ msgstr "Die eingegebene Verfügbarkeit enthält ein ungültiges Datum."
 msgid "Please fill in your availabilities!"
 msgstr "Bitte Verfügbarkeiten eintragen!"
 
-#: AKModel/availability/models.py:43 AKModel/models.py:91 AKModel/models.py:412
-#: AKModel/models.py:489 AKModel/models.py:522 AKModel/models.py:548
-#: AKModel/models.py:602 AKModel/models.py:744 AKModel/models.py:820
-#: AKModel/models.py:967 AKModel/models.py:1024 AKModel/models.py:1215
+#: AKModel/availability/models.py:43 AKModel/models.py:160
+#: AKModel/models.py:489 AKModel/models.py:566 AKModel/models.py:599
+#: AKModel/models.py:625 AKModel/models.py:679 AKModel/models.py:821
+#: AKModel/models.py:897 AKModel/models.py:1053 AKModel/models.py:1110
+#: AKModel/models.py:1301
 msgid "Event"
 msgstr "Event"
 
-#: AKModel/availability/models.py:44 AKModel/models.py:413
-#: AKModel/models.py:490 AKModel/models.py:523 AKModel/models.py:549
-#: AKModel/models.py:603 AKModel/models.py:745 AKModel/models.py:821
-#: AKModel/models.py:968 AKModel/models.py:1025 AKModel/models.py:1216
+#: AKModel/availability/models.py:44 AKModel/models.py:490
+#: AKModel/models.py:567 AKModel/models.py:600 AKModel/models.py:626
+#: AKModel/models.py:680 AKModel/models.py:822 AKModel/models.py:898
+#: AKModel/models.py:1054 AKModel/models.py:1111 AKModel/models.py:1302
 msgid "Associated event"
 msgstr "Zugehöriges Event"
 
@@ -130,8 +132,8 @@ msgstr "Person"
 msgid "Person whose availability this is"
 msgstr "Person deren Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:61 AKModel/models.py:748
-#: AKModel/models.py:810 AKModel/models.py:1034
+#: AKModel/availability/models.py:61 AKModel/models.py:825
+#: AKModel/models.py:887 AKModel/models.py:1120
 msgid "Room"
 msgstr "Raum"
 
@@ -139,8 +141,8 @@ msgstr "Raum"
 msgid "Room whose availability this is"
 msgstr "Raum dessen Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:70 AKModel/models.py:611
-#: AKModel/models.py:809 AKModel/models.py:962
+#: AKModel/availability/models.py:70 AKModel/models.py:688
+#: AKModel/models.py:886 AKModel/models.py:1048
 msgid "AK"
 msgstr "AK"
 
@@ -148,8 +150,8 @@ msgstr "AK"
 msgid "AK whose availability this is"
 msgstr "Verfügbarkeiten"
 
-#: AKModel/availability/models.py:79 AKModel/models.py:493
-#: AKModel/models.py:1040
+#: AKModel/availability/models.py:79 AKModel/models.py:570
+#: AKModel/models.py:1126
 msgid "AK Category"
 msgstr "AK-Kategorie"
 
@@ -157,7 +159,7 @@ msgstr "AK-Kategorie"
 msgid "AK Category whose availability this is"
 msgstr "AK-Kategorie, deren Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:300
+#: AKModel/availability/models.py:300 AKModel/models.py:721
 msgid "Availabilities"
 msgstr "Verfügbarkeiten"
 
@@ -219,7 +221,7 @@ msgstr ""
 "fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen "
 "ausgefüllt zu werden?"
 
-#: AKModel/forms.py:189 AKModel/models.py:1209
+#: AKModel/forms.py:189 AKModel/models.py:1295
 msgid "Default Slots"
 msgstr "Standardslots"
 
@@ -266,7 +268,7 @@ msgstr "JSON-Daten"
 msgid "JSON data from the scheduling solver"
 msgstr "JSON-Daten, die der scheduling-solver produziert hat"
 
-#: AKModel/metaviews/admin.py:156 AKModel/models.py:60
+#: AKModel/metaviews/admin.py:156 AKModel/models.py:129
 msgid "Start"
 msgstr "Start"
 
@@ -291,66 +293,66 @@ msgstr "Aktivieren?"
 msgid "Finish"
 msgstr "Abschluss"
 
-#: AKModel/models.py:51 AKModel/models.py:481 AKModel/models.py:519
-#: AKModel/models.py:546 AKModel/models.py:564 AKModel/models.py:736
+#: AKModel/models.py:120 AKModel/models.py:558 AKModel/models.py:596
+#: AKModel/models.py:623 AKModel/models.py:641 AKModel/models.py:813
 msgid "Name"
 msgstr "Name"
 
-#: AKModel/models.py:52
+#: AKModel/models.py:121
 msgid "Name or iteration of the event"
 msgstr "Name oder Iteration des Events"
 
-#: AKModel/models.py:53
+#: AKModel/models.py:122
 msgid "Short Form"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:54
+#: AKModel/models.py:123
 msgid "Short name of letters/numbers/dots/dashes/underscores used in URLs."
 msgstr ""
 "Kurzname bestehend aus Buchstaben, Nummern, Punkten und Unterstrichen zur "
 "Nutzung in URLs"
 
-#: AKModel/models.py:56
+#: AKModel/models.py:125
 msgid "Place"
 msgstr "Ort"
 
-#: AKModel/models.py:57
+#: AKModel/models.py:126
 msgid "City etc. the event takes place in"
 msgstr "Stadt o.ä. in der das Event stattfindet"
 
-#: AKModel/models.py:59
+#: AKModel/models.py:128
 msgid "Time Zone"
 msgstr "Zeitzone"
 
-#: AKModel/models.py:59
+#: AKModel/models.py:128
 msgid "Time Zone where this event takes place in"
 msgstr "Zeitzone in der das Event stattfindet"
 
-#: AKModel/models.py:60
+#: AKModel/models.py:129
 msgid "Time the event begins"
 msgstr "Zeit zu der das Event beginnt"
 
-#: AKModel/models.py:61
+#: AKModel/models.py:130
 msgid "End"
 msgstr "Ende"
 
-#: AKModel/models.py:61
+#: AKModel/models.py:130
 msgid "Time the event ends"
 msgstr "Zeit zu der das Event endet"
 
-#: AKModel/models.py:62
+#: AKModel/models.py:131
 msgid "Resolution Deadline"
 msgstr "Resolutionsdeadline"
 
-#: AKModel/models.py:63
+#: AKModel/models.py:132
 msgid "When should AKs with intention to submit a resolution be done?"
 msgstr "Wann sollen AKs mit Resolutionsabsicht stattgefunden haben?"
 
-#: AKModel/models.py:65
+#: AKModel/models.py:134
 msgid "Interest Window Start"
 msgstr "Beginn Interessensbekundung"
 
-#: AKModel/models.py:67
+#: AKModel/models.py:136
 msgid ""
 "Opening time for expression of interest. When left blank, no interest "
 "indication will be possible."
@@ -358,71 +360,71 @@ msgstr ""
 "Öffnungszeitpunkt für die Angabe von Interesse an AKs.Wenn das Feld leer "
 "bleibt, wird keine Abgabe von Interesse möglich sein."
 
-#: AKModel/models.py:69
+#: AKModel/models.py:138
 msgid "Interest Window End"
 msgstr "Ende Interessensbekundung"
 
-#: AKModel/models.py:70
+#: AKModel/models.py:139
 msgid "Closing time for expression of interest."
 msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs."
 
-#: AKModel/models.py:72
+#: AKModel/models.py:141
 msgid "Public event"
 msgstr "Öffentliches Event"
 
-#: AKModel/models.py:73
+#: AKModel/models.py:142
 msgid "Show this event on overview page."
 msgstr "Zeige dieses Event auf der Übersichtseite an"
 
-#: AKModel/models.py:75
+#: AKModel/models.py:144
 msgid "Active State"
 msgstr "Aktiver Status"
 
-#: AKModel/models.py:75
+#: AKModel/models.py:144
 msgid "Marks currently active events"
 msgstr "Markiert aktuell aktive Events"
 
-#: AKModel/models.py:76
+#: AKModel/models.py:145
 msgid "Plan Hidden"
 msgstr "Plan verborgen"
 
-#: AKModel/models.py:76
+#: AKModel/models.py:145
 msgid "Hides plan for non-staff users"
 msgstr "Verbirgt den Plan für Nutzer*innen ohne erweiterte Rechte"
 
-#: AKModel/models.py:78
+#: AKModel/models.py:147
 msgid "Plan published at"
 msgstr "Plan veröffentlicht am/um"
 
-#: AKModel/models.py:79
+#: AKModel/models.py:148
 msgid "Timestamp at which the plan was published"
 msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde"
 
-#: AKModel/models.py:81
+#: AKModel/models.py:150
 msgid "Base URL"
 msgstr "URL-Prefix"
 
-#: AKModel/models.py:81
+#: AKModel/models.py:150
 msgid "Prefix for wiki link construction"
 msgstr "Prefix für die automatische Generierung von Wiki-Links"
 
-#: AKModel/models.py:82
+#: AKModel/models.py:151
 msgid "Wiki Export Template Name"
 msgstr "Wiki-Export Templatename"
 
-#: AKModel/models.py:83
+#: AKModel/models.py:152
 msgid "Default Slot Length"
 msgstr "Standardslotlänge"
 
-#: AKModel/models.py:84
+#: AKModel/models.py:153
 msgid "Default length in hours that is assumed for AKs in this event."
 msgstr "Standardlänge von Slots (in Stunden) für dieses Event"
 
-#: AKModel/models.py:86
+#: AKModel/models.py:155
 msgid "Contact email address"
 msgstr "E-Mail Kontaktadresse"
 
-#: AKModel/models.py:87
+#: AKModel/models.py:156
 msgid ""
 "An email address that is displayed on every page and can be used for all "
 "kinds of questions"
@@ -430,75 +432,107 @@ msgstr ""
 "Eine Mailadresse die auf jeder Seite angezeigt wird und für alle Arten von "
 "Fragen genutzt werden kann"
 
-#: AKModel/models.py:92
+#: AKModel/models.py:161
 msgid "Events"
 msgstr "Events"
 
-#: AKModel/models.py:407
+#: AKModel/models.py:430
+#, python-brace-format
+msgid "AK {ak_name} is not assigned any timeslot by the solver"
+msgstr "Dem AK {ak_name} wurde vom Solver kein Zeitslot zugewiesen"
+
+#: AKModel/models.py:440
+#, python-brace-format
+msgid ""
+"Duration of AK {ak_name} assigned by solver ({solver_duration} hours) is "
+"less than the duration required by the slot ({slot_duration} hours)"
+msgstr ""
+"Die dem AK {ak_name} vom Solver zugewiesene Dauer ({solver_duration} Stunden) ist "
+"kürzer als die aktuell vorgesehene Dauer des Slots ({slot_duration} Stunden)"
+
+#: AKModel/models.py:454
+#, python-brace-format
+msgid ""
+"Fixed AK {ak_name} assigned by solver to room {solver_room} is fixed to room "
+"{slot_room}"
+msgstr ""
+"Dem fix geplanten AK {ak_name} wurde vom Solver Raum {solver_room} zugewiesen, "
+"dabei ist der AK bereits fix in Raum {slot_room} eingeplant."
+
+#: AKModel/models.py:465
+#, python-brace-format
+msgid ""
+"Fixed AK {ak_name} assigned by solver to start at {solver_start} is fixed to "
+"start at {slot_start}"
+msgstr ""
+"Dem fix geplanten AK {ak_name} wurde vom Solver die Startzeit {solver_start} zugewiesen, "
+"dabei ist der AK bereits für {slot_start} eingeplant."
+
+#: AKModel/models.py:484
 msgid "Nickname"
 msgstr "Spitzname"
 
-#: AKModel/models.py:407
+#: AKModel/models.py:484
 msgid "Name to identify an AK owner by"
 msgstr "Name, durch den eine AK-Leitung identifiziert wird"
 
-#: AKModel/models.py:408
+#: AKModel/models.py:485
 msgid "Slug"
 msgstr "Slug"
 
-#: AKModel/models.py:408
+#: AKModel/models.py:485
 msgid "Slug for URL generation"
 msgstr "Slug für URL-Generierung"
 
-#: AKModel/models.py:409
+#: AKModel/models.py:486
 msgid "Institution"
 msgstr "Instutution"
 
-#: AKModel/models.py:409
+#: AKModel/models.py:486
 msgid "Uni etc."
 msgstr "Universität o.ä."
 
-#: AKModel/models.py:410 AKModel/models.py:573
+#: AKModel/models.py:487 AKModel/models.py:650
 msgid "Web Link"
 msgstr "Internet Link"
 
-#: AKModel/models.py:410
+#: AKModel/models.py:487
 msgid "Link to Homepage"
 msgstr "Link zu Homepage oder Webseite"
 
-#: AKModel/models.py:416 AKModel/models.py:1033
+#: AKModel/models.py:493 AKModel/models.py:1119
 msgid "AK Owner"
 msgstr "AK-Leitung"
 
-#: AKModel/models.py:417
+#: AKModel/models.py:494
 msgid "AK Owners"
 msgstr "AK-Leitungen"
 
-#: AKModel/models.py:481
+#: AKModel/models.py:558
 msgid "Name of the AK Category"
 msgstr "Name der AK-Kategorie"
 
-#: AKModel/models.py:482 AKModel/models.py:520
+#: AKModel/models.py:559 AKModel/models.py:597
 msgid "Color"
 msgstr "Farbe"
 
-#: AKModel/models.py:482 AKModel/models.py:520
+#: AKModel/models.py:559 AKModel/models.py:597
 msgid "Color for displaying"
 msgstr "Farbe für die Anzeige"
 
-#: AKModel/models.py:483 AKModel/models.py:567
+#: AKModel/models.py:560 AKModel/models.py:644
 msgid "Description"
 msgstr "Beschreibung"
 
-#: AKModel/models.py:484
+#: AKModel/models.py:561
 msgid "Short description of this AK Category"
 msgstr "Beschreibung der AK-Kategorie"
 
-#: AKModel/models.py:485
+#: AKModel/models.py:562
 msgid "Present by default"
 msgstr "Defaultmäßig präsentieren"
 
-#: AKModel/models.py:486
+#: AKModel/models.py:563
 msgid ""
 "Present AKs of this category by default if AK owner did not specify whether "
 "this AK should be presented?"
@@ -506,132 +540,132 @@ msgstr ""
 "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für "
 "ihren AK nicht explizit spezifiziert haben?"
 
-#: AKModel/models.py:494
+#: AKModel/models.py:571
 msgid "AK Categories"
 msgstr "AK-Kategorien"
 
-#: AKModel/models.py:519
+#: AKModel/models.py:596
 msgid "Name of the AK Track"
 msgstr "Name des AK-Tracks"
 
-#: AKModel/models.py:526
+#: AKModel/models.py:603
 msgid "AK Track"
 msgstr "AK-Track"
 
-#: AKModel/models.py:527
+#: AKModel/models.py:604
 msgid "AK Tracks"
 msgstr "AK-Tracks"
 
-#: AKModel/models.py:546
+#: AKModel/models.py:623
 msgid "Name of the Requirement"
 msgstr "Name der Anforderung"
 
-#: AKModel/models.py:552 AKModel/models.py:1037
+#: AKModel/models.py:629 AKModel/models.py:1123
 msgid "AK Requirement"
 msgstr "AK-Anforderung"
 
-#: AKModel/models.py:553
+#: AKModel/models.py:630
 msgid "AK Requirements"
 msgstr "AK-Anforderungen"
 
-#: AKModel/models.py:564
+#: AKModel/models.py:641
 msgid "Name of the AK"
 msgstr "Name des AKs"
 
-#: AKModel/models.py:565
+#: AKModel/models.py:642
 msgid "Short Name"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:566
+#: AKModel/models.py:643
 msgid "Name displayed in the schedule"
 msgstr "Name zur Anzeige im AK-Plan"
 
-#: AKModel/models.py:567
+#: AKModel/models.py:644
 msgid "Description of the AK"
 msgstr "Beschreibung des AKs"
 
-#: AKModel/models.py:569
+#: AKModel/models.py:646
 msgid "Owners"
 msgstr "Leitungen"
 
-#: AKModel/models.py:570
+#: AKModel/models.py:647
 msgid "Those organizing the AK"
 msgstr "Menschen, die den AK organisieren und halten"
 
-#: AKModel/models.py:573
+#: AKModel/models.py:650
 msgid "Link to wiki page"
 msgstr "Link zur Wiki Seite"
 
-#: AKModel/models.py:574
+#: AKModel/models.py:651
 msgid "Protocol Link"
 msgstr "Protokolllink"
 
-#: AKModel/models.py:574
+#: AKModel/models.py:651
 msgid "Link to protocol"
 msgstr "Link zum Protokoll"
 
-#: AKModel/models.py:576
+#: AKModel/models.py:653
 msgid "Category"
 msgstr "Kategorie"
 
-#: AKModel/models.py:577
+#: AKModel/models.py:654
 msgid "Category of the AK"
 msgstr "Kategorie des AKs"
 
-#: AKModel/models.py:578
+#: AKModel/models.py:655
 msgid "Track"
 msgstr "Track"
 
-#: AKModel/models.py:579
+#: AKModel/models.py:656
 msgid "Track the AK belongs to"
 msgstr "Track zu dem der AK gehört"
 
-#: AKModel/models.py:581
+#: AKModel/models.py:658
 msgid "Resolution Intention"
 msgstr "Resolutionsabsicht"
 
-#: AKModel/models.py:582
+#: AKModel/models.py:659
 msgid "Intends to submit a resolution"
 msgstr "Beabsichtigt eine Resolution einzureichen"
 
-#: AKModel/models.py:583
+#: AKModel/models.py:660
 msgid "Present this AK"
 msgstr "AK präsentieren"
 
-#: AKModel/models.py:584
+#: AKModel/models.py:661
 msgid "Present results of this AK"
 msgstr "Die Ergebnisse dieses AKs vorstellen"
 
-#: AKModel/models.py:586 AKModel/views/status.py:170
+#: AKModel/models.py:663 AKModel/models.py:716 AKModel/views/status.py:170
 msgid "Requirements"
 msgstr "Anforderungen"
 
-#: AKModel/models.py:587
+#: AKModel/models.py:664
 msgid "AK's Requirements"
 msgstr "Anforderungen des AKs"
 
-#: AKModel/models.py:589
+#: AKModel/models.py:666
 msgid "Conflicting AKs"
 msgstr "AK-Konflikte"
 
-#: AKModel/models.py:590
+#: AKModel/models.py:667
 msgid "AKs that conflict and thus must not take place at the same time"
 msgstr ""
 "AKs, die Konflikte haben und deshalb nicht gleichzeitig stattfinden dürfen"
 
-#: AKModel/models.py:591
+#: AKModel/models.py:668
 msgid "Prerequisite AKs"
 msgstr "Vorausgesetzte AKs"
 
-#: AKModel/models.py:592
+#: AKModel/models.py:669
 msgid "AKs that should precede this AK in the schedule"
 msgstr "AKs die im AK-Plan vor diesem AK stattfinden müssen"
 
-#: AKModel/models.py:594
+#: AKModel/models.py:671
 msgid "Organizational Notes"
 msgstr "Notizen zur Organisation"
 
-#: AKModel/models.py:595
+#: AKModel/models.py:672
 msgid ""
 "Notes to organizers. These are public. For private notes, please use the "
 "button for private messages on the detail page of this AK (after creation/"
@@ -641,295 +675,303 @@ msgstr ""
 "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem "
 "Anlegen/Bearbeiten)."
 
-#: AKModel/models.py:598
+#: AKModel/models.py:675 AKModel/models.py:714
 msgid "Interest"
 msgstr "Interesse"
 
-#: AKModel/models.py:598
+#: AKModel/models.py:675
 msgid "Expected number of people"
 msgstr "Erwartete Personenzahl"
 
-#: AKModel/models.py:599
+#: AKModel/models.py:676
 msgid "Interest Counter"
 msgstr "Interessenszähler"
 
-#: AKModel/models.py:600
+#: AKModel/models.py:677
 msgid "People who have indicated interest online"
 msgstr "Anzahl Personen, die online Interesse bekundet haben"
 
-#: AKModel/models.py:605
+#: AKModel/models.py:682
 msgid "Export?"
 msgstr "Export?"
 
-#: AKModel/models.py:606
+#: AKModel/models.py:683
 msgid "Include AK in wiki export?"
 msgstr "AK bei Wiki-Export berücksichtigen?"
 
-#: AKModel/models.py:736
+#: AKModel/models.py:718
+msgid "Conflicts"
+msgstr "Konflikte"
+
+#: AKModel/models.py:720
+msgid "Prerequisites"
+msgstr "Voraussetzungen"
+
+#: AKModel/models.py:813
 msgid "Name or number of the room"
 msgstr "Name oder Nummer des Raums"
 
-#: AKModel/models.py:737
+#: AKModel/models.py:814
 msgid "Location"
 msgstr "Ort"
 
-#: AKModel/models.py:738
+#: AKModel/models.py:815
 msgid "Name or number of the location"
 msgstr "Name oder Nummer des Ortes"
 
-#: AKModel/models.py:739
+#: AKModel/models.py:816
 msgid "Capacity"
 msgstr "Kapazität"
 
-#: AKModel/models.py:740
+#: AKModel/models.py:817
 msgid "Maximum number of people (-1 for unlimited)."
 msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)."
 
-#: AKModel/models.py:741
+#: AKModel/models.py:818
 msgid "Properties"
 msgstr "Eigenschaften"
 
-#: AKModel/models.py:742
+#: AKModel/models.py:819
 msgid "AK requirements fulfilled by the room"
 msgstr "AK-Anforderungen, die dieser Raum erfüllt"
 
-#: AKModel/models.py:749 AKModel/views/status.py:59
+#: AKModel/models.py:826 AKModel/views/status.py:59
 msgid "Rooms"
 msgstr "Räume"
 
-#: AKModel/models.py:809
+#: AKModel/models.py:886
 msgid "AK being mapped"
 msgstr "AK, der zugeordnet wird"
 
-#: AKModel/models.py:811
+#: AKModel/models.py:888
 msgid "Room the AK will take place in"
 msgstr "Raum in dem der AK stattfindet"
 
-#: AKModel/models.py:812 AKModel/models.py:1212
+#: AKModel/models.py:889 AKModel/models.py:1298
 msgid "Slot Begin"
 msgstr "Beginn des Slots"
 
-#: AKModel/models.py:812 AKModel/models.py:1212
+#: AKModel/models.py:889 AKModel/models.py:1298
 msgid "Time and date the slot begins"
 msgstr "Zeit und Datum zu der der AK beginnt"
 
-#: AKModel/models.py:814
+#: AKModel/models.py:891
 msgid "Duration"
 msgstr "Dauer"
 
-#: AKModel/models.py:815
+#: AKModel/models.py:892
 msgid "Length in hours"
 msgstr "Länge in Stunden"
 
-#: AKModel/models.py:817
+#: AKModel/models.py:894
 msgid "Scheduling fixed"
 msgstr "Planung fix"
 
-#: AKModel/models.py:818
+#: AKModel/models.py:895
 msgid "Length and time of this AK should not be changed"
 msgstr "Dauer und Zeit dieses AKs sollten nicht verändert werden"
 
-#: AKModel/models.py:823
+#: AKModel/models.py:900
 msgid "Last update"
 msgstr "Letzte Aktualisierung"
 
-#: AKModel/models.py:826
+#: AKModel/models.py:903
 msgid "AK Slot"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:827 AKModel/models.py:1030
+#: AKModel/models.py:904 AKModel/models.py:1116 AKModel/models.py:1151
 msgid "AK Slots"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:849 AKModel/models.py:858
+#: AKModel/models.py:926 AKModel/models.py:935
 msgid "Not scheduled yet"
 msgstr "Noch nicht geplant"
 
-#: AKModel/models.py:963
+#: AKModel/models.py:1049
 msgid "AK this message belongs to"
 msgstr "AK zu dem die Nachricht gehört"
 
-#: AKModel/models.py:964
+#: AKModel/models.py:1050
 msgid "Message text"
 msgstr "Nachrichtentext"
 
-#: AKModel/models.py:965
+#: AKModel/models.py:1051
 msgid "Message to the organizers. This is not publicly visible."
 msgstr ""
 "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar."
 
-#: AKModel/models.py:969
+#: AKModel/models.py:1055
 msgid "Resolved"
 msgstr "Erledigt"
 
-#: AKModel/models.py:970
+#: AKModel/models.py:1056
 msgid "This message has been resolved (no further action needed)"
 msgstr ""
 "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen "
 "notwendig)"
 
-#: AKModel/models.py:973
+#: AKModel/models.py:1059
 msgid "AK Orga Message"
 msgstr "AK-Organachricht"
 
-#: AKModel/models.py:974
+#: AKModel/models.py:1060
 msgid "AK Orga Messages"
 msgstr "AK-Organachrichten"
 
-#: AKModel/models.py:991
+#: AKModel/models.py:1077
 msgid "Constraint Violation"
 msgstr "Constraintverletzung"
 
-#: AKModel/models.py:992
+#: AKModel/models.py:1078
 msgid "Constraint Violations"
 msgstr "Constraintverletzungen"
 
-#: AKModel/models.py:999
+#: AKModel/models.py:1085
 msgid "Owner has two parallel slots"
 msgstr "Leitung hat zwei Slots parallel"
 
-#: AKModel/models.py:1000
+#: AKModel/models.py:1086
 msgid "AK Slot was scheduled outside the AK's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des AKs platziert"
 
-#: AKModel/models.py:1001
+#: AKModel/models.py:1087
 msgid "Room has two AK slots scheduled at the same time"
 msgstr "Raum hat zwei AK Slots gleichzeitig"
 
-#: AKModel/models.py:1002
+#: AKModel/models.py:1088
 msgid "Room does not satisfy the requirement of the scheduled AK"
 msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht"
 
-#: AKModel/models.py:1003
+#: AKModel/models.py:1089
 msgid "AK Slot is scheduled at the same time as an AK listed as a conflict"
 msgstr ""
 "AK Slot wurde wurde zur gleichen Zeit wie ein Konflikt des AKs platziert"
 
-#: AKModel/models.py:1004
+#: AKModel/models.py:1090
 msgid "AK Slot is scheduled before an AK listed as a prerequisite"
 msgstr "AK Slot wurde vor einem als Voraussetzung gelisteten AK platziert"
 
-#: AKModel/models.py:1006
+#: AKModel/models.py:1092
 msgid ""
 "AK Slot for AK with intention to submit a resolution is scheduled after "
 "resolution deadline"
 msgstr ""
 "AK Slot eines AKs mit Resoabsicht wurde nach der Resodeadline platziert"
 
-#: AKModel/models.py:1007
+#: AKModel/models.py:1093
 msgid "AK Slot in a category is outside that categories availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeiten seiner Kategorie"
 
-#: AKModel/models.py:1008
+#: AKModel/models.py:1094
 msgid "Two AK Slots for the same AK scheduled at the same time"
 msgstr "Zwei AK Slots eines AKs wurden zur selben Zeit platziert"
 
-#: AKModel/models.py:1009
+#: AKModel/models.py:1095
 msgid "Room does not have enough space for interest in scheduled AK Slot"
 msgstr "Room hat nicht genug Platz für das Interesse am geplanten AK-Slot"
 
-#: AKModel/models.py:1010
+#: AKModel/models.py:1096
 msgid "AK Slot is scheduled outside the event's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des Events platziert"
 
-#: AKModel/models.py:1016
+#: AKModel/models.py:1102
 msgid "Warning"
 msgstr "Warnung"
 
-#: AKModel/models.py:1017
+#: AKModel/models.py:1103
 msgid "Violation"
 msgstr "Verletzung"
 
-#: AKModel/models.py:1019
+#: AKModel/models.py:1105
 msgid "Type"
 msgstr "Art"
 
-#: AKModel/models.py:1020
+#: AKModel/models.py:1106
 msgid "Type of violation, i.e. what kind of constraint was violated"
 msgstr "Art der Verletzung, gibt an welche Art Constraint verletzt wurde"
 
-#: AKModel/models.py:1021
+#: AKModel/models.py:1107
 msgid "Level"
 msgstr "Level"
 
-#: AKModel/models.py:1022
+#: AKModel/models.py:1108
 msgid "Severity level of the violation"
 msgstr "Schweregrad der Verletzung"
 
-#: AKModel/models.py:1029
+#: AKModel/models.py:1115
 msgid "AK(s) belonging to this constraint"
 msgstr "AK(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:1031
+#: AKModel/models.py:1117
 msgid "AK Slot(s) belonging to this constraint"
 msgstr "AK Slot(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:1033
+#: AKModel/models.py:1119
 msgid "AK Owner belonging to this constraint"
 msgstr "AK Leitung(en), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:1035
+#: AKModel/models.py:1121
 msgid "Room belonging to this constraint"
 msgstr "Raum, der zu diesem Constraint gehört"
 
-#: AKModel/models.py:1038
+#: AKModel/models.py:1124
 msgid "AK Requirement belonging to this constraint"
 msgstr "AK Anforderung, die zu diesem Constraint gehört"
 
-#: AKModel/models.py:1040
+#: AKModel/models.py:1126
 msgid "AK Category belonging to this constraint"
 msgstr "AK Kategorie, di zu diesem Constraint gehört"
 
-#: AKModel/models.py:1042
+#: AKModel/models.py:1128
 msgid "Comment"
 msgstr "Kommentar"
 
-#: AKModel/models.py:1042
+#: AKModel/models.py:1128
 msgid "Comment or further details for this violation"
 msgstr "Kommentar oder weitere Details zu dieser Vereletzung"
 
-#: AKModel/models.py:1045
+#: AKModel/models.py:1131
 msgid "Timestamp"
 msgstr "Timestamp"
 
-#: AKModel/models.py:1045
+#: AKModel/models.py:1131
 msgid "Time of creation"
 msgstr "Zeitpunkt der ERstellung"
 
-#: AKModel/models.py:1046
+#: AKModel/models.py:1132
 msgid "Manually Resolved"
 msgstr "Manuell behoben"
 
-#: AKModel/models.py:1047
+#: AKModel/models.py:1133
 msgid "Mark this violation manually as resolved"
 msgstr "Markiere diese Verletzung manuell als behoben"
 
-#: AKModel/models.py:1074
+#: AKModel/models.py:1160
 #: AKModel/templates/admin/AKModel/requirements_overview.html:27
 msgid "Details"
 msgstr "Details"
 
-#: AKModel/models.py:1208
+#: AKModel/models.py:1294
 msgid "Default Slot"
 msgstr "Standardslot"
 
-#: AKModel/models.py:1213
+#: AKModel/models.py:1299
 msgid "Slot End"
 msgstr "Ende des Slots"
 
-#: AKModel/models.py:1213
+#: AKModel/models.py:1299
 msgid "Time and date the slot ends"
 msgstr "Zeit und Datum zu der der Slot endet"
 
-#: AKModel/models.py:1218
+#: AKModel/models.py:1304
 msgid "Primary categories"
 msgstr "Primäre Kategorien"
 
-#: AKModel/models.py:1219
+#: AKModel/models.py:1305
 msgid "Categories that should be assigned to this slot primarily"
 msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen"
 
-#: AKModel/site.py:14
+#: AKModel/site.py:13 AKModel/site.py:14
 msgid "Administration"
 msgstr "Verwaltung"
 
@@ -1135,35 +1177,35 @@ msgstr "AK-CSV-Export"
 msgid "AK JSON Export"
 msgstr "AK-JSON-Export"
 
-#: AKModel/views/ak.py:175
+#: AKModel/views/ak.py:186
 msgid "AK Wiki Export"
 msgstr "AK-Wiki-Export"
 
-#: AKModel/views/ak.py:186 AKModel/views/manage.py:55
+#: AKModel/views/ak.py:197 AKModel/views/manage.py:55
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKModel/views/ak.py:198
+#: AKModel/views/ak.py:209
 msgid "Delete AK Orga Messages"
 msgstr "AK-Organachrichten löschen"
 
-#: AKModel/views/ak.py:216
+#: AKModel/views/ak.py:227
 msgid "AK Orga Messages successfully deleted"
 msgstr "AK-Organachrichten erfolgreich gelöscht"
 
-#: AKModel/views/ak.py:228
+#: AKModel/views/ak.py:239
 msgid "Interest of the following AKs will be set to not filled (-1):"
 msgstr "Interesse an den folgenden AKs wird auf nicht ausgefüllt (-1) gesetzt:"
 
-#: AKModel/views/ak.py:229
+#: AKModel/views/ak.py:240
 msgid "Reset of interest in AKs successful."
 msgstr "Interesse an AKs erfolgreich zurückgesetzt."
 
-#: AKModel/views/ak.py:243
+#: AKModel/views/ak.py:254
 msgid "Interest counter of the following AKs will be set to 0:"
 msgstr "Interessensbekundungszähler der folgenden AKs wird auf 0 gesetzt:"
 
-#: AKModel/views/ak.py:244
+#: AKModel/views/ak.py:255
 msgid "AKs' interest counters set back to 0."
 msgstr "Interessenszähler der AKs zurückgesetzt"
 
@@ -1265,6 +1307,15 @@ msgstr ""
 msgid "AK Schedule JSON Import"
 msgstr "AK-Plan JSON-Import"
 
+#: AKModel/views/manage.py:265
+#, python-brace-format
+msgid "Successfully imported {n} slot(s)"
+msgstr "Erfolgreich {n} Slot(s) importiert"
+
+#: AKModel/views/manage.py:271
+msgid "Importing an AK schedule failed! Reason: "
+msgstr "AK-Plan importieren fehlgeschlagen! Grund: "
+
 #: AKModel/views/room.py:37
 #, python-format
 msgid "Created Room '%(room)s'"
diff --git a/AKModel/models.py b/AKModel/models.py
index c51b7556..1cd3f1ba 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -6,7 +6,7 @@ from dataclasses import dataclass
 from datetime import datetime, timedelta
 from typing import Iterable, Generator
 
-from django.db import models
+from django.db import models, transaction
 from django.apps import apps
 from django.db.models import Count
 from django.urls import reverse_lazy
@@ -401,7 +401,8 @@ class Event(models.Model):
         else:
             yield from self.uniform_time_slots(slots_in_an_hour=slots_in_an_hour)
 
-    def schedule_from_json(self, schedule: str) -> None:
+    @transaction.atomic
+    def schedule_from_json(self, schedule: str) -> int:
         """Load AK schedule from a json string.
 
         :param schedule: A string that can be decoded to json, describing
@@ -419,18 +420,63 @@ class Event(models.Model):
             for timeslot in block
         }
 
+        slots_updated = 0
         for scheduled_slot in schedule["scheduled_aks"]:
+            scheduled_slot["timeslot_ids"] = list(map(int, scheduled_slot["timeslot_ids"]))
             slot = AKSlot.objects.get(id=int(scheduled_slot["ak_id"]))
-            slot.room = Room.objects.get(id=int(scheduled_slot["room_id"]))
 
-            scheduled_slot["timeslot_ids"] = list(map(int, scheduled_slot["timeslot_ids"]))
+            if not scheduled_slot["timeslot_ids"]:
+                raise ValueError(
+                    _("AK {ak_name} is not assigned any timeslot by the solver").format(ak_name=slot.ak.name)
+                )
 
             start_timeslot = timeslot_dict[min(scheduled_slot["timeslot_ids"])].avail
             end_timeslot = timeslot_dict[max(scheduled_slot["timeslot_ids"])].avail
+            solver_duration = (end_timeslot.end - start_timeslot.start).total_seconds() / 3600.0
+
+            if solver_duration + 2e-4 < slot.duration:
+                raise ValueError(
+                    _(
+                        "Duration of AK {ak_name} assigned by solver ({solver_duration} hours) "
+                        "is less than the duration required by the slot ({slot_duration} hours)"
+                    ).format(
+                        ak_name=slot.ak.name,
+                        solver_duration=solver_duration,
+                        slot_duration=slot.duration,
+                    )
+                )
+
+            if slot.fixed:
+                solver_room = Room.objects.get(id=int(scheduled_slot["room_id"]))
+                if slot.room != solver_room:
+                    raise ValueError(
+                        _(
+                            "Fixed AK {ak_name} assigned by solver to room {solver_room} "
+                            "is fixed to room {slot_room}"
+                        ).format(
+                            ak_name=slot.ak.name,
+                            solver_room=solver_room.name,
+                            slot_room=slot.room.name,
+                        )
+                    )
+                if slot.start != start_timeslot.start:
+                    raise ValueError(
+                        _(
+                            "Fixed AK {ak_name} assigned by solver to start at {solver_start} "
+                            "is fixed to start at {slot_start}"
+                        ).format(
+                            ak_name=slot.ak.name,
+                            solver_start=start_timeslot.start,
+                            slot_start=slot.start,
+                        )
+                    )
+            else:
+                slot.room = Room.objects.get(id=int(scheduled_slot["room_id"]))
+                slot.start = start_timeslot.start
+                slot.save()
+                slots_updated += 1
 
-            slot.start = start_timeslot.start
-            slot.duration = (end_timeslot.end - start_timeslot.start).total_seconds() / 3600.0
-            slot.save()
+        return slots_updated
 
 class AKOwner(models.Model):
     """ An AKOwner describes the person organizing/holding an AK.
diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py
index 1bad9534..3acb05fd 100644
--- a/AKModel/views/manage.py
+++ b/AKModel/views/manage.py
@@ -257,6 +257,18 @@ class AKScheduleJSONImportView(EventSlugMixin, IntermediateAdminView):
     title = _("AK Schedule JSON Import")
 
     def form_valid(self, form):
-        self.event.schedule_from_json(form.data["json_data"])
+        try:
+            number_of_slots_changed = self.event.schedule_from_json(form.data["json_data"])
+            messages.add_message(
+                self.request,
+                messages.SUCCESS,
+                _("Successfully imported {n} slot(s)").format(n=number_of_slots_changed)
+            )
+        except ValueError as ex:
+            messages.add_message(
+                self.request,
+                messages.ERROR,
+                _("Importing an AK schedule failed! Reason: ") + str(ex),
+            )
 
         return redirect("admin:event_status", self.event.slug)
diff --git a/AKScheduling/locale/de_DE/LC_MESSAGES/django.po b/AKScheduling/locale/de_DE/LC_MESSAGES/django.po
index e66d8d55..1e391c1f 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: 2024-04-25 00:24+0200\n"
+"POT-Creation-Date: 2025-01-22 19:00+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"
@@ -27,7 +27,7 @@ msgstr "Ende"
 
 #: AKScheduling/forms.py:26
 msgid "Duration"
-msgstr ""
+msgstr "Dauer"
 
 #: AKScheduling/forms.py:27
 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:171
@@ -107,6 +107,7 @@ msgid "Event Status"
 msgstr "Event-Status"
 
 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:113
+#: AKScheduling/views.py:48
 msgid "Scheduling"
 msgstr "Scheduling"
 
@@ -239,6 +240,7 @@ msgstr[1] ""
 "        "
 
 #: AKScheduling/templates/admin/AKScheduling/unscheduled.html:7
+#: AKScheduling/views.py:25
 msgid "Unscheduled AK Slots"
 msgstr "Noch nicht geschedulte AK-Slots"
 
@@ -246,10 +248,22 @@ msgstr "Noch nicht geschedulte AK-Slots"
 msgid "Count"
 msgstr "Anzahl"
 
+#: AKScheduling/views.py:89
+msgid "Constraint violations for"
+msgstr "Constraintverletzungen für"
+
+#: AKScheduling/views.py:104
+msgid "AKs requiring special attention for"
+msgstr "AKs die besondere Aufmerksamkeit erfordern für"
+
 #: AKScheduling/views.py:150
 msgid "Interest updated"
 msgstr "Interesse aktualisiert"
 
+#: AKScheduling/views.py:157
+msgid "Enter interest"
+msgstr "Interesse eingeben"
+
 #: AKScheduling/views.py:201
 msgid "Wishes"
 msgstr "Wünsche"
diff --git a/AKSubmission/locale/de_DE/LC_MESSAGES/django.po b/AKSubmission/locale/de_DE/LC_MESSAGES/django.po
index e8add44b..e524139c 100644
--- a/AKSubmission/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKSubmission/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: 2024-05-27 01:57+0000\n"
+"POT-Creation-Date: 2025-01-22 19:00+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"
@@ -418,6 +418,10 @@ msgstr ""
 msgid "Currently planned AKs"
 msgstr "Aktuell geplante AKs"
 
+#: AKSubmission/views.py:231
+msgid "AKs with Track"
+msgstr "AK mit Track"
+
 #: AKSubmission/views.py:300
 msgid "Event inactive. Cannot create or update."
 msgstr "Event inaktiv. Hinzufügen/Bearbeiten nicht möglich."
-- 
GitLab


From 7140368303f5bb94bc3278f663fb7a731c4887ba Mon Sep 17 00:00:00 2001
From: Felix Blanke <s6feblan@uni-bonn.de>
Date: Thu, 6 Feb 2025 12:43:45 +0000
Subject: [PATCH 57/77] Add tests on json export

---
 .gitlab-ci.yml                            |   4 +-
 AKDashboard/tests.py                      |   2 +-
 AKModel/availability/models.py            |  13 +-
 AKModel/fixtures/model.json               | 191 +++++-
 AKModel/tests/__init__.py                 |   0
 AKModel/tests/test_json_export.py         | 777 ++++++++++++++++++++++
 AKModel/{tests.py => tests/test_views.py} | 203 ++++--
 AKModel/views/ak.py                       |  11 +-
 AKPlan/tests.py                           |   2 +-
 AKScheduling/api.py                       |   4 +-
 AKScheduling/models.py                    |   4 +-
 AKScheduling/tests.py                     |   2 +-
 AKSubmission/tests.py                     |   2 +-
 13 files changed, 1136 insertions(+), 79 deletions(-)
 create mode 100644 AKModel/tests/__init__.py
 create mode 100644 AKModel/tests/test_json_export.py
 rename AKModel/{tests.py => tests/test_views.py} (60%)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 55ef5901..5855c87c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -38,7 +38,7 @@ test:
   script:
     - source venv/bin/activate
     - echo "GRANT ALL on *.* to '${MYSQL_USER}';"| mysql -u root --password="${MYSQL_ROOT_PASSWORD}" -h mysql
-    - pip install pytest-cov unittest-xml-reporting
+    - pip install pytest-cov unittest-xml-reporting beautifulsoup4
     - coverage run --source='.' manage.py test --settings AKPlanning.settings_ci
   after_script:
     - source venv/bin/activate
@@ -56,6 +56,8 @@ lint:
   extends: .before_script_template
   stage: test
   script:
+    - source venv/bin/activate
+    - pip install beautifulsoup4
     - pylint --load-plugins pylint_django --django-settings-module=AKPlanning.settings_ci --rcfile pylintrc --exit-zero --output-format=text AK* | tee /tmp/pylint.txt
     - sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' /tmp/pylint.txt > public/badges/$CI_JOB_NAME.score
     - pylint --load-plugins pylint_django --django-settings-module=AKPlanning.settings_ci --rcfile pylintrc --exit-zero --output-format=pylint_gitlab.GitlabCodeClimateReporter AK* > codeclimate.json
diff --git a/AKDashboard/tests.py b/AKDashboard/tests.py
index f96af9a1..de25ffbc 100644
--- a/AKDashboard/tests.py
+++ b/AKDashboard/tests.py
@@ -6,7 +6,7 @@ from django.utils.timezone import now
 
 from AKDashboard.models import DashboardButton
 from AKModel.models import Event, AK, AKCategory
-from AKModel.tests import BasicViewTests
+from AKModel.tests.test_views import BasicViewTests
 
 
 class DashboardTests(TestCase):
diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py
index 27a6c228..e2a64b22 100644
--- a/AKModel/availability/models.py
+++ b/AKModel/availability/models.py
@@ -280,6 +280,16 @@ class Availability(models.Model):
         return Availability(start=timeframe_start, end=timeframe_end, event=event, person=person,
                                     room=room, ak=ak, ak_category=ak_category)
 
+    def is_covered(self, availabilities: List['Availability']):
+        """Check if list of availibilities cover this object.
+
+        :param availabilities: availabilities to check.
+        :return: whether the availabilities cover full event.
+        :rtype: bool
+        """
+        avail_union = Availability.union(availabilities)
+        return any(avail.contains(self) for avail in avail_union)
+
     @classmethod
     def is_event_covered(cls, event: Event, availabilities: List['Availability']) -> bool:
         """Check if list of availibilities cover whole event.
@@ -292,8 +302,7 @@ class Availability(models.Model):
         # NOTE: Cannot use `Availability.with_event_length` as its end is the
         #       event end + 1 day
         full_event = Availability(event=event, start=event.start, end=event.end)
-        avail_union = Availability.union(availabilities)
-        return any(avail.contains(full_event) for avail in avail_union)
+        return full_event.is_covered(availabilities)
 
     class Meta:
         verbose_name = _('Availability')
diff --git a/AKModel/fixtures/model.json b/AKModel/fixtures/model.json
index d848041d..42cf6c81 100644
--- a/AKModel/fixtures/model.json
+++ b/AKModel/fixtures/model.json
@@ -93,7 +93,7 @@
     "model": "AKModel.akcategory",
     "pk": 1,
     "fields": {
-        "name": "Spa▀",
+        "name": "Spaß",
         "color": "275246",
         "description": "",
         "present_by_default": true,
@@ -115,7 +115,7 @@
     "model": "AKModel.akcategory",
     "pk": 3,
     "fields": {
-        "name": "Spa▀/Kultur",
+        "name": "Spaß/Kultur",
         "color": "333333",
         "description": "",
         "present_by_default": true,
@@ -436,6 +436,62 @@
         ]
     }
 },
+{
+    "model": "AKModel.ak",
+    "pk": 4,
+    "fields": {
+        "name": "Test AK fixed slots",
+        "short_name": "testfixed",
+        "description": "",
+        "link": "",
+        "protocol_link": "",
+        "category": 4,
+        "track": null,
+        "reso": false,
+        "present": true,
+        "notes": "",
+        "interest": -1,
+        "interest_counter": 0,
+        "include_in_export": false,
+        "event": 2,
+        "owners": [
+            1
+        ],
+        "requirements": [
+            3
+        ],
+        "conflicts": [],
+        "prerequisites": []
+    }
+},
+{
+    "model": "AKModel.ak",
+    "pk": 5,
+    "fields": {
+        "name": "Test AK Ernst",
+        "short_name": "testernst",
+        "description": "",
+        "link": "",
+        "protocol_link": "",
+        "category": 2,
+        "track": null,
+        "reso": false,
+        "present": true,
+        "notes": "",
+        "interest": -1,
+        "interest_counter": 0,
+        "include_in_export": false,
+        "event": 1,
+        "owners": [
+            3
+        ],
+        "requirements": [
+            2
+        ],
+        "conflicts": [],
+        "prerequisites": []
+    }
+},
 {
     "model": "AKModel.room",
     "pk": 1,
@@ -460,6 +516,19 @@
         "properties": []
     }
 },
+{
+    "model": "AKModel.room",
+    "pk": 3,
+    "fields": {
+        "name": "BBB Session 1",
+        "location": "",
+        "capacity": -1,
+        "event": 1,
+        "properties": [
+            2
+        ]
+    }
+},
 {
     "model": "AKModel.akslot",
     "pk": 1,
@@ -525,6 +594,58 @@
         "updated": "2022-12-02T12:23:11.856Z"
     }
 },
+{
+    "model": "AKModel.akslot",
+    "pk": 6,
+    "fields": {
+        "ak": 4,
+        "room": null,
+        "start": "2020-11-08T18:30:00Z",
+        "duration": "2.00",
+        "fixed": true,
+        "event": 2,
+        "updated": "2022-12-02T12:23:11.856Z"
+    }
+},
+{
+    "model": "AKModel.akslot",
+    "pk": 7,
+    "fields": {
+        "ak": 4,
+        "room": 2,
+        "start": null,
+        "duration": "2.00",
+        "fixed": true,
+        "event": 2,
+        "updated": "2022-12-02T12:23:11.856Z"
+    }
+},
+{
+    "model": "AKModel.akslot",
+    "pk": 8,
+    "fields": {
+        "ak": 4,
+        "room": 2,
+        "start": "2020-11-07T16:00:00Z",
+        "duration": "2.00",
+        "fixed": true,
+        "event": 2,
+        "updated": "2022-12-02T12:23:11.856Z"
+    }
+},
+{
+    "model": "AKModel.akslot",
+    "pk": 9,
+    "fields": {
+        "ak": 5,
+        "room": null,
+        "start": null,
+        "duration": "2.00",
+        "fixed": false,
+        "event": 1,
+        "updated": "2022-12-02T12:23:11.856Z"
+    }
+},
 {
     "model": "AKModel.constraintviolation",
     "pk": 1,
@@ -668,5 +789,71 @@
         "start": "2020-11-07T18:30:00Z",
         "end": "2020-11-07T21:30:00Z"
     }
+},
+{
+    "model": "AKModel.availability",
+    "pk": 7,
+    "fields": {
+        "event": 1,
+        "person": null,
+        "room": null,
+        "ak": 5,
+        "ak_category": null,
+        "start": "2020-10-01T17:41:22Z",
+        "end": "2020-10-04T17:41:30Z"
+    }
+},
+{
+    "model": "AKModel.availability",
+    "pk": 8,
+    "fields": {
+        "event": 1,
+        "person": null,
+        "room": 3,
+        "ak": null,
+        "ak_category": null,
+        "start": "2020-10-01T17:41:22Z",
+        "end": "2020-10-04T17:41:30Z"
+    }
+},
+{
+    "model": "AKModel.defaultslot",
+    "pk": 1,
+    "fields": {
+        "event": 2,
+        "start": "2020-11-07T08:00:00Z",
+        "end": "2020-11-07T12:00:00Z",
+        "primary_categories": [5]
+    }
+},
+{
+    "model": "AKModel.defaultslot",
+    "pk": 2,
+    "fields": {
+        "event": 2,
+        "start": "2020-11-07T14:00:00Z",
+        "end": "2020-11-07T17:00:00Z",
+        "primary_categories": [4]
+    }
+},
+{
+    "model": "AKModel.defaultslot",
+    "pk": 3,
+    "fields": {
+        "event": 2,
+        "start": "2020-11-08T08:00:00Z",
+        "end": "2020-11-08T19:00:00Z",
+        "primary_categories": [4, 5]
+    }
+},
+{
+    "model": "AKModel.defaultslot",
+    "pk": 4,
+    "fields": {
+        "event": 2,
+        "start": "2020-11-09T17:00:00Z",
+        "end": "2020-11-10T01:00:00Z",
+        "primary_categories": [4, 5, 3]
+    }
 }
 ]
diff --git a/AKModel/tests/__init__.py b/AKModel/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py
new file mode 100644
index 00000000..d53cd4c5
--- /dev/null
+++ b/AKModel/tests/test_json_export.py
@@ -0,0 +1,777 @@
+import json
+import math
+
+from collections import defaultdict
+from collections.abc import Iterable
+from datetime import datetime, timedelta
+from itertools import chain
+
+from bs4 import BeautifulSoup
+from django.contrib.auth import get_user_model
+from django.test import TestCase
+from django.urls import reverse
+
+from AKModel.availability.models import Availability
+from AKModel.models import (
+    Event,
+    AKOwner,
+    AKCategory,
+    AK,
+    Room,
+    AKSlot,
+    DefaultSlot,
+)
+
+
+class JSONExportTest(TestCase):
+    """Test if JSON export is correct.
+
+    It tests if the output conforms to the KoMa specification:
+    https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
+    """
+
+    fixtures = ["model.json"]
+
+    @classmethod
+    def setUpTestData(cls):
+        """Shared set up by initializing admin user."""
+        cls.admin_user = get_user_model().objects.create(
+            username="Test Admin User",
+            email="testadmin@example.com",
+            password="adminpw",
+            is_staff=True,
+            is_superuser=True,
+            is_active=True,
+        )
+
+    def setUp(self):
+        self.client.force_login(self.admin_user)
+        self.export_dict = {}
+        self.export_objects = {
+            "aks": {},
+            "rooms": {},
+            "participants": {},
+        }
+
+        self.ak_slots: Iterable[AKSlot] = []
+        self.rooms: Iterable[Room] = []
+        self.slots_in_an_hour: float = 1.0
+        self.event: Event | None = None
+
+    def set_up_event(self, event: Event) -> None:
+        """Set up by retrieving json export and initializing data."""
+
+        export_url = reverse("admin:ak_json_export", kwargs={"event_slug": event.slug})
+        response = self.client.get(export_url)
+
+        self.assertEqual(response.status_code, 200, "Export not working at all")
+
+        soup = BeautifulSoup(response.content, features="lxml")
+        self.export_dict = json.loads(soup.find("pre").string)
+
+        self.export_objects["aks"] = {ak["id"]: ak for ak in self.export_dict["aks"]}
+        self.export_objects["rooms"] = {
+            room["id"]: room for room in self.export_dict["rooms"]
+        }
+        self.export_objects["participants"] = {
+            participant["id"]: participant
+            for participant in self.export_dict["participants"]
+        }
+
+        self.ak_slots = AKSlot.objects.filter(event__slug=event.slug).all()
+        self.rooms = Room.objects.filter(event__slug=event.slug).all()
+        self.slots_in_an_hour = 1 / self.export_dict["timeslots"]["info"]["duration"]
+        self.event = event
+
+    def test_all_aks_exported(self):
+        """Test if exported AKs match AKSlots of Event."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+                self.assertEqual(
+                    {str(slot.pk) for slot in self.ak_slots},
+                    self.export_objects["aks"].keys(),
+                    "Exported AKs does not match the AKSlots of the event",
+                )
+
+    def _check_uniqueness(self, lst, name: str, key: str | None = "id"):
+        if key is not None:
+            lst = [entry[key] for entry in lst]
+        self.assertEqual(len(lst), len(set(lst)), f"{name} IDs not unique!")
+
+    def _check_type(self, attr, cls, name: str, item: str) -> None:
+        self.assertTrue(isinstance(attr, cls), f"{item} {name} not a {cls}")
+
+    def _check_lst(
+        self, lst: list[str], name: str, item: str, contained_type=str
+    ) -> None:
+        self.assertTrue(isinstance(lst, list), f"{item} {name} not a list")
+        self.assertTrue(
+            all(isinstance(c, contained_type) for c in lst),
+            f"{item} has non-{contained_type} {name}",
+        )
+        if contained_type in {str, int}:
+            self._check_uniqueness(lst, name, key=None)
+
+    def test_ak_conformity_to_spec(self):
+        """Test if AK JSON structure and types conform to standard."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                self._check_uniqueness(self.export_dict["aks"], "AK")
+                for ak in self.export_dict["aks"]:
+                    item = f"AK {ak['id']}"
+                    self.assertEqual(
+                        ak.keys(),
+                        {
+                            "id",
+                            "duration",
+                            "properties",
+                            "room_constraints",
+                            "time_constraints",
+                            "info",
+                        },
+                        f"{item} keys not as expected",
+                    )
+                    self.assertEqual(
+                        ak["info"].keys(),
+                        {
+                            "name",
+                            "head",
+                            "description",
+                            "reso",
+                            "duration_in_hours",
+                            "django_ak_id",
+                        },
+                        f"{item} info keys not as expected",
+                    )
+                    self.assertEqual(
+                        ak["properties"].keys(),
+                        {"conflicts", "dependencies"},
+                        f"{item} properties keys not as expected",
+                    )
+
+                    self._check_type(ak["id"], str, "id", item=item)
+                    self._check_type(ak["duration"], int, "duration", item=item)
+                    self._check_type(ak["info"]["name"], str, "info/name", item=item)
+                    self._check_type(ak["info"]["head"], str, "info/head", item=item)
+                    self._check_type(
+                        ak["info"]["description"], str, "info/description", item=item
+                    )
+                    self._check_type(ak["info"]["reso"], bool, "info/reso", item=item)
+                    self._check_type(
+                        ak["info"]["duration_in_hours"],
+                        float,
+                        "info/duration_in_hours",
+                        item=item,
+                    )
+                    self._check_type(
+                        ak["info"]["django_ak_id"],
+                        str,
+                        "info/django_ak_id",
+                        item=item,
+                    )
+
+                    self._check_lst(
+                        ak["properties"]["conflicts"], "conflicts", item=item
+                    )
+                    self._check_lst(
+                        ak["properties"]["dependencies"], "dependencies", item=item
+                    )
+                    self._check_lst(
+                        ak["time_constraints"], "time_constraints", item=item
+                    )
+                    self._check_lst(
+                        ak["room_constraints"], "room_constraints", item=item
+                    )
+
+    def test_room_conformity_to_spec(self):
+        """Test if Room JSON structure and types conform to standard."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                self._check_uniqueness(self.export_dict["rooms"], "Room")
+                for room in self.export_dict["rooms"]:
+                    item = f"Room {room['id']}"
+                    self.assertEqual(
+                        room.keys(),
+                        {
+                            "id",
+                            "info",
+                            "capacity",
+                            "fulfilled_room_constraints",
+                            "time_constraints",
+                        },
+                        f"{item} keys not as expected",
+                    )
+                    self.assertEqual(
+                        room["info"].keys(),
+                        {"name"},
+                        f"{item} info keys not as expected",
+                    )
+
+                    self._check_type(room["id"], str, "id", item=item)
+                    self._check_type(room["capacity"], int, "capacity", item=item)
+                    self._check_type(room["info"]["name"], str, "info/name", item=item)
+
+                    self.assertTrue(
+                        room["capacity"] > 0 or room["capacity"] == -1,
+                        "invalid room capacity",
+                    )
+
+                    self._check_lst(
+                        room["time_constraints"], "time_constraints", item=item
+                    )
+                    self._check_lst(
+                        room["fulfilled_room_constraints"],
+                        "fulfilled_room_constraints",
+                        item=item,
+                    )
+
+    def test_timeslots_conformity_to_spec(self):
+        """Test if Timeslots JSON structure and types conform to standard."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                self._check_uniqueness(
+                    chain.from_iterable(self.export_dict["timeslots"]["blocks"]),
+                    "Timeslots",
+                )
+                item = "timeslots"
+                self.assertEqual(
+                    self.export_dict["timeslots"].keys(),
+                    {"info", "blocks"},
+                    "timeslot keys not as expected",
+                )
+                self.assertEqual(
+                    self.export_dict["timeslots"]["info"].keys(),
+                    {"duration"},
+                    "timeslot info keys not as expected",
+                )
+                self._check_type(
+                    self.export_dict["timeslots"]["info"]["duration"],
+                    float,
+                    "info/duration",
+                    item=item,
+                )
+                self._check_lst(
+                    self.export_dict["timeslots"]["blocks"],
+                    "blocks",
+                    item=item,
+                    contained_type=list,
+                )
+
+                prev_id = None
+                for timeslot in chain.from_iterable(
+                    self.export_dict["timeslots"]["blocks"]
+                ):
+                    item = f"timeslot {timeslot['id']}"
+                    self.assertEqual(
+                        timeslot.keys(),
+                        {"id", "info", "fulfilled_time_constraints"},
+                        f"{item} keys not as expected",
+                    )
+                    self.assertEqual(
+                        timeslot["info"].keys(),
+                        {"start", "end"},
+                        f"{item} info keys not as expected",
+                    )
+                    self._check_type(timeslot["id"], str, "id", item=item)
+                    self._check_type(
+                        timeslot["info"]["start"], str, "info/start", item=item
+                    )
+                    self._check_lst(
+                        timeslot["fulfilled_time_constraints"],
+                        "fulfilled_time_constraints",
+                        item=item,
+                    )
+
+                    if prev_id is not None:
+                        self.assertLess(
+                            prev_id,
+                            int(timeslot["id"]),
+                            "timeslot ids must be increasing",
+                        )
+                    prev_id = int(timeslot["id"])
+
+    def test_general_conformity_to_spec(self):
+        """Test if rest of JSON structure and types conform to standard."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                self.assertEqual(
+                    self.export_dict["participants"],
+                    [],
+                    "Empty participant list expected",
+                )
+
+                info_keys = {"title": "name", "slug": "slug"}
+                for attr in ["contact_email", "place"]:
+                    if hasattr(self.event, attr) and getattr(self.event, attr):
+                        info_keys[attr] = attr
+                self.assertEqual(
+                    self.export_dict["info"].keys(),
+                    info_keys.keys(),
+                    "info keys not as expected",
+                )
+                for attr, attr_field in info_keys.items():
+                    self.assertEqual(
+                        getattr(self.event, attr_field), self.export_dict["info"][attr]
+                    )
+
+                self._check_uniqueness(self.export_dict["participants"], "Participants")
+
+    def test_ak_durations(self):
+        """Test if all AK durations are correct."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                for slot in self.ak_slots:
+                    ak = self.export_objects["aks"][str(slot.pk)]
+
+                    self.assertLessEqual(
+                        float(slot.duration) * self.slots_in_an_hour - 1e-4,
+                        ak["duration"],
+                        "Slot duration is too short",
+                    )
+
+                    self.assertEqual(
+                        math.ceil(float(slot.duration) * self.slots_in_an_hour - 1e-4),
+                        ak["duration"],
+                        "Slot duration is wrong",
+                    )
+
+                    self.assertEqual(
+                        float(slot.duration),
+                        ak["info"]["duration_in_hours"],
+                        "Slot duration_in_hours is wrong",
+                    )
+
+    def test_ak_conflicts(self):
+        """Test if all AK conflicts are correct."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                for slot in self.ak_slots:
+                    ak = self.export_objects["aks"][str(slot.pk)]
+                    conflict_slots = self.ak_slots.filter(
+                        ak__in=slot.ak.conflicts.all()
+                    ).values_list("pk", flat=True)
+                    conflict_pks = {str(conflict_pk) for conflict_pk in conflict_slots}
+
+                    other_ak_slots = (
+                        self.ak_slots.filter(ak=slot.ak)
+                        .exclude(pk=slot.pk)
+                        .values_list("pk", flat=True)
+                    )
+                    conflict_pks.update(
+                        str(other_slot_pk) for other_slot_pk in other_ak_slots
+                    )
+
+                    self.assertEqual(
+                        conflict_pks,
+                        set(ak["properties"]["conflicts"]),
+                        f"Conflicts for slot {slot.pk} not as expected",
+                    )
+
+    def test_ak_depenedencies(self):
+        """Test if all AK dependencies are correct."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                for slot in self.ak_slots:
+                    ak = self.export_objects["aks"][str(slot.pk)]
+                    dependency_slots = self.ak_slots.filter(
+                        ak__in=slot.ak.prerequisites.all()
+                    ).values_list("pk", flat=True)
+
+                    self.assertEqual(
+                        {str(dep_pk) for dep_pk in dependency_slots},
+                        set(ak["properties"]["dependencies"]),
+                        f"Dependencies for slot {slot.pk} not as expected",
+                    )
+
+    def test_ak_reso(self):
+        """Test if resolution intent of AKs is correctly exported."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                for slot in self.ak_slots:
+                    ak = self.export_objects["aks"][str(slot.pk)]
+                    self.assertEqual(slot.ak.reso, ak["info"]["reso"])
+                    self.assertEqual(
+                        slot.ak.reso, "resolution" in ak["time_constraints"]
+                    )
+
+    def test_ak_info(self):
+        """Test if contents of AK info dict is correct."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                for slot in self.ak_slots:
+                    ak = self.export_objects["aks"][str(slot.pk)]
+                    self.assertEqual(ak["info"]["name"], slot.ak.name)
+                    self.assertEqual(
+                        ak["info"]["head"], ", ".join(map(str, slot.ak.owners.all()))
+                    )
+                    self.assertEqual(ak["info"]["description"], slot.ak.description)
+                    self.assertEqual(ak["info"]["django_ak_id"], str(slot.ak.pk))
+
+    def test_ak_room_constraints(self):
+        """Test if AK room constraints are exported as expected."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                for slot in self.ak_slots:
+                    ak = self.export_objects["aks"][str(slot.pk)]
+                    requirements = list(
+                        slot.ak.requirements.values_list("name", flat=True)
+                    )
+
+                    # proxy rooms
+                    if not any(constr.startswith("proxy") for constr in requirements):
+                        requirements.append("no-proxy")
+
+                    # fixed slot
+                    if slot.fixed and slot.room is not None:
+                        requirements.append(f"fixed-room-{slot.room.pk}")
+
+                    self.assertEqual(
+                        set(ak["room_constraints"]),
+                        set(requirements),
+                        f"Room constraints for slot {slot.pk} not as expected",
+                    )
+
+    def test_ak_time_constraints(self):
+        """Test if AK time constraints are exported as expected."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                for slot in self.ak_slots:
+                    time_constraints = set()
+
+                    # add time constraints for AK category
+                    if slot.ak.category:
+                        category_constraints = AKCategory.create_category_constraints(
+                            [slot.ak.category]
+                        )
+                        time_constraints |= category_constraints
+
+                    if slot.fixed and slot.start is not None:
+                        # fixed slot
+                        time_constraints.add(f"fixed-akslot-{slot.pk}")
+                    elif not Availability.is_event_covered(
+                        slot.event, slot.ak.availabilities.all()
+                    ):
+                        # restricted AK availability
+                        time_constraints.add(f"availability-ak-{slot.ak.pk}")
+
+                    for owner in slot.ak.owners.all():
+                        # restricted owner availability
+                        if not Availability.is_event_covered(
+                            slot.event, owner.availabilities.all()
+                        ):
+                            time_constraints.add(f"availability-person-{owner.pk}")
+
+                    ak = self.export_objects["aks"][str(slot.pk)]
+                    self.assertEqual(
+                        set(ak["time_constraints"]),
+                        time_constraints,
+                        f"Time constraints for slot {slot.pk} not as expected",
+                    )
+
+    def test_all_rooms_exported(self):
+        """Test if exported Rooms match the rooms of Event."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                self.assertEqual(
+                    {str(room.pk) for room in self.rooms},
+                    self.export_objects["rooms"].keys(),
+                    "Exported Rooms do not match the Rooms of the event",
+                )
+
+    def test_room_capacity(self):
+        """Test if room capacity is exported correctly."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                for room in self.rooms:
+                    export_room = self.export_objects["rooms"][str(room.pk)]
+                    self.assertEqual(room.capacity, export_room["capacity"])
+
+    def test_room_info(self):
+        """Test if contents of Room info dict is correct."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                for room in self.rooms:
+                    export_room = self.export_objects["rooms"][str(room.pk)]
+                    self.assertEqual(room.name, export_room["info"]["name"])
+
+    def test_room_timeconstraints(self):
+        """Test if Room time constraints are exported as expected."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                for room in self.rooms:
+                    time_constraints = set()
+
+                    # test if time availability of room is restricted
+                    if not Availability.is_event_covered(
+                        room.event, room.availabilities.all()
+                    ):
+                        time_constraints.add(f"availability-room-{room.pk}")
+
+                    export_room = self.export_objects["rooms"][str(room.pk)]
+                    self.assertEqual(
+                        time_constraints, set(export_room["time_constraints"])
+                    )
+
+    def test_room_fulfilledroomconstraints(self):
+        """Test if room constraints fulfilled by Room are correct."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                for room in self.rooms:
+                    # room properties
+                    fulfilled_room_constraints = set(
+                        room.properties.values_list("name", flat=True)
+                    )
+
+                    # proxy rooms
+                    if not any(
+                        constr.startswith("proxy")
+                        for constr in fulfilled_room_constraints
+                    ):
+                        fulfilled_room_constraints.add("no-proxy")
+
+                    fulfilled_room_constraints.add(f"fixed-room-{room.pk}")
+
+                    export_room = self.export_objects["rooms"][str(room.pk)]
+                    self.assertEqual(
+                        fulfilled_room_constraints,
+                        set(export_room["fulfilled_room_constraints"]),
+                    )
+
+    def _get_timeslot_start_end(self, timeslot):
+        start = datetime.strptime(timeslot["info"]["start"], "%Y-%m-%d %H:%M").replace(
+            tzinfo=self.event.timezone
+        )
+        end = datetime.strptime(timeslot["info"]["end"], "%Y-%m-%d %H:%M").replace(
+            tzinfo=self.event.timezone
+        )
+        return start, end
+
+    def _get_cat_availability_in_export(self):
+        export_slot_cat_avails = defaultdict(list)
+        for timeslot in chain.from_iterable(self.export_dict["timeslots"]["blocks"]):
+            for constr in timeslot["fulfilled_time_constraints"]:
+                if constr.startswith("availability-cat-"):
+                    cat_name = constr[len("availability-cat-") :]
+                    start, end = self._get_timeslot_start_end(timeslot)
+                    export_slot_cat_avails[cat_name].append(
+                        Availability(event=self.event, start=start, end=end)
+                    )
+        return {
+            cat_name: Availability.union(avail_lst)
+            for cat_name, avail_lst in export_slot_cat_avails.items()
+        }
+
+    def _get_cat_availability(self):
+        if DefaultSlot.objects.filter(event=self.event).exists():
+            # Event has default slots -> use them for category availability
+            default_slots_avails = defaultdict(list)
+            for def_slot in DefaultSlot.objects.filter(event=self.event).all():
+                avail = Availability(
+                    event=self.event,
+                    start=def_slot.start.astimezone(self.event.timezone),
+                    end=def_slot.end.astimezone(self.event.timezone),
+                )
+                for cat in def_slot.primary_categories.all():
+                    default_slots_avails[cat.name].append(avail)
+
+            return {
+                cat_name: Availability.union(avail_lst)
+                for cat_name, avail_lst in default_slots_avails.items()
+            }
+
+        # Event has no default slots -> all categories available through whole event
+        start = self.event.start.astimezone(self.event.timezone)
+        end = self.event.end.astimezone(self.event.timezone)
+        delta = (end - start).total_seconds()
+
+        # tweak event end
+        # 1. shorten event to match discrete slot grid
+        slot_seconds = 3600 / self.slots_in_an_hour
+        remainder_seconds = delta % slot_seconds
+        remainder_seconds += 1  # add a second to compensate rounding errs
+        end -= timedelta(seconds=remainder_seconds)
+
+        # set seconds and microseconds to 0 as they are not exported to the json
+        start -= timedelta(seconds=start.second, microseconds=start.microsecond)
+        end -= timedelta(seconds=end.second, microseconds=end.microsecond)
+        event_avail = Availability(event=self.event, start=start, end=end)
+
+        category_names = AKCategory.objects.filter(event=self.event).values_list(
+            "name", flat=True
+        )
+        return {cat_name: [event_avail] for cat_name in category_names}
+
+    def test_timeslots_consecutive(self):
+        """Test if consecutive timeslots in JSON are in fact consecutive."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                prev_end = None
+                for timeslot in chain.from_iterable(
+                    self.export_dict["timeslots"]["blocks"]
+                ):
+                    start, end = self._get_timeslot_start_end(timeslot)
+                    self.assertLess(start, end)
+
+                    delta = end - start
+                    self.assertAlmostEqual(
+                        delta.total_seconds() / (3600), 1 / self.slots_in_an_hour
+                    )
+
+                    if prev_end is not None:
+                        self.assertLessEqual(prev_end, start)
+                    prev_end = end
+
+    def test_block_cover_categories(self):
+        """Test if blocks covers all default slot resp. whole event per category."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+                category_names = AKCategory.objects.filter(event=event).values_list(
+                    "name", flat=True
+                )
+
+                export_cat_avails = self._get_cat_availability_in_export()
+                cat_avails = self._get_cat_availability()
+
+                for cat_name in category_names:
+                    for avail in cat_avails[cat_name]:
+                        # check that all category availabilities are covered
+                        self.assertTrue(
+                            avail.is_covered(export_cat_avails[cat_name]),
+                            f"AKCategory {cat_name}: avail ({avail.start} – {avail.end}) "
+                            f"not covered by {[f'({a.start} – {a.end})' for a in export_cat_avails[cat_name]]}",
+                        )
+
+    def _is_restricted_and_contained_slot(
+        self, slot: Availability, availabilities: list[Availability]
+    ) -> bool:
+        """Test if object is not available for whole event and may happen during slot."""
+        return slot.is_covered(availabilities) and not Availability.is_event_covered(
+            self.event, availabilities
+        )
+
+    def _is_ak_fixed_in_slot(
+        self,
+        ak_slot: AKSlot,
+        timeslot_avail: Availability,
+    ) -> bool:
+        if not ak_slot.fixed or ak_slot.start is None:
+            return False
+        ak_slot_avail = Availability(
+            event=self.event,
+            start=ak_slot.start.astimezone(self.event.timezone),
+            end=ak_slot.end.astimezone(self.event.timezone),
+        )
+        return timeslot_avail.overlaps(ak_slot_avail, strict=True)
+
+    def test_timeslot_fulfilledconstraints(self):
+        """Test if fulfilled time constraints by timeslot are as expected."""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                cat_avails = self._get_cat_availability()
+                for timeslot in chain.from_iterable(
+                    self.export_dict["timeslots"]["blocks"]
+                ):
+                    start, end = self._get_timeslot_start_end(timeslot)
+                    timeslot_avail = Availability(
+                        event=self.event, start=start, end=end
+                    )
+
+                    fulfilled_time_constraints = set()
+
+                    # reso deadline
+                    if self.event.reso_deadline is not None:
+                        # timeslot ends before deadline
+                        if end < self.event.reso_deadline.astimezone(
+                            self.event.timezone
+                        ):
+                            fulfilled_time_constraints.add("resolution")
+
+                    # add category constraints
+                    fulfilled_time_constraints |= (
+                        AKCategory.create_category_constraints(
+                            [
+                                cat
+                                for cat in AKCategory.objects.filter(
+                                    event=self.event
+                                ).all()
+                                if timeslot_avail.is_covered(cat_avails[cat.name])
+                            ]
+                        )
+                    )
+
+                    # add owner constraints
+                    fulfilled_time_constraints |= {
+                        f"availability-person-{owner.id}"
+                        for owner in AKOwner.objects.filter(event=self.event).all()
+                        if self._is_restricted_and_contained_slot(
+                            timeslot_avail,
+                            Availability.union(owner.availabilities.all()),
+                        )
+                    }
+
+                    # add room constraints
+                    fulfilled_time_constraints |= {
+                        f"availability-room-{room.id}"
+                        for room in self.rooms
+                        if self._is_restricted_and_contained_slot(
+                            timeslot_avail,
+                            Availability.union(room.availabilities.all()),
+                        )
+                    }
+
+                    # add ak constraints
+                    fulfilled_time_constraints |= {
+                        f"availability-ak-{ak.id}"
+                        for ak in AK.objects.filter(event=event)
+                        if self._is_restricted_and_contained_slot(
+                            timeslot_avail, Availability.union(ak.availabilities.all())
+                        )
+                    }
+                    fulfilled_time_constraints |= {
+                        f"fixed-akslot-{slot.id}"
+                        for slot in self.ak_slots
+                        if self._is_ak_fixed_in_slot(slot, timeslot_avail)
+                    }
+
+                    self.assertEqual(
+                        fulfilled_time_constraints,
+                        set(timeslot["fulfilled_time_constraints"]),
+                    )
diff --git a/AKModel/tests.py b/AKModel/tests/test_views.py
similarity index 60%
rename from AKModel/tests.py
rename to AKModel/tests/test_views.py
index 6730315d..63984745 100644
--- a/AKModel/tests.py
+++ b/AKModel/tests/test_views.py
@@ -7,8 +7,19 @@ from django.contrib.messages.storage.base import Message
 from django.test import TestCase
 from django.urls import reverse_lazy, reverse
 
-from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, Room, AKSlot, AKOrgaMessage, \
-    ConstraintViolation, DefaultSlot
+from AKModel.models import (
+    Event,
+    AKOwner,
+    AKCategory,
+    AKTrack,
+    AKRequirement,
+    AK,
+    Room,
+    AKSlot,
+    AKOrgaMessage,
+    ConstraintViolation,
+    DefaultSlot,
+)
 
 
 class BasicViewTests:
@@ -29,9 +40,10 @@ class BasicViewTests:
     since the test framework does not understand the concept of abstract test definitions and would handle this class
     as real test case otherwise, distorting the test results.
     """
+
     # pylint: disable=no-member
     VIEWS = []
-    APP_NAME = ''
+    APP_NAME = ""
     VIEWS_STAFF_ONLY = []
     EDIT_TESTCASES = []
 
@@ -41,16 +53,26 @@ class BasicViewTests:
         """
         user_model = get_user_model()
         self.staff_user = user_model.objects.create(
-            username='Test Staff User', email='teststaff@example.com', password='staffpw',
-            is_staff=True, is_active=True
+            username="Test Staff User",
+            email="teststaff@example.com",
+            password="staffpw",
+            is_staff=True,
+            is_active=True,
         )
         self.admin_user = user_model.objects.create(
-            username='Test Admin User', email='testadmin@example.com', password='adminpw',
-            is_staff=True, is_superuser=True, is_active=True
+            username="Test Admin User",
+            email="testadmin@example.com",
+            password="adminpw",
+            is_staff=True,
+            is_superuser=True,
+            is_active=True,
         )
         self.deactivated_user = user_model.objects.create(
-            username='Test Deactivated User', email='testdeactivated@example.com', password='deactivatedpw',
-            is_staff=True, is_active=False
+            username="Test Deactivated User",
+            email="testdeactivated@example.com",
+            password="deactivatedpw",
+            is_staff=True,
+            is_active=False,
         )
 
     def _name_and_url(self, view_name):
@@ -62,7 +84,9 @@ class BasicViewTests:
         :return: full view name with prefix if applicable, url of the view
         :rtype: str, str
         """
-        view_name_with_prefix = f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0]
+        view_name_with_prefix = (
+            f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0]
+        )
         url = reverse(view_name_with_prefix, kwargs=view_name[1])
         return view_name_with_prefix, url
 
@@ -74,7 +98,7 @@ class BasicViewTests:
         :param expected_message: message that should be shown
         :param msg_prefix: prefix for the error message when test fails
         """
-        messages:List[Message] = list(get_messages(response.wsgi_request))
+        messages: List[Message] = list(get_messages(response.wsgi_request))
 
         msg_count = "No message shown to user"
         msg_content = "Wrong message, expected '{expected_message}'"
@@ -95,10 +119,16 @@ class BasicViewTests:
             view_name_with_prefix, url = self._name_and_url(view_name)
             try:
                 response = self.client.get(url)
-                self.assertEqual(response.status_code, 200, msg=f"{view_name_with_prefix} ({url}) broken")
-            except Exception: # pylint: disable=broad-exception-caught
-                self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
-                          f"\n\n{traceback.format_exc()}")
+                self.assertEqual(
+                    response.status_code,
+                    200,
+                    msg=f"{view_name_with_prefix} ({url}) broken",
+                )
+            except Exception:  # pylint: disable=broad-exception-caught
+                self.fail(
+                    f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
+                    f"\n\n{traceback.format_exc()}"
+                )
 
     def test_access_control_staff_only(self):
         """
@@ -107,11 +137,16 @@ class BasicViewTests:
         # Not logged in? Views should not be visible
         self.client.logout()
         for view_name_info in self.VIEWS_STAFF_ONLY:
-            expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2]
+            expected_response_code = (
+                302 if len(view_name_info) == 2 else view_name_info[2]
+            )
             view_name_with_prefix, url = self._name_and_url(view_name_info)
             response = self.client.get(url)
-            self.assertEqual(response.status_code, expected_response_code,
-                             msg=f"{view_name_with_prefix} ({url}) accessible by non-staff")
+            self.assertEqual(
+                response.status_code,
+                expected_response_code,
+                msg=f"{view_name_with_prefix} ({url}) accessible by non-staff",
+            )
 
         # Logged in? Views should be visible
         self.client.force_login(self.staff_user)
@@ -119,20 +154,30 @@ class BasicViewTests:
             view_name_with_prefix, url = self._name_and_url(view_name_info)
             try:
                 response = self.client.get(url)
-                self.assertEqual(response.status_code, 200,
-                                 msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)")
+                self.assertEqual(
+                    response.status_code,
+                    200,
+                    msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)",
+                )
             except Exception:  # pylint: disable=broad-exception-caught
-                self.fail(f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
-                          f"\n\n{traceback.format_exc()}")
+                self.fail(
+                    f"An error occurred during rendering of {view_name_with_prefix} ({url}):"
+                    f"\n\n{traceback.format_exc()}"
+                )
 
         # Disabled user? Views should not be visible
         self.client.force_login(self.deactivated_user)
         for view_name_info in self.VIEWS_STAFF_ONLY:
-            expected_response_code = 302 if len(view_name_info) == 2 else view_name_info[2]
+            expected_response_code = (
+                302 if len(view_name_info) == 2 else view_name_info[2]
+            )
             view_name_with_prefix, url = self._name_and_url(view_name_info)
             response = self.client.get(url)
-            self.assertEqual(response.status_code, expected_response_code,
-                             msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user")
+            self.assertEqual(
+                response.status_code,
+                expected_response_code,
+                msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user",
+            )
 
     def _to_sendable_value(self, val):
         """
@@ -182,16 +227,26 @@ class BasicViewTests:
             self.client.logout()
 
         response = self.client.get(url)
-        self.assertEqual(response.status_code, 200, msg=f"{name}: Could not load edit form via GET ({url})")
+        self.assertEqual(
+            response.status_code,
+            200,
+            msg=f"{name}: Could not load edit form via GET ({url})",
+        )
 
         form = response.context[form_name]
-        data = {k:self._to_sendable_value(v) for k,v in form.initial.items()}
+        data = {k: self._to_sendable_value(v) for k, v in form.initial.items()}
 
         response = self.client.post(url, data=data)
         if expected_code == 200:
-            self.assertEqual(response.status_code, 200, msg=f"{name}: Did not return 200 ({url}")
+            self.assertEqual(
+                response.status_code, 200, msg=f"{name}: Did not return 200 ({url}"
+            )
         elif expected_code == 302:
-            self.assertRedirects(response, target_url, msg_prefix=f"{name}: Did not redirect ({url} -> {target_url}")
+            self.assertRedirects(
+                response,
+                target_url,
+                msg_prefix=f"{name}: Did not redirect ({url} -> {target_url}",
+            )
         if expected_message != "":
             self._assert_message(response, expected_message, msg_prefix=f"{name}")
 
@@ -200,32 +255,44 @@ class ModelViewTests(BasicViewTests, TestCase):
     """
     Basic view test cases for views from AKModel plus some custom tests
     """
-    fixtures = ['model.json']
+
+    fixtures = ["model.json"]
 
     ADMIN_MODELS = [
-        (Event, 'event'), (AKOwner, 'akowner'), (AKCategory, 'akcategory'), (AKTrack, 'aktrack'),
-        (AKRequirement, 'akrequirement'), (AK, 'ak'), (Room, 'room'), (AKSlot, 'akslot'),
-        (AKOrgaMessage, 'akorgamessage'), (ConstraintViolation, 'constraintviolation'),
-        (DefaultSlot, 'defaultslot')
+        (Event, "event"),
+        (AKOwner, "akowner"),
+        (AKCategory, "akcategory"),
+        (AKTrack, "aktrack"),
+        (AKRequirement, "akrequirement"),
+        (AK, "ak"),
+        (Room, "room"),
+        (AKSlot, "akslot"),
+        (AKOrgaMessage, "akorgamessage"),
+        (ConstraintViolation, "constraintviolation"),
+        (DefaultSlot, "defaultslot"),
     ]
 
     VIEWS_STAFF_ONLY = [
-        ('admin:index', {}),
-        ('admin:event_status', {'event_slug': 'kif42'}),
-        ('admin:event_requirement_overview', {'event_slug': 'kif42'}),
-        ('admin:ak_csv_export', {'event_slug': 'kif42'}),
-        ('admin:ak_json_export', {'event_slug': 'kif42'}),
-        ('admin:ak_wiki_export', {'slug': 'kif42'}),
-        ('admin:ak_schedule_json_import', {'event_slug': 'kif42'}),
-        ('admin:ak_delete_orga_messages', {'event_slug': 'kif42'}),
-        ('admin:ak_slide_export', {'event_slug': 'kif42'}),
-        ('admin:default-slots-editor', {'event_slug': 'kif42'}),
-        ('admin:room-import', {'event_slug': 'kif42'}),
-        ('admin:new_event_wizard_start', {}),
+        ("admin:index", {}),
+        ("admin:event_status", {"event_slug": "kif42"}),
+        ("admin:event_requirement_overview", {"event_slug": "kif42"}),
+        ("admin:ak_csv_export", {"event_slug": "kif42"}),
+        ("admin:ak_json_export", {"event_slug": "kif42"}),
+        ("admin:ak_wiki_export", {"slug": "kif42"}),
+        ("admin:ak_schedule_json_import", {"event_slug": "kif42"}),
+        ("admin:ak_delete_orga_messages", {"event_slug": "kif42"}),
+        ("admin:ak_slide_export", {"event_slug": "kif42"}),
+        ("admin:default-slots-editor", {"event_slug": "kif42"}),
+        ("admin:room-import", {"event_slug": "kif42"}),
+        ("admin:new_event_wizard_start", {}),
     ]
 
     EDIT_TESTCASES = [
-        {'view': 'admin:default-slots-editor', 'kwargs': {'event_slug': 'kif42'}, "admin": True},
+        {
+            "view": "admin:default-slots-editor",
+            "kwargs": {"event_slug": "kif42"},
+            "admin": True,
+        },
     ]
 
     def test_admin(self):
@@ -236,24 +303,32 @@ class ModelViewTests(BasicViewTests, TestCase):
         for model in self.ADMIN_MODELS:
             # Special treatment for a subset of views (where we exchanged default functionality, e.g., create views)
             if model[1] == "event":
-                _, url = self._name_and_url(('admin:new_event_wizard_start', {}))
+                _, url = self._name_and_url(("admin:new_event_wizard_start", {}))
             elif model[1] == "room":
-                _, url = self._name_and_url(('admin:room-new', {}))
+                _, url = self._name_and_url(("admin:room-new", {}))
             # Otherwise, just call the creation form view
             else:
-                _, url = self._name_and_url((f'admin:AKModel_{model[1]}_add', {}))
+                _, url = self._name_and_url((f"admin:AKModel_{model[1]}_add", {}))
             response = self.client.get(url)
-            self.assertEqual(response.status_code, 200, msg=f"Add form for model {model[1]} ({url}) broken")
+            self.assertEqual(
+                response.status_code,
+                200,
+                msg=f"Add form for model {model[1]} ({url}) broken",
+            )
 
         for model in self.ADMIN_MODELS:
             # Test the update view using the first existing instance of each model
             m = model[0].objects.first()
             if m is not None:
                 _, url = self._name_and_url(
-                    (f'admin:AKModel_{model[1]}_change', {'object_id': m.pk})
+                    (f"admin:AKModel_{model[1]}_change", {"object_id": m.pk})
                 )
                 response = self.client.get(url)
-                self.assertEqual(response.status_code, 200, msg=f"Edit form for model {model[1]} ({url}) broken")
+                self.assertEqual(
+                    response.status_code,
+                    200,
+                    msg=f"Edit form for model {model[1]} ({url}) broken",
+                )
 
     def test_wiki_export(self):
         """
@@ -262,17 +337,27 @@ class ModelViewTests(BasicViewTests, TestCase):
         """
         self.client.force_login(self.admin_user)
 
-        export_url = reverse_lazy("admin:ak_wiki_export", kwargs={'slug': 'kif42'})
+        export_url = reverse_lazy("admin:ak_wiki_export", kwargs={"slug": "kif42"})
         response = self.client.get(export_url)
         self.assertEqual(response.status_code, 200, "Export not working at all")
 
         export_count = 0
         for _, aks in response.context["categories_with_aks"]:
             for ak in aks:
-                self.assertEqual(ak.include_in_export, True,
-                                 f"AK with export flag set to False (pk={ak.pk}) included in export")
-                self.assertNotEqual(ak.pk, 1, "AK known to be excluded from export (PK 1) included in export")
+                self.assertEqual(
+                    ak.include_in_export,
+                    True,
+                    f"AK with export flag set to False (pk={ak.pk}) included in export",
+                )
+                self.assertNotEqual(
+                    ak.pk,
+                    1,
+                    "AK known to be excluded from export (PK 1) included in export",
+                )
                 export_count += 1
 
-        self.assertEqual(export_count, AK.objects.filter(event_id=2, include_in_export=True).count(),
-                         "Wiki export contained the wrong number of AKs")
+        self.assertEqual(
+            export_count,
+            AK.objects.filter(event_id=2, include_in_export=True).count(),
+            "Wiki export contained the wrong number of AKs",
+        )
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 5e642dd5..bca1c5b3 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -50,11 +50,6 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
     context_object_name = "slots"
     title = _("AK JSON Export")
 
-
-    def _test_slot_contained(self, slot: Availability, availabilities: List[Availability]) -> bool:
-        """Test if slot is contained in any member of availabilities."""
-        return any(availability.contains(slot) for availability in availabilities)
-
     def _test_event_not_covered(self, availabilities: List[Availability]) -> bool:
         """Test if event is not covered by availabilities."""
         return not Availability.is_event_covered(self.event, availabilities)
@@ -70,8 +65,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
     def _test_add_constraint(self, slot: Availability, availabilities: List[Availability]) -> bool:
         """Test if object is not available for whole event and may happen during slot."""
         return (
-            self._test_event_not_covered(availabilities)
-            and self._test_slot_contained(slot, availabilities)
+            self._test_event_not_covered(availabilities) and slot.is_covered(availabilities)
         )
 
     def _generate_time_constraints(
@@ -161,7 +155,8 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                 current_block.append({
                     "id": str(timeslot.idx),
                     "info": {
-                        "start": timeslot.avail.simplified,
+                        "start": timeslot.avail.start.astimezone(self.event.timezone).strftime("%Y-%m-%d %H:%M"),
+                        "end": timeslot.avail.end.astimezone(self.event.timezone).strftime("%Y-%m-%d %H:%M"),
                     },
                     "fulfilled_time_constraints": time_constraints,
                     })
diff --git a/AKPlan/tests.py b/AKPlan/tests.py
index 69365c2b..3f00061a 100644
--- a/AKPlan/tests.py
+++ b/AKPlan/tests.py
@@ -1,6 +1,6 @@
 from django.test import TestCase
 
-from AKModel.tests import BasicViewTests
+from AKModel.tests.test_views import BasicViewTests
 
 
 class PlanViewTests(BasicViewTests, TestCase):
diff --git a/AKScheduling/api.py b/AKScheduling/api.py
index e78fda78..cfd476ec 100644
--- a/AKScheduling/api.py
+++ b/AKScheduling/api.py
@@ -55,7 +55,9 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView):
     model = AKSlot
 
     def get_queryset(self):
-        return super().get_queryset().select_related('ak').filter(event=self.event, room__isnull=False)
+        return super().get_queryset().select_related('ak').filter(
+            event=self.event, room__isnull=False, start__isnull=False
+        )
 
     def render_to_response(self, context, **response_kwargs):
         return JsonResponse(
diff --git a/AKScheduling/models.py b/AKScheduling/models.py
index aa6be3d0..8f34c151 100644
--- a/AKScheduling/models.py
+++ b/AKScheduling/models.py
@@ -365,8 +365,8 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
     new_violations = []
 
     # For all slots in this room...
-    if instance.room:
-        for other_slot in instance.room.akslot_set.all():
+    if instance.room and instance.start:
+        for other_slot in instance.room.akslot_set.filter(start__isnull=False):
             if other_slot != instance:
                 # ... find overlapping slots...
                 if instance.overlaps(other_slot):
diff --git a/AKScheduling/tests.py b/AKScheduling/tests.py
index 0996eedd..44f25719 100644
--- a/AKScheduling/tests.py
+++ b/AKScheduling/tests.py
@@ -4,7 +4,7 @@ from datetime import timedelta
 from django.test import TestCase
 from django.utils import timezone
 
-from AKModel.tests import BasicViewTests
+from AKModel.tests.test_views import BasicViewTests
 from AKModel.models import AKSlot, Event, Room
 
 class ModelViewTests(BasicViewTests, TestCase):
diff --git a/AKSubmission/tests.py b/AKSubmission/tests.py
index 018289aa..423a9776 100644
--- a/AKSubmission/tests.py
+++ b/AKSubmission/tests.py
@@ -5,7 +5,7 @@ from django.urls import reverse_lazy
 from django.utils.datetime_safe import datetime
 
 from AKModel.models import AK, AKSlot, Event
-from AKModel.tests import BasicViewTests
+from AKModel.tests.test_views import BasicViewTests
 
 
 class ModelViewTests(BasicViewTests, TestCase):
-- 
GitLab


From ab8a00149c2591c231d41ce600908f684846b4b7 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Thu, 6 Feb 2025 16:51:16 +0100
Subject: [PATCH 58/77] Update locale messages

---
 AKModel/locale/de_DE/LC_MESSAGES/django.po | 155 +++++++++++----------
 1 file changed, 78 insertions(+), 77 deletions(-)

diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index 42b38565..88eb85d5 100644
--- a/AKModel/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-01-22 19:00+0000\n"
+"POT-Creation-Date: 2025-02-06 15:48+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"
@@ -33,8 +33,8 @@ msgstr "Plan veröffentlichen"
 msgid "Unpublish plan"
 msgstr "Plan verbergen"
 
-#: AKModel/admin.py:168 AKModel/models.py:689 AKModel/models.py:1114
-#: AKModel/models.py:1150
+#: AKModel/admin.py:168 AKModel/models.py:689 AKModel/models.py:1120
+#: AKModel/models.py:1156
 #: AKModel/templates/admin/AKModel/status/event_aks.html:10
 #: AKModel/views/manage.py:75 AKModel/views/status.py:97
 msgid "AKs"
@@ -60,11 +60,11 @@ msgstr "In Wiki-Syntax exportieren"
 msgid "Cannot export AKs from more than one event at the same time."
 msgstr "Kann nicht AKs von mehreren Events zur selben Zeit exportieren."
 
-#: AKModel/admin.py:320 AKModel/views/ak.py:237
+#: AKModel/admin.py:320 AKModel/views/ak.py:241
 msgid "Reset interest in AKs"
 msgstr "Interesse an AKs zurücksetzen"
 
-#: AKModel/admin.py:330 AKModel/views/ak.py:252
+#: AKModel/admin.py:330 AKModel/views/ak.py:256
 msgid "Reset AKs' interest counters"
 msgstr "Interessenszähler der AKs zurücksetzen"
 
@@ -84,7 +84,7 @@ msgstr "Constraintverletzungen auf Level \"Violation\" setzen"
 msgid "Set Constraint Violations to level \"warning\""
 msgstr "Constraintverletzungen auf Level \"Warning\" setzen"
 
-#: AKModel/availability/forms.py:25 AKModel/availability/models.py:299
+#: AKModel/availability/forms.py:25 AKModel/availability/models.py:308
 msgid "Availability"
 msgstr "Verfügbarkeit"
 
@@ -112,15 +112,15 @@ msgstr "Bitte Verfügbarkeiten eintragen!"
 #: AKModel/availability/models.py:43 AKModel/models.py:160
 #: AKModel/models.py:489 AKModel/models.py:566 AKModel/models.py:599
 #: AKModel/models.py:625 AKModel/models.py:679 AKModel/models.py:821
-#: AKModel/models.py:897 AKModel/models.py:1053 AKModel/models.py:1110
-#: AKModel/models.py:1301
+#: AKModel/models.py:897 AKModel/models.py:1059 AKModel/models.py:1116
+#: AKModel/models.py:1307
 msgid "Event"
 msgstr "Event"
 
 #: AKModel/availability/models.py:44 AKModel/models.py:490
 #: AKModel/models.py:567 AKModel/models.py:600 AKModel/models.py:626
 #: AKModel/models.py:680 AKModel/models.py:822 AKModel/models.py:898
-#: AKModel/models.py:1054 AKModel/models.py:1111 AKModel/models.py:1302
+#: AKModel/models.py:1060 AKModel/models.py:1117 AKModel/models.py:1308
 msgid "Associated event"
 msgstr "Zugehöriges Event"
 
@@ -133,7 +133,7 @@ msgid "Person whose availability this is"
 msgstr "Person deren Verfügbarkeit hier abgebildet wird"
 
 #: AKModel/availability/models.py:61 AKModel/models.py:825
-#: AKModel/models.py:887 AKModel/models.py:1120
+#: AKModel/models.py:887 AKModel/models.py:1126
 msgid "Room"
 msgstr "Raum"
 
@@ -142,7 +142,7 @@ msgid "Room whose availability this is"
 msgstr "Raum dessen Verfügbarkeit hier abgebildet wird"
 
 #: AKModel/availability/models.py:70 AKModel/models.py:688
-#: AKModel/models.py:886 AKModel/models.py:1048
+#: AKModel/models.py:886 AKModel/models.py:1054
 msgid "AK"
 msgstr "AK"
 
@@ -151,7 +151,7 @@ msgid "AK whose availability this is"
 msgstr "Verfügbarkeiten"
 
 #: AKModel/availability/models.py:79 AKModel/models.py:570
-#: AKModel/models.py:1126
+#: AKModel/models.py:1132
 msgid "AK Category"
 msgstr "AK-Kategorie"
 
@@ -159,7 +159,7 @@ msgstr "AK-Kategorie"
 msgid "AK Category whose availability this is"
 msgstr "AK-Kategorie, deren Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:300 AKModel/models.py:721
+#: AKModel/availability/models.py:309 AKModel/models.py:721
 msgid "Availabilities"
 msgstr "Verfügbarkeiten"
 
@@ -221,7 +221,7 @@ msgstr ""
 "fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen "
 "ausgefüllt zu werden?"
 
-#: AKModel/forms.py:189 AKModel/models.py:1295
+#: AKModel/forms.py:189 AKModel/models.py:1301
 msgid "Default Slots"
 msgstr "Standardslots"
 
@@ -447,8 +447,9 @@ msgid ""
 "Duration of AK {ak_name} assigned by solver ({solver_duration} hours) is "
 "less than the duration required by the slot ({slot_duration} hours)"
 msgstr ""
-"Die dem AK {ak_name} vom Solver zugewiesene Dauer ({solver_duration} Stunden) ist "
-"kürzer als die aktuell vorgesehene Dauer des Slots ({slot_duration} Stunden)"
+"Die dem AK {ak_name} vom Solver zugewiesene Dauer ({solver_duration} "
+"Stunden) ist kürzer als die aktuell vorgesehene Dauer des Slots "
+"({slot_duration} Stunden)"
 
 #: AKModel/models.py:454
 #, python-brace-format
@@ -456,8 +457,8 @@ msgid ""
 "Fixed AK {ak_name} assigned by solver to room {solver_room} is fixed to room "
 "{slot_room}"
 msgstr ""
-"Dem fix geplanten AK {ak_name} wurde vom Solver Raum {solver_room} zugewiesen, "
-"dabei ist der AK bereits fix in Raum {slot_room} eingeplant."
+"Dem fix geplanten AK {ak_name} wurde vom Solver Raum {solver_room} "
+"zugewiesen, dabei ist der AK bereits fix in Raum {slot_room} eingeplant."
 
 #: AKModel/models.py:465
 #, python-brace-format
@@ -465,8 +466,8 @@ msgid ""
 "Fixed AK {ak_name} assigned by solver to start at {solver_start} is fixed to "
 "start at {slot_start}"
 msgstr ""
-"Dem fix geplanten AK {ak_name} wurde vom Solver die Startzeit {solver_start} zugewiesen, "
-"dabei ist der AK bereits für {slot_start} eingeplant."
+"Dem fix geplanten AK {ak_name} wurde vom Solver die Startzeit {solver_start} "
+"zugewiesen, dabei ist der AK bereits für {slot_start} eingeplant."
 
 #: AKModel/models.py:484
 msgid "Nickname"
@@ -500,7 +501,7 @@ msgstr "Internet Link"
 msgid "Link to Homepage"
 msgstr "Link zu Homepage oder Webseite"
 
-#: AKModel/models.py:493 AKModel/models.py:1119
+#: AKModel/models.py:493 AKModel/models.py:1125
 msgid "AK Owner"
 msgstr "AK-Leitung"
 
@@ -560,7 +561,7 @@ msgstr "AK-Tracks"
 msgid "Name of the Requirement"
 msgstr "Name der Anforderung"
 
-#: AKModel/models.py:629 AKModel/models.py:1123
+#: AKModel/models.py:629 AKModel/models.py:1129
 msgid "AK Requirement"
 msgstr "AK-Anforderung"
 
@@ -747,11 +748,11 @@ msgstr "AK, der zugeordnet wird"
 msgid "Room the AK will take place in"
 msgstr "Raum in dem der AK stattfindet"
 
-#: AKModel/models.py:889 AKModel/models.py:1298
+#: AKModel/models.py:889 AKModel/models.py:1304
 msgid "Slot Begin"
 msgstr "Beginn des Slots"
 
-#: AKModel/models.py:889 AKModel/models.py:1298
+#: AKModel/models.py:889 AKModel/models.py:1304
 msgid "Time and date the slot begins"
 msgstr "Zeit und Datum zu der der AK beginnt"
 
@@ -779,7 +780,7 @@ msgstr "Letzte Aktualisierung"
 msgid "AK Slot"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:904 AKModel/models.py:1116 AKModel/models.py:1151
+#: AKModel/models.py:904 AKModel/models.py:1122 AKModel/models.py:1157
 msgid "AK Slots"
 msgstr "AK-Slot"
 
@@ -787,187 +788,187 @@ msgstr "AK-Slot"
 msgid "Not scheduled yet"
 msgstr "Noch nicht geplant"
 
-#: AKModel/models.py:1049
+#: AKModel/models.py:1055
 msgid "AK this message belongs to"
 msgstr "AK zu dem die Nachricht gehört"
 
-#: AKModel/models.py:1050
+#: AKModel/models.py:1056
 msgid "Message text"
 msgstr "Nachrichtentext"
 
-#: AKModel/models.py:1051
+#: AKModel/models.py:1057
 msgid "Message to the organizers. This is not publicly visible."
 msgstr ""
 "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar."
 
-#: AKModel/models.py:1055
+#: AKModel/models.py:1061
 msgid "Resolved"
 msgstr "Erledigt"
 
-#: AKModel/models.py:1056
+#: AKModel/models.py:1062
 msgid "This message has been resolved (no further action needed)"
 msgstr ""
 "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen "
 "notwendig)"
 
-#: AKModel/models.py:1059
+#: AKModel/models.py:1065
 msgid "AK Orga Message"
 msgstr "AK-Organachricht"
 
-#: AKModel/models.py:1060
+#: AKModel/models.py:1066
 msgid "AK Orga Messages"
 msgstr "AK-Organachrichten"
 
-#: AKModel/models.py:1077
+#: AKModel/models.py:1083
 msgid "Constraint Violation"
 msgstr "Constraintverletzung"
 
-#: AKModel/models.py:1078
+#: AKModel/models.py:1084
 msgid "Constraint Violations"
 msgstr "Constraintverletzungen"
 
-#: AKModel/models.py:1085
+#: AKModel/models.py:1091
 msgid "Owner has two parallel slots"
 msgstr "Leitung hat zwei Slots parallel"
 
-#: AKModel/models.py:1086
+#: AKModel/models.py:1092
 msgid "AK Slot was scheduled outside the AK's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des AKs platziert"
 
-#: AKModel/models.py:1087
+#: AKModel/models.py:1093
 msgid "Room has two AK slots scheduled at the same time"
 msgstr "Raum hat zwei AK Slots gleichzeitig"
 
-#: AKModel/models.py:1088
+#: AKModel/models.py:1094
 msgid "Room does not satisfy the requirement of the scheduled AK"
 msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht"
 
-#: AKModel/models.py:1089
+#: AKModel/models.py:1095
 msgid "AK Slot is scheduled at the same time as an AK listed as a conflict"
 msgstr ""
 "AK Slot wurde wurde zur gleichen Zeit wie ein Konflikt des AKs platziert"
 
-#: AKModel/models.py:1090
+#: AKModel/models.py:1096
 msgid "AK Slot is scheduled before an AK listed as a prerequisite"
 msgstr "AK Slot wurde vor einem als Voraussetzung gelisteten AK platziert"
 
-#: AKModel/models.py:1092
+#: AKModel/models.py:1098
 msgid ""
 "AK Slot for AK with intention to submit a resolution is scheduled after "
 "resolution deadline"
 msgstr ""
 "AK Slot eines AKs mit Resoabsicht wurde nach der Resodeadline platziert"
 
-#: AKModel/models.py:1093
+#: AKModel/models.py:1099
 msgid "AK Slot in a category is outside that categories availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeiten seiner Kategorie"
 
-#: AKModel/models.py:1094
+#: AKModel/models.py:1100
 msgid "Two AK Slots for the same AK scheduled at the same time"
 msgstr "Zwei AK Slots eines AKs wurden zur selben Zeit platziert"
 
-#: AKModel/models.py:1095
+#: AKModel/models.py:1101
 msgid "Room does not have enough space for interest in scheduled AK Slot"
 msgstr "Room hat nicht genug Platz für das Interesse am geplanten AK-Slot"
 
-#: AKModel/models.py:1096
+#: AKModel/models.py:1102
 msgid "AK Slot is scheduled outside the event's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des Events platziert"
 
-#: AKModel/models.py:1102
+#: AKModel/models.py:1108
 msgid "Warning"
 msgstr "Warnung"
 
-#: AKModel/models.py:1103
+#: AKModel/models.py:1109
 msgid "Violation"
 msgstr "Verletzung"
 
-#: AKModel/models.py:1105
+#: AKModel/models.py:1111
 msgid "Type"
 msgstr "Art"
 
-#: AKModel/models.py:1106
+#: AKModel/models.py:1112
 msgid "Type of violation, i.e. what kind of constraint was violated"
 msgstr "Art der Verletzung, gibt an welche Art Constraint verletzt wurde"
 
-#: AKModel/models.py:1107
+#: AKModel/models.py:1113
 msgid "Level"
 msgstr "Level"
 
-#: AKModel/models.py:1108
+#: AKModel/models.py:1114
 msgid "Severity level of the violation"
 msgstr "Schweregrad der Verletzung"
 
-#: AKModel/models.py:1115
+#: AKModel/models.py:1121
 msgid "AK(s) belonging to this constraint"
 msgstr "AK(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:1117
+#: AKModel/models.py:1123
 msgid "AK Slot(s) belonging to this constraint"
 msgstr "AK Slot(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:1119
+#: AKModel/models.py:1125
 msgid "AK Owner belonging to this constraint"
 msgstr "AK Leitung(en), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:1121
+#: AKModel/models.py:1127
 msgid "Room belonging to this constraint"
 msgstr "Raum, der zu diesem Constraint gehört"
 
-#: AKModel/models.py:1124
+#: AKModel/models.py:1130
 msgid "AK Requirement belonging to this constraint"
 msgstr "AK Anforderung, die zu diesem Constraint gehört"
 
-#: AKModel/models.py:1126
+#: AKModel/models.py:1132
 msgid "AK Category belonging to this constraint"
 msgstr "AK Kategorie, di zu diesem Constraint gehört"
 
-#: AKModel/models.py:1128
+#: AKModel/models.py:1134
 msgid "Comment"
 msgstr "Kommentar"
 
-#: AKModel/models.py:1128
+#: AKModel/models.py:1134
 msgid "Comment or further details for this violation"
 msgstr "Kommentar oder weitere Details zu dieser Vereletzung"
 
-#: AKModel/models.py:1131
+#: AKModel/models.py:1137
 msgid "Timestamp"
 msgstr "Timestamp"
 
-#: AKModel/models.py:1131
+#: AKModel/models.py:1137
 msgid "Time of creation"
 msgstr "Zeitpunkt der ERstellung"
 
-#: AKModel/models.py:1132
+#: AKModel/models.py:1138
 msgid "Manually Resolved"
 msgstr "Manuell behoben"
 
-#: AKModel/models.py:1133
+#: AKModel/models.py:1139
 msgid "Mark this violation manually as resolved"
 msgstr "Markiere diese Verletzung manuell als behoben"
 
-#: AKModel/models.py:1160
+#: AKModel/models.py:1166
 #: AKModel/templates/admin/AKModel/requirements_overview.html:27
 msgid "Details"
 msgstr "Details"
 
-#: AKModel/models.py:1294
+#: AKModel/models.py:1300
 msgid "Default Slot"
 msgstr "Standardslot"
 
-#: AKModel/models.py:1299
+#: AKModel/models.py:1305
 msgid "Slot End"
 msgstr "Ende des Slots"
 
-#: AKModel/models.py:1299
+#: AKModel/models.py:1305
 msgid "Time and date the slot ends"
 msgstr "Zeit und Datum zu der der Slot endet"
 
-#: AKModel/models.py:1304
+#: AKModel/models.py:1310
 msgid "Primary categories"
 msgstr "Primäre Kategorien"
 
-#: AKModel/models.py:1305
+#: AKModel/models.py:1311
 msgid "Categories that should be assigned to this slot primarily"
 msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen"
 
@@ -1177,35 +1178,35 @@ msgstr "AK-CSV-Export"
 msgid "AK JSON Export"
 msgstr "AK-JSON-Export"
 
-#: AKModel/views/ak.py:186
+#: AKModel/views/ak.py:190
 msgid "AK Wiki Export"
 msgstr "AK-Wiki-Export"
 
-#: AKModel/views/ak.py:197 AKModel/views/manage.py:55
+#: AKModel/views/ak.py:201 AKModel/views/manage.py:55
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKModel/views/ak.py:209
+#: AKModel/views/ak.py:213
 msgid "Delete AK Orga Messages"
 msgstr "AK-Organachrichten löschen"
 
-#: AKModel/views/ak.py:227
+#: AKModel/views/ak.py:231
 msgid "AK Orga Messages successfully deleted"
 msgstr "AK-Organachrichten erfolgreich gelöscht"
 
-#: AKModel/views/ak.py:239
+#: AKModel/views/ak.py:243
 msgid "Interest of the following AKs will be set to not filled (-1):"
 msgstr "Interesse an den folgenden AKs wird auf nicht ausgefüllt (-1) gesetzt:"
 
-#: AKModel/views/ak.py:240
+#: AKModel/views/ak.py:244
 msgid "Reset of interest in AKs successful."
 msgstr "Interesse an AKs erfolgreich zurückgesetzt."
 
-#: AKModel/views/ak.py:254
+#: AKModel/views/ak.py:258
 msgid "Interest counter of the following AKs will be set to 0:"
 msgstr "Interessensbekundungszähler der folgenden AKs wird auf 0 gesetzt:"
 
-#: AKModel/views/ak.py:255
+#: AKModel/views/ak.py:259
 msgid "AKs' interest counters set back to 0."
 msgstr "Interessenszähler der AKs zurückgesetzt"
 
-- 
GitLab


From 74ac670a74e517abed97b72e285280486b97ecce Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Thu, 6 Feb 2025 17:41:16 +0100
Subject: [PATCH 59/77] Add export slot length to event

---
 AKModel/migrations/0061_event_export_slot.py | 24 ++++++++++++++++++++
 AKModel/models.py                            | 18 +++++++++++----
 AKModel/views/ak.py                          | 10 ++------
 3 files changed, 39 insertions(+), 13 deletions(-)
 create mode 100644 AKModel/migrations/0061_event_export_slot.py

diff --git a/AKModel/migrations/0061_event_export_slot.py b/AKModel/migrations/0061_event_export_slot.py
new file mode 100644
index 00000000..3b40b88c
--- /dev/null
+++ b/AKModel/migrations/0061_event_export_slot.py
@@ -0,0 +1,24 @@
+# Generated by Django 4.2.13 on 2025-02-06 16:09
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("AKModel", "0060_orga_message_resolved"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="event",
+            name="export_slot",
+            field=models.DecimalField(
+                decimal_places=2,
+                default=1,
+                help_text="Slot duration in hours that is used in the timeslot discretization, when this event is exported for the solver.",
+                max_digits=4,
+                verbose_name="Export Slot Length",
+            ),
+        ),
+    ]
diff --git a/AKModel/models.py b/AKModel/models.py
index 1cd3f1ba..b3820a15 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -151,6 +151,12 @@ class Event(models.Model):
     wiki_export_template_name = models.CharField(verbose_name=_("Wiki Export Template Name"), blank=True, max_length=50)
     default_slot = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Default Slot Length'),
                                        help_text=_('Default length in hours that is assumed for AKs in this event.'))
+    export_slot = models.DecimalField(max_digits=4, decimal_places=2, default=1, verbose_name=_('Export Slot Length'),
+                                        help_text=_(
+                                            'Slot duration in hours that is used in the timeslot discretization, when this event '
+                                            'is exported for the solver.'
+                                        ))
+
 
     contact_email = models.EmailField(verbose_name=_("Contact email address"), blank=True,
                                         help_text=_("An email address that is displayed on every page "
@@ -336,7 +342,7 @@ class Event(models.Model):
 
         return slot_index
 
-    def uniform_time_slots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]:
+    def uniform_time_slots(self, *, slots_in_an_hour: float) -> Iterable[TimeslotBlock]:
         """Uniformly discretize the entire event into blocks of timeslots.
 
         Discretizes entire event uniformly. May not necessarily result in a single block
@@ -358,7 +364,7 @@ class Event(models.Model):
             constraints=all_category_constraints,
         )
 
-    def default_time_slots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]:
+    def default_time_slots(self, *, slots_in_an_hour: float) -> Iterable[TimeslotBlock]:
         """Discretize all default slots into blocks of timeslots.
 
         In the discretization each default slot corresponds to one block.
@@ -384,7 +390,7 @@ class Event(models.Model):
                 constraints=category_constraints,
             )
 
-    def discretize_timeslots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]:
+    def discretize_timeslots(self, *, slots_in_an_hour: float | None = None) -> Iterable[TimeslotBlock]:
         """"Choose discretization scheme.
 
         Uses default_time_slots if the event has any DefaultSlot, otherwise uniform_time_slots.
@@ -395,6 +401,9 @@ class Event(models.Model):
         :ytype: list of TimeslotBlock
         """
 
+        if slots_in_an_hour is None:
+            slots_in_an_hour = float(self.export_slot)
+
         if DefaultSlot.objects.filter(event=self).exists():
             # discretize default slots if they exists
             yield from merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour))
@@ -1007,10 +1016,9 @@ class AKSlot(models.Model):
 
         ceil_offet_eps = decimal.Decimal(1e-4)
 
-        # self.slots_in_an_hour is set in AKJSONExportView
         data = {
             "id": str(self.pk),
-            "duration": math.ceil(self.duration * self.slots_in_an_hour - ceil_offet_eps),
+            "duration": math.ceil(self.duration * self.event.export_slot - ceil_offet_eps),
             "properties": {
                 "conflicts":
                     [str(conflict.pk) for conflict in conflict_slots.all()]
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index bca1c5b3..d9a5e6a8 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -91,17 +91,11 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         rooms = Room.objects.filter(event=self.event)
         context["rooms"] = rooms
 
-        # TODO: Configure magic number in event
-        SLOTS_IN_AN_HOUR = 1
-
         timeslots = {
-            "info": {"duration": (1.0 / SLOTS_IN_AN_HOUR), },
+            "info": {"duration": (1.0 / float(self.event.export_slot)), },
             "blocks": [],
             }
 
-        for slot in context["slots"]:
-            slot.slots_in_an_hour = SLOTS_IN_AN_HOUR
-
         ak_availabilities = {
             ak.pk: Availability.union(ak.availabilities.all())
             for ak in AK.objects.filter(event=self.event).all()
@@ -115,7 +109,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             for person in AKOwner.objects.filter(event=self.event)
         }
 
-        blocks = self.event.discretize_timeslots(slots_in_an_hour=SLOTS_IN_AN_HOUR)
+        blocks = self.event.discretize_timeslots()
 
         for block in blocks:
             current_block = []
-- 
GitLab


From 599aec3562b2ff67d8ab183e33beea8225b60fcf Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Thu, 6 Feb 2025 17:41:45 +0100
Subject: [PATCH 60/77] Update language files

---
 AKModel/locale/de_DE/LC_MESSAGES/django.po | 332 +++++++++++----------
 1 file changed, 172 insertions(+), 160 deletions(-)

diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index 88eb85d5..d4f3db1b 100644
--- a/AKModel/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-02-06 15:48+0000\n"
+"POT-Creation-Date: 2025-02-06 16:38+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"
@@ -33,8 +33,8 @@ msgstr "Plan veröffentlichen"
 msgid "Unpublish plan"
 msgstr "Plan verbergen"
 
-#: AKModel/admin.py:168 AKModel/models.py:689 AKModel/models.py:1120
-#: AKModel/models.py:1156
+#: AKModel/admin.py:168 AKModel/models.py:698 AKModel/models.py:1128
+#: AKModel/models.py:1164
 #: AKModel/templates/admin/AKModel/status/event_aks.html:10
 #: AKModel/views/manage.py:75 AKModel/views/status.py:97
 msgid "AKs"
@@ -60,11 +60,11 @@ msgstr "In Wiki-Syntax exportieren"
 msgid "Cannot export AKs from more than one event at the same time."
 msgstr "Kann nicht AKs von mehreren Events zur selben Zeit exportieren."
 
-#: AKModel/admin.py:320 AKModel/views/ak.py:241
+#: AKModel/admin.py:320 AKModel/views/ak.py:235
 msgid "Reset interest in AKs"
 msgstr "Interesse an AKs zurücksetzen"
 
-#: AKModel/admin.py:330 AKModel/views/ak.py:256
+#: AKModel/admin.py:330 AKModel/views/ak.py:250
 msgid "Reset AKs' interest counters"
 msgstr "Interessenszähler der AKs zurücksetzen"
 
@@ -109,18 +109,18 @@ msgstr "Die eingegebene Verfügbarkeit enthält ein ungültiges Datum."
 msgid "Please fill in your availabilities!"
 msgstr "Bitte Verfügbarkeiten eintragen!"
 
-#: AKModel/availability/models.py:43 AKModel/models.py:160
-#: AKModel/models.py:489 AKModel/models.py:566 AKModel/models.py:599
-#: AKModel/models.py:625 AKModel/models.py:679 AKModel/models.py:821
-#: AKModel/models.py:897 AKModel/models.py:1059 AKModel/models.py:1116
-#: AKModel/models.py:1307
+#: AKModel/availability/models.py:43 AKModel/models.py:166
+#: AKModel/models.py:498 AKModel/models.py:575 AKModel/models.py:608
+#: AKModel/models.py:634 AKModel/models.py:688 AKModel/models.py:830
+#: AKModel/models.py:906 AKModel/models.py:1067 AKModel/models.py:1124
+#: AKModel/models.py:1315
 msgid "Event"
 msgstr "Event"
 
-#: AKModel/availability/models.py:44 AKModel/models.py:490
-#: AKModel/models.py:567 AKModel/models.py:600 AKModel/models.py:626
-#: AKModel/models.py:680 AKModel/models.py:822 AKModel/models.py:898
-#: AKModel/models.py:1060 AKModel/models.py:1117 AKModel/models.py:1308
+#: AKModel/availability/models.py:44 AKModel/models.py:499
+#: AKModel/models.py:576 AKModel/models.py:609 AKModel/models.py:635
+#: AKModel/models.py:689 AKModel/models.py:831 AKModel/models.py:907
+#: AKModel/models.py:1068 AKModel/models.py:1125 AKModel/models.py:1316
 msgid "Associated event"
 msgstr "Zugehöriges Event"
 
@@ -132,8 +132,8 @@ msgstr "Person"
 msgid "Person whose availability this is"
 msgstr "Person deren Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:61 AKModel/models.py:825
-#: AKModel/models.py:887 AKModel/models.py:1126
+#: AKModel/availability/models.py:61 AKModel/models.py:834
+#: AKModel/models.py:896 AKModel/models.py:1134
 msgid "Room"
 msgstr "Raum"
 
@@ -141,8 +141,8 @@ msgstr "Raum"
 msgid "Room whose availability this is"
 msgstr "Raum dessen Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:70 AKModel/models.py:688
-#: AKModel/models.py:886 AKModel/models.py:1054
+#: AKModel/availability/models.py:70 AKModel/models.py:697
+#: AKModel/models.py:895 AKModel/models.py:1062
 msgid "AK"
 msgstr "AK"
 
@@ -150,8 +150,8 @@ msgstr "AK"
 msgid "AK whose availability this is"
 msgstr "Verfügbarkeiten"
 
-#: AKModel/availability/models.py:79 AKModel/models.py:570
-#: AKModel/models.py:1132
+#: AKModel/availability/models.py:79 AKModel/models.py:579
+#: AKModel/models.py:1140
 msgid "AK Category"
 msgstr "AK-Kategorie"
 
@@ -159,7 +159,7 @@ msgstr "AK-Kategorie"
 msgid "AK Category whose availability this is"
 msgstr "AK-Kategorie, deren Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:309 AKModel/models.py:721
+#: AKModel/availability/models.py:309 AKModel/models.py:730
 msgid "Availabilities"
 msgstr "Verfügbarkeiten"
 
@@ -221,7 +221,7 @@ msgstr ""
 "fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen "
 "ausgefüllt zu werden?"
 
-#: AKModel/forms.py:189 AKModel/models.py:1301
+#: AKModel/forms.py:189 AKModel/models.py:1309
 msgid "Default Slots"
 msgstr "Standardslots"
 
@@ -293,8 +293,8 @@ msgstr "Aktivieren?"
 msgid "Finish"
 msgstr "Abschluss"
 
-#: AKModel/models.py:120 AKModel/models.py:558 AKModel/models.py:596
-#: AKModel/models.py:623 AKModel/models.py:641 AKModel/models.py:813
+#: AKModel/models.py:120 AKModel/models.py:567 AKModel/models.py:605
+#: AKModel/models.py:632 AKModel/models.py:650 AKModel/models.py:822
 msgid "Name"
 msgstr "Name"
 
@@ -420,11 +420,23 @@ msgstr "Standardslotlänge"
 msgid "Default length in hours that is assumed for AKs in this event."
 msgstr "Standardlänge von Slots (in Stunden) für dieses Event"
 
-#: AKModel/models.py:155
+#: AKModel/models.py:154
+msgid "Export Slot Length"
+msgstr "Export-Slotlänge"
+
+#: AKModel/models.py:156
+msgid ""
+"Slot duration in hours that is used in the timeslot discretization, when "
+"this event is exported for the solver."
+msgstr ""
+"Länge von Slots (in Stunden) in der Zeitslot-Diskretisierung beim "
+"JSON-Export dieses Events."
+
+#: AKModel/models.py:161
 msgid "Contact email address"
 msgstr "E-Mail Kontaktadresse"
 
-#: AKModel/models.py:156
+#: AKModel/models.py:162
 msgid ""
 "An email address that is displayed on every page and can be used for all "
 "kinds of questions"
@@ -432,16 +444,16 @@ msgstr ""
 "Eine Mailadresse die auf jeder Seite angezeigt wird und für alle Arten von "
 "Fragen genutzt werden kann"
 
-#: AKModel/models.py:161
+#: AKModel/models.py:167
 msgid "Events"
 msgstr "Events"
 
-#: AKModel/models.py:430
+#: AKModel/models.py:439
 #, python-brace-format
 msgid "AK {ak_name} is not assigned any timeslot by the solver"
 msgstr "Dem AK {ak_name} wurde vom Solver kein Zeitslot zugewiesen"
 
-#: AKModel/models.py:440
+#: AKModel/models.py:449
 #, python-brace-format
 msgid ""
 "Duration of AK {ak_name} assigned by solver ({solver_duration} hours) is "
@@ -451,7 +463,7 @@ msgstr ""
 "Stunden) ist kürzer als die aktuell vorgesehene Dauer des Slots "
 "({slot_duration} Stunden)"
 
-#: AKModel/models.py:454
+#: AKModel/models.py:463
 #, python-brace-format
 msgid ""
 "Fixed AK {ak_name} assigned by solver to room {solver_room} is fixed to room "
@@ -460,7 +472,7 @@ msgstr ""
 "Dem fix geplanten AK {ak_name} wurde vom Solver Raum {solver_room} "
 "zugewiesen, dabei ist der AK bereits fix in Raum {slot_room} eingeplant."
 
-#: AKModel/models.py:465
+#: AKModel/models.py:474
 #, python-brace-format
 msgid ""
 "Fixed AK {ak_name} assigned by solver to start at {solver_start} is fixed to "
@@ -469,71 +481,71 @@ msgstr ""
 "Dem fix geplanten AK {ak_name} wurde vom Solver die Startzeit {solver_start} "
 "zugewiesen, dabei ist der AK bereits für {slot_start} eingeplant."
 
-#: AKModel/models.py:484
+#: AKModel/models.py:493
 msgid "Nickname"
 msgstr "Spitzname"
 
-#: AKModel/models.py:484
+#: AKModel/models.py:493
 msgid "Name to identify an AK owner by"
 msgstr "Name, durch den eine AK-Leitung identifiziert wird"
 
-#: AKModel/models.py:485
+#: AKModel/models.py:494
 msgid "Slug"
 msgstr "Slug"
 
-#: AKModel/models.py:485
+#: AKModel/models.py:494
 msgid "Slug for URL generation"
 msgstr "Slug für URL-Generierung"
 
-#: AKModel/models.py:486
+#: AKModel/models.py:495
 msgid "Institution"
 msgstr "Instutution"
 
-#: AKModel/models.py:486
+#: AKModel/models.py:495
 msgid "Uni etc."
 msgstr "Universität o.ä."
 
-#: AKModel/models.py:487 AKModel/models.py:650
+#: AKModel/models.py:496 AKModel/models.py:659
 msgid "Web Link"
 msgstr "Internet Link"
 
-#: AKModel/models.py:487
+#: AKModel/models.py:496
 msgid "Link to Homepage"
 msgstr "Link zu Homepage oder Webseite"
 
-#: AKModel/models.py:493 AKModel/models.py:1125
+#: AKModel/models.py:502 AKModel/models.py:1133
 msgid "AK Owner"
 msgstr "AK-Leitung"
 
-#: AKModel/models.py:494
+#: AKModel/models.py:503
 msgid "AK Owners"
 msgstr "AK-Leitungen"
 
-#: AKModel/models.py:558
+#: AKModel/models.py:567
 msgid "Name of the AK Category"
 msgstr "Name der AK-Kategorie"
 
-#: AKModel/models.py:559 AKModel/models.py:597
+#: AKModel/models.py:568 AKModel/models.py:606
 msgid "Color"
 msgstr "Farbe"
 
-#: AKModel/models.py:559 AKModel/models.py:597
+#: AKModel/models.py:568 AKModel/models.py:606
 msgid "Color for displaying"
 msgstr "Farbe für die Anzeige"
 
-#: AKModel/models.py:560 AKModel/models.py:644
+#: AKModel/models.py:569 AKModel/models.py:653
 msgid "Description"
 msgstr "Beschreibung"
 
-#: AKModel/models.py:561
+#: AKModel/models.py:570
 msgid "Short description of this AK Category"
 msgstr "Beschreibung der AK-Kategorie"
 
-#: AKModel/models.py:562
+#: AKModel/models.py:571
 msgid "Present by default"
 msgstr "Defaultmäßig präsentieren"
 
-#: AKModel/models.py:563
+#: AKModel/models.py:572
 msgid ""
 "Present AKs of this category by default if AK owner did not specify whether "
 "this AK should be presented?"
@@ -541,132 +553,132 @@ msgstr ""
 "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für "
 "ihren AK nicht explizit spezifiziert haben?"
 
-#: AKModel/models.py:571
+#: AKModel/models.py:580
 msgid "AK Categories"
 msgstr "AK-Kategorien"
 
-#: AKModel/models.py:596
+#: AKModel/models.py:605
 msgid "Name of the AK Track"
 msgstr "Name des AK-Tracks"
 
-#: AKModel/models.py:603
+#: AKModel/models.py:612
 msgid "AK Track"
 msgstr "AK-Track"
 
-#: AKModel/models.py:604
+#: AKModel/models.py:613
 msgid "AK Tracks"
 msgstr "AK-Tracks"
 
-#: AKModel/models.py:623
+#: AKModel/models.py:632
 msgid "Name of the Requirement"
 msgstr "Name der Anforderung"
 
-#: AKModel/models.py:629 AKModel/models.py:1129
+#: AKModel/models.py:638 AKModel/models.py:1137
 msgid "AK Requirement"
 msgstr "AK-Anforderung"
 
-#: AKModel/models.py:630
+#: AKModel/models.py:639
 msgid "AK Requirements"
 msgstr "AK-Anforderungen"
 
-#: AKModel/models.py:641
+#: AKModel/models.py:650
 msgid "Name of the AK"
 msgstr "Name des AKs"
 
-#: AKModel/models.py:642
+#: AKModel/models.py:651
 msgid "Short Name"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:643
+#: AKModel/models.py:652
 msgid "Name displayed in the schedule"
 msgstr "Name zur Anzeige im AK-Plan"
 
-#: AKModel/models.py:644
+#: AKModel/models.py:653
 msgid "Description of the AK"
 msgstr "Beschreibung des AKs"
 
-#: AKModel/models.py:646
+#: AKModel/models.py:655
 msgid "Owners"
 msgstr "Leitungen"
 
-#: AKModel/models.py:647
+#: AKModel/models.py:656
 msgid "Those organizing the AK"
 msgstr "Menschen, die den AK organisieren und halten"
 
-#: AKModel/models.py:650
+#: AKModel/models.py:659
 msgid "Link to wiki page"
 msgstr "Link zur Wiki Seite"
 
-#: AKModel/models.py:651
+#: AKModel/models.py:660
 msgid "Protocol Link"
 msgstr "Protokolllink"
 
-#: AKModel/models.py:651
+#: AKModel/models.py:660
 msgid "Link to protocol"
 msgstr "Link zum Protokoll"
 
-#: AKModel/models.py:653
+#: AKModel/models.py:662
 msgid "Category"
 msgstr "Kategorie"
 
-#: AKModel/models.py:654
+#: AKModel/models.py:663
 msgid "Category of the AK"
 msgstr "Kategorie des AKs"
 
-#: AKModel/models.py:655
+#: AKModel/models.py:664
 msgid "Track"
 msgstr "Track"
 
-#: AKModel/models.py:656
+#: AKModel/models.py:665
 msgid "Track the AK belongs to"
 msgstr "Track zu dem der AK gehört"
 
-#: AKModel/models.py:658
+#: AKModel/models.py:667
 msgid "Resolution Intention"
 msgstr "Resolutionsabsicht"
 
-#: AKModel/models.py:659
+#: AKModel/models.py:668
 msgid "Intends to submit a resolution"
 msgstr "Beabsichtigt eine Resolution einzureichen"
 
-#: AKModel/models.py:660
+#: AKModel/models.py:669
 msgid "Present this AK"
 msgstr "AK präsentieren"
 
-#: AKModel/models.py:661
+#: AKModel/models.py:670
 msgid "Present results of this AK"
 msgstr "Die Ergebnisse dieses AKs vorstellen"
 
-#: AKModel/models.py:663 AKModel/models.py:716 AKModel/views/status.py:170
+#: AKModel/models.py:672 AKModel/models.py:725 AKModel/views/status.py:170
 msgid "Requirements"
 msgstr "Anforderungen"
 
-#: AKModel/models.py:664
+#: AKModel/models.py:673
 msgid "AK's Requirements"
 msgstr "Anforderungen des AKs"
 
-#: AKModel/models.py:666
+#: AKModel/models.py:675
 msgid "Conflicting AKs"
 msgstr "AK-Konflikte"
 
-#: AKModel/models.py:667
+#: AKModel/models.py:676
 msgid "AKs that conflict and thus must not take place at the same time"
 msgstr ""
 "AKs, die Konflikte haben und deshalb nicht gleichzeitig stattfinden dürfen"
 
-#: AKModel/models.py:668
+#: AKModel/models.py:677
 msgid "Prerequisite AKs"
 msgstr "Vorausgesetzte AKs"
 
-#: AKModel/models.py:669
+#: AKModel/models.py:678
 msgid "AKs that should precede this AK in the schedule"
 msgstr "AKs die im AK-Plan vor diesem AK stattfinden müssen"
 
-#: AKModel/models.py:671
+#: AKModel/models.py:680
 msgid "Organizational Notes"
 msgstr "Notizen zur Organisation"
 
-#: AKModel/models.py:672
+#: AKModel/models.py:681
 msgid ""
 "Notes to organizers. These are public. For private notes, please use the "
 "button for private messages on the detail page of this AK (after creation/"
@@ -676,299 +688,299 @@ msgstr ""
 "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem "
 "Anlegen/Bearbeiten)."
 
-#: AKModel/models.py:675 AKModel/models.py:714
+#: AKModel/models.py:684 AKModel/models.py:723
 msgid "Interest"
 msgstr "Interesse"
 
-#: AKModel/models.py:675
+#: AKModel/models.py:684
 msgid "Expected number of people"
 msgstr "Erwartete Personenzahl"
 
-#: AKModel/models.py:676
+#: AKModel/models.py:685
 msgid "Interest Counter"
 msgstr "Interessenszähler"
 
-#: AKModel/models.py:677
+#: AKModel/models.py:686
 msgid "People who have indicated interest online"
 msgstr "Anzahl Personen, die online Interesse bekundet haben"
 
-#: AKModel/models.py:682
+#: AKModel/models.py:691
 msgid "Export?"
 msgstr "Export?"
 
-#: AKModel/models.py:683
+#: AKModel/models.py:692
 msgid "Include AK in wiki export?"
 msgstr "AK bei Wiki-Export berücksichtigen?"
 
-#: AKModel/models.py:718
+#: AKModel/models.py:727
 msgid "Conflicts"
 msgstr "Konflikte"
 
-#: AKModel/models.py:720
+#: AKModel/models.py:729
 msgid "Prerequisites"
 msgstr "Voraussetzungen"
 
-#: AKModel/models.py:813
+#: AKModel/models.py:822
 msgid "Name or number of the room"
 msgstr "Name oder Nummer des Raums"
 
-#: AKModel/models.py:814
+#: AKModel/models.py:823
 msgid "Location"
 msgstr "Ort"
 
-#: AKModel/models.py:815
+#: AKModel/models.py:824
 msgid "Name or number of the location"
 msgstr "Name oder Nummer des Ortes"
 
-#: AKModel/models.py:816
+#: AKModel/models.py:825
 msgid "Capacity"
 msgstr "Kapazität"
 
-#: AKModel/models.py:817
+#: AKModel/models.py:826
 msgid "Maximum number of people (-1 for unlimited)."
 msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)."
 
-#: AKModel/models.py:818
+#: AKModel/models.py:827
 msgid "Properties"
 msgstr "Eigenschaften"
 
-#: AKModel/models.py:819
+#: AKModel/models.py:828
 msgid "AK requirements fulfilled by the room"
 msgstr "AK-Anforderungen, die dieser Raum erfüllt"
 
-#: AKModel/models.py:826 AKModel/views/status.py:59
+#: AKModel/models.py:835 AKModel/views/status.py:59
 msgid "Rooms"
 msgstr "Räume"
 
-#: AKModel/models.py:886
+#: AKModel/models.py:895
 msgid "AK being mapped"
 msgstr "AK, der zugeordnet wird"
 
-#: AKModel/models.py:888
+#: AKModel/models.py:897
 msgid "Room the AK will take place in"
 msgstr "Raum in dem der AK stattfindet"
 
-#: AKModel/models.py:889 AKModel/models.py:1304
+#: AKModel/models.py:898 AKModel/models.py:1312
 msgid "Slot Begin"
 msgstr "Beginn des Slots"
 
-#: AKModel/models.py:889 AKModel/models.py:1304
+#: AKModel/models.py:898 AKModel/models.py:1312
 msgid "Time and date the slot begins"
 msgstr "Zeit und Datum zu der der AK beginnt"
 
-#: AKModel/models.py:891
+#: AKModel/models.py:900
 msgid "Duration"
 msgstr "Dauer"
 
-#: AKModel/models.py:892
+#: AKModel/models.py:901
 msgid "Length in hours"
 msgstr "Länge in Stunden"
 
-#: AKModel/models.py:894
+#: AKModel/models.py:903
 msgid "Scheduling fixed"
 msgstr "Planung fix"
 
-#: AKModel/models.py:895
+#: AKModel/models.py:904
 msgid "Length and time of this AK should not be changed"
 msgstr "Dauer und Zeit dieses AKs sollten nicht verändert werden"
 
-#: AKModel/models.py:900
+#: AKModel/models.py:909
 msgid "Last update"
 msgstr "Letzte Aktualisierung"
 
-#: AKModel/models.py:903
+#: AKModel/models.py:912
 msgid "AK Slot"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:904 AKModel/models.py:1122 AKModel/models.py:1157
+#: AKModel/models.py:913 AKModel/models.py:1130 AKModel/models.py:1165
 msgid "AK Slots"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:926 AKModel/models.py:935
+#: AKModel/models.py:935 AKModel/models.py:944
 msgid "Not scheduled yet"
 msgstr "Noch nicht geplant"
 
-#: AKModel/models.py:1055
+#: AKModel/models.py:1063
 msgid "AK this message belongs to"
 msgstr "AK zu dem die Nachricht gehört"
 
-#: AKModel/models.py:1056
+#: AKModel/models.py:1064
 msgid "Message text"
 msgstr "Nachrichtentext"
 
-#: AKModel/models.py:1057
+#: AKModel/models.py:1065
 msgid "Message to the organizers. This is not publicly visible."
 msgstr ""
 "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar."
 
-#: AKModel/models.py:1061
+#: AKModel/models.py:1069
 msgid "Resolved"
 msgstr "Erledigt"
 
-#: AKModel/models.py:1062
+#: AKModel/models.py:1070
 msgid "This message has been resolved (no further action needed)"
 msgstr ""
 "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen "
 "notwendig)"
 
-#: AKModel/models.py:1065
+#: AKModel/models.py:1073
 msgid "AK Orga Message"
 msgstr "AK-Organachricht"
 
-#: AKModel/models.py:1066
+#: AKModel/models.py:1074
 msgid "AK Orga Messages"
 msgstr "AK-Organachrichten"
 
-#: AKModel/models.py:1083
+#: AKModel/models.py:1091
 msgid "Constraint Violation"
 msgstr "Constraintverletzung"
 
-#: AKModel/models.py:1084
+#: AKModel/models.py:1092
 msgid "Constraint Violations"
 msgstr "Constraintverletzungen"
 
-#: AKModel/models.py:1091
+#: AKModel/models.py:1099
 msgid "Owner has two parallel slots"
 msgstr "Leitung hat zwei Slots parallel"
 
-#: AKModel/models.py:1092
+#: AKModel/models.py:1100
 msgid "AK Slot was scheduled outside the AK's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des AKs platziert"
 
-#: AKModel/models.py:1093
+#: AKModel/models.py:1101
 msgid "Room has two AK slots scheduled at the same time"
 msgstr "Raum hat zwei AK Slots gleichzeitig"
 
-#: AKModel/models.py:1094
+#: AKModel/models.py:1102
 msgid "Room does not satisfy the requirement of the scheduled AK"
 msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht"
 
-#: AKModel/models.py:1095
+#: AKModel/models.py:1103
 msgid "AK Slot is scheduled at the same time as an AK listed as a conflict"
 msgstr ""
 "AK Slot wurde wurde zur gleichen Zeit wie ein Konflikt des AKs platziert"
 
-#: AKModel/models.py:1096
+#: AKModel/models.py:1104
 msgid "AK Slot is scheduled before an AK listed as a prerequisite"
 msgstr "AK Slot wurde vor einem als Voraussetzung gelisteten AK platziert"
 
-#: AKModel/models.py:1098
+#: AKModel/models.py:1106
 msgid ""
 "AK Slot for AK with intention to submit a resolution is scheduled after "
 "resolution deadline"
 msgstr ""
 "AK Slot eines AKs mit Resoabsicht wurde nach der Resodeadline platziert"
 
-#: AKModel/models.py:1099
+#: AKModel/models.py:1107
 msgid "AK Slot in a category is outside that categories availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeiten seiner Kategorie"
 
-#: AKModel/models.py:1100
+#: AKModel/models.py:1108
 msgid "Two AK Slots for the same AK scheduled at the same time"
 msgstr "Zwei AK Slots eines AKs wurden zur selben Zeit platziert"
 
-#: AKModel/models.py:1101
+#: AKModel/models.py:1109
 msgid "Room does not have enough space for interest in scheduled AK Slot"
 msgstr "Room hat nicht genug Platz für das Interesse am geplanten AK-Slot"
 
-#: AKModel/models.py:1102
+#: AKModel/models.py:1110
 msgid "AK Slot is scheduled outside the event's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des Events platziert"
 
-#: AKModel/models.py:1108
+#: AKModel/models.py:1116
 msgid "Warning"
 msgstr "Warnung"
 
-#: AKModel/models.py:1109
+#: AKModel/models.py:1117
 msgid "Violation"
 msgstr "Verletzung"
 
-#: AKModel/models.py:1111
+#: AKModel/models.py:1119
 msgid "Type"
 msgstr "Art"
 
-#: AKModel/models.py:1112
+#: AKModel/models.py:1120
 msgid "Type of violation, i.e. what kind of constraint was violated"
 msgstr "Art der Verletzung, gibt an welche Art Constraint verletzt wurde"
 
-#: AKModel/models.py:1113
+#: AKModel/models.py:1121
 msgid "Level"
 msgstr "Level"
 
-#: AKModel/models.py:1114
+#: AKModel/models.py:1122
 msgid "Severity level of the violation"
 msgstr "Schweregrad der Verletzung"
 
-#: AKModel/models.py:1121
+#: AKModel/models.py:1129
 msgid "AK(s) belonging to this constraint"
 msgstr "AK(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:1123
+#: AKModel/models.py:1131
 msgid "AK Slot(s) belonging to this constraint"
 msgstr "AK Slot(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:1125
+#: AKModel/models.py:1133
 msgid "AK Owner belonging to this constraint"
 msgstr "AK Leitung(en), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:1127
+#: AKModel/models.py:1135
 msgid "Room belonging to this constraint"
 msgstr "Raum, der zu diesem Constraint gehört"
 
-#: AKModel/models.py:1130
+#: AKModel/models.py:1138
 msgid "AK Requirement belonging to this constraint"
 msgstr "AK Anforderung, die zu diesem Constraint gehört"
 
-#: AKModel/models.py:1132
+#: AKModel/models.py:1140
 msgid "AK Category belonging to this constraint"
 msgstr "AK Kategorie, di zu diesem Constraint gehört"
 
-#: AKModel/models.py:1134
+#: AKModel/models.py:1142
 msgid "Comment"
 msgstr "Kommentar"
 
-#: AKModel/models.py:1134
+#: AKModel/models.py:1142
 msgid "Comment or further details for this violation"
 msgstr "Kommentar oder weitere Details zu dieser Vereletzung"
 
-#: AKModel/models.py:1137
+#: AKModel/models.py:1145
 msgid "Timestamp"
 msgstr "Timestamp"
 
-#: AKModel/models.py:1137
+#: AKModel/models.py:1145
 msgid "Time of creation"
 msgstr "Zeitpunkt der ERstellung"
 
-#: AKModel/models.py:1138
+#: AKModel/models.py:1146
 msgid "Manually Resolved"
 msgstr "Manuell behoben"
 
-#: AKModel/models.py:1139
+#: AKModel/models.py:1147
 msgid "Mark this violation manually as resolved"
 msgstr "Markiere diese Verletzung manuell als behoben"
 
-#: AKModel/models.py:1166
+#: AKModel/models.py:1174
 #: AKModel/templates/admin/AKModel/requirements_overview.html:27
 msgid "Details"
 msgstr "Details"
 
-#: AKModel/models.py:1300
+#: AKModel/models.py:1308
 msgid "Default Slot"
 msgstr "Standardslot"
 
-#: AKModel/models.py:1305
+#: AKModel/models.py:1313
 msgid "Slot End"
 msgstr "Ende des Slots"
 
-#: AKModel/models.py:1305
+#: AKModel/models.py:1313
 msgid "Time and date the slot ends"
 msgstr "Zeit und Datum zu der der Slot endet"
 
-#: AKModel/models.py:1310
+#: AKModel/models.py:1318
 msgid "Primary categories"
 msgstr "Primäre Kategorien"
 
-#: AKModel/models.py:1311
+#: AKModel/models.py:1319
 msgid "Categories that should be assigned to this slot primarily"
 msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen"
 
@@ -1178,35 +1190,35 @@ msgstr "AK-CSV-Export"
 msgid "AK JSON Export"
 msgstr "AK-JSON-Export"
 
-#: AKModel/views/ak.py:190
+#: AKModel/views/ak.py:184
 msgid "AK Wiki Export"
 msgstr "AK-Wiki-Export"
 
-#: AKModel/views/ak.py:201 AKModel/views/manage.py:55
+#: AKModel/views/ak.py:195 AKModel/views/manage.py:55
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKModel/views/ak.py:213
+#: AKModel/views/ak.py:207
 msgid "Delete AK Orga Messages"
 msgstr "AK-Organachrichten löschen"
 
-#: AKModel/views/ak.py:231
+#: AKModel/views/ak.py:225
 msgid "AK Orga Messages successfully deleted"
 msgstr "AK-Organachrichten erfolgreich gelöscht"
 
-#: AKModel/views/ak.py:243
+#: AKModel/views/ak.py:237
 msgid "Interest of the following AKs will be set to not filled (-1):"
 msgstr "Interesse an den folgenden AKs wird auf nicht ausgefüllt (-1) gesetzt:"
 
-#: AKModel/views/ak.py:244
+#: AKModel/views/ak.py:238
 msgid "Reset of interest in AKs successful."
 msgstr "Interesse an AKs erfolgreich zurückgesetzt."
 
-#: AKModel/views/ak.py:258
+#: AKModel/views/ak.py:252
 msgid "Interest counter of the following AKs will be set to 0:"
 msgstr "Interessensbekundungszähler der folgenden AKs wird auf 0 gesetzt:"
 
-#: AKModel/views/ak.py:259
+#: AKModel/views/ak.py:253
 msgid "AKs' interest counters set back to 0."
 msgstr "Interessenszähler der AKs zurückgesetzt"
 
-- 
GitLab


From 8501afaf52df8c23716109cb7d6c9270e5fbb363 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Thu, 6 Feb 2025 17:46:26 +0100
Subject: [PATCH 61/77] Address line length

---
 AKModel/models.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index b3820a15..e3530861 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -153,8 +153,8 @@ class Event(models.Model):
                                        help_text=_('Default length in hours that is assumed for AKs in this event.'))
     export_slot = models.DecimalField(max_digits=4, decimal_places=2, default=1, verbose_name=_('Export Slot Length'),
                                         help_text=_(
-                                            'Slot duration in hours that is used in the timeslot discretization, when this event '
-                                            'is exported for the solver.'
+                                            'Slot duration in hours that is used in the timeslot discretization, '
+                                            'when this event is exported for the solver.'
                                         ))
 
 
-- 
GitLab


From ed98da8ded352f56fc838552d7133839423c916b Mon Sep 17 00:00:00 2001
From: Lorenzo Conti <lorenzo@uni-bonn.de>
Date: Thu, 6 Feb 2025 18:14:09 +0000
Subject: [PATCH 62/77] Apply 3 suggestion(s) to 2 file(s)

Co-authored-by: Felix Blanke <s6feblan@uni-bonn.de>
---
 AKModel/models.py   | 4 ++--
 AKModel/views/ak.py | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index e3530861..8601a992 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -402,7 +402,7 @@ class Event(models.Model):
         """
 
         if slots_in_an_hour is None:
-            slots_in_an_hour = float(self.export_slot)
+            slots_in_an_hour = 1.0 / float(self.export_slot)
 
         if DefaultSlot.objects.filter(event=self).exists():
             # discretize default slots if they exists
@@ -1018,7 +1018,7 @@ class AKSlot(models.Model):
 
         data = {
             "id": str(self.pk),
-            "duration": math.ceil(self.duration * self.event.export_slot - ceil_offet_eps),
+            "duration": math.ceil(self.duration / self.event.export_slot - ceil_offet_eps),
             "properties": {
                 "conflicts":
                     [str(conflict.pk) for conflict in conflict_slots.all()]
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index d9a5e6a8..f6fd7932 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -92,7 +92,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         context["rooms"] = rooms
 
         timeslots = {
-            "info": {"duration": (1.0 / float(self.event.export_slot)), },
+            "info": {"duration": float(self.event.export_slot)},
             "blocks": [],
             }
 
-- 
GitLab


From dafdcf591994c91df1d0ea001a560a48862e3280 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Sat, 8 Feb 2025 12:59:44 +0100
Subject: [PATCH 63/77] If no owner availability is given, assume avail for
 full event

---
 AKModel/models.py                 | 3 ++-
 AKModel/tests/test_json_export.py | 4 ++++
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 8601a992..f11c66e2 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -1006,7 +1006,8 @@ class AKSlot(models.Model):
             ak_time_constraints = []
 
         def _owner_time_constraints(owner: AKOwner):
-            if Availability.is_event_covered(self.event, owner.availabilities.all()):
+            owner_avails = owner.availabilities.all()
+            if not owner_avails or Availability.is_event_covered(self.event, owner_avails):
                 return []
             return [f"availability-person-{owner.pk}"]
 
diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py
index d53cd4c5..5a604dc7 100644
--- a/AKModel/tests/test_json_export.py
+++ b/AKModel/tests/test_json_export.py
@@ -479,6 +479,10 @@ class JSONExportTest(TestCase):
 
                     for owner in slot.ak.owners.all():
                         # restricted owner availability
+                        if not owner.availabilities.all():
+                            # no availability for owner -> assume full event is covered
+                            continue
+
                         if not Availability.is_event_covered(
                             slot.event, owner.availabilities.all()
                         ):
-- 
GitLab


From 97d3bb1f5ca9d27dec669450bfc23dc092bdb429 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Sun, 9 Feb 2025 23:52:03 +0100
Subject: [PATCH 64/77] Store IDs as integer in JSON export

---
 AKModel/models.py                 | 12 +++---
 AKModel/tests/test_json_export.py | 67 +++++++++++++++++--------------
 AKModel/views/ak.py               |  2 +-
 3 files changed, 43 insertions(+), 38 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index f11c66e2..5bd570dc 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -871,7 +871,7 @@ class Room(models.Model):
             time_constraints = [f"availability-room-{self.pk}"]
 
         data = {
-            "id": str(self.pk),
+            "id": self.pk,
             "info": {
                 "name": self.name,
             },
@@ -1018,13 +1018,13 @@ class AKSlot(models.Model):
         ceil_offet_eps = decimal.Decimal(1e-4)
 
         data = {
-            "id": str(self.pk),
+            "id": self.pk,
             "duration": math.ceil(self.duration / self.event.export_slot - ceil_offet_eps),
             "properties": {
                 "conflicts":
-                    [str(conflict.pk) for conflict in conflict_slots.all()]
-                  + [str(second_slot.pk) for second_slot in other_ak_slots.all()],
-                "dependencies": [str(dep.pk) for dep in dependency_slots.all()],
+                    [conflict.pk for conflict in conflict_slots.all()]
+                  + [second_slot.pk for second_slot in other_ak_slots.all()],
+                "dependencies": [dep.pk for dep in dependency_slots.all()],
             },
             "room_constraints": [constraint.name
                                  for constraint in self.ak.requirements.all()],
@@ -1036,7 +1036,7 @@ class AKSlot(models.Model):
                 "description": self.ak.description,
                 "reso": self.ak.reso,
                 "duration_in_hours": float(self.duration),
-                "django_ak_id": str(self.ak.pk),
+                "django_ak_id": self.ak.pk,
                 },
             }
 
diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py
index 5a604dc7..ee3774be 100644
--- a/AKModel/tests/test_json_export.py
+++ b/AKModel/tests/test_json_export.py
@@ -89,7 +89,7 @@ class JSONExportTest(TestCase):
             with self.subTest(event=event):
                 self.set_up_event(event=event)
                 self.assertEqual(
-                    {str(slot.pk) for slot in self.ak_slots},
+                    {slot.pk for slot in self.ak_slots},
                     self.export_objects["aks"].keys(),
                     "Exported AKs does not match the AKSlots of the event",
                 )
@@ -152,7 +152,7 @@ class JSONExportTest(TestCase):
                         f"{item} properties keys not as expected",
                     )
 
-                    self._check_type(ak["id"], str, "id", item=item)
+                    self._check_type(ak["id"], int, "id", item=item)
                     self._check_type(ak["duration"], int, "duration", item=item)
                     self._check_type(ak["info"]["name"], str, "info/name", item=item)
                     self._check_type(ak["info"]["head"], str, "info/head", item=item)
@@ -168,16 +168,22 @@ class JSONExportTest(TestCase):
                     )
                     self._check_type(
                         ak["info"]["django_ak_id"],
-                        str,
+                        int,
                         "info/django_ak_id",
                         item=item,
                     )
 
                     self._check_lst(
-                        ak["properties"]["conflicts"], "conflicts", item=item
+                        ak["properties"]["conflicts"],
+                        "conflicts",
+                        item=item,
+                        contained_type=int,
                     )
                     self._check_lst(
-                        ak["properties"]["dependencies"], "dependencies", item=item
+                        ak["properties"]["dependencies"],
+                        "dependencies",
+                        item=item,
+                        contained_type=int,
                     )
                     self._check_lst(
                         ak["time_constraints"], "time_constraints", item=item
@@ -212,7 +218,7 @@ class JSONExportTest(TestCase):
                         f"{item} info keys not as expected",
                     )
 
-                    self._check_type(room["id"], str, "id", item=item)
+                    self._check_type(room["id"], int, "id", item=item)
                     self._check_type(room["capacity"], int, "capacity", item=item)
                     self._check_type(room["info"]["name"], str, "info/name", item=item)
 
@@ -279,7 +285,7 @@ class JSONExportTest(TestCase):
                         {"start", "end"},
                         f"{item} info keys not as expected",
                     )
-                    self._check_type(timeslot["id"], str, "id", item=item)
+                    self._check_type(timeslot["id"], int, "id", item=item)
                     self._check_type(
                         timeslot["info"]["start"], str, "info/start", item=item
                     )
@@ -292,10 +298,10 @@ class JSONExportTest(TestCase):
                     if prev_id is not None:
                         self.assertLess(
                             prev_id,
-                            int(timeslot["id"]),
+                            timeslot["id"],
                             "timeslot ids must be increasing",
                         )
-                    prev_id = int(timeslot["id"])
+                    prev_id = timeslot["id"]
 
     def test_general_conformity_to_spec(self):
         """Test if rest of JSON structure and types conform to standard."""
@@ -332,7 +338,7 @@ class JSONExportTest(TestCase):
                 self.set_up_event(event=event)
 
                 for slot in self.ak_slots:
-                    ak = self.export_objects["aks"][str(slot.pk)]
+                    ak = self.export_objects["aks"][slot.pk]
 
                     self.assertLessEqual(
                         float(slot.duration) * self.slots_in_an_hour - 1e-4,
@@ -359,23 +365,22 @@ class JSONExportTest(TestCase):
                 self.set_up_event(event=event)
 
                 for slot in self.ak_slots:
-                    ak = self.export_objects["aks"][str(slot.pk)]
-                    conflict_slots = self.ak_slots.filter(
-                        ak__in=slot.ak.conflicts.all()
-                    ).values_list("pk", flat=True)
-                    conflict_pks = {str(conflict_pk) for conflict_pk in conflict_slots}
+                    ak = self.export_objects["aks"][slot.pk]
+                    conflict_slots = set(
+                        self.ak_slots.filter(
+                            ak__in=slot.ak.conflicts.all()
+                        ).values_list("pk", flat=True)
+                    )
 
                     other_ak_slots = (
                         self.ak_slots.filter(ak=slot.ak)
                         .exclude(pk=slot.pk)
                         .values_list("pk", flat=True)
                     )
-                    conflict_pks.update(
-                        str(other_slot_pk) for other_slot_pk in other_ak_slots
-                    )
+                    conflict_slots.update(other_ak_slots)
 
                     self.assertEqual(
-                        conflict_pks,
+                        conflict_slots,
                         set(ak["properties"]["conflicts"]),
                         f"Conflicts for slot {slot.pk} not as expected",
                     )
@@ -387,13 +392,13 @@ class JSONExportTest(TestCase):
                 self.set_up_event(event=event)
 
                 for slot in self.ak_slots:
-                    ak = self.export_objects["aks"][str(slot.pk)]
+                    ak = self.export_objects["aks"][slot.pk]
                     dependency_slots = self.ak_slots.filter(
                         ak__in=slot.ak.prerequisites.all()
                     ).values_list("pk", flat=True)
 
                     self.assertEqual(
-                        {str(dep_pk) for dep_pk in dependency_slots},
+                        set(dependency_slots),
                         set(ak["properties"]["dependencies"]),
                         f"Dependencies for slot {slot.pk} not as expected",
                     )
@@ -405,7 +410,7 @@ class JSONExportTest(TestCase):
                 self.set_up_event(event=event)
 
                 for slot in self.ak_slots:
-                    ak = self.export_objects["aks"][str(slot.pk)]
+                    ak = self.export_objects["aks"][slot.pk]
                     self.assertEqual(slot.ak.reso, ak["info"]["reso"])
                     self.assertEqual(
                         slot.ak.reso, "resolution" in ak["time_constraints"]
@@ -418,13 +423,13 @@ class JSONExportTest(TestCase):
                 self.set_up_event(event=event)
 
                 for slot in self.ak_slots:
-                    ak = self.export_objects["aks"][str(slot.pk)]
+                    ak = self.export_objects["aks"][slot.pk]
                     self.assertEqual(ak["info"]["name"], slot.ak.name)
                     self.assertEqual(
                         ak["info"]["head"], ", ".join(map(str, slot.ak.owners.all()))
                     )
                     self.assertEqual(ak["info"]["description"], slot.ak.description)
-                    self.assertEqual(ak["info"]["django_ak_id"], str(slot.ak.pk))
+                    self.assertEqual(ak["info"]["django_ak_id"], slot.ak.pk)
 
     def test_ak_room_constraints(self):
         """Test if AK room constraints are exported as expected."""
@@ -433,7 +438,7 @@ class JSONExportTest(TestCase):
                 self.set_up_event(event=event)
 
                 for slot in self.ak_slots:
-                    ak = self.export_objects["aks"][str(slot.pk)]
+                    ak = self.export_objects["aks"][slot.pk]
                     requirements = list(
                         slot.ak.requirements.values_list("name", flat=True)
                     )
@@ -488,7 +493,7 @@ class JSONExportTest(TestCase):
                         ):
                             time_constraints.add(f"availability-person-{owner.pk}")
 
-                    ak = self.export_objects["aks"][str(slot.pk)]
+                    ak = self.export_objects["aks"][slot.pk]
                     self.assertEqual(
                         set(ak["time_constraints"]),
                         time_constraints,
@@ -502,7 +507,7 @@ class JSONExportTest(TestCase):
                 self.set_up_event(event=event)
 
                 self.assertEqual(
-                    {str(room.pk) for room in self.rooms},
+                    {room.pk for room in self.rooms},
                     self.export_objects["rooms"].keys(),
                     "Exported Rooms do not match the Rooms of the event",
                 )
@@ -514,7 +519,7 @@ class JSONExportTest(TestCase):
                 self.set_up_event(event=event)
 
                 for room in self.rooms:
-                    export_room = self.export_objects["rooms"][str(room.pk)]
+                    export_room = self.export_objects["rooms"][room.pk]
                     self.assertEqual(room.capacity, export_room["capacity"])
 
     def test_room_info(self):
@@ -524,7 +529,7 @@ class JSONExportTest(TestCase):
                 self.set_up_event(event=event)
 
                 for room in self.rooms:
-                    export_room = self.export_objects["rooms"][str(room.pk)]
+                    export_room = self.export_objects["rooms"][room.pk]
                     self.assertEqual(room.name, export_room["info"]["name"])
 
     def test_room_timeconstraints(self):
@@ -542,7 +547,7 @@ class JSONExportTest(TestCase):
                     ):
                         time_constraints.add(f"availability-room-{room.pk}")
 
-                    export_room = self.export_objects["rooms"][str(room.pk)]
+                    export_room = self.export_objects["rooms"][room.pk]
                     self.assertEqual(
                         time_constraints, set(export_room["time_constraints"])
                     )
@@ -568,7 +573,7 @@ class JSONExportTest(TestCase):
 
                     fulfilled_room_constraints.add(f"fixed-room-{room.pk}")
 
-                    export_room = self.export_objects["rooms"][str(room.pk)]
+                    export_room = self.export_objects["rooms"][room.pk]
                     self.assertEqual(
                         fulfilled_room_constraints,
                         set(export_room["fulfilled_room_constraints"]),
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index f6fd7932..d2f17676 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -147,7 +147,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                 time_constraints.extend(timeslot.constraints)
 
                 current_block.append({
-                    "id": str(timeslot.idx),
+                    "id": timeslot.idx,
                     "info": {
                         "start": timeslot.avail.start.astimezone(self.event.timezone).strftime("%Y-%m-%d %H:%M"),
                         "end": timeslot.avail.end.astimezone(self.event.timezone).strftime("%Y-%m-%d %H:%M"),
-- 
GitLab


From 444e22061cef65960ef19e203e5ac8d1e82fb002 Mon Sep 17 00:00:00 2001
From: Felix Blanke <s6feblan@uni-bonn.de>
Date: Tue, 11 Feb 2025 11:20:04 +0000
Subject: [PATCH 65/77] Expand block info in JSON export

---
 AKModel/tests/test_json_export.py | 187 ++++++++++++++++++++----------
 AKModel/views/ak.py               |  27 ++++-
 2 files changed, 150 insertions(+), 64 deletions(-)

diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py
index 5a604dc7..48c8d7fd 100644
--- a/AKModel/tests/test_json_export.py
+++ b/AKModel/tests/test_json_export.py
@@ -248,7 +248,7 @@ class JSONExportTest(TestCase):
                 )
                 self.assertEqual(
                     self.export_dict["timeslots"]["info"].keys(),
-                    {"duration"},
+                    {"duration", "blocknames"},
                     "timeslot info keys not as expected",
                 )
                 self._check_type(
@@ -257,6 +257,21 @@ class JSONExportTest(TestCase):
                     "info/duration",
                     item=item,
                 )
+                self._check_lst(
+                    self.export_dict["timeslots"]["info"]["blocknames"],
+                    "info/blocknames",
+                    item=item,
+                    contained_type=list,
+                )
+                for blockname in self.export_dict["timeslots"]["info"]["blocknames"]:
+                    self.assertEqual(len(blockname), 2)
+                    self._check_lst(
+                        blockname,
+                        "info/blocknames/entry",
+                        item=item,
+                        contained_type=str,
+                    )
+
                 self._check_lst(
                     self.export_dict["timeslots"]["blocks"],
                     "blocks",
@@ -710,72 +725,120 @@ class JSONExportTest(TestCase):
                 self.set_up_event(event=event)
 
                 cat_avails = self._get_cat_availability()
-                for timeslot in chain.from_iterable(
+                num_blocks = len(self.export_dict["timeslots"]["blocks"])
+                for block_idx, block in enumerate(
                     self.export_dict["timeslots"]["blocks"]
                 ):
-                    start, end = self._get_timeslot_start_end(timeslot)
-                    timeslot_avail = Availability(
-                        event=self.event, start=start, end=end
-                    )
-
-                    fulfilled_time_constraints = set()
+                    for timeslot in block:
+                        start, end = self._get_timeslot_start_end(timeslot)
+                        timeslot_avail = Availability(
+                            event=self.event, start=start, end=end
+                        )
 
-                    # reso deadline
-                    if self.event.reso_deadline is not None:
-                        # timeslot ends before deadline
-                        if end < self.event.reso_deadline.astimezone(
-                            self.event.timezone
-                        ):
-                            fulfilled_time_constraints.add("resolution")
-
-                    # add category constraints
-                    fulfilled_time_constraints |= (
-                        AKCategory.create_category_constraints(
-                            [
-                                cat
-                                for cat in AKCategory.objects.filter(
-                                    event=self.event
-                                ).all()
-                                if timeslot_avail.is_covered(cat_avails[cat.name])
-                            ]
+                        fulfilled_time_constraints = set()
+
+                        # reso deadline
+                        if self.event.reso_deadline is not None:
+                            # timeslot ends before deadline
+                            if end < self.event.reso_deadline.astimezone(
+                                self.event.timezone
+                            ):
+                                fulfilled_time_constraints.add("resolution")
+
+                        # add category constraints
+                        fulfilled_time_constraints |= (
+                            AKCategory.create_category_constraints(
+                                [
+                                    cat
+                                    for cat in AKCategory.objects.filter(
+                                        event=self.event
+                                    ).all()
+                                    if timeslot_avail.is_covered(cat_avails[cat.name])
+                                ]
+                            )
                         )
-                    )
 
-                    # add owner constraints
-                    fulfilled_time_constraints |= {
-                        f"availability-person-{owner.id}"
-                        for owner in AKOwner.objects.filter(event=self.event).all()
-                        if self._is_restricted_and_contained_slot(
-                            timeslot_avail,
-                            Availability.union(owner.availabilities.all()),
+                        # add owner constraints
+                        fulfilled_time_constraints |= {
+                            f"availability-person-{owner.id}"
+                            for owner in AKOwner.objects.filter(event=self.event).all()
+                            if self._is_restricted_and_contained_slot(
+                                timeslot_avail,
+                                Availability.union(owner.availabilities.all()),
+                            )
+                        }
+
+                        # add room constraints
+                        fulfilled_time_constraints |= {
+                            f"availability-room-{room.id}"
+                            for room in self.rooms
+                            if self._is_restricted_and_contained_slot(
+                                timeslot_avail,
+                                Availability.union(room.availabilities.all()),
+                            )
+                        }
+
+                        # add ak constraints
+                        fulfilled_time_constraints |= {
+                            f"availability-ak-{ak.id}"
+                            for ak in AK.objects.filter(event=event)
+                            if self._is_restricted_and_contained_slot(
+                                timeslot_avail,
+                                Availability.union(ak.availabilities.all()),
+                            )
+                        }
+                        fulfilled_time_constraints |= {
+                            f"fixed-akslot-{slot.id}"
+                            for slot in self.ak_slots
+                            if self._is_ak_fixed_in_slot(slot, timeslot_avail)
+                        }
+
+                        fulfilled_time_constraints |= {
+                            f"notblock{idx}"
+                            for idx in range(num_blocks)
+                            if idx != block_idx
+                        }
+
+                        self.assertEqual(
+                            fulfilled_time_constraints,
+                            set(timeslot["fulfilled_time_constraints"]),
                         )
-                    }
-
-                    # add room constraints
-                    fulfilled_time_constraints |= {
-                        f"availability-room-{room.id}"
-                        for room in self.rooms
-                        if self._is_restricted_and_contained_slot(
-                            timeslot_avail,
-                            Availability.union(room.availabilities.all()),
+
+    def test_timeslots_info(self):
+        """Test timeslots info dict"""
+        for event in Event.objects.all():
+            with self.subTest(event=event):
+                self.set_up_event(event=event)
+
+                self.assertAlmostEqual(
+                    self.export_dict["timeslots"]["info"]["duration"],
+                    float(self.event.export_slot),
+                )
+
+                block_names = []
+                for block in self.export_dict["timeslots"]["blocks"]:
+                    if not block:
+                        continue
+
+                    block_start, _ = self._get_timeslot_start_end(block[0])
+                    _, block_end = self._get_timeslot_start_end(block[-1])
+
+                    start_day = block_start.strftime("%A, %d. %b")
+                    if block_start.date() == block_end.date():
+                        # same day
+                        time_str = (
+                            block_start.strftime("%H:%M")
+                            + " – "
+                            + block_end.strftime("%H:%M")
                         )
-                    }
-
-                    # add ak constraints
-                    fulfilled_time_constraints |= {
-                        f"availability-ak-{ak.id}"
-                        for ak in AK.objects.filter(event=event)
-                        if self._is_restricted_and_contained_slot(
-                            timeslot_avail, Availability.union(ak.availabilities.all())
+                    else:
+                        # different days
+                        time_str = (
+                            block_start.strftime("%a %H:%M")
+                            + " – "
+                            + block_end.strftime("%a %H:%M")
                         )
-                    }
-                    fulfilled_time_constraints |= {
-                        f"fixed-akslot-{slot.id}"
-                        for slot in self.ak_slots
-                        if self._is_ak_fixed_in_slot(slot, timeslot_avail)
-                    }
-
-                    self.assertEqual(
-                        fulfilled_time_constraints,
-                        set(timeslot["fulfilled_time_constraints"]),
-                    )
+                    block_names.append([start_day, time_str])
+                self.assertEqual(
+                    block_names, self.export_dict["timeslots"]["info"]["blocknames"]
+                )
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index f6fd7932..f8b83f04 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -1,4 +1,5 @@
 import json
+from datetime import datetime
 from typing import List
 
 from django.contrib import messages
@@ -109,11 +110,30 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             for person in AKOwner.objects.filter(event=self.event)
         }
 
-        blocks = self.event.discretize_timeslots()
+        blocks = list(self.event.discretize_timeslots())
 
-        for block in blocks:
+        block_names = []
+
+        for block_idx, block in enumerate(blocks):
             current_block = []
 
+            if not block:
+                continue
+
+            block_start = block[0].avail.start.astimezone(self.event.timezone)
+            block_end = block[-1].avail.end.astimezone(self.event.timezone)
+
+            start_day = block_start.strftime("%A, %d. %b")
+            if block_start.date() == block_end.date():
+                # same day
+                time_str = block_start.strftime("%H:%M") + " – " + block_end.strftime("%H:%M")
+            else:
+                # different days
+                time_str = block_start.strftime("%a %H:%M") + " – " + block_end.strftime("%a %H:%M")
+            block_names.append([start_day, time_str])
+
+            block_timeconstraints = [f"notblock{idx}" for idx in range(len(blocks)) if idx != block_idx]
+
             for timeslot in block:
                 time_constraints = []
                 # if reso_deadline is set and timeslot ends before it,
@@ -145,6 +165,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
                 ])
 
                 time_constraints.extend(timeslot.constraints)
+                time_constraints.extend(block_timeconstraints)
 
                 current_block.append({
                     "id": str(timeslot.idx),
@@ -157,6 +178,8 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
 
             timeslots["blocks"].append(current_block)
 
+        timeslots["info"]["blocknames"] = block_names
+
         context["timeslots"] = json.dumps(timeslots)
 
         info_dict = {
-- 
GitLab


From abbe82a1862b07e1826dfdfc0664970850d77cc7 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Wed, 26 Feb 2025 13:59:49 +0100
Subject: [PATCH 66/77] Address lint remarks

---
 AKModel/models.py   | 1 +
 AKModel/views/ak.py | 1 -
 2 files changed, 1 insertion(+), 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 5bd570dc..7c1a920b 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -273,6 +273,7 @@ class Event(models.Model):
         start: datetime,
         end: datetime,
         slot_duration: timedelta,
+        *,
         slot_index: int = 0,
         constraints: set[str] | None = None,
     ) -> Generator[TimeslotBlock, None, int]:
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 43fe489b..40a6e102 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -1,5 +1,4 @@
 import json
-from datetime import datetime
 from typing import List
 
 from django.contrib import messages
-- 
GitLab


From a54da7ac0b8988674542e850259ea9b632b7b48f Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 10 Feb 2025 17:03:00 +0100
Subject: [PATCH 67/77] Change funcs to export json dict instead of str

---
 AKModel/models.py                                  | 10 +++++-----
 .../templates/admin/AKModel/ak_json_export.html    | 13 +------------
 AKModel/views/ak.py                                | 14 +++++++++-----
 3 files changed, 15 insertions(+), 22 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 7c1a920b..4b3a3ceb 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -4,7 +4,7 @@ import json
 import math
 from dataclasses import dataclass
 from datetime import datetime, timedelta
-from typing import Iterable, Generator
+from typing import Any, Iterable, Generator
 
 from django.db import models, transaction
 from django.apps import apps
@@ -852,7 +852,7 @@ class Room(models.Model):
     def __str__(self):
         return self.title
 
-    def as_json(self) -> str:
+    def as_json_dict(self) -> dict[str, Any]:
         """Return a json string representation of this room object.
 
         :return: The json string representation is constructed
@@ -887,7 +887,7 @@ class Room(models.Model):
         if not any(constr.startswith("proxy") for constr in data["fulfilled_room_constraints"]):
             data["fulfilled_room_constraints"].append("no-proxy")
 
-        return json.dumps(data)
+        return data
 
 
 class AKSlot(models.Model):
@@ -984,7 +984,7 @@ class AKSlot(models.Model):
             self.duration = min(self.duration, event_duration_hours)
         super().save(force_insert, force_update, using, update_fields)
 
-    def as_json(self) -> str:
+    def as_json_dict(self) -> dict[str, Any]:
         """Return a json string representation of the AK object of this slot.
 
         :return: The json string representation is constructed
@@ -1055,7 +1055,7 @@ class AKSlot(models.Model):
         if not any(constr.startswith("proxy") for constr in data["room_constraints"]):
             data["room_constraints"].append("no-proxy")
 
-        return json.dumps(data)
+        return data
 
 class AKOrgaMessage(models.Model):
     """
diff --git a/AKModel/templates/admin/AKModel/ak_json_export.html b/AKModel/templates/admin/AKModel/ak_json_export.html
index 38e5526e..7ae242ca 100644
--- a/AKModel/templates/admin/AKModel/ak_json_export.html
+++ b/AKModel/templates/admin/AKModel/ak_json_export.html
@@ -4,17 +4,6 @@
 
 {% block content %}
 <pre>
-  {"aks": [
-      {% for slot in slots %}{{ slot.as_json }}{% if not forloop.last %},
-      {% endif %}{% endfor %}
-    ],
-  "rooms": [
-      {% for room in rooms %}{{ room.as_json }}{% if not forloop.last %},
-      {% endif %}{% endfor %}
-    ],
-  "participants": {{ participants }},
-  "timeslots": {{ timeslots }},
-  "info": {{ info_dict }}
-  }
+{{ json_data }}
 </pre>
 {% endblock %}
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 40a6e102..32d840fb 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -86,10 +86,10 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
-        context["participants"] = json.dumps([])
+        data = {}
 
         rooms = Room.objects.filter(event=self.event)
-        context["rooms"] = rooms
+        data["rooms"] = [r.as_json_dict() for r in rooms]
 
         timeslots = {
             "info": {"duration": float(self.event.export_slot)},
@@ -179,17 +179,21 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
 
         timeslots["info"]["blocknames"] = block_names
 
-        context["timeslots"] = json.dumps(timeslots)
-
         info_dict = {
             "title": self.event.name,
             "slug": self.event.slug
         }
+
         for attr in ["contact_email", "place"]:
             if hasattr(self.event, attr) and getattr(self.event, attr):
                 info_dict[attr] = getattr(self.event, attr)
 
-        context["info_dict"] = json.dumps(info_dict)
+        data["timeslots"] = timeslots
+        data["info"] = info_dict
+        data["participants"] = []
+        data["aks"] = [ak.as_json_dict() for ak in context["slots"]]
+
+        context["json_data"] = json.dumps(data, indent=2)
 
         return context
 
-- 
GitLab


From 8ac9cd0bbe363d8bc8b083f1cc496f4061280a82 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 10 Feb 2025 22:17:08 +0100
Subject: [PATCH 68/77] Add oneliner

---
 AKModel/templates/admin/AKModel/ak_json_export.html | 11 +++++++++++
 AKModel/views/ak.py                                 |  1 +
 2 files changed, 12 insertions(+)

diff --git a/AKModel/templates/admin/AKModel/ak_json_export.html b/AKModel/templates/admin/AKModel/ak_json_export.html
index 7ae242ca..65e459a5 100644
--- a/AKModel/templates/admin/AKModel/ak_json_export.html
+++ b/AKModel/templates/admin/AKModel/ak_json_export.html
@@ -3,7 +3,18 @@
 {% load tz %}
 
 {% block content %}
+
+<p>
+Exported JSON:
+<pre>
+{{ json_data_oneline }}
+</pre>
+</p>
+
+<p>
+Exported JSON (indented for better readability):
 <pre>
 {{ json_data }}
 </pre>
+</p>
 {% endblock %}
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 32d840fb..15abf0df 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -193,6 +193,7 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         data["participants"] = []
         data["aks"] = [ak.as_json_dict() for ak in context["slots"]]
 
+        context["json_data_oneline"] = json.dumps(data)
         context["json_data"] = json.dumps(data, indent=2)
 
         return context
-- 
GitLab


From 12bce4441c0eec7772bd159d7ea1acd13582e9be Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 10 Feb 2025 22:20:25 +0100
Subject: [PATCH 69/77] refactor

---
 AKModel/views/ak.py | 17 +++++++++--------
 1 file changed, 9 insertions(+), 8 deletions(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 15abf0df..518ddbb5 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -86,16 +86,14 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
-        data = {}
-
-        rooms = Room.objects.filter(event=self.event)
-        data["rooms"] = [r.as_json_dict() for r in rooms]
 
         timeslots = {
             "info": {"duration": float(self.event.export_slot)},
             "blocks": [],
             }
 
+        rooms = Room.objects.filter(event=self.event)
+
         ak_availabilities = {
             ak.pk: Availability.union(ak.availabilities.all())
             for ak in AK.objects.filter(event=self.event).all()
@@ -188,10 +186,13 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             if hasattr(self.event, attr) and getattr(self.event, attr):
                 info_dict[attr] = getattr(self.event, attr)
 
-        data["timeslots"] = timeslots
-        data["info"] = info_dict
-        data["participants"] = []
-        data["aks"] = [ak.as_json_dict() for ak in context["slots"]]
+        data = {
+            "participants": [],
+            "rooms": [r.as_json_dict() for r in rooms],
+            "timeslots": timeslots,
+            "info": info_dict,
+            "aks": [ak.as_json_dict() for ak in context["slots"]],
+        }
 
         context["json_data_oneline"] = json.dumps(data)
         context["json_data"] = json.dumps(data, indent=2)
-- 
GitLab


From fb1905a59fb17610e290b16ae7a23981f1d7589e Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Wed, 26 Feb 2025 16:52:26 +0100
Subject: [PATCH 70/77] Move json dict assembly from view to model

---
 AKModel/models.py   | 151 ++++++++++++++++++++++++++++++++++++++++++++
 AKModel/views/ak.py | 144 +-----------------------------------------
 2 files changed, 153 insertions(+), 142 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 4b3a3ceb..22e0012e 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -488,6 +488,157 @@ class Event(models.Model):
 
         return slots_updated
 
+    def as_json_dict(self) -> dict[str, Any]:
+        """Return the json representation of this Event.
+
+        :return: The json dict representation is constructed
+            following the input specification of the KoMa conference optimizer, cf.
+            https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
+        :rtype: dict[str, Any]
+        """
+
+        # local import to prevent cyclic import
+        # pylint: disable=import-outside-toplevel
+        from AKModel.availability.models import Availability
+
+        def _test_event_not_covered(availabilities: list[Availability]) -> bool:
+            """Test if event is not covered by availabilities."""
+            return not Availability.is_event_covered(self, availabilities)
+
+        def _test_akslot_fixed_in_timeslot(ak_slot: AKSlot, timeslot: Availability) -> bool:
+            """Test if an AKSlot is fixed to overlap a timeslot slot."""
+            if not ak_slot.fixed or ak_slot.start is None:
+                return False
+
+            fixed_avail = Availability(event=self, start=ak_slot.start, end=ak_slot.end)
+            return fixed_avail.overlaps(timeslot, strict=True)
+
+        def _test_add_constraint(slot: Availability, availabilities: list[Availability]) -> bool:
+            """Test if object is not available for whole event and may happen during slot."""
+            return (
+                _test_event_not_covered(availabilities) and slot.is_covered(availabilities)
+            )
+
+        def _generate_time_constraints(
+            avail_label: str,
+            avail_dict: dict,
+            timeslot_avail: Availability,
+            prefix: str = "availability",
+        ) -> list[str]:
+            return [
+                f"{prefix}-{avail_label}-{pk}"
+                for pk, availabilities in avail_dict.items()
+                if _test_add_constraint(timeslot_avail, availabilities)
+            ]
+
+        timeslots = {
+            "info": {"duration": float(self.export_slot)},
+            "blocks": [],
+            }
+
+        rooms = Room.objects.filter(event=self)
+        slots = AKSlot.objects.filter(event=self)
+
+        ak_availabilities = {
+            ak.pk: Availability.union(ak.availabilities.all())
+            for ak in AK.objects.filter(event=self).all()
+        }
+        room_availabilities = {
+            room.pk: Availability.union(room.availabilities.all())
+            for room in rooms
+        }
+        person_availabilities = {
+            person.pk: Availability.union(person.availabilities.all())
+            for person in AKOwner.objects.filter(event=self)
+        }
+
+        blocks = list(self.discretize_timeslots())
+
+        block_names = []
+
+        for block_idx, block in enumerate(blocks):
+            current_block = []
+
+            if not block:
+                continue
+
+            block_start = block[0].avail.start.astimezone(self.timezone)
+            block_end = block[-1].avail.end.astimezone(self.timezone)
+
+            start_day = block_start.strftime("%A, %d. %b")
+            if block_start.date() == block_end.date():
+                # same day
+                time_str = block_start.strftime("%H:%M") + " – " + block_end.strftime("%H:%M")
+            else:
+                # different days
+                time_str = block_start.strftime("%a %H:%M") + " – " + block_end.strftime("%a %H:%M")
+            block_names.append([start_day, time_str])
+
+            block_timeconstraints = [f"notblock{idx}" for idx in range(len(blocks)) if idx != block_idx]
+
+            for timeslot in block:
+                time_constraints = []
+                # if reso_deadline is set and timeslot ends before it,
+                #   add fulfilled time constraint 'resolution'
+                if self.reso_deadline is None or timeslot.avail.end < self.reso_deadline:
+                    time_constraints.append("resolution")
+
+                # add fulfilled time constraints for all AKs that cannot happen during full event
+                time_constraints.extend(
+                    _generate_time_constraints("ak", ak_availabilities, timeslot.avail)
+                )
+
+                # add fulfilled time constraints for all persons that are not available for full event
+                time_constraints.extend(
+                    _generate_time_constraints("person", person_availabilities, timeslot.avail)
+                )
+
+                # add fulfilled time constraints for all rooms that are not available for full event
+                time_constraints.extend(
+                    _generate_time_constraints("room", room_availabilities, timeslot.avail)
+                )
+
+                # add fulfilled time constraints for all AKSlots fixed to happen during timeslot
+                time_constraints.extend([
+                    f"fixed-akslot-{slot.id}"
+                    for slot in AKSlot.objects.filter(event=self, fixed=True).exclude(start__isnull=True)
+                    if _test_akslot_fixed_in_timeslot(slot, timeslot.avail)
+                ])
+
+                time_constraints.extend(timeslot.constraints)
+                time_constraints.extend(block_timeconstraints)
+
+                current_block.append({
+                    "id": timeslot.idx,
+                    "info": {
+                        "start": timeslot.avail.start.astimezone(self.timezone).strftime("%Y-%m-%d %H:%M"),
+                        "end": timeslot.avail.end.astimezone(self.timezone).strftime("%Y-%m-%d %H:%M"),
+                    },
+                    "fulfilled_time_constraints": time_constraints,
+                    })
+
+            timeslots["blocks"].append(current_block)
+
+        timeslots["info"]["blocknames"] = block_names
+
+        info_dict = {
+            "title": self.name,
+            "slug": self.slug
+        }
+
+        for attr in ["contact_email", "place"]:
+            if hasattr(self, attr) and getattr(self, attr):
+                info_dict[attr] = getattr(self, attr)
+
+        return {
+            "participants": [],
+            "rooms": [r.as_json_dict() for r in rooms],
+            "timeslots": timeslots,
+            "info": info_dict,
+            "aks": [ak.as_json_dict() for ak in slots],
+        }
+
+
 class AKOwner(models.Model):
     """ An AKOwner describes the person organizing/holding an AK.
     """
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 518ddbb5..b4af5a8f 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -1,15 +1,13 @@
 import json
-from typing import List
 
 from django.contrib import messages
 from django.urls import reverse_lazy
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import ListView, DetailView
 
-from AKModel.availability.models import Availability
 from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \
     IntermediateAdminActionView
-from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner
+from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK
 
 
 class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
@@ -50,157 +48,19 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
     context_object_name = "slots"
     title = _("AK JSON Export")
 
-    def _test_event_not_covered(self, availabilities: List[Availability]) -> bool:
-        """Test if event is not covered by availabilities."""
-        return not Availability.is_event_covered(self.event, availabilities)
-
-    def _test_akslot_fixed_in_timeslot(self, ak_slot: AKSlot, timeslot: Availability) -> bool:
-        """Test if an AKSlot is fixed to overlap a timeslot slot."""
-        if not ak_slot.fixed or ak_slot.start is None:
-            return False
-
-        fixed_avail = Availability(event=self.event, start=ak_slot.start, end=ak_slot.end)
-        return fixed_avail.overlaps(timeslot, strict=True)
-
-    def _test_add_constraint(self, slot: Availability, availabilities: List[Availability]) -> bool:
-        """Test if object is not available for whole event and may happen during slot."""
-        return (
-            self._test_event_not_covered(availabilities) and slot.is_covered(availabilities)
-        )
-
-    def _generate_time_constraints(
-        self,
-        avail_label: str,
-        avail_dict: dict,
-        timeslot_avail: Availability,
-        prefix: str = "availability",
-    ) -> list[str]:
-        return [
-            f"{prefix}-{avail_label}-{pk}"
-            for pk, availabilities in avail_dict.items()
-            if self._test_add_constraint(timeslot_avail, availabilities)
-        ]
-
     def get_queryset(self):
         return super().get_queryset().order_by("ak__track")
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
 
-        timeslots = {
-            "info": {"duration": float(self.event.export_slot)},
-            "blocks": [],
-            }
-
-        rooms = Room.objects.filter(event=self.event)
-
-        ak_availabilities = {
-            ak.pk: Availability.union(ak.availabilities.all())
-            for ak in AK.objects.filter(event=self.event).all()
-        }
-        room_availabilities = {
-            room.pk: Availability.union(room.availabilities.all())
-            for room in rooms
-        }
-        person_availabilities = {
-            person.pk: Availability.union(person.availabilities.all())
-            for person in AKOwner.objects.filter(event=self.event)
-        }
-
-        blocks = list(self.event.discretize_timeslots())
-
-        block_names = []
-
-        for block_idx, block in enumerate(blocks):
-            current_block = []
-
-            if not block:
-                continue
-
-            block_start = block[0].avail.start.astimezone(self.event.timezone)
-            block_end = block[-1].avail.end.astimezone(self.event.timezone)
-
-            start_day = block_start.strftime("%A, %d. %b")
-            if block_start.date() == block_end.date():
-                # same day
-                time_str = block_start.strftime("%H:%M") + " – " + block_end.strftime("%H:%M")
-            else:
-                # different days
-                time_str = block_start.strftime("%a %H:%M") + " – " + block_end.strftime("%a %H:%M")
-            block_names.append([start_day, time_str])
-
-            block_timeconstraints = [f"notblock{idx}" for idx in range(len(blocks)) if idx != block_idx]
-
-            for timeslot in block:
-                time_constraints = []
-                # if reso_deadline is set and timeslot ends before it,
-                #   add fulfilled time constraint 'resolution'
-                if self.event.reso_deadline is None or timeslot.avail.end < self.event.reso_deadline:
-                    time_constraints.append("resolution")
-
-                # add fulfilled time constraints for all AKs that cannot happen during full event
-                time_constraints.extend(
-                    self._generate_time_constraints("ak", ak_availabilities, timeslot.avail)
-                )
-
-                # add fulfilled time constraints for all persons that are not available for full event
-                time_constraints.extend(
-                    self._generate_time_constraints("person", person_availabilities, timeslot.avail)
-                )
-
-                # add fulfilled time constraints for all rooms that are not available for full event
-                time_constraints.extend(
-                    self._generate_time_constraints("room", room_availabilities, timeslot.avail)
-                )
-
-                # add fulfilled time constraints for all AKSlots fixed to happen during timeslot
-                time_constraints.extend([
-                    f"fixed-akslot-{slot.id}"
-                    for slot in AKSlot.objects.filter(event=self.event, fixed=True)
-                                              .exclude(start__isnull=True)
-                    if self._test_akslot_fixed_in_timeslot(slot, timeslot.avail)
-                ])
-
-                time_constraints.extend(timeslot.constraints)
-                time_constraints.extend(block_timeconstraints)
-
-                current_block.append({
-                    "id": timeslot.idx,
-                    "info": {
-                        "start": timeslot.avail.start.astimezone(self.event.timezone).strftime("%Y-%m-%d %H:%M"),
-                        "end": timeslot.avail.end.astimezone(self.event.timezone).strftime("%Y-%m-%d %H:%M"),
-                    },
-                    "fulfilled_time_constraints": time_constraints,
-                    })
-
-            timeslots["blocks"].append(current_block)
-
-        timeslots["info"]["blocknames"] = block_names
-
-        info_dict = {
-            "title": self.event.name,
-            "slug": self.event.slug
-        }
-
-        for attr in ["contact_email", "place"]:
-            if hasattr(self.event, attr) and getattr(self.event, attr):
-                info_dict[attr] = getattr(self.event, attr)
-
-        data = {
-            "participants": [],
-            "rooms": [r.as_json_dict() for r in rooms],
-            "timeslots": timeslots,
-            "info": info_dict,
-            "aks": [ak.as_json_dict() for ak in context["slots"]],
-        }
+        data = self.event.as_json_dict()
 
         context["json_data_oneline"] = json.dumps(data)
         context["json_data"] = json.dumps(data, indent=2)
 
         return context
 
-
-
 class AKWikiExportView(AdminViewMixin, DetailView):
     """
     View: Export AKs of this event in wiki syntax
-- 
GitLab


From 48cb609ea03728a07527de9d42cad3cbd12fd650 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Wed, 26 Feb 2025 16:52:36 +0100
Subject: [PATCH 71/77] Fix docstrings

---
 AKModel/models.py | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index 22e0012e..a649fd0d 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -888,7 +888,7 @@ class AK(models.Model):
         Get a list of stringified representations of all owners
 
         :return: list of owners
-        :rtype: List[str]
+        :rtype: list[str]
         """
         return ", ".join(str(owner) for owner in self.owners.all())
 
@@ -898,7 +898,7 @@ class AK(models.Model):
         Get a list of stringified representations of all durations of associated slots
 
         :return: list of durations
-        :rtype: List[str]
+        :rtype: list[str]
         """
         return ", ".join(str(slot.duration_simplified) for slot in self.akslot_set.select_related('event').all())
 
@@ -1004,12 +1004,12 @@ class Room(models.Model):
         return self.title
 
     def as_json_dict(self) -> dict[str, Any]:
-        """Return a json string representation of this room object.
+        """Return a json representation of this room object.
 
-        :return: The json string representation is constructed
+        :return: The json dict representation is constructed
             following the input specification of the KoMa conference optimizer, cf.
             https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
-        :rtype: str
+        :rtype: dict[str, Any]
         """
         # local import to prevent cyclic import
         # pylint: disable=import-outside-toplevel
@@ -1136,12 +1136,12 @@ class AKSlot(models.Model):
         super().save(force_insert, force_update, using, update_fields)
 
     def as_json_dict(self) -> dict[str, Any]:
-        """Return a json string representation of the AK object of this slot.
+        """Return a json representation of the AK object of this slot.
 
-        :return: The json string representation is constructed
+        :return: The json dict representation is constructed
             following the input specification of the KoMa conference optimizer, cf.
             https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
-        :rtype: str
+        :rtype: dict[str, Any]
         """
         # local import to prevent cyclic import
         # pylint: disable=import-outside-toplevel
-- 
GitLab


From a4ab46ffcde89236f149941d7775a8d58dc2fdcc Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Wed, 26 Feb 2025 17:32:15 +0100
Subject: [PATCH 72/77] Sort export lists to make dict eq test reliable

---
 AKModel/models.py | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index a649fd0d..fc2157a3 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -536,8 +536,8 @@ class Event(models.Model):
             "blocks": [],
             }
 
-        rooms = Room.objects.filter(event=self)
-        slots = AKSlot.objects.filter(event=self)
+        rooms = Room.objects.filter(event=self).order_by()
+        slots = AKSlot.objects.filter(event=self).order_by()
 
         ak_availabilities = {
             ak.pk: Availability.union(ak.availabilities.all())
@@ -614,7 +614,7 @@ class Event(models.Model):
                         "start": timeslot.avail.start.astimezone(self.timezone).strftime("%Y-%m-%d %H:%M"),
                         "end": timeslot.avail.end.astimezone(self.timezone).strftime("%Y-%m-%d %H:%M"),
                     },
-                    "fulfilled_time_constraints": time_constraints,
+                    "fulfilled_time_constraints": sorted(time_constraints),
                     })
 
             timeslots["blocks"].append(current_block)
@@ -1038,6 +1038,7 @@ class Room(models.Model):
         if not any(constr.startswith("proxy") for constr in data["fulfilled_room_constraints"]):
             data["fulfilled_room_constraints"].append("no-proxy")
 
+        data["fulfilled_room_constraints"].sort()
         return data
 
 
@@ -1174,9 +1175,11 @@ class AKSlot(models.Model):
             "duration": math.ceil(self.duration / self.event.export_slot - ceil_offet_eps),
             "properties": {
                 "conflicts":
-                    [conflict.pk for conflict in conflict_slots.all()]
-                  + [second_slot.pk for second_slot in other_ak_slots.all()],
-                "dependencies": [dep.pk for dep in dependency_slots.all()],
+                    sorted(
+                        [conflict.pk for conflict in conflict_slots.all()]
+                      + [second_slot.pk for second_slot in other_ak_slots.all()]
+                    ),
+                "dependencies": sorted([dep.pk for dep in dependency_slots.all()]),
             },
             "room_constraints": [constraint.name
                                  for constraint in self.ak.requirements.all()],
@@ -1206,6 +1209,9 @@ class AKSlot(models.Model):
         if not any(constr.startswith("proxy") for constr in data["room_constraints"]):
             data["room_constraints"].append("no-proxy")
 
+        data["room_constraints"].sort()
+        data["time_constraints"].sort()
+
         return data
 
 class AKOrgaMessage(models.Model):
-- 
GitLab


From c2a4b946fb63280798769a68edbd446a918bf7d0 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Wed, 26 Feb 2025 17:32:23 +0100
Subject: [PATCH 73/77] Add inconsistency check

---
 AKModel/models.py | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index fc2157a3..a25e0b05 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -412,7 +412,7 @@ class Event(models.Model):
             yield from self.uniform_time_slots(slots_in_an_hour=slots_in_an_hour)
 
     @transaction.atomic
-    def schedule_from_json(self, schedule: str) -> int:
+    def schedule_from_json(self, schedule: str, *, check_for_data_inconsistency: bool = True) -> int:
         """Load AK schedule from a json string.
 
         :param schedule: A string that can be decoded to json, describing
@@ -421,6 +421,10 @@ class Event(models.Model):
             https://github.com/Die-KoMa/ak-plan-optimierung/wiki/Input-&-output-format
         """
         schedule = json.loads(schedule)
+        export_dict = self.as_json_dict()
+
+        if check_for_data_inconsistency and schedule["input"] != export_dict:
+            raise ValueError("Data has changed since the export. Reexport and run the solver again.")
 
         slots_in_an_hour = schedule["input"]["timeslots"]["info"]["duration"]
 
-- 
GitLab


From da0e1a2885d4deaa07e3f69435210912634a23d4 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Thu, 27 Feb 2025 20:28:15 +0100
Subject: [PATCH 74/77] Add AK types to json export

---
 AKModel/models.py                 | 1 +
 AKModel/tests/test_json_export.py | 5 +++++
 2 files changed, 6 insertions(+)

diff --git a/AKModel/models.py b/AKModel/models.py
index c2383c33..a612f49e 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -1075,6 +1075,7 @@ class AKSlot(models.Model):
                 "reso": self.ak.reso,
                 "duration_in_hours": float(self.duration),
                 "django_ak_id": self.ak.pk,
+                "types": list(self.ak.types.values_list("name", flat=True).order_by()),
                 },
             }
 
diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py
index 8bfe554d..cbd37013 100644
--- a/AKModel/tests/test_json_export.py
+++ b/AKModel/tests/test_json_export.py
@@ -143,6 +143,7 @@ class JSONExportTest(TestCase):
                             "reso",
                             "duration_in_hours",
                             "django_ak_id",
+                            "types",
                         },
                         f"{item} info keys not as expected",
                     )
@@ -445,6 +446,10 @@ class JSONExportTest(TestCase):
                     )
                     self.assertEqual(ak["info"]["description"], slot.ak.description)
                     self.assertEqual(ak["info"]["django_ak_id"], slot.ak.pk)
+                    self.assertEqual(
+                        ak["info"]["types"],
+                        list(slot.ak.types.values_list("name", flat=True).order_by()),
+                    )
 
     def test_ak_room_constraints(self):
         """Test if AK room constraints are exported as expected."""
-- 
GitLab


From bb75b77162f1b8883a8ddf6301c9f36bd3970fa8 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Fri, 28 Feb 2025 14:41:32 +0100
Subject: [PATCH 75/77] Prefetch in test and fix typing

---
 AKModel/availability/forms.py     |  2 +-
 AKModel/tests/test_json_export.py | 10 ++++++++--
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/AKModel/availability/forms.py b/AKModel/availability/forms.py
index 994949a8..24a7c4f0 100644
--- a/AKModel/availability/forms.py
+++ b/AKModel/availability/forms.py
@@ -183,7 +183,7 @@ class AvailabilitiesFormMixin(forms.Form):
         for avail in availabilities:
             setattr(avail, reference_name, instance.id)
 
-    def _replace_availabilities(self, instance, availabilities: [Availability]):
+    def _replace_availabilities(self, instance, availabilities: list[Availability]):
         """
         Replace the existing list of availabilities belonging to an entity with a new, updated one
 
diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py
index cbd37013..e8aac8fe 100644
--- a/AKModel/tests/test_json_export.py
+++ b/AKModel/tests/test_json_export.py
@@ -78,7 +78,13 @@ class JSONExportTest(TestCase):
             for participant in self.export_dict["participants"]
         }
 
-        self.ak_slots = AKSlot.objects.filter(event__slug=event.slug).all()
+        self.ak_slots = (
+            AKSlot.objects.filter(event__slug=event.slug)
+            .select_related("ak")
+            .prefetch_related("ak__conflicts")
+            .prefetch_related("ak__prerequisites")
+            .all()
+        )
         self.rooms = Room.objects.filter(event__slug=event.slug).all()
         self.slots_in_an_hour = 1 / self.export_dict["timeslots"]["info"]["duration"]
         self.event = event
@@ -563,7 +569,7 @@ class JSONExportTest(TestCase):
 
                     # test if time availability of room is restricted
                     if not Availability.is_event_covered(
-                        room.event, room.availabilities.all()
+                        event, room.availabilities.all()
                     ):
                         time_constraints.add(f"availability-room-{room.pk}")
 
-- 
GitLab


From 3a211c60c59b9a7ce496df5e4f606ece80aea609 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Sun, 2 Mar 2025 23:53:17 +0100
Subject: [PATCH 76/77] Change JSONExportView to DetailView of an Event

---
 AKModel/views/ak.py | 13 +++++--------
 1 file changed, 5 insertions(+), 8 deletions(-)

diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index b4af5a8f..461edd3a 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -39,23 +39,20 @@ class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         return super().get_queryset().order_by("ak__track")
 
 
-class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
+class AKJSONExportView(AdminViewMixin, DetailView):
     """
     View: Export all AK slots of this event in JSON format ordered by tracks
     """
     template_name = "admin/AKModel/ak_json_export.html"
-    model = AKSlot
-    context_object_name = "slots"
+    model = Event
+    context_object_name = "event"
     title = _("AK JSON Export")
-
-    def get_queryset(self):
-        return super().get_queryset().order_by("ak__track")
+    slug_url_kwarg = "event_slug"
 
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
 
-        data = self.event.as_json_dict()
-
+        data = context["event"].as_json_dict()
         context["json_data_oneline"] = json.dumps(data)
         context["json_data"] = json.dumps(data, indent=2)
 
-- 
GitLab


From abe8fc86082eff8b70f543ded88ad3bbd01d5cf4 Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Mon, 3 Mar 2025 01:43:49 +0100
Subject: [PATCH 77/77] Update translation lines

---
 AKModel/locale/de_DE/LC_MESSAGES/django.po | 332 ++++++++++-----------
 1 file changed, 166 insertions(+), 166 deletions(-)

diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index 00b78ea9..38b3d54a 100644
--- a/AKModel/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2025-02-27 15:13+0000\n"
+"POT-Creation-Date: 2025-03-03 00:42+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"
@@ -33,8 +33,8 @@ msgstr "Plan veröffentlichen"
 msgid "Unpublish plan"
 msgstr "Plan verbergen"
 
-#: AKModel/admin.py:170 AKModel/models.py:720 AKModel/models.py:1167
-#: AKModel/models.py:1203 AKModel/templates/admin/AKModel/aks_by_user.html:12
+#: AKModel/admin.py:170 AKModel/models.py:875 AKModel/models.py:1329
+#: AKModel/models.py:1365 AKModel/templates/admin/AKModel/aks_by_user.html:12
 #: AKModel/templates/admin/AKModel/status/event_aks.html:10
 #: AKModel/views/manage.py:75 AKModel/views/status.py:102
 msgid "AKs"
@@ -60,11 +60,11 @@ msgstr "In Wiki-Syntax exportieren"
 msgid "Cannot export AKs from more than one event at the same time."
 msgstr "Kann nicht AKs von mehreren Events zur selben Zeit exportieren."
 
-#: AKModel/admin.py:335 AKModel/views/ak.py:257
+#: AKModel/admin.py:335 AKModel/views/ak.py:123
 msgid "Reset interest in AKs"
 msgstr "Interesse an AKs zurücksetzen"
 
-#: AKModel/admin.py:345 AKModel/views/ak.py:272
+#: AKModel/admin.py:345 AKModel/views/ak.py:138
 msgid "Reset AKs' interest counters"
 msgstr "Interessenszähler der AKs zurücksetzen"
 
@@ -126,18 +126,18 @@ msgid "Please fill in your availabilities!"
 msgstr "Bitte Verfügbarkeiten eintragen!"
 
 #: AKModel/availability/models.py:43 AKModel/models.py:166
-#: AKModel/models.py:499 AKModel/models.py:576 AKModel/models.py:609
-#: AKModel/models.py:635 AKModel/models.py:654 AKModel/models.py:710
-#: AKModel/models.py:868 AKModel/models.py:944 AKModel/models.py:1106
-#: AKModel/models.py:1163 AKModel/models.py:1354
+#: AKModel/models.py:654 AKModel/models.py:731 AKModel/models.py:764
+#: AKModel/models.py:790 AKModel/models.py:809 AKModel/models.py:865
+#: AKModel/models.py:1023 AKModel/models.py:1100 AKModel/models.py:1268
+#: AKModel/models.py:1325 AKModel/models.py:1516
 msgid "Event"
 msgstr "Event"
 
-#: AKModel/availability/models.py:44 AKModel/models.py:500
-#: AKModel/models.py:577 AKModel/models.py:610 AKModel/models.py:636
-#: AKModel/models.py:655 AKModel/models.py:711 AKModel/models.py:869
-#: AKModel/models.py:945 AKModel/models.py:1107 AKModel/models.py:1164
-#: AKModel/models.py:1355
+#: AKModel/availability/models.py:44 AKModel/models.py:655
+#: AKModel/models.py:732 AKModel/models.py:765 AKModel/models.py:791
+#: AKModel/models.py:810 AKModel/models.py:866 AKModel/models.py:1024
+#: AKModel/models.py:1101 AKModel/models.py:1269 AKModel/models.py:1326
+#: AKModel/models.py:1517
 msgid "Associated event"
 msgstr "Zugehöriges Event"
 
@@ -149,8 +149,8 @@ msgstr "Person"
 msgid "Person whose availability this is"
 msgstr "Person deren Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:61 AKModel/models.py:872
-#: AKModel/models.py:934 AKModel/models.py:1173
+#: AKModel/availability/models.py:61 AKModel/models.py:1027
+#: AKModel/models.py:1090 AKModel/models.py:1335
 msgid "Room"
 msgstr "Raum"
 
@@ -158,8 +158,8 @@ msgstr "Raum"
 msgid "Room whose availability this is"
 msgstr "Raum dessen Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:70 AKModel/models.py:719
-#: AKModel/models.py:933 AKModel/models.py:1101
+#: AKModel/availability/models.py:70 AKModel/models.py:874
+#: AKModel/models.py:1089 AKModel/models.py:1263
 msgid "AK"
 msgstr "AK"
 
@@ -167,8 +167,8 @@ msgstr "AK"
 msgid "AK whose availability this is"
 msgstr "Verfügbarkeiten"
 
-#: AKModel/availability/models.py:79 AKModel/models.py:580
-#: AKModel/models.py:1179
+#: AKModel/availability/models.py:79 AKModel/models.py:735
+#: AKModel/models.py:1341
 msgid "AK Category"
 msgstr "AK-Kategorie"
 
@@ -176,7 +176,7 @@ msgstr "AK-Kategorie"
 msgid "AK Category whose availability this is"
 msgstr "AK-Kategorie, deren Verfügbarkeit hier abgebildet wird"
 
-#: AKModel/availability/models.py:309 AKModel/models.py:768
+#: AKModel/availability/models.py:309 AKModel/models.py:923
 msgid "Availabilities"
 msgstr "Verfügbarkeiten"
 
@@ -242,7 +242,7 @@ msgstr ""
 "fürWünsche markieren, z.B. um während der Präsentation auf einem Touchscreen "
 "ausgefüllt zu werden?"
 
-#: AKModel/forms.py:198 AKModel/models.py:1348
+#: AKModel/forms.py:198 AKModel/models.py:1510
 msgid "Default Slots"
 msgstr "Standardslots"
 
@@ -314,9 +314,9 @@ msgstr "Aktivieren?"
 msgid "Finish"
 msgstr "Abschluss"
 
-#: AKModel/models.py:120 AKModel/models.py:568 AKModel/models.py:606
-#: AKModel/models.py:633 AKModel/models.py:652 AKModel/models.py:670
-#: AKModel/models.py:860
+#: AKModel/models.py:120 AKModel/models.py:723 AKModel/models.py:761
+#: AKModel/models.py:788 AKModel/models.py:807 AKModel/models.py:825
+#: AKModel/models.py:1015
 msgid "Name"
 msgstr "Name"
 
@@ -470,12 +470,12 @@ msgstr ""
 msgid "Events"
 msgstr "Events"
 
-#: AKModel/models.py:440
+#: AKModel/models.py:444
 #, python-brace-format
 msgid "AK {ak_name} is not assigned any timeslot by the solver"
 msgstr "Dem AK {ak_name} wurde vom Solver kein Zeitslot zugewiesen"
 
-#: AKModel/models.py:450
+#: AKModel/models.py:454
 #, python-brace-format
 msgid ""
 "Duration of AK {ak_name} assigned by solver ({solver_duration} hours) is "
@@ -485,7 +485,7 @@ msgstr ""
 "Stunden) ist kürzer als die aktuell vorgesehene Dauer des Slots "
 "({slot_duration} Stunden)"
 
-#: AKModel/models.py:464
+#: AKModel/models.py:468
 #, python-brace-format
 msgid ""
 "Fixed AK {ak_name} assigned by solver to room {solver_room} is fixed to room "
@@ -494,7 +494,7 @@ msgstr ""
 "Dem fix geplanten AK {ak_name} wurde vom Solver Raum {solver_room} "
 "zugewiesen, dabei ist der AK bereits fix in Raum {slot_room} eingeplant."
 
-#: AKModel/models.py:475
+#: AKModel/models.py:479
 #, python-brace-format
 msgid ""
 "Fixed AK {ak_name} assigned by solver to start at {solver_start} is fixed to "
@@ -503,71 +503,71 @@ msgstr ""
 "Dem fix geplanten AK {ak_name} wurde vom Solver die Startzeit {solver_start} "
 "zugewiesen, dabei ist der AK bereits für {slot_start} eingeplant."
 
-#: AKModel/models.py:494
+#: AKModel/models.py:649
 msgid "Nickname"
 msgstr "Spitzname"
 
-#: AKModel/models.py:494
+#: AKModel/models.py:649
 msgid "Name to identify an AK owner by"
 msgstr "Name, durch den eine AK-Leitung identifiziert wird"
 
-#: AKModel/models.py:495
+#: AKModel/models.py:650
 msgid "Slug"
 msgstr "Slug"
 
-#: AKModel/models.py:495
+#: AKModel/models.py:650
 msgid "Slug for URL generation"
 msgstr "Slug für URL-Generierung"
 
-#: AKModel/models.py:496
+#: AKModel/models.py:651
 msgid "Institution"
 msgstr "Instutution"
 
-#: AKModel/models.py:496
+#: AKModel/models.py:651
 msgid "Uni etc."
 msgstr "Universität o.ä."
 
-#: AKModel/models.py:497 AKModel/models.py:679
+#: AKModel/models.py:652 AKModel/models.py:834
 msgid "Web Link"
 msgstr "Internet Link"
 
-#: AKModel/models.py:497
+#: AKModel/models.py:652
 msgid "Link to Homepage"
 msgstr "Link zu Homepage oder Webseite"
 
-#: AKModel/models.py:503 AKModel/models.py:1172
+#: AKModel/models.py:658 AKModel/models.py:1334
 msgid "AK Owner"
 msgstr "AK-Leitung"
 
-#: AKModel/models.py:504
+#: AKModel/models.py:659
 msgid "AK Owners"
 msgstr "AK-Leitungen"
 
-#: AKModel/models.py:568
+#: AKModel/models.py:723
 msgid "Name of the AK Category"
 msgstr "Name der AK-Kategorie"
 
-#: AKModel/models.py:569 AKModel/models.py:607
+#: AKModel/models.py:724 AKModel/models.py:762
 msgid "Color"
 msgstr "Farbe"
 
-#: AKModel/models.py:569 AKModel/models.py:607
+#: AKModel/models.py:724 AKModel/models.py:762
 msgid "Color for displaying"
 msgstr "Farbe für die Anzeige"
 
-#: AKModel/models.py:570 AKModel/models.py:673
+#: AKModel/models.py:725 AKModel/models.py:828
 msgid "Description"
 msgstr "Beschreibung"
 
-#: AKModel/models.py:571
+#: AKModel/models.py:726
 msgid "Short description of this AK Category"
 msgstr "Beschreibung der AK-Kategorie"
 
-#: AKModel/models.py:572
+#: AKModel/models.py:727
 msgid "Present by default"
 msgstr "Defaultmäßig präsentieren"
 
-#: AKModel/models.py:573
+#: AKModel/models.py:728
 msgid ""
 "Present AKs of this category by default if AK owner did not specify whether "
 "this AK should be presented?"
@@ -575,152 +575,152 @@ msgstr ""
 "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für "
 "ihren AK nicht explizit spezifiziert haben?"
 
-#: AKModel/models.py:581
+#: AKModel/models.py:736
 msgid "AK Categories"
 msgstr "AK-Kategorien"
 
-#: AKModel/models.py:606
+#: AKModel/models.py:761
 msgid "Name of the AK Track"
 msgstr "Name des AK-Tracks"
 
-#: AKModel/models.py:613
+#: AKModel/models.py:768
 msgid "AK Track"
 msgstr "AK-Track"
 
-#: AKModel/models.py:614
+#: AKModel/models.py:769
 msgid "AK Tracks"
 msgstr "AK-Tracks"
 
-#: AKModel/models.py:633
+#: AKModel/models.py:788
 msgid "Name of the Requirement"
 msgstr "Name der Anforderung"
 
-#: AKModel/models.py:639 AKModel/models.py:1176
+#: AKModel/models.py:794 AKModel/models.py:1338
 msgid "AK Requirement"
 msgstr "AK-Anforderung"
 
-#: AKModel/models.py:640
+#: AKModel/models.py:795
 msgid "AK Requirements"
 msgstr "AK-Anforderungen"
 
-#: AKModel/models.py:652
+#: AKModel/models.py:807
 msgid "Name describing the type"
 msgstr "Name, der den Typ beschreibt"
 
-#: AKModel/models.py:658
+#: AKModel/models.py:813
 msgid "AK Type"
 msgstr "AK Typ"
 
-#: AKModel/models.py:659
+#: AKModel/models.py:814
 msgid "AK Types"
 msgstr "AK-Typen"
 
-#: AKModel/models.py:670
+#: AKModel/models.py:825
 msgid "Name of the AK"
 msgstr "Name des AKs"
 
-#: AKModel/models.py:671
+#: AKModel/models.py:826
 msgid "Short Name"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:672
+#: AKModel/models.py:827
 msgid "Name displayed in the schedule"
 msgstr "Name zur Anzeige im AK-Plan"
 
-#: AKModel/models.py:673
+#: AKModel/models.py:828
 msgid "Description of the AK"
 msgstr "Beschreibung des AKs"
 
-#: AKModel/models.py:675
+#: AKModel/models.py:830
 msgid "Owners"
 msgstr "Leitungen"
 
-#: AKModel/models.py:676
+#: AKModel/models.py:831
 msgid "Those organizing the AK"
 msgstr "Menschen, die den AK organisieren und halten"
 
-#: AKModel/models.py:679
+#: AKModel/models.py:834
 msgid "Link to wiki page"
 msgstr "Link zur Wiki Seite"
 
-#: AKModel/models.py:680
+#: AKModel/models.py:835
 msgid "Protocol Link"
 msgstr "Protokolllink"
 
-#: AKModel/models.py:680
+#: AKModel/models.py:835
 msgid "Link to protocol"
 msgstr "Link zum Protokoll"
 
-#: AKModel/models.py:682
+#: AKModel/models.py:837
 msgid "Category"
 msgstr "Kategorie"
 
-#: AKModel/models.py:683
+#: AKModel/models.py:838
 msgid "Category of the AK"
 msgstr "Kategorie des AKs"
 
-#: AKModel/models.py:684 AKModel/models.py:749
+#: AKModel/models.py:839 AKModel/models.py:904
 msgid "Types"
 msgstr "Typen"
 
-#: AKModel/models.py:685
+#: AKModel/models.py:840
 msgid "This AK is"
 msgstr "Dieser AK ist"
 
-#: AKModel/models.py:686
+#: AKModel/models.py:841
 msgid "Track"
 msgstr "Track"
 
-#: AKModel/models.py:687
+#: AKModel/models.py:842
 msgid "Track the AK belongs to"
 msgstr "Track zu dem der AK gehört"
 
-#: AKModel/models.py:689
+#: AKModel/models.py:844
 msgid "Resolution Intention"
 msgstr "Resolutionsabsicht"
 
-#: AKModel/models.py:690
+#: AKModel/models.py:845
 msgid "Intends to submit a resolution"
 msgstr "Beabsichtigt eine Resolution einzureichen"
 
-#: AKModel/models.py:691
+#: AKModel/models.py:846
 msgid "Present this AK"
 msgstr "AK präsentieren"
 
-#: AKModel/models.py:692
+#: AKModel/models.py:847
 msgid "Present results of this AK"
 msgstr "Die Ergebnisse dieses AKs vorstellen"
 
-#: AKModel/models.py:694 AKModel/models.py:747 AKModel/views/status.py:175
+#: AKModel/models.py:849 AKModel/models.py:902 AKModel/views/status.py:175
 msgid "Requirements"
 msgstr "Anforderungen"
 
-#: AKModel/models.py:695
+#: AKModel/models.py:850
 msgid "AK's Requirements"
 msgstr "Anforderungen des AKs"
 
-#: AKModel/models.py:697
+#: AKModel/models.py:852
 msgid "Conflicting AKs"
 msgstr "AK-Konflikte"
 
-#: AKModel/models.py:698
+#: AKModel/models.py:853
 msgid "AKs that conflict and thus must not take place at the same time"
 msgstr ""
 "AKs, die Konflikte haben und deshalb nicht gleichzeitig stattfinden dürfen"
 
-#: AKModel/models.py:699
+#: AKModel/models.py:854
 msgid "Prerequisite AKs"
 msgstr "Vorausgesetzte AKs"
 
-#: AKModel/models.py:700
+#: AKModel/models.py:855
 msgid "AKs that should precede this AK in the schedule"
 msgstr "AKs die im AK-Plan vor diesem AK stattfinden müssen"
 
-#: AKModel/models.py:702
+#: AKModel/models.py:857
 msgid "Organizational Notes"
 msgstr "Notizen zur Organisation"
 
-#: AKModel/models.py:703
+#: AKModel/models.py:858
 msgid ""
 "Notes to organizers. These are public. For private notes, please use the "
 "button for private messages on the detail page of this AK (after creation/"
@@ -730,299 +730,299 @@ msgstr ""
 "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem "
 "Anlegen/Bearbeiten)."
 
-#: AKModel/models.py:706 AKModel/models.py:745
+#: AKModel/models.py:861 AKModel/models.py:900
 msgid "Interest"
 msgstr "Interesse"
 
-#: AKModel/models.py:706
+#: AKModel/models.py:861
 msgid "Expected number of people"
 msgstr "Erwartete Personenzahl"
 
-#: AKModel/models.py:707
+#: AKModel/models.py:862
 msgid "Interest Counter"
 msgstr "Interessenszähler"
 
-#: AKModel/models.py:708
+#: AKModel/models.py:863
 msgid "People who have indicated interest online"
 msgstr "Anzahl Personen, die online Interesse bekundet haben"
 
-#: AKModel/models.py:713
+#: AKModel/models.py:868
 msgid "Export?"
 msgstr "Export?"
 
-#: AKModel/models.py:714
+#: AKModel/models.py:869
 msgid "Include AK in wiki export?"
 msgstr "AK bei Wiki-Export berücksichtigen?"
 
-#: AKModel/models.py:764
+#: AKModel/models.py:919
 msgid "Conflicts"
 msgstr "Konflikte"
 
-#: AKModel/models.py:767
+#: AKModel/models.py:922
 msgid "Prerequisites"
 msgstr "Voraussetzungen"
 
-#: AKModel/models.py:860
+#: AKModel/models.py:1015
 msgid "Name or number of the room"
 msgstr "Name oder Nummer des Raums"
 
-#: AKModel/models.py:861
+#: AKModel/models.py:1016
 msgid "Location"
 msgstr "Ort"
 
-#: AKModel/models.py:862
+#: AKModel/models.py:1017
 msgid "Name or number of the location"
 msgstr "Name oder Nummer des Ortes"
 
-#: AKModel/models.py:863
+#: AKModel/models.py:1018
 msgid "Capacity"
 msgstr "Kapazität"
 
-#: AKModel/models.py:864
+#: AKModel/models.py:1019
 msgid "Maximum number of people (-1 for unlimited)."
 msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)."
 
-#: AKModel/models.py:865
+#: AKModel/models.py:1020
 msgid "Properties"
 msgstr "Eigenschaften"
 
-#: AKModel/models.py:866
+#: AKModel/models.py:1021
 msgid "AK requirements fulfilled by the room"
 msgstr "AK-Anforderungen, die dieser Raum erfüllt"
 
-#: AKModel/models.py:873 AKModel/views/status.py:59
+#: AKModel/models.py:1028 AKModel/views/status.py:59
 msgid "Rooms"
 msgstr "Räume"
 
-#: AKModel/models.py:933
+#: AKModel/models.py:1089
 msgid "AK being mapped"
 msgstr "AK, der zugeordnet wird"
 
-#: AKModel/models.py:935
+#: AKModel/models.py:1091
 msgid "Room the AK will take place in"
 msgstr "Raum in dem der AK stattfindet"
 
-#: AKModel/models.py:936 AKModel/models.py:1351
+#: AKModel/models.py:1092 AKModel/models.py:1513
 msgid "Slot Begin"
 msgstr "Beginn des Slots"
 
-#: AKModel/models.py:936 AKModel/models.py:1351
+#: AKModel/models.py:1092 AKModel/models.py:1513
 msgid "Time and date the slot begins"
 msgstr "Zeit und Datum zu der der AK beginnt"
 
-#: AKModel/models.py:938
+#: AKModel/models.py:1094
 msgid "Duration"
 msgstr "Dauer"
 
-#: AKModel/models.py:939
+#: AKModel/models.py:1095
 msgid "Length in hours"
 msgstr "Länge in Stunden"
 
-#: AKModel/models.py:941
+#: AKModel/models.py:1097
 msgid "Scheduling fixed"
 msgstr "Planung fix"
 
-#: AKModel/models.py:942
+#: AKModel/models.py:1098
 msgid "Length and time of this AK should not be changed"
 msgstr "Dauer und Zeit dieses AKs sollten nicht verändert werden"
 
-#: AKModel/models.py:947
+#: AKModel/models.py:1103
 msgid "Last update"
 msgstr "Letzte Aktualisierung"
 
-#: AKModel/models.py:950
+#: AKModel/models.py:1106
 msgid "AK Slot"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:951 AKModel/models.py:1169 AKModel/models.py:1204
+#: AKModel/models.py:1107 AKModel/models.py:1331 AKModel/models.py:1366
 msgid "AK Slots"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:973 AKModel/models.py:982
+#: AKModel/models.py:1129 AKModel/models.py:1138
 msgid "Not scheduled yet"
 msgstr "Noch nicht geplant"
 
-#: AKModel/models.py:1102
+#: AKModel/models.py:1264
 msgid "AK this message belongs to"
 msgstr "AK zu dem die Nachricht gehört"
 
-#: AKModel/models.py:1103
+#: AKModel/models.py:1265
 msgid "Message text"
 msgstr "Nachrichtentext"
 
-#: AKModel/models.py:1104
+#: AKModel/models.py:1266
 msgid "Message to the organizers. This is not publicly visible."
 msgstr ""
 "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar."
 
-#: AKModel/models.py:1108
+#: AKModel/models.py:1270
 msgid "Resolved"
 msgstr "Erledigt"
 
-#: AKModel/models.py:1109
+#: AKModel/models.py:1271
 msgid "This message has been resolved (no further action needed)"
 msgstr ""
 "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen "
 "notwendig)"
 
-#: AKModel/models.py:1112
+#: AKModel/models.py:1274
 msgid "AK Orga Message"
 msgstr "AK-Organachricht"
 
-#: AKModel/models.py:1113
+#: AKModel/models.py:1275
 msgid "AK Orga Messages"
 msgstr "AK-Organachrichten"
 
-#: AKModel/models.py:1130
+#: AKModel/models.py:1292
 msgid "Constraint Violation"
 msgstr "Constraintverletzung"
 
-#: AKModel/models.py:1131
+#: AKModel/models.py:1293
 msgid "Constraint Violations"
 msgstr "Constraintverletzungen"
 
-#: AKModel/models.py:1138
+#: AKModel/models.py:1300
 msgid "Owner has two parallel slots"
 msgstr "Leitung hat zwei Slots parallel"
 
-#: AKModel/models.py:1139
+#: AKModel/models.py:1301
 msgid "AK Slot was scheduled outside the AK's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des AKs platziert"
 
-#: AKModel/models.py:1140
+#: AKModel/models.py:1302
 msgid "Room has two AK slots scheduled at the same time"
 msgstr "Raum hat zwei AK Slots gleichzeitig"
 
-#: AKModel/models.py:1141
+#: AKModel/models.py:1303
 msgid "Room does not satisfy the requirement of the scheduled AK"
 msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht"
 
-#: AKModel/models.py:1142
+#: AKModel/models.py:1304
 msgid "AK Slot is scheduled at the same time as an AK listed as a conflict"
 msgstr ""
 "AK Slot wurde wurde zur gleichen Zeit wie ein Konflikt des AKs platziert"
 
-#: AKModel/models.py:1143
+#: AKModel/models.py:1305
 msgid "AK Slot is scheduled before an AK listed as a prerequisite"
 msgstr "AK Slot wurde vor einem als Voraussetzung gelisteten AK platziert"
 
-#: AKModel/models.py:1145
+#: AKModel/models.py:1307
 msgid ""
 "AK Slot for AK with intention to submit a resolution is scheduled after "
 "resolution deadline"
 msgstr ""
 "AK Slot eines AKs mit Resoabsicht wurde nach der Resodeadline platziert"
 
-#: AKModel/models.py:1146
+#: AKModel/models.py:1308
 msgid "AK Slot in a category is outside that categories availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeiten seiner Kategorie"
 
-#: AKModel/models.py:1147
+#: AKModel/models.py:1309
 msgid "Two AK Slots for the same AK scheduled at the same time"
 msgstr "Zwei AK Slots eines AKs wurden zur selben Zeit platziert"
 
-#: AKModel/models.py:1148
+#: AKModel/models.py:1310
 msgid "Room does not have enough space for interest in scheduled AK Slot"
 msgstr "Room hat nicht genug Platz für das Interesse am geplanten AK-Slot"
 
-#: AKModel/models.py:1149
+#: AKModel/models.py:1311
 msgid "AK Slot is scheduled outside the event's availabilities"
 msgstr "AK Slot wurde außerhalb der Verfügbarkeit des Events platziert"
 
-#: AKModel/models.py:1155
+#: AKModel/models.py:1317
 msgid "Warning"
 msgstr "Warnung"
 
-#: AKModel/models.py:1156
+#: AKModel/models.py:1318
 msgid "Violation"
 msgstr "Verletzung"
 
-#: AKModel/models.py:1158
+#: AKModel/models.py:1320
 msgid "Type"
 msgstr "Art"
 
-#: AKModel/models.py:1159
+#: AKModel/models.py:1321
 msgid "Type of violation, i.e. what kind of constraint was violated"
 msgstr "Art der Verletzung, gibt an welche Art Constraint verletzt wurde"
 
-#: AKModel/models.py:1160
+#: AKModel/models.py:1322
 msgid "Level"
 msgstr "Level"
 
-#: AKModel/models.py:1161
+#: AKModel/models.py:1323
 msgid "Severity level of the violation"
 msgstr "Schweregrad der Verletzung"
 
-#: AKModel/models.py:1168
+#: AKModel/models.py:1330
 msgid "AK(s) belonging to this constraint"
 msgstr "AK(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:1170
+#: AKModel/models.py:1332
 msgid "AK Slot(s) belonging to this constraint"
 msgstr "AK Slot(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:1172
+#: AKModel/models.py:1334
 msgid "AK Owner belonging to this constraint"
 msgstr "AK Leitung(en), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:1174
+#: AKModel/models.py:1336
 msgid "Room belonging to this constraint"
 msgstr "Raum, der zu diesem Constraint gehört"
 
-#: AKModel/models.py:1177
+#: AKModel/models.py:1339
 msgid "AK Requirement belonging to this constraint"
 msgstr "AK Anforderung, die zu diesem Constraint gehört"
 
-#: AKModel/models.py:1179
+#: AKModel/models.py:1341
 msgid "AK Category belonging to this constraint"
 msgstr "AK Kategorie, di zu diesem Constraint gehört"
 
-#: AKModel/models.py:1181
+#: AKModel/models.py:1343
 msgid "Comment"
 msgstr "Kommentar"
 
-#: AKModel/models.py:1181
+#: AKModel/models.py:1343
 msgid "Comment or further details for this violation"
 msgstr "Kommentar oder weitere Details zu dieser Vereletzung"
 
-#: AKModel/models.py:1184
+#: AKModel/models.py:1346
 msgid "Timestamp"
 msgstr "Timestamp"
 
-#: AKModel/models.py:1184
+#: AKModel/models.py:1346
 msgid "Time of creation"
 msgstr "Zeitpunkt der ERstellung"
 
-#: AKModel/models.py:1185
+#: AKModel/models.py:1347
 msgid "Manually Resolved"
 msgstr "Manuell behoben"
 
-#: AKModel/models.py:1186
+#: AKModel/models.py:1348
 msgid "Mark this violation manually as resolved"
 msgstr "Markiere diese Verletzung manuell als behoben"
 
-#: AKModel/models.py:1213 AKModel/templates/admin/AKModel/aks_by_user.html:22
+#: AKModel/models.py:1375 AKModel/templates/admin/AKModel/aks_by_user.html:22
 #: AKModel/templates/admin/AKModel/requirements_overview.html:27
 msgid "Details"
 msgstr "Details"
 
-#: AKModel/models.py:1347
+#: AKModel/models.py:1509
 msgid "Default Slot"
 msgstr "Standardslot"
 
-#: AKModel/models.py:1352
+#: AKModel/models.py:1514
 msgid "Slot End"
 msgstr "Ende des Slots"
 
-#: AKModel/models.py:1352
+#: AKModel/models.py:1514
 msgid "Time and date the slot ends"
 msgstr "Zeit und Datum zu der der Slot endet"
 
-#: AKModel/models.py:1357
+#: AKModel/models.py:1519
 msgid "Primary categories"
 msgstr "Primäre Kategorien"
 
-#: AKModel/models.py:1358
+#: AKModel/models.py:1520
 msgid "Categories that should be assigned to this slot primarily"
 msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen"
 
@@ -1229,47 +1229,47 @@ msgstr "Login"
 msgid "Register"
 msgstr "Registrieren"
 
-#: AKModel/views/ak.py:21
+#: AKModel/views/ak.py:19
 msgid "Requirements for Event"
 msgstr "Anforderungen für das Event"
 
-#: AKModel/views/ak.py:38
+#: AKModel/views/ak.py:36
 msgid "AK CSV Export"
 msgstr "AK-CSV-Export"
 
-#: AKModel/views/ak.py:51
+#: AKModel/views/ak.py:49
 msgid "AK JSON Export"
 msgstr "AK-JSON-Export"
 
-#: AKModel/views/ak.py:206
+#: AKModel/views/ak.py:72
 msgid "AK Wiki Export"
 msgstr "AK-Wiki-Export"
 
-#: AKModel/views/ak.py:217 AKModel/views/manage.py:55
+#: AKModel/views/ak.py:83 AKModel/views/manage.py:55
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKModel/views/ak.py:229
+#: AKModel/views/ak.py:95
 msgid "Delete AK Orga Messages"
 msgstr "AK-Organachrichten löschen"
 
-#: AKModel/views/ak.py:247
+#: AKModel/views/ak.py:113
 msgid "AK Orga Messages successfully deleted"
 msgstr "AK-Organachrichten erfolgreich gelöscht"
 
-#: AKModel/views/ak.py:259
+#: AKModel/views/ak.py:125
 msgid "Interest of the following AKs will be set to not filled (-1):"
 msgstr "Interesse an den folgenden AKs wird auf nicht ausgefüllt (-1) gesetzt:"
 
-#: AKModel/views/ak.py:260
+#: AKModel/views/ak.py:126
 msgid "Reset of interest in AKs successful."
 msgstr "Interesse an AKs erfolgreich zurückgesetzt."
 
-#: AKModel/views/ak.py:274
+#: AKModel/views/ak.py:140
 msgid "Interest counter of the following AKs will be set to 0:"
 msgstr "Interessensbekundungszähler der folgenden AKs wird auf 0 gesetzt:"
 
-#: AKModel/views/ak.py:275
+#: AKModel/views/ak.py:141
 msgid "AKs' interest counters set back to 0."
 msgstr "Interessenszähler der AKs zurückgesetzt"
 
-- 
GitLab