From 17ae90884b1e8fa912d8670d55439b14a9e3edc6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?=
 <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de>
Date: Wed, 4 Nov 2020 14:23:39 +0100
Subject: [PATCH] Prepare automatic constraint checking

Add helper fields and methods to ConstraintViolation model
Introduce helper method do determine whether two AKSlots overlap
Add receivers to AKScheduling
Implement stub for OWNER_TWO_SLOTS violation
---
 AKModel/models.py      | 46 +++++++++++++++++++++++-
 AKScheduling/models.py | 81 +++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 125 insertions(+), 2 deletions(-)

diff --git a/AKModel/models.py b/AKModel/models.py
index a49e87f4..972953a9 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -325,7 +325,8 @@ class AKSlot(models.Model):
     duration = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Duration'),
                                    help_text=_('Length in hours'))
 
-    fixed = models.BooleanField(default=False, verbose_name=_('Scheduling fixed'), help_text=_('Length and time of this AK should not be changed'))
+    fixed = models.BooleanField(default=False, verbose_name=_('Scheduling fixed'),
+                                help_text=_('Length and time of this AK should not be changed'))
 
     event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
                               help_text=_('Associated event'))
@@ -388,6 +389,9 @@ class AKSlot(models.Model):
         """
         return (timezone.now() - self.updated).total_seconds()
 
+    def overlaps(self, other: "AKSlot"):
+        return self.start <= other.end  <= self.end or self.start <= other.start <= self.end
+
 
 class AKOrgaMessage(models.Model):
     ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'),
@@ -496,5 +500,45 @@ class ConstraintViolation(models.Model):
     def timestamp_display(self):
         return self.timestamp.astimezone(self.event.timezone).strftime('%d.%m.%y %H:%M')
 
+    # TODO Automatically save this
+    aks_tmp = set()
+
+    @property
+    def _aks(self):
+        """
+        Get all AKs belonging to this constraint violation
+
+        The distinction between real and tmp relationships is needed since many to many
+        relations only work for objects already persisted in the database
+
+        :return: set of all AKs belonging to this constraint violation
+        :rtype: set(AK)
+        """
+        if self.pk and self.pk > 0:
+            return set(self.aks.all())
+        return self.aks_tmp
+
+    # TODO Automatically save this
+    ak_slots_tmp = set()
+
+    @property
+    def _ak_slots(self):
+        """
+        Get all AK Slots belonging to this constraint violation
+
+        The distinction between real and tmp relationships is needed since many to many
+        relations only work for objects already persisted in the database
+
+        :return: set of all AK Slots belonging to this constraint violation
+        :rtype: set(AKSlot)
+        """
+        if self.pk and self.pk > 0:
+            return set(self.ak_slots.all())
+        return self.ak_slots_tmp
+
     def __str__(self):
         return f"{self.get_level_display()}: {self.get_type_display()} [{self.get_details()}]"
+
+    def __eq__(self, other):
+        # TODO Check if FIELDS and FIELDS_MM are equal
+        return super().__eq__(other)
diff --git a/AKScheduling/models.py b/AKScheduling/models.py
index 6b202199..990bcd4a 100644
--- a/AKScheduling/models.py
+++ b/AKScheduling/models.py
@@ -1 +1,80 @@
-# Create your models here.
+from django.db.models.signals import post_save
+from django.dispatch import receiver
+
+from AKModel.availability.models import Availability
+from AKModel.models import AK, AKSlot, Room, Event, AKOwner, ConstraintViolation
+
+
+@receiver(post_save, sender=AK)
+def ak_changed_handler(sender, instance: AK, **kwargs):
+    # Changes might affect: Owner(s), Requirements, Conflicts, Prerequisites, Category, Interest
+    print(f"{instance} changed")
+
+    event = instance.event
+
+    # Owner might have changed: Might affect multiple AKs by the same owner at the same time
+    conflicts = []
+    type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS
+    # For all owners...
+    for owner in instance.owners.all():
+        # ...find overlapping AKs...
+        slots_by_owner : [AKSlot] = []
+        slots_by_owner_this_ak : [AKSlot] = []
+        aks_by_owner = owner.ak_set.all()
+        for ak in aks_by_owner:
+            if ak != instance:
+                slots_by_owner.extend(ak.akslot_set.filter(start__isnull=False))
+            else:
+                # ToDo Fill this outside of loop?
+                slots_by_owner_this_ak.extend(ak.akslot_set.filter(start__isnull=False))
+        for slot in slots_by_owner_this_ak:
+            for other_slot in slots_by_owner:
+                if slot.overlaps(other_slot):
+                    # TODO Create ConstraintViolation here
+                    c = ConstraintViolation(
+                        type=type,
+                        level=ConstraintViolation.ViolationLevel.VIOLATION,
+                        event=event,
+                        ak_owner=owner
+                    )
+                    c.aks_tmp.add(instance)
+                    c.aks_tmp.add(other_slot.ak)
+                    c.ak_slots_tmp.add(slot)
+                    c.ak_slots_tmp.add(other_slot)
+                    conflicts.append(c)
+        print(f"{owner} has the following conflicts: {conflicts}")
+    # ... and compare to/update list of existing violations of this type:
+    current_violations = instance.constraintviolation_set.filter(type=type)
+    for conflict in conflicts:
+        pass
+        # TODO Remove from list of current_violations if an equal new one is found
+        # TODO Otherwise, store this conflict in db
+    # TODO Remove all violations still in current_violations
+
+
+@receiver(post_save, sender=AKSlot)
+def akslot_changed_handler(sender, instance, **kwargs):
+    # Changes might affect: Duplicate parallel, Two in room
+    print(f"{sender} changed")
+    # TODO Replace with real handling
+
+
+@receiver(post_save, sender=Room)
+def room_changed_handler(sender, **kwargs):
+    # Changes might affect: Room size, Requirement
+    print(f"{sender} changed")
+    # TODO Replace with real handling
+
+
+@receiver(post_save, sender=Availability)
+def availability_changed_handler(sender, **kwargs):
+    # Changes might affect: category availability, AK availability, Room availability
+    print(f"{sender} changed")
+    # TODO Replace with real handling
+
+
+@receiver(post_save, sender=Event)
+def room_changed_handler(sender, **kwargs):
+    # Changes might affect: Reso-Deadline
+    print(f"{sender} changed")
+    # TODO Replace with real handling
-- 
GitLab