diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py
index 35814ee06d2bf49fe9416710c71cbebbb4fc7bb4..27a6c2287fd804dbf0f6c9c3d52b725f4afeb18c 100644
--- a/AKModel/availability/models.py
+++ b/AKModel/availability/models.py
@@ -293,7 +293,7 @@ class Availability(models.Model):
         #       event end + 1 day
         full_event = Availability(event=event, start=event.start, end=event.end)
         avail_union = Availability.union(availabilities)
-        return not avail_union or avail_union[0].contains(full_event)
+        return any(avail.contains(full_event) for avail in avail_union)
 
     class Meta:
         verbose_name = _('Availability')
diff --git a/AKModel/forms.py b/AKModel/forms.py
index 74ca1b6813f0365d1179166da18aeb8a8c59ca4e..bf77085c6c21a75fafc2711c2cc58c94a8953d46 100644
--- a/AKModel/forms.py
+++ b/AKModel/forms.py
@@ -274,7 +274,7 @@ class RoomFormWithAvailabilities(AvailabilitiesFormMixin, RoomForm):
             self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event)
 
 
-class JSONImportForm(AdminIntermediateForm):
+class JSONScheduleImportForm(AdminIntermediateForm):
     """Form to import an AK schedule from a json file."""
     json_data = forms.CharField(
         required=True,
diff --git a/AKModel/locale/de_DE/LC_MESSAGES/django.po b/AKModel/locale/de_DE/LC_MESSAGES/django.po
index 4cc752ae7d4ac4da883547ad18a5fbe866d816d7..4fcbd8a8b0d5c9e5dbaab566dd88652847e2058c 100644
--- a/AKModel/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKModel/locale/de_DE/LC_MESSAGES/django.po
@@ -1262,8 +1262,8 @@ msgstr ""
 "{u} Slot(s) aktualisiert, {c} Slot(s) hinzugefügt und {d} Slot(s) gelöscht"
 
 #: AKModel/views/manage.py:257
-msgid "AK JSON Import"
-msgstr "AK-JSON-Import"
+msgid "AK Schedule JSON Import"
+msgstr "AK-Plan JSON-Import"
 
 #: AKModel/views/room.py:37
 #, python-format
diff --git a/AKModel/models.py b/AKModel/models.py
index 2d58a45651b419899932171a0fdae2a394e85d6b..d30912d3046b8ce4a874e775398cf7b6afbace81 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -2,7 +2,7 @@ import itertools
 import json
 from dataclasses import dataclass
 from datetime import datetime, timedelta
-from typing import Iterable
+from typing import Iterable, Generator
 
 from django.db import models
 from django.apps import apps
@@ -17,11 +17,16 @@ from timezone_field import TimeZoneField
 
 @dataclass
 class OptimizerTimeslot:
-    """Class describing a timeslot. Used to interface with an optimizer."""
+    """Class describing a discrete timeslot. Used to interface with an optimizer."""
 
     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.
@@ -33,7 +38,6 @@ class OptimizerTimeslot:
         """
         avail = self.avail.merge_with(other.avail)
         constraints = self.constraints.union(other.constraints)
-        # we simply use the index of result[-1]
         return OptimizerTimeslot(
             avail=avail, idx=self.idx, constraints=constraints
         )
@@ -44,6 +48,69 @@ class OptimizerTimeslot:
 TimeslotBlock = list[OptimizerTimeslot]
 
 
+def merge_blocks(
+    blocks: Iterable[TimeslotBlock]
+) -> Iterable[TimeslotBlock]:
+    """Merge iterable of blocks together.
+
+    The timeslots of all blocks are grouped into maximal blocks.
+    Timeslots with the same start and end are identified with each other
+    and merged (cf `OptimizerTimeslot.merge`).
+    Throws a ValueError if any timeslots are overlapping but do not
+    share the same start and end, i.e. partial overlap is not allowed.
+
+    :param blocks: iterable of blocks to merge.
+    :return: iterable of merged blocks.
+    :rtype: iterable over lists of OptimizerTimeslot objects
+    """
+    if not blocks:
+        return []
+
+    # flatten timeslot iterables to single chain
+    timeslot_chain = itertools.chain.from_iterable(blocks)
+
+    # sort timeslots according to start
+    timeslots = sorted(
+        timeslot_chain,
+        key=lambda slot: slot.avail.start
+    )
+
+    if not timeslots:
+        return []
+
+    all_blocks = []
+    current_block = [timeslots[0]]
+    timeslots = timeslots[1:]
+
+    for slot in timeslots:
+        if current_block and slot.avail.overlaps(current_block[-1].avail, strict=True):
+            if (
+                slot.avail.start == current_block[-1].avail.start
+                and slot.avail.end == current_block[-1].avail.end
+            ):
+                # the same timeslot -> merge
+                current_block[-1] = current_block[-1].merge(slot)
+            else:
+                # partial overlap of interiors -> not supported
+                # TODO: Show comprehensive message in production
+                raise ValueError(
+                    "Partially overlapping timeslots are not supported!"
+                    f" ({current_block[-1].avail.simplified}, {slot.avail.simplified})"
+                )
+        elif not current_block or slot.avail.overlaps(current_block[-1].avail, strict=False):
+            # only endpoints in intersection -> same block
+            current_block.append(slot)
+        else:
+            # no overlap at all -> new block
+            all_blocks.append(current_block)
+            current_block = [slot]
+
+    if current_block:
+        all_blocks.append(current_block)
+
+    return all_blocks
+
+
 class Event(models.Model):
     """
     An event supplies the frame for all Aks.
@@ -200,10 +267,10 @@ class Event(models.Model):
         slot_duration: timedelta,
         slot_index: int = 0,
         constraints: set[str] | None = None,
-    ) -> Iterable[TimeslotBlock]:
+    ) -> Generator[TimeslotBlock, None, int]:
         """Discretize a time range into timeslots.
 
-        Uses a uniform discretization into blocks of length `slot_duration`,
+        Uses a uniform discretization into discrete slots of length `slot_duration`,
         starting at `start`. No incomplete timeslots are generated, i.e.
         if (`end` - `start`) is not a whole number multiple of `slot_duration`
         then the last incomplete timeslot is dropped.
@@ -214,7 +281,11 @@ class Event(models.Model):
         :param slot_index: index of the first timeslot. Defaults to 0.
 
         :yield: Block of optimizer timeslots as the discretization result.
-        :ytype: list of TimeslotBlock
+        :ytype: list of OptimizerTimeslot
+
+        :return: The first slot index after the yielded blocks, i.e.
+            `slot_index` + total # generated timeslots
+        :rtype: int
         """
         # local import to prevent cyclic import
         # pylint: disable=import-outside-toplevel
@@ -264,12 +335,15 @@ class Event(models.Model):
         return slot_index
 
     def uniform_time_slots(self, *, slots_in_an_hour: float = 1.0) -> Iterable[TimeslotBlock]:
-        """Uniformly discretize the entire event into a single block of timeslots.
+        """Uniformly discretize the entire event into blocks of timeslots.
+
+        Discretizes entire event uniformly. May not necessarily result in a single block
+        as slots with no room availability are dropped.
 
         :param slots_in_an_hour: The percentage of an hour covered by a single slot.
             Determines the discretization granularity.
         :yield: Block of optimizer timeslots as the discretization result.
-        :ytype: a single list of TimeslotBlock
+        :ytype: list of OptimizerTimeslot
         """
         all_category_constraints = AKCategory.create_category_constraints(
             AKCategory.objects.filter(event=self).all()
@@ -308,68 +382,6 @@ class Event(models.Model):
                 constraints=category_constraints,
             )
 
-    def merge_blocks(
-        self, blocks: Iterable[TimeslotBlock]
-    ) -> Iterable[TimeslotBlock]:
-        """Merge iterable of blocks together.
-
-        The timeslots of all blocks are grouped into maximal blocks.
-        Timeslots with the same start and end are identified with each other
-        and merged (cf `OptimizerTimeslot.merge`).
-        Throws a ValueError if any timeslots are overlapping but do not
-        share the same start and end, i.e. partial overlap is not allowed.
-
-        :param blocks: iterable of blocks to merge.
-        :return: iterable of merged blocks.
-        :rtype: iterable over lists of OptimizerTimeslot objects
-        """
-        if not blocks:
-            return []
-
-        # flatten timeslot iterables to single chain
-        timeslot_chain = itertools.chain.from_iterable(blocks)
-
-        # sort timeslots according to start
-        timeslots = sorted(
-            timeslot_chain,
-            key=lambda slot: slot.avail.start
-        )
-
-        if not timeslots:
-            return []
-
-        all_blocks = []
-        current_block = [timeslots[0]]
-        timeslots = timeslots[1:]
-
-        for slot in timeslots:
-            if current_block and slot.avail.overlaps(current_block[-1].avail, strict=True):
-                if (
-                    slot.avail.start == current_block[-1].avail.start
-                    and slot.avail.end == current_block[-1].avail.end
-                ):
-                    # the same timeslot -> merge
-                    current_block[-1] = current_block[-1].merge(slot)
-                else:
-                    # partial overlap of interiors -> not supported
-                    # TODO: Show comprehensive message in production
-                    raise ValueError(
-                        "Partially overlapping timeslots are not supported!"
-                        f" ({current_block[-1].avail.simplified}, {slot.avail.simplified})"
-                    )
-            elif not current_block or slot.avail.overlaps(current_block[-1].avail, strict=False):
-                # only endpoints in intersection -> same block
-                current_block.append(slot)
-            else:
-                # no overlap at all -> new block
-                all_blocks.append(current_block)
-                current_block = [slot]
-
-        if current_block:
-            all_blocks.append(current_block)
-
-        return all_blocks
-
     def schedule_from_json(self, schedule: str) -> None:
         """Load AK schedule from a json string.
 
@@ -384,7 +396,7 @@ class Event(models.Model):
 
         timeslot_dict = {
             timeslot.idx: timeslot
-            for block in self.merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour))
+            for block in merge_blocks(self.default_time_slots(slots_in_an_hour=slots_in_an_hour))
             for timeslot in block
         }
 
@@ -631,7 +643,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}"""
@@ -925,7 +937,7 @@ class AKSlot(models.Model):
         # self.slots_in_an_hour is set in AKJSONExportView
         data = {
             "id": str(self.pk),
-            "duration": int(self.duration * self.slots_in_an_hour),
+            "duration": round(self.duration * self.slots_in_an_hour),
             "properties": {},
             "room_constraints": [constraint.name
                                  for constraint in self.ak.requirements.all()],
diff --git a/AKModel/tests.py b/AKModel/tests.py
index fb24dc08a7891425c24d6017be0a51e25566e52d..6730315d27c81f320f34a7af5a2c3e56c72c3f2c 100644
--- a/AKModel/tests.py
+++ b/AKModel/tests.py
@@ -214,7 +214,9 @@ class ModelViewTests(BasicViewTests, TestCase):
         ('admin:event_status', {'event_slug': 'kif42'}),
         ('admin:event_requirement_overview', {'event_slug': 'kif42'}),
         ('admin:ak_csv_export', {'event_slug': 'kif42'}),
+        ('admin:ak_json_export', {'event_slug': 'kif42'}),
         ('admin:ak_wiki_export', {'slug': 'kif42'}),
+        ('admin:ak_schedule_json_import', {'event_slug': 'kif42'}),
         ('admin:ak_delete_orga_messages', {'event_slug': 'kif42'}),
         ('admin:ak_slide_export', {'event_slug': 'kif42'}),
         ('admin:default-slots-editor', {'event_slug': 'kif42'}),
diff --git a/AKModel/urls.py b/AKModel/urls.py
index 9871b4119949d31350ecc64db568d515b61eb3cb..9c10340546b787c92cbd02cb2c3dbbeb6fe1ff94 100644
--- a/AKModel/urls.py
+++ b/AKModel/urls.py
@@ -5,7 +5,7 @@ from rest_framework.routers import DefaultRouter
 
 import AKModel.views.api
 from AKModel.views.manage import ExportSlidesView, PlanPublishView, PlanUnpublishView, DefaultSlotEditorView, \
-    AKsByUserView, AKJSONImportView
+    AKsByUserView, AKScheduleJSONImportView
 from AKModel.views.ak import AKRequirementOverview, AKCSVExportView, AKJSONExportView, AKWikiExportView, \
      AKMessageDeleteView
 from AKModel.views.event_wizard import NewEventWizardStartView, NewEventWizardPrepareImportView, \
@@ -99,8 +99,8 @@ def get_admin_urls_event(admin_site):
              name="ak_csv_export"),
         path('<slug:event_slug>/ak-json-export/', admin_site.admin_view(AKJSONExportView.as_view()),
              name="ak_json_export"),
-        path('<slug:event_slug>/ak-json-import/', admin_site.admin_view(AKJSONImportView.as_view()),
-             name="ak_json_import"),
+        path('<slug:event_slug>/ak-schedule-json-import/', admin_site.admin_view(AKScheduleJSONImportView.as_view()),
+             name="ak_schedule_json_import"),
         path('<slug:slug>/ak-wiki-export/', admin_site.admin_view(AKWikiExportView.as_view()),
              name="ak_wiki_export"),
         path('<slug:event_slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()),
diff --git a/AKModel/views/ak.py b/AKModel/views/ak.py
index 76bfdb284e42cc71dc9be791a171ba762e2a9b5f..90599a1ba26809c21c17495891ce4c527b827c26 100644
--- a/AKModel/views/ak.py
+++ b/AKModel/views/ak.py
@@ -9,7 +9,7 @@ from django.views.generic import ListView, DetailView
 from AKModel.availability.models import Availability
 from AKModel.metaviews.admin import AdminViewMixin, FilterByEventSlugMixin, EventSlugMixin, IntermediateAdminView, \
     IntermediateAdminActionView
-from AKModel.models import AKRequirement, AKSlot, Event, AKOrgaMessage, AK, Room, AKOwner
+from AKModel.models import AKRequirement, AKSlot, DefaultSlot, Event, AKOrgaMessage, AK, Room, AKOwner, merge_blocks
 
 
 class AKRequirementOverview(AdminViewMixin, FilterByEventSlugMixin, ListView):
@@ -52,12 +52,15 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
 
 
     def _test_slot_contained(self, slot: Availability, availabilities: List[Availability]) -> bool:
+        """Test if slot is contained in any member of availabilities."""
         return any(availability.contains(slot) for availability in availabilities)
 
-    def _test_event_covered(self, availabilities: List[Availability]) -> bool:
+    def _test_event_not_covered(self, availabilities: List[Availability]) -> bool:
+        """Test if event is not covered by availabilities."""
         return not Availability.is_event_covered(self.event, availabilities)
 
-    def _test_fixed_ak(self, ak_id, slot: Availability, ak_fixed: dict) -> bool:
+    def _test_ak_fixed_in_slot(self, ak_id, slot: Availability, ak_fixed: dict) -> bool:
+        """Test if AK defined by `ak_id` is fixed to happen during slot."""
         if not ak_id in ak_fixed:
             return False
 
@@ -65,8 +68,9 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         return fixed_slot.overlaps(slot, strict=True)
 
     def _test_add_constraint(self, slot: Availability, availabilities: List[Availability]) -> bool:
+        """Test if object is not available for whole event and may happen during slot."""
         return (
-            self._test_event_covered(availabilities)
+            self._test_event_not_covered(availabilities)
             and self._test_slot_contained(slot, availabilities)
         )
 
@@ -110,27 +114,38 @@ class AKJSONExportView(AdminViewMixin, FilterByEventSlugMixin, ListView):
             if (values := AKSlot.objects.select_related().filter(ak__pk=ak_id, fixed=True)).exists()
         }
 
-        for block in self.event.merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)):
+        if DefaultSlot.objects.filter(event=self.event).exists():
+            # discretize default slots if they exists
+            blocks = merge_blocks(self.event.default_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR))
+        else:
+            blocks = self.event.uniform_time_slots(slots_in_an_hour=SLOTS_IN_AN_HOUR)
+
+        for block in blocks:
             current_block = []
 
             for timeslot in block:
                 time_constraints = []
+                # if reso_deadline is set and timeslot ends before it,
+                #   add fulfilled time constraint 'resolution'
                 if self.event.reso_deadline is None or timeslot.avail.end < self.event.reso_deadline:
                     time_constraints.append("resolution")
 
+                # add fulfilled time constraints for all AKs that cannot happen during full event
                 time_constraints.extend([
                     f"availability-ak-{ak_id}"
                     for ak_id, availabilities in ak_availabilities.items()
                     if (
                         self._test_add_constraint(timeslot.avail, availabilities)
-                        or self._test_fixed_ak(ak_id, timeslot.avail, ak_fixed)
+                        or self._test_ak_fixed_in_slot(ak_id, timeslot.avail, ak_fixed)
                     )
                 ])
+                # add fulfilled time constraints for all persons that are not available for full event
                 time_constraints.extend([
                     f"availability-person-{person_id}"
                     for person_id, availabilities in person_availabilities.items()
                     if self._test_add_constraint(timeslot.avail, availabilities)
                 ])
+                # add fulfilled time constraints for all rooms that are not available for full event
                 time_constraints.extend([
                     f"availability-room-{room_id}"
                     for room_id, availabilities in room_availabilities.items()
diff --git a/AKModel/views/manage.py b/AKModel/views/manage.py
index ec5076fbd850c71a6f7d1a1d202a46461e43ebbd..1bad9534efd6fe81fa70ebf30fbdf84aad476967 100644
--- a/AKModel/views/manage.py
+++ b/AKModel/views/manage.py
@@ -14,7 +14,7 @@ from django.views.generic import TemplateView, DetailView
 from django_tex.core import render_template_with_context, run_tex_in_directory
 from django_tex.response import PDFResponse
 
-from AKModel.forms import SlideExportForm, DefaultSlotEditorForm, JSONImportForm
+from AKModel.forms import SlideExportForm, DefaultSlotEditorForm, JSONScheduleImportForm
 from AKModel.metaviews.admin import EventSlugMixin, IntermediateAdminView, IntermediateAdminActionView, AdminViewMixin
 from AKModel.models import ConstraintViolation, Event, DefaultSlot, AKOwner
 
@@ -249,12 +249,12 @@ class AKsByUserView(AdminViewMixin, EventSlugMixin, DetailView):
     template_name = "admin/AKModel/aks_by_user.html"
 
 
-class AKJSONImportView(EventSlugMixin, IntermediateAdminView):
+class AKScheduleJSONImportView(EventSlugMixin, IntermediateAdminView):
     """
     View: Import an AK schedule from a json file that can be pasted into this view.
     """
-    form_class = JSONImportForm
-    title = _("AK JSON Import")
+    form_class = JSONScheduleImportForm
+    title = _("AK Schedule JSON Import")
 
     def form_valid(self, form):
         self.event.schedule_from_json(form.data["json_data"])
diff --git a/AKModel/views/status.py b/AKModel/views/status.py
index 0c12b30348c63d6178f0b5c38d2fcec6cfebf664..f7baa0da13d4ea40002eac18339a494c3dd3e0a5 100644
--- a/AKModel/views/status.py
+++ b/AKModel/views/status.py
@@ -135,7 +135,7 @@ class EventAKsWidget(TemplateStatusWidget):
                 },
                 {
                     "text": _("Import AK schedule from JSON"),
-                    "url": reverse_lazy("admin:ak_json_import", kwargs={"event_slug": context["event"].slug}),
+                    "url": reverse_lazy("admin:ak_schedule_json_import", kwargs={"event_slug": context["event"].slug}),
                 },
                 {
                     "text": _("Export AKs as CSV"),