From d8bc1ca5a517bdd05f988b20c43e5383595e4517 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 00:31:49 +0100
Subject: [PATCH] Introduce model for constraint violation

---
 AKModel/admin.py                              | 10 ++-
 .../migrations/0041_constraint_violation.py   | 37 +++++++++++
 AKModel/models.py                             | 65 +++++++++++++++++++
 3 files changed, 111 insertions(+), 1 deletion(-)
 create mode 100644 AKModel/migrations/0041_constraint_violation.py

diff --git a/AKModel/admin.py b/AKModel/admin.py
index 0e7cef56..b0bae79c 100644
--- a/AKModel/admin.py
+++ b/AKModel/admin.py
@@ -12,7 +12,8 @@ from simple_history.admin import SimpleHistoryAdmin
 
 from AKModel.availability.forms import AvailabilitiesFormMixin
 from AKModel.availability.models import Availability
-from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKTag, AKRequirement, AK, AKSlot, Room, AKOrgaMessage
+from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKTag, AKRequirement, AK, AKSlot, Room, AKOrgaMessage, \
+    ConstraintViolation
 from AKModel.views import EventStatusView, AKCSVExportView, AKWikiExportView, AKMessageDeleteView
 
 
@@ -259,3 +260,10 @@ class AKOrgaMessageAdmin(admin.ModelAdmin):
     list_display = ['timestamp', 'ak', 'text']
     list_filter = ['ak__event']
     readonly_fields = ['timestamp', 'ak', 'text']
+
+
+@admin.register(ConstraintViolation)
+class ConstraintViolationAdmin(admin.ModelAdmin):
+    list_display = ['type', 'level', 'get_details']
+    list_filter = ['event']
+    readonly_fields = ['timestamp']
diff --git a/AKModel/migrations/0041_constraint_violation.py b/AKModel/migrations/0041_constraint_violation.py
new file mode 100644
index 00000000..c323bea7
--- /dev/null
+++ b/AKModel/migrations/0041_constraint_violation.py
@@ -0,0 +1,37 @@
+# Generated by Django 3.0.6 on 2020-11-03 23:27
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('AKModel', '0040_event_reso_deadline'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='ConstraintViolation',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('type', models.CharField(choices=[('ots', 'Owner has two parallel slots'), ('soa', "AK Slot was scheduled outside the AK's availabilities"), ('rts', 'Room has two AK slots scheduled at the same time'), ('rng', 'Room does not satisfy the requirement of the scheduled AK'), ('acc', 'AK Slot is scheduled at the same time as an AK listed as a conflict'), ('abp', 'AK Slot is scheduled before an AK listed as a prerequisite'), ('aar', 'AK Slot for AK with intention to submit a resolution is scheduled after resolution deadline'), ('acm', 'AK Slot in a category is outside that categories availabilities'), ('asc', 'Two AK Slots for the same AK scheduled at the same time'), ('rce', 'AK Slot is scheduled in a room with less space than interest'), ('soe', "AK Slot is scheduled outside the event's availabilities")], help_text='Type of violation, i.e. what kind of constraint was violated', max_length=3, verbose_name='Type')),
+                ('level', models.PositiveSmallIntegerField(choices=[(1, 'Warning'), (10, 'Violation')], help_text='Severity level of the violation', verbose_name='Level')),
+                ('comment', models.TextField(blank=True, help_text='Comment or further details for this violation', verbose_name='Comment')),
+                ('timestamp', models.DateTimeField(auto_now_add=True, help_text='Time of creation', verbose_name='Timestamp')),
+                ('manually_resolved', models.BooleanField(default=False, help_text='Mark this violation manually as resolved', verbose_name='Manually Resolved')),
+                ('ak_owner', models.ForeignKey(blank=True, help_text='AK Owner belonging to this constraint', null=True, on_delete=django.db.models.deletion.CASCADE, to='AKModel.AKOwner', verbose_name='AK Owner')),
+                ('ak_slots', models.ManyToManyField(blank=True, help_text='AK Slot(s) belonging to this constraint', to='AKModel.AKSlot', verbose_name='AK Slots')),
+                ('aks', models.ManyToManyField(blank=True, help_text='AK(s) belonging to this constraint', to='AKModel.AK', verbose_name='AKs')),
+                ('category', models.ForeignKey(blank=True, help_text='AK Category belonging to this constraint', null=True, on_delete=django.db.models.deletion.CASCADE, to='AKModel.AKCategory', verbose_name='AK Category')),
+                ('event', models.ForeignKey(help_text='Associated event', on_delete=django.db.models.deletion.CASCADE, to='AKModel.Event', verbose_name='Event')),
+                ('requirement', models.ForeignKey(blank=True, help_text='AK Requirement belonging to this constraint', null=True, on_delete=django.db.models.deletion.CASCADE, to='AKModel.AKRequirement', verbose_name='AK Requirement')),
+                ('room', models.ForeignKey(blank=True, help_text='Room belonging to this constraint', null=True, on_delete=django.db.models.deletion.CASCADE, to='AKModel.Room', verbose_name='Room')),
+            ],
+            options={
+                'verbose_name': 'Constraint Violation',
+                'verbose_name_plural': 'Constraint Violations',
+                'ordering': ['-timestamp'],
+            },
+        ),
+    ]
diff --git a/AKModel/models.py b/AKModel/models.py
index 2d5a72c0..ac454086 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -359,3 +359,68 @@ class AKOrgaMessage(models.Model):
 
     def __str__(self):
         return f'AK Orga Message for "{self.ak}" @ {self.timestamp}'
+
+
+class ConstraintViolation(models.Model):
+    class Meta:
+        verbose_name = _('Constraint Violation')
+        verbose_name_plural = _('Constraint Violations')
+        ordering = ['-timestamp']
+
+    class ViolationType(models.TextChoices):
+        OWNER_TWO_SLOTS = 'ots', _('Owner has two parallel slots')
+        SLOT_OUTSIDE_AVAIL = 'soa', _('AK Slot was scheduled outside the AK\'s availabilities')
+        ROOM_TWO_SLOTS = 'rts', _('Room has two AK slots scheduled at the same time')
+        REQUIRE_NOT_GIVEN = 'rng', _('Room does not satisfy the requirement of the scheduled AK')
+        AK_CONFLICT_COLLISION = 'acc', _('AK Slot is scheduled at the same time as an AK listed as a conflict')
+        AK_BEFORE_PREREQUISITE = 'abp', _('AK Slot is scheduled before an AK listed as a prerequisite')
+        AK_AFTER_RESODEADLINE = 'aar', _('AK Slot for AK with intention to submit a resolution is scheduled after resolution deadline')
+        AK_CATEGORY_MISMATCH = 'acm', _('AK Slot in a category is outside that categories availabilities')
+        AK_SLOT_COLLISION = 'asc', _('Two AK Slots for the same AK scheduled at the same time')
+        ROOM_CAPACITY_EXCEEDED = 'rce', _('AK Slot is scheduled in a room with less space than interest')
+        SLOT_OUTSIDE_EVENT = 'soe', _('AK Slot is scheduled outside the event\'s availabilities')
+
+    class ViolationLevel(models.IntegerChoices):
+        WARNING = 1, _('Warning')
+        VIOLATION = 10, _('Violation')
+
+    type = models.CharField(verbose_name=_('Type'), max_length=3, choices=ViolationType.choices, help_text=_('Type of violation, i.e. what kind of constraint was violated'))
+    level = models.PositiveSmallIntegerField(verbose_name=_('Level'), choices=ViolationLevel.choices, help_text=_('Severity level of the violation'))
+
+    event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'), help_text=_('Associated event'))
+
+    aks = models.ManyToManyField(to=AK, blank=True, verbose_name=_('AKs'), help_text=_('AK(s) belonging to this constraint'))
+    ak_slots = models.ManyToManyField(to=AKSlot, blank=True, verbose_name=_('AK Slots'), help_text=_('AK Slot(s) belonging to this constraint'))
+    ak_owner = models.ForeignKey(to=AKOwner, on_delete=models.CASCADE, blank=True, null=True, verbose_name=_('AK Owner'), help_text=_('AK Owner belonging to this constraint'))
+    room = models.ForeignKey(to=Room, on_delete=models.CASCADE, blank=True, null=True, verbose_name=_('Room'), help_text=_('Room belonging to this constraint'))
+    requirement = models.ForeignKey(to=AKRequirement, on_delete=models.CASCADE, blank=True, null=True, verbose_name=_('AK Requirement'), help_text=_('AK Requirement belonging to this constraint'))
+    category = models.ForeignKey(to=AKCategory, on_delete=models.CASCADE, blank=True, null=True, verbose_name=_('AK Category'), help_text=_('AK Category belonging to this constraint'))
+
+    comment = models.TextField(verbose_name=_('Comment'), help_text=_('Comment or further details for this violation'), blank=True)
+
+    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']
+
+    def get_details(self):
+        """
+        Get details of this constraint (all fields connected to it)
+        :return: string of details
+        :rtype: str
+        """
+        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())}")
+        # Stringify all other 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')
+
+    def __str__(self):
+        return f"{self.get_level_display()}: {self.get_type_display()} [{self.get_details()}]"
-- 
GitLab