diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3bf923f3e82095d5184f4131e4dcd725dd847262..5cb605d57647a7083c60cb64af9db563b906601c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -29,18 +29,15 @@ cache:
 migrations:
   extends: .before_script_template
   script:
-    - source venv/bin/activate
     - ./manage.py makemigrations --dry-run --check
 
 test:
   extends: .before_script_template
   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
     - coverage run --source='.' manage.py test --settings AKPlanning.settings_ci
   after_script:
-    - source venv/bin/activate
     - coverage report
     - coverage xml
   coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
diff --git a/AKDashboard/locale/de_DE/LC_MESSAGES/django.po b/AKDashboard/locale/de_DE/LC_MESSAGES/django.po
index feb56e89e977b73e6bc069f546008f063f1fa290..75b92633f39f1c658d216594c9fc6516a0c250f8 100644
--- a/AKDashboard/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKDashboard/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: 2025-01-01 17:28+0100\n"
+"POT-Creation-Date: 2025-02-27 15:13+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"
@@ -113,22 +113,22 @@ msgstr "AK-Einreichung"
 msgid "AK History"
 msgstr "AK-Verlauf"
 
-#: AKDashboard/views.py:69
+#: AKDashboard/views.py:70
 #, python-format
 msgid "New AK: %(ak)s."
 msgstr "Neuer AK: %(ak)s."
 
-#: AKDashboard/views.py:72
+#: AKDashboard/views.py:73
 #, python-format
 msgid "AK \"%(ak)s\" edited."
 msgstr "AK \"%(ak)s\" bearbeitet."
 
-#: AKDashboard/views.py:75
+#: AKDashboard/views.py:76
 #, python-format
 msgid "AK \"%(ak)s\" deleted."
 msgstr "AK \"%(ak)s\" gelöscht."
 
-#: AKDashboard/views.py:90
+#: AKDashboard/views.py:91
 #, python-format
 msgid "AK \"%(ak)s\" (re-)scheduled."
 msgstr "AK \"%(ak)s\" (um-)geplant."
diff --git a/AKDashboard/tests.py b/AKDashboard/tests.py
index 0cd3e62690f58a6d1d76487f19bf17a10a6a5c83..59328adf517d22a1793df63f67782254b0958f94 100644
--- a/AKDashboard/tests.py
+++ b/AKDashboard/tests.py
@@ -7,7 +7,7 @@ from django.utils.timezone import now
 
 from AKDashboard.models import DashboardButton
 from AKModel.models import AK, AKCategory, Event
-from AKModel.tests import BasicViewTests
+from AKModel.tests.test_views import BasicViewTests
 
 
 class DashboardTests(TestCase):
diff --git a/AKModel/availability/forms.py b/AKModel/availability/forms.py
index 994949a8ef98eadad4eef68b5a94c3abfdf9979b..24a7c4f031974cad0c89ddb6afebf86a485a08fd 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/availability/models.py b/AKModel/availability/models.py
index 7ce794dcda52fcbde462584379e1447ad2b124f2..e2a64b225d2ca4a3da117003e29f5ff399a76976 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 &
@@ -247,7 +253,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.
@@ -267,6 +280,30 @@ 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.
+
+        :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)
+        return full_event.is_covered(availabilities)
+
     class Meta:
         verbose_name = _('Availability')
         verbose_name_plural = _('Availabilities')
diff --git a/AKModel/fixtures/model.json b/AKModel/fixtures/model.json
index 3ef181bcb612f7eb3708077069f597a90ef44a7e..7694f2c901b0a056025fcba292b26b9b66af7f19 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,
@@ -437,6 +437,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,
@@ -461,6 +517,19 @@
         "properties": []
     }
 },
+{
+    "model": "AKModel.room",
+    "pk": 3,
+    "fields": {
+        "name": "BBB Session 1",
+        "location": "",
+        "capacity": -1,
+        "event": 1,
+        "properties": [
+            2
+        ]
+    }
+},
 {
     "model": "AKModel.akslot",
     "pk": 1,
@@ -526,6 +595,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,
@@ -669,5 +790,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/forms.py b/AKModel/forms.py
index be4929c4fd8c8af10eaafecd00c2fa9295e07284..9ec3d24ec5f73148be7f135b8aa9f97965b3a4cc 100644
--- a/AKModel/forms.py
+++ b/AKModel/forms.py
@@ -281,3 +281,13 @@ 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 JSONScheduleImportForm(AdminIntermediateForm):
+    """Form to import an AK schedule from a json file."""
+    json_data = forms.CharField(
+        required=True,
+        widget=forms.Textarea,
+        label=_("JSON data"),
+        help_text=_("JSON data from the scheduling solver"),
+    )
diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index c44a1fab5c315f6ae5193186726c0b651c87d54b..9ef95b03bc63a8bf36540a2e4b674d6c9e2b259a 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-03-03 20:47+0100\n"
+"POT-Creation-Date: 2025-03-04 14:49+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,18 +25,18 @@ msgstr "Status"
 msgid "Toggle plan visibility"
 msgstr "Plansichtbarkeit ändern"
 
-#: AKModel/admin.py:112 AKModel/admin.py:123 AKModel/views/manage.py:138
+#: AKModel/admin.py:112 AKModel/admin.py:123 AKModel/views/manage.py:140
 msgid "Publish plan"
 msgstr "Plan veröffentlichen"
 
-#: AKModel/admin.py:115 AKModel/admin.py:131 AKModel/views/manage.py:151
+#: AKModel/admin.py:115 AKModel/admin.py:131 AKModel/views/manage.py:153
 msgid "Unpublish plan"
 msgstr "Plan verbergen"
 
-#: AKModel/admin.py:170 AKModel/models.py:396 AKModel/models.py:736
+#: AKModel/admin.py:170 AKModel/models.py:890 AKModel/models.py:1348
 #: AKModel/templates/admin/AKModel/aks_by_user.html:12
 #: AKModel/templates/admin/AKModel/status/event_aks.html:10
-#: AKModel/views/manage.py:73 AKModel/views/status.py:102
+#: AKModel/views/manage.py:75 AKModel/views/status.py:102
 msgid "AKs"
 msgstr "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:99
+#: AKModel/admin.py:335 AKModel/views/ak.py:120
 msgid "Reset interest in AKs"
 msgstr "Interesse an AKs zurücksetzen"
 
-#: AKModel/admin.py:345 AKModel/views/ak.py:114
+#: AKModel/admin.py:345 AKModel/views/ak.py:135
 msgid "Reset AKs' interest counters"
 msgstr "Interessenszähler der AKs zurücksetzen"
 
@@ -72,15 +72,15 @@ msgstr "Interessenszähler der AKs zurücksetzen"
 msgid "AK Details"
 msgstr "AK-Details"
 
-#: AKModel/admin.py:520 AKModel/views/manage.py:99
+#: AKModel/admin.py:520 AKModel/views/manage.py:101
 msgid "Mark Constraint Violations as manually resolved"
 msgstr "Markiere Constraintverletzungen als manuell behoben"
 
-#: AKModel/admin.py:529 AKModel/views/manage.py:112
+#: AKModel/admin.py:529 AKModel/views/manage.py:114
 msgid "Set Constraint Violations to level \"violation\""
 msgstr "Constraintverletzungen auf Level \"Violation\" setzen"
 
-#: AKModel/admin.py:538 AKModel/views/manage.py:125
+#: AKModel/admin.py:538 AKModel/views/manage.py:127
 msgid "Set Constraint Violations to level \"warning\""
 msgstr "Constraintverletzungen auf Level \"Warning\" setzen"
 
@@ -100,7 +100,7 @@ msgstr "Ausgewählte Benutzer*innen deaktivieren"
 msgid "The selected users have been deactivated."
 msgstr "Benutzer*innen deaktiviert"
 
-#: AKModel/availability/forms.py:25 AKModel/availability/models.py:271
+#: AKModel/availability/forms.py:25 AKModel/availability/models.py:308
 msgid "Availability"
 msgstr "Verfügbarkeit"
 
@@ -125,19 +125,19 @@ 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:71 AKModel/models.py:187
-#: AKModel/models.py:264 AKModel/models.py:283 AKModel/models.py:309
-#: AKModel/models.py:328 AKModel/models.py:386 AKModel/models.py:546
-#: AKModel/models.py:585 AKModel/models.py:675 AKModel/models.py:732
-#: AKModel/models.py:923
+#: AKModel/availability/models.py:43 AKModel/models.py:177
+#: AKModel/models.py:667 AKModel/models.py:744 AKModel/models.py:777
+#: AKModel/models.py:803 AKModel/models.py:822 AKModel/models.py:880
+#: AKModel/models.py:1039 AKModel/models.py:1116 AKModel/models.py:1286
+#: AKModel/models.py:1344 AKModel/models.py:1536
 msgid "Event"
 msgstr "Event"
 
-#: AKModel/availability/models.py:44 AKModel/models.py:188
-#: AKModel/models.py:265 AKModel/models.py:284 AKModel/models.py:310
-#: AKModel/models.py:329 AKModel/models.py:387 AKModel/models.py:547
-#: AKModel/models.py:586 AKModel/models.py:676 AKModel/models.py:733
-#: AKModel/models.py:924
+#: AKModel/availability/models.py:44 AKModel/models.py:668
+#: AKModel/models.py:745 AKModel/models.py:778 AKModel/models.py:804
+#: AKModel/models.py:823 AKModel/models.py:881 AKModel/models.py:1040
+#: AKModel/models.py:1117 AKModel/models.py:1287 AKModel/models.py:1345
+#: AKModel/models.py:1537
 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:550
-#: AKModel/models.py:575 AKModel/models.py:742
+#: AKModel/availability/models.py:61 AKModel/models.py:1043
+#: AKModel/models.py:1106 AKModel/models.py:1354
 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:395
-#: AKModel/models.py:574 AKModel/models.py:670
+#: AKModel/availability/models.py:70 AKModel/models.py:889
+#: AKModel/models.py:1105 AKModel/models.py:1281
 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:268
-#: AKModel/models.py:748
+#: AKModel/availability/models.py:79 AKModel/models.py:748
+#: AKModel/models.py:1360
 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:272
+#: AKModel/availability/models.py:309
 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:917
+#: AKModel/forms.py:198 AKModel/models.py:1530
 msgid "Default Slots"
 msgstr "Standardslots"
 
@@ -281,7 +281,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:40
+#: AKModel/forms.py:291
+msgid "JSON data"
+msgstr "JSON-Daten"
+
+#: AKModel/forms.py:292
+msgid "JSON data from the scheduling solver"
+msgstr "JSON-Daten, die der scheduling-solver produziert hat"
+
+#: AKModel/metaviews/admin.py:156 AKModel/models.py:140
 msgid "Start"
 msgstr "Start"
 
@@ -306,75 +314,75 @@ msgstr "Aktivieren?"
 msgid "Finish"
 msgstr "Abschluss"
 
-#: AKModel/models.py:21
+#: AKModel/models.py:24
 msgid "May not contain quotation marks"
 msgstr "Darf keine Anführungszeichen enthalten"
 
-#: AKModel/models.py:24
+#: AKModel/models.py:27
 msgid "Must contain at least one letter or digit"
 msgstr "Muss mindestens einen Buchstaben oder eine Ziffer enthalten"
 
-#: AKModel/models.py:31 AKModel/models.py:256 AKModel/models.py:280
-#: AKModel/models.py:307 AKModel/models.py:326 AKModel/models.py:344
-#: AKModel/models.py:536
+#: AKModel/models.py:131 AKModel/models.py:736 AKModel/models.py:774
+#: AKModel/models.py:801 AKModel/models.py:820 AKModel/models.py:838
+#: AKModel/models.py:1031
 msgid "Name"
 msgstr "Name"
 
-#: AKModel/models.py:32
+#: AKModel/models.py:132
 msgid "Name or iteration of the event"
 msgstr "Name oder Iteration des Events"
 
-#: AKModel/models.py:33
+#: AKModel/models.py:133
 msgid "Short Form"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:34
+#: AKModel/models.py:134
 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:36
+#: AKModel/models.py:136
 msgid "Place"
 msgstr "Ort"
 
-#: AKModel/models.py:37
+#: AKModel/models.py:137
 msgid "City etc. the event takes place in"
 msgstr "Stadt o.ä. in der das Event stattfindet"
 
-#: AKModel/models.py:39
+#: AKModel/models.py:139
 msgid "Time Zone"
 msgstr "Zeitzone"
 
-#: AKModel/models.py:39
+#: AKModel/models.py:139
 msgid "Time Zone where this event takes place in"
 msgstr "Zeitzone in der das Event stattfindet"
 
-#: AKModel/models.py:40
+#: AKModel/models.py:140
 msgid "Time the event begins"
 msgstr "Zeit zu der das Event beginnt"
 
-#: AKModel/models.py:41
+#: AKModel/models.py:141
 msgid "End"
 msgstr "Ende"
 
-#: AKModel/models.py:41
+#: AKModel/models.py:141
 msgid "Time the event ends"
 msgstr "Zeit zu der das Event endet"
 
-#: AKModel/models.py:42
+#: AKModel/models.py:142
 msgid "Resolution Deadline"
 msgstr "Resolutionsdeadline"
 
-#: AKModel/models.py:43
+#: AKModel/models.py:143
 msgid "When should AKs with intention to submit a resolution be done?"
 msgstr "Wann sollen AKs mit Resolutionsabsicht stattgefunden haben?"
 
-#: AKModel/models.py:45
+#: AKModel/models.py:145
 msgid "Interest Window Start"
 msgstr "Beginn Interessensbekundung"
 
-#: AKModel/models.py:47
+#: AKModel/models.py:147
 msgid ""
 "Opening time for expression of interest. When left blank, no interest "
 "indication will be possible."
@@ -382,71 +390,83 @@ 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:49
+#: AKModel/models.py:150
 msgid "Interest Window End"
 msgstr "Ende Interessensbekundung"
 
-#: AKModel/models.py:50
+#: AKModel/models.py:151
 msgid "Closing time for expression of interest."
 msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs."
 
-#: AKModel/models.py:52
+#: AKModel/models.py:153
 msgid "Public event"
 msgstr "Öffentliches Event"
 
-#: AKModel/models.py:53
+#: AKModel/models.py:154
 msgid "Show this event on overview page."
 msgstr "Zeige dieses Event auf der Übersichtseite an"
 
-#: AKModel/models.py:55
+#: AKModel/models.py:156
 msgid "Active State"
 msgstr "Aktiver Status"
 
-#: AKModel/models.py:55
+#: AKModel/models.py:156
 msgid "Marks currently active events"
 msgstr "Markiert aktuell aktive Events"
 
-#: AKModel/models.py:56
+#: AKModel/models.py:157
 msgid "Plan Hidden"
 msgstr "Plan verborgen"
 
-#: AKModel/models.py:56
+#: AKModel/models.py:157
 msgid "Hides plan for non-staff users"
 msgstr "Verbirgt den Plan für Nutzer*innen ohne erweiterte Rechte"
 
-#: AKModel/models.py:58
+#: AKModel/models.py:159
 msgid "Plan published at"
 msgstr "Plan veröffentlicht am/um"
 
-#: AKModel/models.py:59
+#: AKModel/models.py:160
 msgid "Timestamp at which the plan was published"
 msgstr "Zeitpunkt, zu dem der Plan veröffentlicht wurde"
 
-#: AKModel/models.py:61
+#: AKModel/models.py:162
 msgid "Base URL"
 msgstr "URL-Prefix"
 
-#: AKModel/models.py:61
+#: AKModel/models.py:162
 msgid "Prefix for wiki link construction"
 msgstr "Prefix für die automatische Generierung von Wiki-Links"
 
-#: AKModel/models.py:62
+#: AKModel/models.py:163
 msgid "Wiki Export Template Name"
 msgstr "Wiki-Export Templatename"
 
-#: AKModel/models.py:63
+#: AKModel/models.py:164
 msgid "Default Slot Length"
 msgstr "Standardslotlänge"
 
-#: AKModel/models.py:64
+#: AKModel/models.py:165
 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:66
+#: AKModel/models.py:166
+msgid "Export Slot Length"
+msgstr "Export-Slotlänge"
+
+#: AKModel/models.py:168
+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:172
 msgid "Contact email address"
 msgstr "E-Mail Kontaktadresse"
 
-#: AKModel/models.py:67
+#: AKModel/models.py:173
 msgid ""
 "An email address that is displayed on every page and can be used for all "
 "kinds of questions"
@@ -454,75 +474,108 @@ msgstr ""
 "Eine Mailadresse die auf jeder Seite angezeigt wird und für alle Arten von "
 "Fragen genutzt werden kann"
 
-#: AKModel/models.py:72
+#: AKModel/models.py:178
 msgid "Events"
 msgstr "Events"
 
-#: AKModel/models.py:180
+#: AKModel/models.py:455
+#, 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:465
+#, 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:479
+#, 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:490
+#, 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:660
 msgid "Nickname"
 msgstr "Spitzname"
 
-#: AKModel/models.py:182
+#: AKModel/models.py:662
 msgid "Name to identify an AK owner by"
 msgstr "Name, durch den eine AK-Leitung identifiziert wird"
 
-#: AKModel/models.py:183
+#: AKModel/models.py:663
 msgid "Slug"
 msgstr "Slug"
 
-#: AKModel/models.py:183
+#: AKModel/models.py:663
 msgid "Slug for URL generation"
 msgstr "Slug für URL-Generierung"
 
-#: AKModel/models.py:184
+#: AKModel/models.py:664
 msgid "Institution"
 msgstr "Instutution"
 
-#: AKModel/models.py:184
+#: AKModel/models.py:664
 msgid "Uni etc."
 msgstr "Universität o.ä."
 
-#: AKModel/models.py:185 AKModel/models.py:355
+#: AKModel/models.py:665 AKModel/models.py:849
 msgid "Web Link"
 msgstr "Internet Link"
 
-#: AKModel/models.py:185
+#: AKModel/models.py:665
 msgid "Link to Homepage"
 msgstr "Link zu Homepage oder Webseite"
 
-#: AKModel/models.py:191 AKModel/models.py:741
+#: AKModel/models.py:671 AKModel/models.py:1353
 msgid "AK Owner"
 msgstr "AK-Leitung"
 
-#: AKModel/models.py:192
+#: AKModel/models.py:672
 msgid "AK Owners"
 msgstr "AK-Leitungen"
 
-#: AKModel/models.py:256
+#: AKModel/models.py:736
 msgid "Name of the AK Category"
 msgstr "Name der AK-Kategorie"
 
-#: AKModel/models.py:257 AKModel/models.py:281
+#: AKModel/models.py:737 AKModel/models.py:775
 msgid "Color"
 msgstr "Farbe"
 
-#: AKModel/models.py:257 AKModel/models.py:281
+#: AKModel/models.py:737 AKModel/models.py:775
 msgid "Color for displaying"
 msgstr "Farbe für die Anzeige"
 
-#: AKModel/models.py:258 AKModel/models.py:349
+#: AKModel/models.py:738 AKModel/models.py:843
 msgid "Description"
 msgstr "Beschreibung"
 
-#: AKModel/models.py:259
+#: AKModel/models.py:739
 msgid "Short description of this AK Category"
 msgstr "Beschreibung der AK-Kategorie"
 
-#: AKModel/models.py:260
+#: AKModel/models.py:740
 msgid "Present by default"
 msgstr "Defaultmäßig präsentieren"
 
-#: AKModel/models.py:261
+#: AKModel/models.py:741
 msgid ""
 "Present AKs of this category by default if AK owner did not specify whether "
 "this AK should be presented?"
@@ -530,152 +583,152 @@ msgstr ""
 "AKs dieser Kategorie standardmäßig vorstellen, wenn die Leitungen das für "
 "ihren AK nicht explizit spezifiziert haben?"
 
-#: AKModel/models.py:269
+#: AKModel/models.py:749
 msgid "AK Categories"
 msgstr "AK-Kategorien"
 
-#: AKModel/models.py:280
+#: AKModel/models.py:774
 msgid "Name of the AK Track"
 msgstr "Name des AK-Tracks"
 
-#: AKModel/models.py:287
+#: AKModel/models.py:781
 msgid "AK Track"
 msgstr "AK-Track"
 
-#: AKModel/models.py:288
+#: AKModel/models.py:782
 msgid "AK Tracks"
 msgstr "AK-Tracks"
 
-#: AKModel/models.py:307
+#: AKModel/models.py:801
 msgid "Name of the Requirement"
 msgstr "Name der Anforderung"
 
-#: AKModel/models.py:313 AKModel/models.py:745
+#: AKModel/models.py:807 AKModel/models.py:1357
 msgid "AK Requirement"
 msgstr "AK-Anforderung"
 
-#: AKModel/models.py:314
+#: AKModel/models.py:808
 msgid "AK Requirements"
 msgstr "AK-Anforderungen"
 
-#: AKModel/models.py:326
+#: AKModel/models.py:820
 msgid "Name describing the type"
 msgstr "Name, der den Typ beschreibt"
 
-#: AKModel/models.py:332
+#: AKModel/models.py:826
 msgid "AK Type"
 msgstr "AK Typ"
 
-#: AKModel/models.py:333
+#: AKModel/models.py:827
 msgid "AK Types"
 msgstr "AK-Typen"
 
-#: AKModel/models.py:344
+#: AKModel/models.py:838
 msgid "Name of the AK"
 msgstr "Name des AKs"
 
-#: AKModel/models.py:346
+#: AKModel/models.py:840
 msgid "Short Name"
 msgstr "Kurzer Name"
 
-#: AKModel/models.py:348
+#: AKModel/models.py:842
 msgid "Name displayed in the schedule"
 msgstr "Name zur Anzeige im AK-Plan"
 
-#: AKModel/models.py:349
+#: AKModel/models.py:843
 msgid "Description of the AK"
 msgstr "Beschreibung des AKs"
 
-#: AKModel/models.py:351
+#: AKModel/models.py:845
 msgid "Owners"
 msgstr "Leitungen"
 
-#: AKModel/models.py:352
+#: AKModel/models.py:846
 msgid "Those organizing the AK"
 msgstr "Menschen, die den AK organisieren und halten"
 
-#: AKModel/models.py:355
+#: AKModel/models.py:849
 msgid "Link to wiki page"
 msgstr "Link zur Wiki Seite"
 
-#: AKModel/models.py:356
+#: AKModel/models.py:850
 msgid "Protocol Link"
 msgstr "Protokolllink"
 
-#: AKModel/models.py:356
+#: AKModel/models.py:850
 msgid "Link to protocol"
 msgstr "Link zum Protokoll"
 
-#: AKModel/models.py:358
+#: AKModel/models.py:852
 msgid "Category"
 msgstr "Kategorie"
 
-#: AKModel/models.py:359
+#: AKModel/models.py:853
 msgid "Category of the AK"
 msgstr "Kategorie des AKs"
 
-#: AKModel/models.py:360
+#: AKModel/models.py:854
 msgid "Types"
 msgstr "Typen"
 
-#: AKModel/models.py:361
+#: AKModel/models.py:855
 msgid "This AK is"
 msgstr "Dieser AK ist"
 
-#: AKModel/models.py:362
+#: AKModel/models.py:856
 msgid "Track"
 msgstr "Track"
 
-#: AKModel/models.py:363
+#: AKModel/models.py:857
 msgid "Track the AK belongs to"
 msgstr "Track zu dem der AK gehört"
 
-#: AKModel/models.py:365
+#: AKModel/models.py:859
 msgid "Resolution Intention"
 msgstr "Resolutionsabsicht"
 
-#: AKModel/models.py:366
+#: AKModel/models.py:860
 msgid "Intends to submit a resolution"
 msgstr "Beabsichtigt eine Resolution einzureichen"
 
-#: AKModel/models.py:367
+#: AKModel/models.py:861
 msgid "Present this AK"
 msgstr "AK präsentieren"
 
-#: AKModel/models.py:368
+#: AKModel/models.py:862
 msgid "Present results of this AK"
 msgstr "Die Ergebnisse dieses AKs vorstellen"
 
-#: AKModel/models.py:370 AKModel/views/status.py:167
+#: AKModel/models.py:864 AKModel/views/status.py:175
 msgid "Requirements"
 msgstr "Anforderungen"
 
-#: AKModel/models.py:371
+#: AKModel/models.py:865
 msgid "AK's Requirements"
 msgstr "Anforderungen des AKs"
 
-#: AKModel/models.py:373
+#: AKModel/models.py:867
 msgid "Conflicting AKs"
 msgstr "AK-Konflikte"
 
-#: AKModel/models.py:374
+#: AKModel/models.py:868
 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:375
+#: AKModel/models.py:869
 msgid "Prerequisite AKs"
 msgstr "Vorausgesetzte AKs"
 
-#: AKModel/models.py:376
+#: AKModel/models.py:870
 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:378
+#: AKModel/models.py:872
 msgid "Organizational Notes"
 msgstr "Notizen zur Organisation"
 
-#: AKModel/models.py:379
+#: AKModel/models.py:873
 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/"
@@ -685,291 +738,291 @@ msgstr ""
 "Anmerkungen bitte den Button für Direktnachrichten verwenden (nach dem "
 "Anlegen/Bearbeiten)."
 
-#: AKModel/models.py:382
+#: AKModel/models.py:876
 msgid "Interest"
 msgstr "Interesse"
 
-#: AKModel/models.py:382
+#: AKModel/models.py:876
 msgid "Expected number of people"
 msgstr "Erwartete Personenzahl"
 
-#: AKModel/models.py:383
+#: AKModel/models.py:877
 msgid "Interest Counter"
 msgstr "Interessenszähler"
 
-#: AKModel/models.py:384
+#: AKModel/models.py:878
 msgid "People who have indicated interest online"
 msgstr "Anzahl Personen, die online Interesse bekundet haben"
 
-#: AKModel/models.py:389
+#: AKModel/models.py:883
 msgid "Export?"
 msgstr "Export?"
 
-#: AKModel/models.py:390
+#: AKModel/models.py:884
 msgid "Include AK in wiki export?"
 msgstr "AK bei Wiki-Export berücksichtigen?"
 
-#: AKModel/models.py:536
+#: AKModel/models.py:1031
 msgid "Name or number of the room"
 msgstr "Name oder Nummer des Raums"
 
-#: AKModel/models.py:537
+#: AKModel/models.py:1032
 msgid "Location"
 msgstr "Ort"
 
-#: AKModel/models.py:538
+#: AKModel/models.py:1033
 msgid "Name or number of the location"
 msgstr "Name oder Nummer des Ortes"
 
-#: AKModel/models.py:539
+#: AKModel/models.py:1034
 msgid "Capacity"
 msgstr "Kapazität"
 
-#: AKModel/models.py:540
+#: AKModel/models.py:1035
 msgid "Maximum number of people (-1 for unlimited)."
 msgstr "Maximale Personenzahl (-1 wenn unbeschränkt)."
 
-#: AKModel/models.py:541
+#: AKModel/models.py:1036
 msgid "Properties"
 msgstr "Eigenschaften"
 
-#: AKModel/models.py:542
+#: AKModel/models.py:1037
 msgid "AK requirements fulfilled by the room"
 msgstr "AK-Anforderungen, die dieser Raum erfüllt"
 
-#: AKModel/models.py:551 AKModel/views/status.py:59
+#: AKModel/models.py:1044 AKModel/views/status.py:59
 msgid "Rooms"
 msgstr "Räume"
 
-#: AKModel/models.py:574
+#: AKModel/models.py:1105
 msgid "AK being mapped"
 msgstr "AK, der zugeordnet wird"
 
-#: AKModel/models.py:576
+#: AKModel/models.py:1107
 msgid "Room the AK will take place in"
 msgstr "Raum in dem der AK stattfindet"
 
-#: AKModel/models.py:577 AKModel/models.py:920
+#: AKModel/models.py:1108 AKModel/models.py:1533
 msgid "Slot Begin"
 msgstr "Beginn des Slots"
 
-#: AKModel/models.py:577 AKModel/models.py:920
+#: AKModel/models.py:1108 AKModel/models.py:1533
 msgid "Time and date the slot begins"
 msgstr "Zeit und Datum zu der der AK beginnt"
 
-#: AKModel/models.py:579
+#: AKModel/models.py:1110
 msgid "Duration"
 msgstr "Dauer"
 
-#: AKModel/models.py:580
+#: AKModel/models.py:1111
 msgid "Length in hours"
 msgstr "Länge in Stunden"
 
-#: AKModel/models.py:582
+#: AKModel/models.py:1113
 msgid "Scheduling fixed"
 msgstr "Planung fix"
 
-#: AKModel/models.py:583
+#: AKModel/models.py:1114
 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:588
+#: AKModel/models.py:1119
 msgid "Last update"
 msgstr "Letzte Aktualisierung"
 
-#: AKModel/models.py:591
+#: AKModel/models.py:1122
 msgid "AK Slot"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:592 AKModel/models.py:738
+#: AKModel/models.py:1123 AKModel/models.py:1350
 msgid "AK Slots"
 msgstr "AK-Slot"
 
-#: AKModel/models.py:614 AKModel/models.py:623
+#: AKModel/models.py:1145 AKModel/models.py:1154
 msgid "Not scheduled yet"
 msgstr "Noch nicht geplant"
 
-#: AKModel/models.py:671
+#: AKModel/models.py:1282
 msgid "AK this message belongs to"
 msgstr "AK zu dem die Nachricht gehört"
 
-#: AKModel/models.py:672
+#: AKModel/models.py:1283
 msgid "Message text"
 msgstr "Nachrichtentext"
 
-#: AKModel/models.py:673
+#: AKModel/models.py:1284
 msgid "Message to the organizers. This is not publicly visible."
 msgstr ""
 "Nachricht an die Organisator*innen. Diese ist nicht öffentlich sichtbar."
 
-#: AKModel/models.py:677
+#: AKModel/models.py:1288
 msgid "Resolved"
 msgstr "Erledigt"
 
-#: AKModel/models.py:678
+#: AKModel/models.py:1289
 msgid "This message has been resolved (no further action needed)"
 msgstr ""
 "Diese Nachricht wurde vollständig bearbeitet (keine weiteren Aktionen "
 "notwendig)"
 
-#: AKModel/models.py:681
+#: AKModel/models.py:1292
 msgid "AK Orga Message"
 msgstr "AK-Organachricht"
 
-#: AKModel/models.py:682
+#: AKModel/models.py:1293
 msgid "AK Orga Messages"
 msgstr "AK-Organachrichten"
 
-#: AKModel/models.py:699
+#: AKModel/models.py:1311
 msgid "Constraint Violation"
 msgstr "Constraintverletzung"
 
-#: AKModel/models.py:700
+#: AKModel/models.py:1312
 msgid "Constraint Violations"
 msgstr "Constraintverletzungen"
 
-#: AKModel/models.py:707
+#: AKModel/models.py:1319
 msgid "Owner has two parallel slots"
 msgstr "Leitung hat zwei Slots parallel"
 
-#: AKModel/models.py:708
+#: AKModel/models.py:1320
 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:709
+#: AKModel/models.py:1321
 msgid "Room has two AK slots scheduled at the same time"
 msgstr "Raum hat zwei AK Slots gleichzeitig"
 
-#: AKModel/models.py:710
+#: AKModel/models.py:1322
 msgid "Room does not satisfy the requirement of the scheduled AK"
 msgstr "Room erfüllt die Anforderungen des platzierten AKs nicht"
 
-#: AKModel/models.py:711
+#: AKModel/models.py:1323
 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:712
+#: AKModel/models.py:1324
 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:714
+#: AKModel/models.py:1326
 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:715
+#: AKModel/models.py:1327
 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:716
+#: AKModel/models.py:1328
 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:717
+#: AKModel/models.py:1329
 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:718
+#: AKModel/models.py:1330
 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:724
+#: AKModel/models.py:1336
 msgid "Warning"
 msgstr "Warnung"
 
-#: AKModel/models.py:725
+#: AKModel/models.py:1337
 msgid "Violation"
 msgstr "Verletzung"
 
-#: AKModel/models.py:727
+#: AKModel/models.py:1339
 msgid "Type"
 msgstr "Art"
 
-#: AKModel/models.py:728
+#: AKModel/models.py:1340
 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:729
+#: AKModel/models.py:1341
 msgid "Level"
 msgstr "Level"
 
-#: AKModel/models.py:730
+#: AKModel/models.py:1342
 msgid "Severity level of the violation"
 msgstr "Schweregrad der Verletzung"
 
-#: AKModel/models.py:737
+#: AKModel/models.py:1349
 msgid "AK(s) belonging to this constraint"
 msgstr "AK(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:739
+#: AKModel/models.py:1351
 msgid "AK Slot(s) belonging to this constraint"
 msgstr "AK Slot(s), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:741
+#: AKModel/models.py:1353
 msgid "AK Owner belonging to this constraint"
 msgstr "AK Leitung(en), die zu diesem Constraint gehören"
 
-#: AKModel/models.py:743
+#: AKModel/models.py:1355
 msgid "Room belonging to this constraint"
 msgstr "Raum, der zu diesem Constraint gehört"
 
-#: AKModel/models.py:746
+#: AKModel/models.py:1358
 msgid "AK Requirement belonging to this constraint"
 msgstr "AK Anforderung, die zu diesem Constraint gehört"
 
-#: AKModel/models.py:748
+#: AKModel/models.py:1360
 msgid "AK Category belonging to this constraint"
 msgstr "AK Kategorie, di zu diesem Constraint gehört"
 
-#: AKModel/models.py:750
+#: AKModel/models.py:1362
 msgid "Comment"
 msgstr "Kommentar"
 
-#: AKModel/models.py:750
+#: AKModel/models.py:1362
 msgid "Comment or further details for this violation"
 msgstr "Kommentar oder weitere Details zu dieser Vereletzung"
 
-#: AKModel/models.py:753
+#: AKModel/models.py:1365
 msgid "Timestamp"
 msgstr "Timestamp"
 
-#: AKModel/models.py:753
+#: AKModel/models.py:1365
 msgid "Time of creation"
 msgstr "Zeitpunkt der ERstellung"
 
-#: AKModel/models.py:754
+#: AKModel/models.py:1366
 msgid "Manually Resolved"
 msgstr "Manuell behoben"
 
-#: AKModel/models.py:755
+#: AKModel/models.py:1367
 msgid "Mark this violation manually as resolved"
 msgstr "Markiere diese Verletzung manuell als behoben"
 
-#: AKModel/models.py:782 AKModel/templates/admin/AKModel/aks_by_user.html:22
+#: AKModel/models.py:1394 AKModel/templates/admin/AKModel/aks_by_user.html:22
 #: AKModel/templates/admin/AKModel/requirements_overview.html:27
 msgid "Details"
 msgstr "Details"
 
-#: AKModel/models.py:916
+#: AKModel/models.py:1529
 msgid "Default Slot"
 msgstr "Standardslot"
 
-#: AKModel/models.py:921
+#: AKModel/models.py:1534
 msgid "Slot End"
 msgstr "Ende des Slots"
 
-#: AKModel/models.py:921
+#: AKModel/models.py:1534
 msgid "Time and date the slot ends"
 msgstr "Zeit und Datum zu der der Slot endet"
 
-#: AKModel/models.py:926
+#: AKModel/models.py:1539
 msgid "Primary categories"
 msgstr "Primäre Kategorien"
 
-#: AKModel/models.py:927
+#: AKModel/models.py:1541
 msgid "Categories that should be assigned to this slot primarily"
 msgstr "Kategorieren, die diesem Slot primär zugewiesen werden sollen"
 
@@ -1090,7 +1143,7 @@ msgid "No AKs with this requirement"
 msgstr "Kein AK mit dieser Anforderung"
 
 #: AKModel/templates/admin/AKModel/requirements_overview.html:45
-#: AKModel/views/status.py:183
+#: AKModel/views/status.py:191
 msgid "Add Requirement"
 msgstr "Anforderung hinzufügen"
 
@@ -1176,43 +1229,47 @@ msgstr "Login"
 msgid "Register"
 msgstr "Registrieren"
 
-#: AKModel/views/ak.py:17
+#: AKModel/views/ak.py:19
 msgid "Requirements for Event"
 msgstr "Anforderungen für das Event"
 
-#: AKModel/views/ak.py:34
+#: AKModel/views/ak.py:36
 msgid "AK CSV Export"
 msgstr "AK-CSV-Export"
 
-#: AKModel/views/ak.py:48
+#: AKModel/views/ak.py:49
+msgid "AK JSON Export"
+msgstr "AK-JSON-Export"
+
+#: AKModel/views/ak.py:69
 msgid "AK Wiki Export"
 msgstr "AK-Wiki-Export"
 
-#: AKModel/views/ak.py:59 AKModel/views/manage.py:53
+#: AKModel/views/ak.py:80 AKModel/views/manage.py:55
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKModel/views/ak.py:71
+#: AKModel/views/ak.py:92
 msgid "Delete AK Orga Messages"
 msgstr "AK-Organachrichten löschen"
 
-#: AKModel/views/ak.py:89
+#: AKModel/views/ak.py:110
 msgid "AK Orga Messages successfully deleted"
 msgstr "AK-Organachrichten erfolgreich gelöscht"
 
-#: AKModel/views/ak.py:101
+#: AKModel/views/ak.py:122
 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:123
 msgid "Reset of interest in AKs successful."
 msgstr "Interesse an AKs erfolgreich zurückgesetzt."
 
-#: AKModel/views/ak.py:116
+#: AKModel/views/ak.py:137
 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:138
 msgid "AKs' interest counters set back to 0."
 msgstr "Interessenszähler der AKs zurückgesetzt"
 
@@ -1226,90 +1283,103 @@ 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:150
+#: AKModel/views/manage.py:37 AKModel/views/status.py:158
 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:134
+#: AKModel/views/manage.py:168 AKModel/views/status.py:134
 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:257
+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'"
@@ -1362,21 +1432,35 @@ msgid "Manage ak tracks"
 msgstr "AK-Tracks verwalten"
 
 #: AKModel/views/status.py:142
+msgid "Import AK schedule from JSON"
+msgstr "AK-Plan aus JSON importieren"
+
+#: AKModel/views/status.py:146
 msgid "Export AKs as CSV"
 msgstr "AKs als CSV exportieren"
 
-#: AKModel/views/status.py:146
+#: AKModel/views/status.py:150
+msgid "Export AKs as JSON"
+msgstr "AKs als JSON exportieren"
+
+#: AKModel/views/status.py:154
 msgid "Export AKs for Wiki"
 msgstr "AKs im Wiki-Format exportieren"
 
-#: AKModel/views/status.py:179
+#: AKModel/views/status.py:187
 msgid "Show AKs for requirements"
 msgstr "Zu Anforderungen gehörige AKs anzeigen"
 
-#: AKModel/views/status.py:193
+#: AKModel/views/status.py:201
 msgid "Event Status"
 msgstr "Eventstatus"
 
+#~ msgid "Conflicts"
+#~ msgstr "Konflikte"
+
+#~ msgid "Prerequisites"
+#~ msgstr "Voraussetzungen"
+
 #~ msgid "Opening time for expression of interest."
 #~ msgstr "Öffnungszeitpunkt für die Angabe von Interesse an AKs."
 
diff --git a/AKModel/migrations/0061_event_export_slot.py b/AKModel/migrations/0061_event_export_slot.py
new file mode 100644
index 0000000000000000000000000000000000000000..3b40b88cafa836b16077c81117f81fbc95146808
--- /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/migrations/0063_merge_0061_event_export_slot_0062_interest_no_history.py b/AKModel/migrations/0063_merge_0061_event_export_slot_0062_interest_no_history.py
new file mode 100644
index 0000000000000000000000000000000000000000..1e64026c1e18c08a82126443030c598da0ddf8ed
--- /dev/null
+++ b/AKModel/migrations/0063_merge_0061_event_export_slot_0062_interest_no_history.py
@@ -0,0 +1,13 @@
+# Generated by Django 4.2.13 on 2025-02-27 18:10
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("AKModel", "0061_event_export_slot"),
+        ("AKModel", "0062_interest_no_history"),
+    ]
+
+    operations = []
diff --git a/AKModel/migrations/0064_merge_20250304_1416.py b/AKModel/migrations/0064_merge_20250304_1416.py
new file mode 100644
index 0000000000000000000000000000000000000000..456a64455182a03bd08b5c030bed1284adaccc67
--- /dev/null
+++ b/AKModel/migrations/0064_merge_20250304_1416.py
@@ -0,0 +1,14 @@
+# Generated by Django 5.1.6 on 2025-03-04 14:16
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('AKModel', '0063_field_validators'),
+        ('AKModel', '0063_merge_0061_event_export_slot_0062_interest_no_history'),
+    ]
+
+    operations = [
+    ]
diff --git a/AKModel/models.py b/AKModel/models.py
index 1b23b8732566652abf022f159bd8dbe063c68bb7..9d7b6c2e15dc332fec9472a0553878670a7288a2 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -1,9 +1,14 @@
+import decimal
 import itertools
+import json
+import math
+from dataclasses import dataclass
 from datetime import datetime, timedelta
+from typing import Any, Generator, Iterable
 
-from django.core.validators import RegexValidator
 from django.apps import apps
-from django.db import models
+from django.core.validators import RegexValidator
+from django.db import models, transaction
 from django.db.models import Count
 from django.urls import reverse_lazy
 from django.utils import timezone
@@ -12,7 +17,6 @@ from django.utils.translation import gettext_lazy as _
 from simple_history.models import HistoricalRecords
 from timezone_field import TimeZoneField
 
-
 # Custom validators to be used for some of the fields
 # Prevent inclusion of the quotation marks ' " ´ `
 # This may be necessary to prevent javascript issues
@@ -23,6 +27,103 @@ no_quotation_marks_validator = RegexValidator(regex=r"['\"´`]+", inverse_match=
 slugable_validator = RegexValidator(regex=r"[\w\s]+", message=_('Must contain at least one letter or digit'))
 
 
+@dataclass
+class OptimizerTimeslot:
+    """Class describing a discrete timeslot. Used to interface with an optimizer."""
+
+    avail: "Availability"
+    """The availability object corresponding to this timeslot."""
+
+    idx: int
+    """The unique index of this optimizer timeslot."""
+
+    constraints: set[str]
+    """The set of time constraints fulfilled by this object."""
+
+    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)
+        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]
+
+
+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.
@@ -62,6 +163,11 @@ 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 "
@@ -173,6 +279,380 @@ class Event(models.Model):
                 .filter(availabilities__count=0, owners__count__gt=0)
                 )
 
+    def _generate_slots_from_block(
+            self,
+            start: datetime,
+            end: datetime,
+            slot_duration: timedelta,
+            *,
+            slot_index: int = 0,
+            constraints: set[str] | None = None,
+    ) -> Generator[TimeslotBlock, None, int]:
+        """Discretize a time range into timeslots.
+
+        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.
+
+        :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 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
+        from AKModel.availability.models import Availability
+
+        current_slot_start = start
+        previous_slot_start: datetime | None = None
+
+        if constraints is None:
+            constraints = set()
+
+        current_block = []
+
+        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(
+                        OptimizerTimeslot(avail=slot, idx=slot_index, constraints=constraints)
+                )
+                previous_slot_start = current_slot_start
+
+            slot_index += 1
+            current_slot_start += slot_duration
+
+        if current_block:
+            yield current_block
+
+        return slot_index
+
+    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
+        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: list of OptimizerTimeslot
+        """
+        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: float) -> 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 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"):
+            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 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.
+
+        :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 slots_in_an_hour is None:
+            slots_in_an_hour = 1.0 / 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))
+        else:
+            yield from self.uniform_time_slots(slots_in_an_hour=slots_in_an_hour)
+
+    @transaction.atomic
+    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
+            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)
+        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"]
+
+        timeslot_dict = {
+            timeslot.idx: timeslot
+            for block in self.discretize_timeslots(slots_in_an_hour=slots_in_an_hour)
+            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"]))
+
+            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
+
+        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).order_by()
+        slots = AKSlot.objects.filter(event=self).order_by()
+
+        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": sorted(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.
@@ -273,6 +753,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.
@@ -415,7 +909,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}"""
@@ -450,7 +944,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())
 
@@ -460,7 +954,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())
 
@@ -566,6 +1060,44 @@ class Room(models.Model):
     def __str__(self):
         return self.title
 
+    def as_json_dict(self) -> dict[str, Any]:
+        """Return a json representation of this room object.
+
+        :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
+
+        # check if room is available for the whole event
+        # -> no time constraint needs to be introduced
+        if Availability.is_event_covered(self.event, self.availabilities.all()):
+            time_constraints = []
+        else:
+            time_constraints = [f"availability-room-{self.pk}"]
+
+        data = {
+            "id": self.pk,
+            "info": {
+                "name": self.name,
+            },
+            "capacity": self.capacity,
+            "fulfilled_room_constraints": [constraint.name
+                                           for constraint in self.properties.all()],
+            "time_constraints": time_constraints
+        }
+
+        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")
+
+        data["fulfilled_room_constraints"].sort()
+        return data
+
 
 class AKSlot(models.Model):
     """ An AK Mapping matches an AK to a room during a certain time.
@@ -662,6 +1194,85 @@ class AKSlot(models.Model):
         super().save(*args,
                      force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
 
+    def as_json_dict(self) -> dict[str, Any]:
+        """Return a json representation of the AK object of this slot.
+
+        :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
+
+        # check if ak resp. owner is available for the whole event
+        # -> no time constraint needs to be introduced
+
+        if self.fixed and self.start is not None:
+            ak_time_constraints = [f"fixed-akslot-{self.id}"]
+        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 = []
+
+        def _owner_time_constraints(owner: AKOwner):
+            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}"]
+
+        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)
+
+        data = {
+            "id": self.pk,
+            "duration": math.ceil(self.duration / self.event.export_slot - ceil_offet_eps),
+            "properties": {
+                "conflicts":
+                    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()],
+            "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,
+                "duration_in_hours": float(self.duration),
+                "django_ak_id": self.ak.pk,
+                "types": list(self.ak.types.values_list("name", flat=True).order_by()),
+            },
+        }
+
+        data["time_constraints"].extend(ak_time_constraints)
+        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.fixed and self.room is not None:
+            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")
+
+        data["room_constraints"].sort()
+        data["time_constraints"].sort()
+
+        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
new file mode 100644
index 0000000000000000000000000000000000000000..65e459a5b3f7509ed98a1329e2d89f06fa2abb54
--- /dev/null
+++ b/AKModel/templates/admin/AKModel/ak_json_export.html
@@ -0,0 +1,20 @@
+{% extends "admin/base_site.html" %}
+
+{% 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/tests/__init__.py b/AKModel/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py
new file mode 100644
index 0000000000000000000000000000000000000000..e8aac8fed36ba4f6917c3e22e87ae2ebec739775
--- /dev/null
+++ b/AKModel/tests/test_json_export.py
@@ -0,0 +1,860 @@
+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)
+            .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
+
+    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(
+                    {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",
+                            "types",
+                        },
+                        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"], 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)
+                    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"],
+                        int,
+                        "info/django_ak_id",
+                        item=item,
+                    )
+
+                    self._check_lst(
+                        ak["properties"]["conflicts"],
+                        "conflicts",
+                        item=item,
+                        contained_type=int,
+                    )
+                    self._check_lst(
+                        ak["properties"]["dependencies"],
+                        "dependencies",
+                        item=item,
+                        contained_type=int,
+                    )
+                    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"], int, "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", "blocknames"},
+                    "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"]["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",
+                    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"], int, "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,
+                            timeslot["id"],
+                            "timeslot ids must be increasing",
+                        )
+                    prev_id = 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"][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"][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_slots.update(other_ak_slots)
+
+                    self.assertEqual(
+                        conflict_slots,
+                        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"][slot.pk]
+                    dependency_slots = self.ak_slots.filter(
+                        ak__in=slot.ak.prerequisites.all()
+                    ).values_list("pk", flat=True)
+
+                    self.assertEqual(
+                        set(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"][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"][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"], 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."""
+        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"][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 owner.availabilities.all():
+                            # no availability for owner -> assume full event is covered
+                            continue
+
+                        if not Availability.is_event_covered(
+                            slot.event, owner.availabilities.all()
+                        ):
+                            time_constraints.add(f"availability-person-{owner.pk}")
+
+                    ak = self.export_objects["aks"][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(
+                    {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"][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"][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(
+                        event, room.availabilities.all()
+                    ):
+                        time_constraints.add(f"availability-room-{room.pk}")
+
+                    export_room = self.export_objects["rooms"][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"][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()
+                num_blocks = len(self.export_dict["timeslots"]["blocks"])
+                for block_idx, block in enumerate(
+                    self.export_dict["timeslots"]["blocks"]
+                ):
+                    for timeslot in block:
+                        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)
+                        }
+
+                        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"]),
+                        )
+
+    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")
+                        )
+                    else:
+                        # different days
+                        time_str = (
+                            block_start.strftime("%a %H:%M")
+                            + " – "
+                            + block_end.strftime("%a %H:%M")
+                        )
+                    block_names.append([start_day, time_str])
+                self.assertEqual(
+                    block_names, self.export_dict["timeslots"]["info"]["blocknames"]
+                )
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 fb24dc08a7891425c24d6017be0a51e25566e52d..639847458a866c62304164bc64b08570b8d151d5 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,30 +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_wiki_export', {'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):
@@ -234,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):
         """
@@ -260,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/urls.py b/AKModel/urls.py
index 3abf646058afdf71c7938032236942e7bb0ed995..9c10340546b787c92cbd02cb2c3dbbeb6fe1ff94 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, AKScheduleJSONImportView
+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-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/ak.py b/AKModel/views/ak.py
index 3afec5ac2de03a62ad3d103d17062927c3b97026..461edd3a92402d706e3190899f76317dc13dae1c 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -1,3 +1,5 @@
+import json
+
 from django.contrib import messages
 from django.urls import reverse_lazy
 from django.utils.translation import gettext_lazy as _
@@ -37,6 +39,25 @@ class AKCSVExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         return super().get_queryset().order_by("ak__track")
 
 
+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 = Event
+    context_object_name = "event"
+    title = _("AK JSON Export")
+    slug_url_kwarg = "event_slug"
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+
+        data = context["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
diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py
index 64443cb8f7de27832f518df75378f1fc2ea59571..3acb05fd29a6e91cd17f45e9ed43d889a67da22c 100644
--- a/AKModel/views/manage.py
+++ b/AKModel/views/manage.py
@@ -4,15 +4,17 @@ 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
 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, JSONScheduleImportForm
 from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin
 from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner
 
@@ -58,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)
@@ -245,3 +247,28 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView):
     model = AKOwner
     context_object_name = 'owner'
     template_name = "admin/AKModel/aks_by_user.html"
+
+
+class AKScheduleJSONImportView(EventSlugMixin, IntermediateAdminView):
+    """
+    View: Import an AK schedule from a json file that can be pasted into this view.
+    """
+    form_class = JSONScheduleImportForm
+    title = _("AK Schedule JSON Import")
+
+    def form_valid(self, form):
+        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/AKModel/views/status.py b/AKModel/views/status.py
index 233d4cc9be571db5b4a1ee31a607d1fa04b9305e..0cb26c3cc80fdae32ea3870222f54bd6640a0fad 100644
--- a/AKModel/views/status.py
+++ b/AKModel/views/status.py
@@ -138,10 +138,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_schedule_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}),
diff --git a/AKPlan/locale/de_DE/LC_MESSAGES/django.po b/AKPlan/locale/de_DE/LC_MESSAGES/django.po
index 90ee78c19d2da3dc385e336b58a4b37d6b76c000..1bbfe72524190a8feb747dfe0e0c2e6ae88d09a6 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/AKPlan/tests.py b/AKPlan/tests.py
index 69365c2ba783311708a7babde1fda534c978b61c..3f00061af127307d8c5b64bed7b6c1ffa4d4eb82 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/AKPlanning/locale/de_DE/LC_MESSAGES/django.po b/AKPlanning/locale/de_DE/LC_MESSAGES/django.po
index 3f47223fa6145f6d4c55f87c6f4c1c660f2bd311..68529ec4e06f679135124da17e2aa86e6d2757a7 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/AKScheduling/api.py b/AKScheduling/api.py
index e78fda781df734d93edc001ae7933152a536e92e..cfd476ec7e112f5350b9814e29f3d037da47d9c9 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/locale/de_DE/LC_MESSAGES/django.po b/AKScheduling/locale/de_DE/LC_MESSAGES/django.po
index e66d8d55bcda42f3271b2400d6b98cf84f64ee1d..3944e8722fe004177ef48f6119fa1bd5dd6e38d3 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-03-04 14:49+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,14 +27,14 @@ msgstr "Ende"
 
 #: AKScheduling/forms.py:26
 msgid "Duration"
-msgstr ""
+msgstr "Dauer"
 
-#: AKScheduling/forms.py:27
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:171
+#: AKScheduling/forms.py:27 AKScheduling/forms.py:28
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:172
 msgid "Room"
 msgstr "Raum"
 
-#: AKScheduling/forms.py:31
+#: AKScheduling/forms.py:32
 msgid "AK"
 msgstr "AK"
 
@@ -62,13 +62,13 @@ msgstr ""
 
 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:44
 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:105
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:240
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:375
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:241
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:378
 msgid "No violations"
 msgstr "Keine Verletzungen"
 
 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:82
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:346
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:347
 msgid "Violation(s)"
 msgstr "Verletzung(en)"
 
@@ -81,12 +81,12 @@ msgid "Reload now"
 msgstr "Jetzt neu laden"
 
 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:95
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:228
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:229
 msgid "Violation"
 msgstr "Verletzung"
 
 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:96
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:369
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:372
 msgid "Problem"
 msgstr "Problem"
 
@@ -100,8 +100,8 @@ msgstr "Seit"
 
 #: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:111
 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:256
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:332
-#: AKScheduling/templates/admin/AKScheduling/special_attention.html:48
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:333
+#: AKScheduling/templates/admin/AKScheduling/special_attention.html:58
 #: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34
 msgid "Event Status"
 msgstr "Event-Status"
@@ -116,7 +116,7 @@ msgstr "Abschicken"
 
 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:11
 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:21
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:329
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:330
 msgid "Scheduling for"
 msgstr "Scheduling für"
 
@@ -168,31 +168,31 @@ msgstr "Event (horizontal)"
 msgid "Event (Vertical)"
 msgstr "Event (vertikal)"
 
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:271
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:272
 msgid "Please choose AK"
 msgstr "Bitte AK auswählen"
 
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:291
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:292
 msgid "Could not create slot"
 msgstr "Konnte Slot nicht anlegen"
 
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:307
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:308
 msgid "Add slot"
 msgstr "Slot hinzufügen"
 
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:315
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:316
 msgid "Add"
 msgstr "Hinzufügen"
 
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:316
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:317
 msgid "Cancel"
 msgstr "Abbrechen"
 
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:343
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:344
 msgid "Unscheduled"
 msgstr "Nicht gescheduled"
 
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:368
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:371
 msgid "Level"
 msgstr "Level"
 
@@ -200,23 +200,23 @@ msgstr "Level"
 msgid "AKs with public notes"
 msgstr "AKs mit öffentlichen Kommentaren"
 
-#: AKScheduling/templates/admin/AKScheduling/special_attention.html:21
+#: AKScheduling/templates/admin/AKScheduling/special_attention.html:24
 msgid "AKs without availabilities"
 msgstr "AKs ohne Verfügbarkeiten"
 
-#: AKScheduling/templates/admin/AKScheduling/special_attention.html:28
+#: AKScheduling/templates/admin/AKScheduling/special_attention.html:33
 msgid "Create default availabilities"
 msgstr "Standardverfügbarkeiten anlegen"
 
-#: AKScheduling/templates/admin/AKScheduling/special_attention.html:31
+#: AKScheduling/templates/admin/AKScheduling/special_attention.html:36
 msgid "AK wishes with slots"
 msgstr "AK-Wünsche mit Slots"
 
-#: AKScheduling/templates/admin/AKScheduling/special_attention.html:38
+#: AKScheduling/templates/admin/AKScheduling/special_attention.html:46
 msgid "Delete slots for wishes"
 msgstr ""
 
-#: AKScheduling/templates/admin/AKScheduling/special_attention.html:40
+#: AKScheduling/templates/admin/AKScheduling/special_attention.html:48
 msgid "AKs without slots"
 msgstr "AKs ohne Slots"
 
@@ -246,19 +246,19 @@ msgstr "Noch nicht geschedulte AK-Slots"
 msgid "Count"
 msgstr "Anzahl"
 
-#: AKScheduling/views.py:150
+#: AKScheduling/views.py:152
 msgid "Interest updated"
 msgstr "Interesse aktualisiert"
 
-#: AKScheduling/views.py:201
+#: AKScheduling/views.py:210
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKScheduling/views.py:219
+#: AKScheduling/views.py:228
 msgid "Cleanup: Delete unscheduled slots for wishes"
 msgstr "Aufräumen: Noch nicht geplante Slots für Wünsche löschen"
 
-#: AKScheduling/views.py:226
+#: AKScheduling/views.py:235
 #, python-brace-format
 msgid ""
 "The following {count} unscheduled slots of wishes will be deleted:\n"
@@ -270,15 +270,15 @@ msgstr ""
 "\n"
 " {slots}"
 
-#: AKScheduling/views.py:233
+#: AKScheduling/views.py:242
 msgid "Unscheduled slots for wishes successfully deleted"
 msgstr "Noch nicht geplante Slots für Wünsche erfolgreich gelöscht"
 
-#: AKScheduling/views.py:247
+#: AKScheduling/views.py:256
 msgid "Create default availabilities for AKs"
 msgstr "Standardverfügbarkeiten für AKs anlegen"
 
-#: AKScheduling/views.py:254
+#: AKScheduling/views.py:263
 #, python-brace-format
 msgid ""
 "The following {count} AKs don't have any availability information. Create "
@@ -291,20 +291,29 @@ msgstr ""
 "\n"
 " {aks}"
 
-#: AKScheduling/views.py:274
+#: AKScheduling/views.py:283
 #, python-brace-format
 msgid "Could not create default availabilities for AK: {ak}"
 msgstr "Konnte keine Verfügbarkeit anlegen für AK: {ak}"
 
-#: AKScheduling/views.py:279
+#: AKScheduling/views.py:288
 #, python-brace-format
 msgid "Created default availabilities for {count} AKs"
 msgstr "Standardverfügbarkeiten für {count} AKs angelegt"
 
-#: AKScheduling/views.py:290
+#: AKScheduling/views.py:299
 msgid "Constraint Violations"
 msgstr "Constraintverletzungen"
 
+#~ msgid "Constraint violations for"
+#~ msgstr "Constraintverletzungen für"
+
+#~ msgid "AKs requiring special attention for"
+#~ msgstr "AKs die besondere Aufmerksamkeit erfordern für"
+
+#~ msgid "Enter interest"
+#~ msgstr "Interesse eingeben"
+
 #~ msgid "Bitte AK auswählen"
 #~ msgstr "Please sel"
 
diff --git a/AKScheduling/models.py b/AKScheduling/models.py
index 1495f311935583a945aef0d737ca44e21a0a2663..8f34c151d4616c5a709057f5b529c9555d08c8b0 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():
@@ -363,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 0996eedd905259f0f463589a89f68bde055bc01a..44f25719233cab9eaf4316c6268cf7145989e3cf 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/locale/de_DE/LC_MESSAGES/django.po b/AKSubmission/locale/de_DE/LC_MESSAGES/django.po
index f3a20fd7f478292d140ee405f78490d60fac7df7..1ccfaeb613e93c43db06c116b542e49efca85658 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: 2025-02-25 22:33+0100\n"
+"POT-Creation-Date: 2025-03-04 14:49+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"
@@ -109,21 +109,25 @@ msgstr "AK-Wunsch"
 #: AKSubmission/templates/AKSubmission/ak_detail.html:186
 #, python-format
 msgid ""
-"This AK currently takes place for another <span v-html=\"timeUntilEnd\">"
-"%(featured_slot_remaining)s</span> minute(s) in %(room)s.&nbsp;"
+"This AK currently takes place for another <span v-"
+"html=\"timeUntilEnd\">%(featured_slot_remaining)s</span> minute(s) in "
+"%(room)s.&nbsp;"
 msgstr ""
-"Dieser AK findet noch <span v-html=\"timeUntilEnd\">"
-"%(featured_slot_remaining)s</span> Minute(n) in %(room)s statt.&nbsp;\n"
+"Dieser AK findet noch <span v-"
+"html=\"timeUntilEnd\">%(featured_slot_remaining)s</span> Minute(n) in "
+"%(room)s statt.&nbsp;\n"
 "                    "
 
 #: AKSubmission/templates/AKSubmission/ak_detail.html:189
 #, python-format
 msgid ""
-"This AK starts in <span v-html=\"timeUntilStart\">"
-"%(featured_slot_remaining)s</span> minute(s) in %(room)s.&nbsp;"
+"This AK starts in <span v-"
+"html=\"timeUntilStart\">%(featured_slot_remaining)s</span> minute(s) in "
+"%(room)s.&nbsp;"
 msgstr ""
-"Dieser AK beginnt in <span v-html=\"timeUntilStart\">"
-"%(featured_slot_remaining)s</span> Minute(n) in %(room)s.&nbsp;\n"
+"Dieser AK beginnt in <span v-"
+"html=\"timeUntilStart\">%(featured_slot_remaining)s</span> Minute(n) in "
+"%(room)s.&nbsp;\n"
 "                    "
 
 #: AKSubmission/templates/AKSubmission/ak_detail.html:194
@@ -274,7 +278,7 @@ msgstr "Die Ergebnisse dieses AKs vorstellen"
 msgid "Intends to submit a resolution"
 msgstr "Beabsichtigt eine Resolution einzureichen"
 
-#: AKSubmission/templates/AKSubmission/ak_list.html:6 AKSubmission/views.py:84
+#: AKSubmission/templates/AKSubmission/ak_list.html:6 AKSubmission/views.py:82
 msgid "All AKs"
 msgstr "Alle AKs"
 
@@ -400,77 +404,80 @@ msgstr ""
 msgid "Submit"
 msgstr "Eintragen"
 
-#: AKSubmission/views.py:127
+#: AKSubmission/views.py:125
 msgid "Wishes"
 msgstr "Wünsche"
 
-#: AKSubmission/views.py:127
+#: AKSubmission/views.py:125
 msgid "AKs one would like to have"
 msgstr ""
 "AKs die sich gewünscht wurden, aber bei denen noch nicht klar ist, wer sie "
 "macht. Falls du dir das vorstellen kannst, trag dich einfach ein"
 
-#: AKSubmission/views.py:169
+#: AKSubmission/views.py:167
 msgid "Currently planned AKs"
 msgstr "Aktuell geplante AKs"
 
-#: AKSubmission/views.py:302
+#: AKSubmission/views.py:305
 msgid "Event inactive. Cannot create or update."
 msgstr "Event inaktiv. Hinzufügen/Bearbeiten nicht möglich."
 
-#: AKSubmission/views.py:327
+#: AKSubmission/views.py:330
 msgid "AK successfully created"
 msgstr "AK erfolgreich angelegt"
 
-#: AKSubmission/views.py:400
+#: AKSubmission/views.py:404
 msgid "AK successfully updated"
 msgstr "AK erfolgreich aktualisiert"
 
-#: AKSubmission/views.py:451
+#: AKSubmission/views.py:455
 #, python-brace-format
 msgid "Added '{owner}' as new owner of '{ak.name}'"
 msgstr "'{owner}' als neue Leitung von '{ak.name}' hinzugefügt"
 
-#: AKSubmission/views.py:555
+#: AKSubmission/views.py:558
 msgid "No user selected"
 msgstr "Keine Person ausgewählt"
 
-#: AKSubmission/views.py:571
+#: AKSubmission/views.py:574
 msgid "Person Info successfully updated"
 msgstr "Personen-Info erfolgreich aktualisiert"
 
-#: AKSubmission/views.py:607
+#: AKSubmission/views.py:610
 msgid "AK Slot successfully added"
 msgstr "AK-Slot erfolgreich angelegt"
 
-#: AKSubmission/views.py:626
+#: AKSubmission/views.py:629
 msgid "You cannot edit a slot that has already been scheduled"
 msgstr "Bereits geplante AK-Slots können nicht mehr bearbeitet werden"
 
-#: AKSubmission/views.py:636
+#: AKSubmission/views.py:639
 msgid "AK Slot successfully updated"
 msgstr "AK-Slot erfolgreich aktualisiert"
 
-#: AKSubmission/views.py:654
+#: AKSubmission/views.py:657
 msgid "You cannot delete a slot that has already been scheduled"
 msgstr "Bereits geplante AK-Slots können nicht mehr gelöscht werden"
 
-#: AKSubmission/views.py:664
+#: AKSubmission/views.py:667
 msgid "AK Slot successfully deleted"
 msgstr "AK-Slot erfolgreich angelegt"
 
-#: AKSubmission/views.py:676
+#: AKSubmission/views.py:679
 msgid "Messages"
 msgstr "Nachrichten"
 
-#: AKSubmission/views.py:686
+#: AKSubmission/views.py:689
 msgid "Delete all messages"
 msgstr "Alle Nachrichten löschen"
 
-#: AKSubmission/views.py:713
+#: AKSubmission/views.py:716
 msgid "Message to organizers successfully saved"
 msgstr "Nachricht an die Organisator*innen erfolgreich gespeichert"
 
+#~ msgid "AKs with Track"
+#~ msgstr "AK mit Track"
+
 #~ msgid ""
 #~ "Due to technical reasons, the link you entered was truncated to a length "
 #~ "of 200 characters"
diff --git a/AKSubmission/tests.py b/AKSubmission/tests.py
index b3da0e6c431f964d1de0884034c2b19d387b04a0..fd31454391bbc15ed42ba5e77a1e6da5b04f1461 100644
--- a/AKSubmission/tests.py
+++ b/AKSubmission/tests.py
@@ -4,7 +4,7 @@ from django.test import TestCase
 from django.urls import reverse_lazy
 
 from AKModel.models import AK, AKSlot, Event
-from AKModel.tests import BasicViewTests
+from AKModel.tests.test_views import BasicViewTests
 from AKSubmission.forms import AKSubmissionForm
 
 
diff --git a/INSTALL.md b/INSTALL.md
index cbf8ea28d4a6954f7d359b7dd678df2709d7864a..4c98ce7b4bdbfdbc21ac3429199ff4a5786e1294 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -10,7 +10,7 @@ setup.
 
 ### System Requirements
 
-* Python3.11+ incl. development tools
+* Python 3.11+ 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.11``
 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.11``
 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/check.sh b/Utils/check.sh
index a8a9d4b7a9d6e78e7d93cabecb6bd4f36af4f89f..1cdbdf7d85687b820c0a16f98217d367ac8b3ccf 100755
--- a/Utils/check.sh
+++ b/Utils/check.sh
@@ -19,7 +19,6 @@ for arg in "$@"; do
     if [[ "$arg" == "--prod" ]]; then
       export DJANGO_SETTINGS_MODULE=AKPlanning.settings_production
       ./manage.py check --deploy
-      ./manage.py makemigrations --dry-run --check
     fi
 done
 
diff --git a/Utils/update.sh b/Utils/update.sh
index a711b73b912fcd07e8543b6b49d4eb8950ff0d79..d81aae52947404ac629050ba2c2d69c19e4b37e5 100755
--- a/Utils/update.sh
+++ b/Utils/update.sh
@@ -19,7 +19,7 @@ fi
 mkdir -p backups/
 python manage.py dumpdata --indent=2 > "backups/$(date +"%Y%m%d%H%M")_datadump.json" --traceback
 
-git pull
+# git pull
 pip install --upgrade setuptools pip wheel
 pip install --upgrade -r requirements.txt
 
diff --git a/locale/de_DE/LC_MESSAGES/django.po b/locale/de_DE/LC_MESSAGES/django.po
index c1bbd2db4e5c22f703d2acc6a7388eb37116792b..67103fbfa9c4d81665a77037fd3cb432b1a117f2 100644
--- a/locale/de_DE/LC_MESSAGES/django.po
+++ b/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: 2025-02-27 15:13+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"
@@ -18,24 +18,24 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
 
-#: templates/base.html:43
+#: templates/base.html:44
 msgid ""
 "Are you sure you want to change the language now? This will clear the form!"
 msgstr "Wirklich jetzt die Sprache ändern? Das wird das Formular zurücksetzen!"
 
-#: templates/base.html:108
+#: templates/base.html:109
 msgid "Go to backend"
 msgstr "Zum Backend"
 
-#: templates/base.html:109
+#: templates/base.html:110
 msgid "Docs"
 msgstr "Doku"
 
-#: templates/base.html:115
+#: templates/base.html:116
 msgid "Impress"
 msgstr "Impressum"
 
-#: templates/base.html:118
+#: templates/base.html:119
 msgid "This software is open source"
 msgstr "Diese Software ist Open Source"
 
diff --git a/requirements.txt b/requirements.txt
index 9b0938d4a76aff97222757ebd6ac30ac3efef24a..be46f86ad6caa490c27ac703e3de38154a542b6b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -15,9 +15,13 @@ django_csp==3.8
 djangorestframework==3.15.2
 
 fontawesomefree==6.6.0  # Makes static files (css, fonts) available locally
-mysqlclient==2.2.7  # for production deployment
+# mysqlclient==2.2.7  # for production deployment
 tzdata==2025.1
 
+# Tests
+beautifulsoup4==4.13.3
+lxml==5.3.1
+
 # Documentation
 Sphinx==8.2.3
 sphinx-rtd-theme==3.0.2