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

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

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