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