Skip to content
Snippets Groups Projects

Constraint Violation checking & visualization

Merged Benjamin Hättasch requested to merge scheduling-constraints into main
3 files
+ 37
2
Compare changes
  • Side-by-side
  • Inline
Files
3
+ 112
6
@@ -360,7 +360,8 @@ class AKSlot(models.Model):
@@ -360,7 +360,8 @@ class AKSlot(models.Model):
duration = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Duration'),
duration = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Duration'),
help_text=_('Length in hours'))
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'),
event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'),
help_text=_('Associated event'))
help_text=_('Associated event'))
@@ -423,6 +424,9 @@ class AKSlot(models.Model):
@@ -423,6 +424,9 @@ class AKSlot(models.Model):
"""
"""
return (timezone.now() - self.updated).total_seconds()
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):
class AKOrgaMessage(models.Model):
ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'),
ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'),
@@ -494,7 +498,12 @@ class ConstraintViolation(models.Model):
@@ -494,7 +498,12 @@ class ConstraintViolation(models.Model):
help_text=_('Mark this violation manually as resolved'))
help_text=_('Mark this violation manually as resolved'))
fields = ['ak_owner', 'room', 'requirement', 'category']
fields = ['ak_owner', 'room', 'requirement', 'category']
fields_mm = ['aks', 'ak_slots']
fields_mm = ['_aks', '_ak_slots']
 
 
def __init__(self, *args, **kwargs):
 
super().__init__(*args, **kwargs)
 
self.aks_tmp = set()
 
self.ak_slots_tmp = set()
def get_details(self):
def get_details(self):
"""
"""
@@ -502,10 +511,10 @@ class ConstraintViolation(models.Model):
@@ -502,10 +511,10 @@ class ConstraintViolation(models.Model):
:return: string of details
:return: string of details
:rtype: str
:rtype: str
"""
"""
output = []
# Stringify aks and ak slots fields (m2m)
# Stringify all ManyToMany fields
output = [f"{_('AKs')}: {self._aks_str}",
for field_mm in self.fields_mm:
f"{_('AK Slots')}: {self._ak_slots_str}"]
output.append(f"{field_mm}: {', '.join(str(a) for a in getattr(self, field_mm).all())}")
# Stringify all other fields
# Stringify all other fields
for field in self.fields:
for field in self.fields:
a = getattr(self, field, None)
a = getattr(self, field, None)
@@ -515,5 +524,102 @@ class ConstraintViolation(models.Model):
@@ -515,5 +524,102 @@ class ConstraintViolation(models.Model):
get_details.short_description = _('Details')
get_details.short_description = _('Details')
 
@property
 
def details(self):
 
return self.get_details()
 
 
@property
 
def level_display(self):
 
return self.get_level_display()
 
 
@property
 
def type_display(self):
 
return self.get_type_display()
 
 
@property
 
def timestamp_display(self):
 
return self.timestamp.astimezone(self.event.timezone).strftime('%d.%m.%y %H:%M')
 
 
@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
 
 
@property
 
def _aks_str(self):
 
if self.pk and self.pk > 0:
 
return ', '.join(str(a) for a in self.aks.all())
 
return ', '.join(str(a) for a in self.aks_tmp)
 
 
@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
 
 
@property
 
def _ak_slots_str(self):
 
if self.pk and self.pk > 0:
 
return ', '.join(str(a) for a in self.ak_slots.all())
 
return ', '.join(str(a) for a in self.ak_slots_tmp)
 
 
def save(self, *args, **kwargs):
 
super().save(*args, **kwargs)
 
# Store temporary m2m-relations in db
 
for ak in self.aks_tmp:
 
self.aks.add(ak)
 
for ak_slot in self.ak_slots_tmp:
 
self.ak_slots.add(ak_slot)
 
def __str__(self):
def __str__(self):
return f"{self.get_level_display()}: {self.get_type_display()} [{self.get_details()}]"
return f"{self.get_level_display()}: {self.get_type_display()} [{self.get_details()}]"
 
 
def matches(self, other):
 
"""
 
Check whether one constraint violation instance matches another,
 
this means has the same type, room, requirement, owner, category
 
as well as the same lists of aks and ak slots.
 
PK, timestamp, comments and manual resolving are ignored.
 
 
:param other: second instance to compare to
 
:type other: ConstraintViolation
 
:return: true if both instances are similar in the way described, false if not
 
:rtype: bool
 
"""
 
if not isinstance(other, ConstraintViolation):
 
return False
 
# Check type
 
if self.type != other.type:
 
return False
 
# Make sure both have the same aks and ak slots
 
for field_mm in self.fields_mm:
 
s: set = getattr(self, field_mm)
 
o: set = getattr(other, field_mm)
 
if len(s) != len(o):
 
return False
 
if len(s.intersection(o)) != len(s):
 
return False
 
# Check other "defining" fields
 
for field in self.fields:
 
if getattr(self, field) != getattr(other, field):
 
return False
 
return True
Loading