diff --git a/AKModel/models.py b/AKModel/models.py index ac454086a8592a5dfb8882049c56522f7a983bc6..5a257cd56d25ceb187f7d4337395d87ae6638ae5 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -346,6 +346,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'), help_text=_('AK this message belongs to')) @@ -401,8 +404,8 @@ class ConstraintViolation(models.Model): timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_('Timestamp'), help_text=_('Time of creation')) manually_resolved = models.BooleanField(verbose_name=_('Manually Resolved'), default=False, help_text=_('Mark this violation manually as resolved')) - fields = ['ak_owner', 'room', 'requirement', 'category'] - fields_mm = ['aks', 'ak_slots'] + FIELDS = ['ak_owner', 'room', 'requirement', 'category'] + FIELDS_MM = ['_aks', '_ak_slots'] def get_details(self): """ @@ -412,15 +415,53 @@ class ConstraintViolation(models.Model): """ output = [] # Stringify all ManyToMany fields - for field_mm in self.fields_mm: - output.append(f"{field_mm}: {', '.join(str(a) for a in getattr(self, field_mm).all())}") + for field_mm in self.FIELDS_MM: + output.append(f"{field_mm[1:]}: {', '.join(str(a) for a in getattr(self, field_mm))}") # Stringify all other fields - for field in self.fields: + for field in self.FIELDS: a = getattr(self, field, None) if a is not None: output.append(f"{field}: {a}") return ", ".join(output) get_details.short_description = _('Details') + # 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 6b2021999398416a78191ac543b7e0e34d86bc2c..990bcd4a74b6efebdc865830abd5cafb8600ae21 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