From 22476850dcf7d050a88e0040ff36cb94573cff6a Mon Sep 17 00:00:00 2001
From: Felix Blanke <info@fblanke.de>
Date: Tue, 11 Feb 2025 00:43:48 +0100
Subject: [PATCH] Address findings

---
 AKModel/admin.py                  |  9 ++++++-
 AKModel/availability/models.py    | 22 +++++++++++++++--
 AKModel/models.py                 | 40 +++++++++++++------------------
 AKModel/tests/test_json_export.py |  3 +++
 4 files changed, 47 insertions(+), 27 deletions(-)

diff --git a/AKModel/admin.py b/AKModel/admin.py
index 6a348862..0b17c552 100644
--- a/AKModel/admin.py
+++ b/AKModel/admin.py
@@ -574,6 +574,9 @@ class DefaultSlotAdmin(EventTimezoneFormMixin, admin.ModelAdmin):
 
 @admin.register(EventParticipant)
 class EventParticipantAdmin(PrepopulateWithNextActiveEventMixin, admin.ModelAdmin):
+    """
+    Admin interface for EventParticipant
+    """
     model = EventParticipant
     list_display = ['name', 'institution', 'event']
     list_filter = ['event', 'institution']
@@ -583,7 +586,7 @@ class EventParticipantAdmin(PrepopulateWithNextActiveEventMixin, admin.ModelAdmi
 
 class AKPreferenceAdminForm(forms.ModelForm):
     """
-    Adapted admin form for constraint violations for usage in :class:`ConstraintViolationAdmin`)
+    Adapted admin form for AK preferences for usage in :class:`AKPreferenceAdmin`)
     """
     class Meta:
         widgets = {
@@ -601,6 +604,10 @@ class AKPreferenceAdminForm(forms.ModelForm):
 
 @admin.register(AKPreference)
 class AKPreferenceAdmin(PrepopulateWithNextActiveEventMixin, admin.ModelAdmin):
+    """
+    Admin interface for AK preferences.
+    Uses an adapted form (see :class:`AKPreferenceAdminForm`)
+    """
     model = AKPreference
     form = AKPreferenceAdminForm
     list_display = ['preference', 'participant', 'slot', 'event']
diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py
index cc76cce9..e58ba070 100644
--- a/AKModel/availability/models.py
+++ b/AKModel/availability/models.py
@@ -99,11 +99,29 @@ class Availability(models.Model):
         event = getattr(getattr(self, 'event', None), 'name', None)
         ak = getattr(self.ak, 'name', None)
         ak_category = getattr(self.ak_category, 'name', None)
-        return f'Availability(event={event}, person={person}, room={room}, ak={ak}, ak category={ak_category}, participant={participant})'
+        arg_list = [
+            f"event={event}",
+            f"person={person}",
+            f"room={room}",
+            f"ak={ak}",
+            f"ak category={ak_category}",
+            f"participant={participant}",
+        ]
+        return f'Availability({", ".join(arg_list)})'
 
     def __hash__(self):
         return hash(
-            (getattr(self, 'event', None), self.person, self.room, self.ak, self.ak_category, self.participant, self.start, self.end))
+            (
+                getattr(self, 'event', None),
+                self.person,
+                self.room,
+                self.ak,
+                self.ak_category,
+                self.participant,
+                self.start,
+                self.end,
+            )
+        )
 
     def __eq__(self, other: 'Availability') -> bool:
         """Comparisons like ``availability1 == availability2``.
diff --git a/AKModel/models.py b/AKModel/models.py
index 89b40671..02160ec4 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -560,7 +560,7 @@ class Event(models.Model):
         rooms = Room.objects.filter(event=self).order_by()
         slots = AKSlot.objects.filter(event=self).order_by()
         participants = EventParticipant.objects.filter(event=self).order_by()
-        owners = AKOwner.objects.filter(event=self.event).order_by().all()
+        owners = AKOwner.objects.filter(event=self).order_by().all()
 
         ak_availabilities = {
             ak.pk: Availability.union(ak.availabilities.all())
@@ -576,7 +576,7 @@ class Event(models.Model):
         }
         participant_availabilities = {
             participant.pk: Availability.union(participant.availabilities.all())
-            for participant in EventParticipant.objects.filter(event=self.event)
+            for participant in EventParticipant.objects.filter(event=self)
         }
 
         blocks = list(self.discretize_timeslots())
@@ -627,7 +627,7 @@ class Event(models.Model):
 
                 # add fulfilled time constraints for all participants that are not available for full event
                 time_constraints.extend(
-                    self._generate_time_constraints("participant", participant_availabilities, timeslot.avail)
+                    _generate_time_constraints("participant", participant_availabilities, timeslot.avail)
                 )
 
                 # add fulfilled time constraints for all AKSlots fixed to happen during timeslot
@@ -639,6 +639,7 @@ class Event(models.Model):
 
                 time_constraints.extend(timeslot.constraints)
                 time_constraints.extend(block_timeconstraints)
+                time_constraints.sort()
 
                 current_block.append({
                     "id": timeslot.idx,
@@ -646,7 +647,7 @@ class Event(models.Model):
                         "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),
+                    "fulfilled_time_constraints": time_constraints,
                     })
 
             timeslots["blocks"].append(current_block)
@@ -670,18 +671,14 @@ class Event(models.Model):
             "aks": [ak.as_json_dict() for ak in slots],
         }
 
-        event_ak_slots = AKSlot.objects.filter(event=self.event)
         if EventParticipant.objects.exists():
             next_participant_pk = EventParticipant.objects.latest("pk").pk + 1
         else:
             next_participant_pk = 1
         # add one dummy participant per owner
         # this ensures that the hard constraints from each owner are considered
-        for new_pk, owner in enumerate(
-            owners,
-            next_participant_pk,
-        ):
-            owned_slots = event_ak_slots.filter(ak__owners=owner).all()
+        for new_pk, owner in enumerate(owners, next_participant_pk):
+            owned_slots = slots.filter(ak__owners=owner).order_by().all()
             if not owned_slots:
                 continue
             new_participant_data = {
@@ -695,6 +692,7 @@ class Event(models.Model):
                 ]
             }
             data["participants"].append(new_participant_data)
+        return data
 
 
 class AKOwner(models.Model):
@@ -1128,8 +1126,7 @@ class Room(models.Model):
                 "name": self.name,
             },
             "capacity": self.capacity,
-            "fulfilled_room_constraints": [constraint.name
-                                           for constraint in self.properties.all()],
+            "fulfilled_room_constraints": list(self.properties.values_list("name", flat=True)),
             "time_constraints": time_constraints
         }
 
@@ -1275,20 +1272,14 @@ class AKSlot(models.Model):
             "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()]),
+                "conflicts": list((conflict_slots | other_ak_slots).values_list("pk", flat=True).order_by()),
+                "dependencies": list(dependency_slots.values_list("pk", flat=True).order_by()),
             },
-            "room_constraints": [constraint.name
-                                 for constraint in self.ak.requirements.all()],
+            "room_constraints": list(self.ak.requirements.values_list("name", flat=True).order_by()),
             "time_constraints": ["resolution"] if self.ak.reso else [],
             "info": {
                 "name": self.ak.name,
-                "head": ", ".join([str(owner)
-                                   for owner in self.ak.owners.all()]),
+                "head": ", ".join([str(owner) for owner in self.ak.owners.order_by().all()]),
                 "description": self.ak.description,
                 "reso": self.ak.reso,
                 "duration_in_hours": float(self.duration),
@@ -1662,14 +1653,14 @@ class EventParticipant(models.Model):
         data = {
             "id": self.pk,
             "info": {"name": str(self)},
-            "room_constraints": [constraint.name for constraint in self.requirements.all()],
+            "room_constraints": list(self.requirements.values_list("name", flat=True).order_by()),
             "time_constraints": [],
         }
         data["preferences"] = [
             pref.as_json_dict()
             for pref in AKPreference.objects.filter(
                 participant=self, preference__gt=0
-            ).select_related("slot")
+            ).select_related("slot").order_by()
         ]
 
         avails = self.availabilities.all()
@@ -1683,6 +1674,7 @@ class EventParticipant(models.Model):
                 # partipant is actually required for AKs
                 data["time_constraints"].append(f"availability-participant-{self.pk}")
 
+        data["time_constraints"].sort()
         return data
 
 
diff --git a/AKModel/tests/test_json_export.py b/AKModel/tests/test_json_export.py
index 8932df6a..50482ce1 100644
--- a/AKModel/tests/test_json_export.py
+++ b/AKModel/tests/test_json_export.py
@@ -62,7 +62,10 @@ class JSONExportTest(TestCase):
 
         self.ak_slots: Iterable[AKSlot] = []
         self.rooms: Iterable[Room] = []
+        self.participants: Iterable[EventParticipant] = []
+        self.owners: Iterable[AKOwner] = []
         self.slots_in_an_hour: float = 1.0
+        self.max_participant_pk = 0
         self.event: Event | None = None
 
     def set_up_event(self, event: Event) -> None:
-- 
GitLab