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

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

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