From cef8782a6f8a8be6051c013f4043d57baa488eef Mon Sep 17 00:00:00 2001
From: "N. Geisler" <ngeisler@fachschaft.informatik.tu-darmstadt.de>
Date: Thu, 13 May 2021 01:05:48 +0200
Subject: [PATCH] Implement several constraint violation checks

---
 AKScheduling/models.py | 367 +++++++++++++++++++++++++++++++++++++----
 1 file changed, 337 insertions(+), 30 deletions(-)

diff --git a/AKScheduling/models.py b/AKScheduling/models.py
index 3104d90e..f148da47 100644
--- a/AKScheduling/models.py
+++ b/AKScheduling/models.py
@@ -42,7 +42,7 @@ def update_cv_reso_deadline_for_slot(slot):
     :type slot: AKSlot
     """
     event = slot.event
-    if slot.ak.reso and slot.event.reso_deadline:
+    if slot.ak.reso and slot.event.reso_deadline and slot.start:
         violation_type = ConstraintViolation.ViolationType.AK_AFTER_RESODEADLINE
         new_violations = []
         if slot.end > event.reso_deadline:
@@ -56,14 +56,17 @@ def update_cv_reso_deadline_for_slot(slot):
             new_violations.append(c)
         update_constraint_violations(new_violations, list(slot.constraintviolation_set.filter(type=violation_type)))
 
+
 @receiver(post_save, sender=AK)
 def ak_changed_handler(sender, instance: AK, **kwargs):
-    # Changes might affect: Owner(s), Requirements, Conflicts, Prerequisites, Category, Interest
+    # Changes might affect: Reso intention, Category, Interest
+    # TODO Reso intention changes
     pass
 
 
+# TODO adapt for Room's reauirements
 @receiver(m2m_changed, sender=AK.owners.through)
-def ak_changed_handler(sender, instance: AK, action: str, **kwargs):
+def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs):
     """
     Owners of AK changed
     """
@@ -104,7 +107,148 @@ def ak_changed_handler(sender, instance: AK, action: str, **kwargs):
                             c.ak_slots_tmp.add(other_slot)
                             new_violations.append(c)
 
-        # print(f"{owner} has the following conflicts: {new_violations}")
+        print(f"{owner} has the following conflicts: {new_violations}")
+
+    # ... and compare to/update list of existing violations of this type
+    # belonging to the AK that was recently changed (important!)
+    existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
+    # print(existing_violations_to_check)
+    update_constraint_violations(new_violations, existing_violations_to_check)
+
+
+@receiver(m2m_changed, sender=AK.conflicts.through)
+def ak_conflicts_changed_handler(sender, instance: AK, action: str, **kwargs):
+    """
+    Conflicts of AK changed
+    """
+    # Only signal after change (post_add, post_delete, post_clear) are relevant
+    if not action.startswith("post"):
+        return
+
+    # print(f"{instance} changed")
+
+    event = instance.event
+
+    # Conflict(s) changed: Might affect multiple AKs that are conflicts of each other
+    violation_type = ConstraintViolation.ViolationType.AK_CONFLICT_COLLISION
+    new_violations = []
+
+    slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
+    conflicts_of_this_ak: [AK] = instance.conflicts.all()
+
+    for ak in conflicts_of_this_ak:
+        if ak != instance:
+            for other_slot in ak.akslot_set.filter(start__isnull=False):
+                for slot in slots_of_this_ak:
+                    # ...find overlapping slots...
+                    if slot.overlaps(other_slot):
+                        # ...and create a temporary violation if necessary...
+                        c = ConstraintViolation(
+                            type=violation_type,
+                            level=ConstraintViolation.ViolationLevel.VIOLATION,
+                            event=event,
+                        )
+                        c.aks_tmp.add(instance)
+                        c.ak_slots_tmp.add(slot)
+                        c.ak_slots_tmp.add(other_slot)
+                        new_violations.append(c)
+
+        print(f"{instance} has the following conflicts: {new_violations}")
+
+    # ... and compare to/update list of existing violations of this type
+    # belonging to the AK that was recently changed (important!)
+    existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
+    # print(existing_violations_to_check)
+    update_constraint_violations(new_violations, existing_violations_to_check)
+
+
+@receiver(m2m_changed, sender=AK.prerequisites.through)
+def ak_prerequisites_changed_handler(sender, instance: AK, action: str, **kwargs):
+    """
+    Prerequisites of AK changed
+    """
+    # Only signal after change (post_add, post_delete, post_clear) are relevant
+    if not action.startswith("post"):
+        return
+
+    # print(f"{instance} changed")
+
+    event = instance.event
+
+    # Conflict(s) changed: Might affect multiple AKs that are conflicts of each other
+    violation_type = ConstraintViolation.ViolationType.AK_BEFORE_PREREQUISITE
+    new_violations = []
+
+    slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
+    prerequisites_of_this_ak: [AK] = instance.prerequisites.all()
+
+    for ak in prerequisites_of_this_ak:
+        if ak != instance:
+            for other_slot in ak.akslot_set.filter(start__isnull=False):
+                for slot in slots_of_this_ak:
+                    # ...find overlapping slots...
+                    if other_slot.end > slot.start:
+                        # ...and create a temporary violation if necessary...
+                        c = ConstraintViolation(
+                            type=violation_type,
+                            level=ConstraintViolation.ViolationLevel.VIOLATION,
+                            event=event,
+                        )
+                        c.aks_tmp.add(instance)
+                        c.ak_slots_tmp.add(slot)
+                        c.ak_slots_tmp.add(other_slot)
+                        new_violations.append(c)
+
+        print(f"{instance} has the following conflicts: {new_violations}")
+
+    # ... and compare to/update list of existing violations of this type
+    # belonging to the AK that was recently changed (important!)
+    existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
+    # print(existing_violations_to_check)
+    update_constraint_violations(new_violations, existing_violations_to_check)
+
+
+@receiver(m2m_changed, sender=AK.requirements.through)
+def ak_requirements_changed_handler(sender, instance: AK, action: str, **kwargs):
+    """
+    Requirements of AK changed
+    """
+    # Only signal after change (post_add, post_delete, post_clear) are relevant
+    if not action.startswith("post"):
+        return
+
+    # print(f"{instance} changed")
+
+    event = instance.event
+
+    # Requirement(s) changed: Might affect slots and rooms
+    violation_type = ConstraintViolation.ViolationType.REQUIRE_NOT_GIVEN
+    new_violations = []
+
+    slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
+
+    # For all requirements (after recent change)...
+    for slot in slots_of_this_ak:
+
+        room = slot.room
+        room_requirements = room.properties.all()
+
+        for requirement in instance.requirements.all():
+
+            if not requirement in room_requirements:
+                # ...and create a temporary violation if necessary...
+                c = ConstraintViolation(
+                    type=violation_type,
+                    level=ConstraintViolation.ViolationLevel.VIOLATION,
+                    event=event,
+                    requirement=requirement,
+                    room=room,
+                )
+                c.aks_tmp.add(instance)
+                c.ak_slots_tmp.add(slot)
+                new_violations.append(c)
+
+    print(f"{instance} has the following conflicts: {new_violations}")
 
     # ... and compare to/update list of existing violations of this type
     # belonging to the AK that was recently changed (important!)
@@ -151,7 +295,7 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
     # ... and compare to/update list of existing violations of this type
     # belonging to the AK that was recently changed (important!)
     existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
-    print(existing_violations_to_check)
+    # print(existing_violations_to_check)
     update_constraint_violations(new_violations, existing_violations_to_check)
 
     # == Check for two aks in the same room at the same time ==
@@ -160,7 +304,43 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
     new_violations = []
 
     # For all slots in this room...
-    for other_slot in instance.room.akslot_set.all():
+    if instance.room:
+        for other_slot in instance.room.akslot_set.all():
+            if other_slot != instance:
+                # ... find overlapping slots...
+                if instance.overlaps(other_slot):
+                    # ...and create a temporary violation if necessary...
+                    c = ConstraintViolation(
+                        type=violation_type,
+                        level=ConstraintViolation.ViolationLevel.WARNING,
+                        event=event,
+                        room=instance.room
+                    )
+                    c.aks_tmp.add(instance.ak)
+                    c.aks_tmp.add(other_slot.ak)
+                    c.ak_slots_tmp.add(instance)
+                    c.ak_slots_tmp.add(other_slot)
+                    new_violations.append(c)
+
+        print(f"Multiple slots in room {instance.room}: {new_violations}")
+
+        # ... and compare to/update list of existing violations of this type
+        # belonging to the slot that was recently changed (important!)
+        existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
+        # print(existing_violations_to_check)
+        update_constraint_violations(new_violations, existing_violations_to_check)
+
+    # == Check for reso ak after reso deadline ==
+
+    update_cv_reso_deadline_for_slot(instance)
+
+    # == Check for two slots of the same AK at the same time (warning) ==
+
+    violation_type = ConstraintViolation.ViolationType.AK_SLOT_COLLISION
+    new_violations = []
+
+    # For all other slots of this ak...
+    for other_slot in instance.ak.akslot_set.filter(start__isnull=False):
         if other_slot != instance:
             # ... find overlapping slots...
             if instance.overlaps(other_slot):
@@ -169,69 +349,196 @@ def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
                     type=violation_type,
                     level=ConstraintViolation.ViolationLevel.WARNING,
                     event=event,
-                    room=instance.room
                 )
                 c.aks_tmp.add(instance.ak)
-                c.aks_tmp.add(other_slot.ak)
                 c.ak_slots_tmp.add(instance)
                 c.ak_slots_tmp.add(other_slot)
                 new_violations.append(c)
 
-    print(f"Multiple slots in room {instance.room}: {new_violations}")
-
     # ... and compare to/update list of existing violations of this type
     # belonging to the slot that was recently changed (important!)
-    existing_violations_to_check = list(instance.room.constraintviolation_set.filter(type=violation_type))
-    print(existing_violations_to_check)
+    existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
     update_constraint_violations(new_violations, existing_violations_to_check)
 
-    # == Check for reso ak after reso deadline ==
+    # == Check for slot outside availability ==
 
-    update_cv_reso_deadline_for_slot(instance)
+    # An AK's availability changed: Might affect AK slots scheduled outside the permitted time
+    violation_type = ConstraintViolation.ViolationType.SLOT_OUTSIDE_AVAIL
+    new_violations = []
 
-    # == Check for two slots of the same AK at the same time (warning) ==
+    if instance.start:
+        availabilities_of_this_ak: [Availability] = instance.ak.availabilities.all()
 
-    violation_type = ConstraintViolation.ViolationType.AK_SLOT_COLLISION
+        covered = False
+
+        for availability in availabilities_of_this_ak:
+            covered = availability.start <= instance.start and availability.end >= instance.end
+            if covered:
+                break
+        if not covered:
+            c = ConstraintViolation(
+                type=violation_type,
+                level=ConstraintViolation.ViolationLevel.VIOLATION,
+                event=event
+            )
+            c.aks_tmp.add(instance.ak)
+            c.ak_slots_tmp.add(instance)
+            new_violations.append(c)
+
+    print(f"{instance.ak} has the following slots outside availabilities: {new_violations}")
+
+    # ... and compare to/update list of existing violations of this type
+    # belonging to the AK that was recently changed (important!)
+    existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
+    # print(existing_violations_to_check)
+    update_constraint_violations(new_violations, existing_violations_to_check)
+
+    # == Check for requirement not fulfilled by room ==
+
+    # Room(s) changed: Might affect slots and rooms
+    violation_type = ConstraintViolation.ViolationType.REQUIRE_NOT_GIVEN
     new_violations = []
 
-    # For all other slots of this ak...
-    for other_slot in instance.ak.akslot_set.filter(start__isnull=False):
-        if other_slot != instance:
-            # ... find overlapping slots...
-            if instance.overlaps(other_slot):
+    if instance.room:
+
+        room_requirements = instance.room.properties.all()
+
+        for requirement in instance.ak.requirements.all():
+
+            if requirement not in room_requirements:
                 # ...and create a temporary violation if necessary...
                 c = ConstraintViolation(
                     type=violation_type,
-                    level=ConstraintViolation.ViolationLevel.WARNING,
+                    level=ConstraintViolation.ViolationLevel.VIOLATION,
                     event=event,
+                    requirement=requirement,
+                    room=instance.room,
                 )
                 c.aks_tmp.add(instance.ak)
                 c.ak_slots_tmp.add(instance)
-                c.ak_slots_tmp.add(other_slot)
                 new_violations.append(c)
 
+    print(f"{instance} has the following conflicts: {new_violations}")
+
     # ... and compare to/update list of existing violations of this type
-    # belonging to the slot that was recently changed (important!)
+    # belonging to the AK that was recently changed (important!)
     existing_violations_to_check = list(instance.constraintviolation_set.filter(type=violation_type))
+    # print(existing_violations_to_check)
+    update_constraint_violations(new_violations, existing_violations_to_check)
+
+    # == check for simultaneous slots of conflicting AKs ==
+
+    violation_type = ConstraintViolation.ViolationType.AK_CONFLICT_COLLISION
+    new_violations = []
+
+    if instance.start:
+        conflicts_of_this_ak: [AK] = instance.ak.conflicts.all()
+
+        for ak in conflicts_of_this_ak:
+            if ak != instance.ak:
+                for other_slot in ak.akslot_set.filter(start__isnull=False):
+                    # ...find overlapping slots...
+                    if instance.overlaps(other_slot):
+                        # ...and create a temporary violation if necessary...
+                        c = ConstraintViolation(
+                            type=violation_type,
+                            level=ConstraintViolation.ViolationLevel.VIOLATION,
+                            event=event,
+                        )
+                        c.aks_tmp.add(instance.ak)
+                        c.ak_slots_tmp.add(instance)
+                        c.ak_slots_tmp.add(other_slot)
+                        new_violations.append(c)
+
+            print(f"{instance} has the following conflicts: {new_violations}")
+
+    # ... and compare to/update list of existing violations of this type
+    # belonging to the AK that was recently changed (important!)
+    existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
+    # print(existing_violations_to_check)
+    update_constraint_violations(new_violations, existing_violations_to_check)
+
+    # == check for missing prerequisites ==
+
+    violation_type = ConstraintViolation.ViolationType.AK_BEFORE_PREREQUISITE
+    new_violations = []
+
+    if instance.start:
+        prerequisites_of_this_ak: [AK] = instance.ak.prerequisites.all()
+
+        for ak in prerequisites_of_this_ak:
+            if ak != instance.ak:
+                for other_slot in ak.akslot_set.filter(start__isnull=False):
+                    # ...find slots in the wrong order...
+                    if other_slot.end > instance.start:
+                        # ...and create a temporary violation if necessary...
+                        c = ConstraintViolation(
+                            type=violation_type,
+                            level=ConstraintViolation.ViolationLevel.VIOLATION,
+                            event=event,
+                        )
+                        c.aks_tmp.add(instance.ak)
+                        c.ak_slots_tmp.add(instance)
+                        c.ak_slots_tmp.add(other_slot)
+                        new_violations.append(c)
+
+            print(f"{instance} has the following conflicts: {new_violations}")
+
+    # ... and compare to/update list of existing violations of this type
+    # belonging to the AK that was recently changed (important!)
+    existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
+    # print(existing_violations_to_check)
     update_constraint_violations(new_violations, existing_violations_to_check)
 
 
 @receiver(post_save, sender=Room)
 def room_changed_handler(sender, **kwargs):
-    # Changes might affect: Room size, Requirement
+    # Changes might affect: Room size
     print(f"{sender} changed")
-    # TODO Replace with real handling
 
 
 @receiver(post_save, sender=Availability)
-def availability_changed_handler(sender, **kwargs):
+def availability_changed_handler(sender, instance: Availability, **kwargs):
     # Changes might affect: category availability, AK availability, Room availability
-    print(f"{sender} changed")
-    # TODO Replace with real handling
+    print(f"{instance} changed")
+
+    event = instance.event
+
+    # An AK's availability changed: Might affect AK slots scheduled outside the permitted time
+    if instance.ak:
+        violation_type = ConstraintViolation.ViolationType.SLOT_OUTSIDE_AVAIL
+        new_violations = []
+
+        availabilities_of_this_ak: [Availability] = instance.ak.availabilities.all()
+        slots_of_this_ak: [AKSlot] = instance.ak.akslot_set.filter(start__isnull=False)
+
+        for slot in slots_of_this_ak:
+            covered = False
+            for availability in availabilities_of_this_ak:
+                covered = availability.start <= slot.start and availability.end >= slot.end
+                if covered:
+                    break
+            if not covered:
+                c = ConstraintViolation(
+                    type=violation_type,
+                    level=ConstraintViolation.ViolationLevel.VIOLATION,
+                    event=event
+                )
+                c.aks_tmp.add(instance.ak)
+                c.ak_slots_tmp.add(slot)
+                new_violations.append(c)
+
+        print(f"{instance.ak} has the following slots putside availabilities: {new_violations}")
+
+        # ... and compare to/update list of existing violations of this type
+        # belonging to the AK that was recently changed (important!)
+        existing_violations_to_check = list(instance.ak.constraintviolation_set.filter(type=violation_type))
+        # print(existing_violations_to_check)
+        update_constraint_violations(new_violations, existing_violations_to_check)
 
 
 @receiver(post_save, sender=Event)
-def room_changed_handler(sender, instance, **kwargs):
+def event_changed_handler(sender, instance, **kwargs):
     # == Check for reso ak after reso deadline (which might have changed) ==
     if instance.reso_deadline:
         for slot in instance.akslot_set.filter(start__isnull=False, ak__reso=True):
-- 
GitLab