diff --git a/AKModel/admin.py b/AKModel/admin.py
index 01a450cbf4b3a4c92a35e09b3085de7f596524d9..c5d067e6c725cf1cc201626b7d23b741f2f1af66 100644
--- a/AKModel/admin.py
+++ b/AKModel/admin.py
@@ -4,7 +4,7 @@ from django.contrib import admin
 from django.contrib.admin import SimpleListFilter
 from django.db.models import Count, F
 from django.shortcuts import render, redirect
-from django.urls import path, reverse_lazy
+from django.urls import reverse_lazy
 from django.utils import timezone
 from django.utils.html import format_html
 from django.utils.safestring import mark_safe
@@ -16,11 +16,7 @@ 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, \
     ConstraintViolation
-from AKModel.views import EventStatusView, AKCSVExportView, AKWikiExportView, AKMessageDeleteView, \
-    AKRequirementOverview, \
-    NewEventWizardStartView, NewEventWizardSettingsView, NewEventWizardPrepareImportView, NewEventWizardFinishView, \
-    NewEventWizardImportView, NewEventWizardActivateView
-from AKModel.views import export_slides
+from AKModel.urls import get_admin_urls_event_wizard, get_admin_urls_event
 
 
 @admin.register(Event)
@@ -32,42 +28,24 @@ class EventAdmin(admin.ModelAdmin):
     ordering = ['-start']
 
     def add_view(self, request, form_url='', extra_context=None):
-        # Always use wizard to create new events
-        # (the built-in form wouldn't work anyways since the timezone cannot be specified before starting to fill the form)
+        # Always use wizard to create new events (the built-in form wouldn't work anyways since the timezone cannot
+        # be specified before starting to fill the form)
         return redirect("admin:new_event_wizard_start")
 
     def get_urls(self):
-        urls = super().get_urls()
-        custom_urls = [
-            path('add/wizard/start/', self.admin_site.admin_view(NewEventWizardStartView.as_view()),
-                 name="new_event_wizard_start"),
-            path('add/wizard/settings/', self.admin_site.admin_view(NewEventWizardSettingsView.as_view()),
-                 name="new_event_wizard_settings"),
-            path('add/wizard/created/<slug:event_slug>/', self.admin_site.admin_view(NewEventWizardPrepareImportView.as_view()),
-                 name="new_event_wizard_prepare_import"),
-            path('add/wizard/import/<slug:event_slug>/from/<slug:import_slug>/',
-                 self.admin_site.admin_view(NewEventWizardImportView.as_view()),
-                 name="new_event_wizard_import"),
-            path('add/wizard/activate/<slug:slug>/',
-                 self.admin_site.admin_view(NewEventWizardActivateView.as_view()),
-                 name="new_event_wizard_activate"),
-            path('add/wizard/finish/<slug:slug>/',
-                 self.admin_site.admin_view(NewEventWizardFinishView.as_view()),
-                 name="new_event_wizard_finish"),
-            path('<slug:slug>/status/', self.admin_site.admin_view(EventStatusView.as_view()), name="event_status"),
-            path('<slug:event_slug>/requirements/', self.admin_site.admin_view(AKRequirementOverview.as_view()), name="event_requirement_overview"),
-            path('<slug:event_slug>/ak-csv-export/', self.admin_site.admin_view(AKCSVExportView.as_view()), name="ak_csv_export"),
-            path('<slug:slug>/ak-wiki-export/', self.admin_site.admin_view(AKWikiExportView.as_view()), name="ak_wiki_export"),
-            path('<slug:event_slug>/ak-slide-export/', export_slides, name="ak_slide_export"),
-            path('<slug:slug>/delete-orga-messages/', self.admin_site.admin_view(AKMessageDeleteView.as_view()),
-                 name="ak_delete_orga_messages"),
-        ]
-        return custom_urls + urls
+        urls = get_admin_urls_event_wizard(self.admin_site)
+        urls.extend(get_admin_urls_event(self.admin_site))
+        if apps.is_installed("AKScheduling"):
+            from AKScheduling.urls import get_admin_urls_scheduling
+            urls.extend(get_admin_urls_scheduling(self.admin_site))
+        urls.extend(super().get_urls())
+        return urls
 
     def status_url(self, obj):
         return format_html("<a href='{url}'>{text}</a>",
                            url=reverse_lazy('admin:event_status', kwargs={'slug': obj.slug}), text=_("Status"))
-    status_url.short_description = text=_("Status")
+
+    status_url.short_description = _("Status")
 
     def get_form(self, request, obj=None, change=False, **kwargs):
         # Use timezone of event
@@ -116,18 +94,6 @@ class AKTrackAdmin(admin.ModelAdmin):
             kwargs['initial'] = Event.get_next_active()
         return super(AKTrackAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
 
-    def get_urls(self):
-        urls = super().get_urls()
-        custom_urls = []
-        if apps.is_installed("AKScheduling"):
-            from AKScheduling.views import TrackAdminView
-
-            custom_urls.extend([
-                path('<slug:event_slug>/manage/', self.admin_site.admin_view(TrackAdminView.as_view()),
-                     name="tracks_manage"),
-            ])
-        return custom_urls + urls
-
 
 @admin.register(AKTag)
 class AKTagAdmin(admin.ModelAdmin):
@@ -240,7 +206,6 @@ class RoomForm(AvailabilitiesFormMixin, forms.ModelForm):
             self.fields["properties"].queryset = AKRequirement.objects.filter(event=self.instance.event)
 
 
-
 @admin.register(Room)
 class RoomAdmin(admin.ModelAdmin):
     model = Room
@@ -281,20 +246,6 @@ class AKSlotAdmin(admin.ModelAdmin):
     readonly_fields = ['ak_details_link', 'updated']
     form = AKSlotAdminForm
 
-    def get_urls(self):
-        urls = super().get_urls()
-        custom_urls = []
-        if apps.is_installed("AKScheduling"):
-            from AKScheduling.views import SchedulingAdminView, UnscheduledSlotsAdminView
-
-            custom_urls.extend([
-                path('<slug:event_slug>/schedule/', self.admin_site.admin_view(SchedulingAdminView.as_view()),
-                     name="schedule"),
-                path('<slug:event_slug>/unscheduled/', self.admin_site.admin_view(UnscheduledSlotsAdminView.as_view()),
-                     name="slots_unscheduled"),
-            ])
-        return custom_urls + urls
-
     def get_form(self, request, obj=None, change=False, **kwargs):
         # Use timezone of associated event
         if obj is not None and obj.event.timezone:
@@ -310,10 +261,11 @@ class AKSlotAdmin(admin.ModelAdmin):
         return super(AKSlotAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
 
     def ak_details_link(self, akslot):
-        if apps.is_installed("AKScheduling") and akslot.ak is not None:
+        if apps.is_installed("AKSubmission") and akslot.ak is not None:
             link = f"<a href={reverse('submit:ak_detail', args=[akslot.event.slug, akslot.ak.pk])}>{str(akslot.ak)}</a>"
             return mark_safe(link)
         return "-"
+
     ak_details_link.short_description = _('AK Details')
 
 
diff --git a/AKModel/environment.py b/AKModel/environment.py
index 5883361b0d8a80f647e131185dabca8aaa4d4bd7..a6536beecbdbb0945b73ad414744d15d8fc87bdb 100644
--- a/AKModel/environment.py
+++ b/AKModel/environment.py
@@ -7,6 +7,7 @@ from django_tex.environment import environment
 # and would hence cause compilation errors
 utf8_replace_pattern = re.compile(u'[^\u0000-\u206F]', re.UNICODE)
 
+
 def latex_escape_utf8(value):
     """
     Escape latex special chars and remove invalid utf-8 values
@@ -16,7 +17,10 @@ def latex_escape_utf8(value):
     :return: escaped string
     :rtype: str
     """
-    return utf8_replace_pattern.sub('', value).replace('&', '\&').replace('_', '\_').replace('#', '\#').replace('$', '\$').replace('%', '\%').replace('{', '\{').replace('}', '\}')
+    return utf8_replace_pattern.sub('', value).replace('&', '\&').replace('_', '\_').replace('#', '\#').replace('$',
+                                                                                                                '\$').replace(
+        '%', '\%').replace('{', '\{').replace('}', '\}')
+
 
 def improved_tex_environment(**options):
     env = environment(**options)
diff --git a/AKModel/models.py b/AKModel/models.py
index ee27d4f95cfce116ad9f26729cec599099f9bb3e..b2f15fd54693a27ac8b2ae316540f4f25da4a97a 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -360,7 +360,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'))
@@ -423,6 +424,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'),
@@ -494,7 +498,12 @@ class ConstraintViolation(models.Model):
                                             help_text=_('Mark this violation manually as resolved'))
 
     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):
         """
@@ -502,10 +511,10 @@ class ConstraintViolation(models.Model):
         :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 aks and ak slots fields (m2m)
+        output = [f"{_('AKs')}: {self._aks_str}",
+                  f"{_('AK Slots')}: {self._ak_slots_str}"]
+
         # Stringify all other fields
         for field in self.fields:
             a = getattr(self, field, None)
@@ -515,5 +524,102 @@ class ConstraintViolation(models.Model):
 
     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):
         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
diff --git a/AKModel/templates/admin/AKModel/status.html b/AKModel/templates/admin/AKModel/status.html
index 0f5369fac2d89e1d444a98f0855a2b1234148e2e..8ccf44e3dfca2f4ba843c75f4739a5d853dac717 100644
--- a/AKModel/templates/admin/AKModel/status.html
+++ b/AKModel/templates/admin/AKModel/status.html
@@ -74,6 +74,10 @@
 
                     <a class="btn btn-success"
                        href="{% url 'admin:schedule' event_slug=event.slug %}">{% trans "Scheduling" %}</a>
+                    {% if "AKScheduling | is_installed" %}
+                        <a class="btn btn-success"
+                       href="{% url 'admin:constraint-violations' slug=event.slug %}">{% trans "Constraint Violations" %} <span class="badge badge-secondary">{{ event.constraintviolation_set.count }}</span></a>
+                    {% endif %}
                     <a class="btn btn-success"
                        href="{% url 'admin:tracks_manage' event_slug=event.slug %}">{% trans "Manage ak tracks" %}</a>
                     <a class="btn btn-success"
diff --git a/AKModel/urls.py b/AKModel/urls.py
index 9b1f7591ee877043baca3cb63aac843dcb64ac4d..f70ed739547180a017cdb799a126be082cb05d7d 100644
--- a/AKModel/urls.py
+++ b/AKModel/urls.py
@@ -3,6 +3,9 @@ from django.urls import include, path
 from rest_framework.routers import DefaultRouter
 
 from AKModel import views
+from AKModel.views import NewEventWizardStartView, NewEventWizardSettingsView, NewEventWizardPrepareImportView, \
+    NewEventWizardImportView, NewEventWizardActivateView, NewEventWizardFinishView, EventStatusView, \
+    AKRequirementOverview, AKCSVExportView, AKWikiExportView, AKMessageDeleteView, export_slides
 
 api_router = DefaultRouter()
 api_router.register('akowner', views.AKOwnerViewSet, basename='AKOwner')
@@ -12,29 +15,29 @@ api_router.register('ak', views.AKViewSet, basename='AK')
 api_router.register('room', views.RoomViewSet, basename='Room')
 api_router.register('akslot', views.AKSlotViewSet, basename='AKSlot')
 
-
 extra_paths = []
 if apps.is_installed("AKScheduling"):
-    from AKScheduling.api import ResourcesViewSet, RoomAvailabilitiesView, EventsView, EventsViewSet
+    from AKScheduling.api import ResourcesViewSet, RoomAvailabilitiesView, EventsView, EventsViewSet, \
+        ConstraintViolationsViewSet
 
     api_router.register('scheduling-resources', ResourcesViewSet, basename='scheduling-resources')
     api_router.register('scheduling-event', EventsViewSet, basename='scheduling-event')
+    api_router.register('scheduling-constraint-violations', ConstraintViolationsViewSet,
+                        basename='scheduling-constraint-violations')
 
     extra_paths = [
         path('api/scheduling-events/', EventsView.as_view(), name='scheduling-events'),
-        path('api/scheduling-room-availabilities/', RoomAvailabilitiesView.as_view(), name='scheduling-room-availabilities'),
+        path('api/scheduling-room-availabilities/', RoomAvailabilitiesView.as_view(),
+             name='scheduling-room-availabilities'),
     ]
 
-
 event_specific_paths = [
-            path('api/', include(api_router.urls), name='api'),
-        ]
+    path('api/', include(api_router.urls), name='api'),
+]
 event_specific_paths.extend(extra_paths)
 
-
 app_name = 'model'
 
-
 urlpatterns = [
     path(
         '<slug:event_slug>/',
@@ -42,3 +45,39 @@ urlpatterns = [
     ),
     path('user/', views.UserView.as_view(), name="user"),
 ]
+
+
+def get_admin_urls_event_wizard(admin_site):
+    return [
+        path('add/wizard/start/', admin_site.admin_view(NewEventWizardStartView.as_view()),
+             name="new_event_wizard_start"),
+        path('add/wizard/settings/', admin_site.admin_view(NewEventWizardSettingsView.as_view()),
+             name="new_event_wizard_settings"),
+        path('add/wizard/created/<slug:event_slug>/', admin_site.admin_view(NewEventWizardPrepareImportView.as_view()),
+             name="new_event_wizard_prepare_import"),
+        path('add/wizard/import/<slug:event_slug>/from/<slug:import_slug>/',
+             admin_site.admin_view(NewEventWizardImportView.as_view()),
+             name="new_event_wizard_import"),
+        path('add/wizard/activate/<slug:slug>/',
+             admin_site.admin_view(NewEventWizardActivateView.as_view()),
+             name="new_event_wizard_activate"),
+        path('add/wizard/finish/<slug:slug>/',
+             admin_site.admin_view(NewEventWizardFinishView.as_view()),
+             name="new_event_wizard_finish"),
+    ]
+
+
+def get_admin_urls_event(admin_site):
+    return [
+        path('<slug:slug>/status/', admin_site.admin_view(EventStatusView.as_view()), name="event_status"),
+        path('<slug:event_slug>/requirements/', admin_site.admin_view(AKRequirementOverview.as_view()),
+             name="event_requirement_overview"),
+        path('<slug:event_slug>/ak-csv-export/', admin_site.admin_view(AKCSVExportView.as_view()),
+             name="ak_csv_export"),
+        path('<slug:slug>/ak-wiki-export/', admin_site.admin_view(AKWikiExportView.as_view()),
+             name="ak_wiki_export"),
+        path('<slug:slug>/delete-orga-messages/', admin_site.admin_view(AKMessageDeleteView.as_view()),
+             name="ak_delete_orga_messages"),
+        path('<slug:event_slug>/ak-slide-export/', export_slides, name="ak_slide_export"),
+
+    ]
diff --git a/AKModel/views.py b/AKModel/views.py
index 63b0e7d2fab1d7a6c9076b6355580f7660467f14..3132de5f9fcb3c6eaa70cfb3f51902ae0adc73f4 100644
--- a/AKModel/views.py
+++ b/AKModel/views.py
@@ -7,8 +7,8 @@ from django.shortcuts import get_object_or_404, redirect
 from django.urls import reverse_lazy
 from django.utils.translation import gettext_lazy as _
 from django.views.generic import TemplateView, DetailView, ListView, DeleteView, CreateView, FormView, UpdateView
-from rest_framework import viewsets, permissions, mixins
 from django_tex.shortcuts import render_to_pdf
+from rest_framework import viewsets, permissions, mixins
 
 from AKModel.forms import NewEventWizardStartForm, NewEventWizardSettingsForm, NewEventWizardPrepareImportForm, \
     NewEventWizardImportForm, NewEventWizardActivateForm
diff --git a/AKPlanning/settings_ci.py b/AKPlanning/settings_ci.py
index 84a135c53da491761facfa52f1f2b41084d331a8..55de6ac5c1caee213bd2f61b144512e2ea0ff8c2 100644
--- a/AKPlanning/settings_ci.py
+++ b/AKPlanning/settings_ci.py
@@ -17,7 +17,7 @@ DATABASES = {
         'OPTIONS': {
             'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"
         },
-        'TEST' : {
+        'TEST': {
             'NAME': 'test',
         },
     }
diff --git a/AKScheduling/api.py b/AKScheduling/api.py
index 27b4252e94adc1ffa44e4d2481bc94987d7087f1..af73c791053ddab2e6922f8b69610914640d9ff6 100644
--- a/AKScheduling/api.py
+++ b/AKScheduling/api.py
@@ -7,7 +7,7 @@ from django.views.generic import ListView
 from rest_framework import viewsets, mixins, serializers, permissions
 
 from AKModel.availability.models import Availability
-from AKModel.models import Room, AKSlot
+from AKModel.models import Room, AKSlot, ConstraintViolation
 from AKModel.views import EventSlugMixin
 
 
@@ -109,3 +109,20 @@ class EventsViewSet(EventSlugMixin, viewsets.ModelViewSet):
 
     def get_queryset(self):
         return AKSlot.objects.filter(event=self.event)
+
+
+class ConstraintViolationSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = ConstraintViolation
+        fields = ['pk', 'type_display', 'aks', 'ak_slots', 'ak_owner', 'room', 'requirement', 'category', 'comment', 'timestamp_display', 'manually_resolved', 'level_display', 'details']
+
+
+class ConstraintViolationsViewSet(EventSlugMixin, viewsets.ModelViewSet):
+    permission_classes = (permissions.DjangoModelPermissions,)
+    serializer_class = ConstraintViolationSerializer
+
+    def get_object(self):
+        return get_object_or_404(ConstraintViolation, pk=self.kwargs["pk"])
+
+    def get_queryset(self):
+        return ConstraintViolation.objects.filter(event=self.event)
diff --git a/AKScheduling/locale/de_DE/LC_MESSAGES/django.po b/AKScheduling/locale/de_DE/LC_MESSAGES/django.po
index f25c002dbd9be02bcac1bd45292140a48688de0d..5ed003e89f16deaac8e309561323c1cb974345d0 100644
--- a/AKScheduling/locale/de_DE/LC_MESSAGES/django.po
+++ b/AKScheduling/locale/de_DE/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-04-29 22:48+0000\n"
+"POT-Creation-Date: 2021-05-09 18:23+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,49 +17,92 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 
+#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:11
 #: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:11
 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:10
 msgid "Scheduling for"
 msgstr "Scheduling für"
 
-#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:126
+#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:74
+#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:128
+msgid "No violations"
+msgstr "Keine Verletzungen"
+
+#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:81
+msgid "Cannot load current violations from server"
+msgstr "Kann die aktuellen Verletzungen nicht vom Server laden"
+
+#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:106
+msgid "Violation(s)"
+msgstr "Verletzung(en)"
+
+#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:109
+msgid "Auto reload?"
+msgstr "Automatisch neu laden?"
+
+#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:113
+msgid "Reload now"
+msgstr "Jetzt neu laden"
+
+#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:118
+msgid "Violation"
+msgstr "Verletzung"
+
+#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:119
+msgid "Problem"
+msgstr "Problem"
+
+#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:120
+msgid "Details"
+msgstr "Details"
+
+#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:121
+msgid "Since"
+msgstr "Seit"
+
+#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:134
+#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:243
+#: AKScheduling/templates/admin/AKScheduling/scheduling.html:197
+#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34
+msgid "Event Status"
+msgstr "Event-Status"
+
+#: AKScheduling/templates/admin/AKScheduling/constraint_violations.html:136
+msgid "Scheduling"
+msgstr "Scheduling"
+
+#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:129
 msgid "Name of new ak track"
 msgstr "Name des neuen AK-Tracks"
 
-#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:142
+#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:145
 msgid "Could not create ak track"
 msgstr "Konnte neuen AK-Track nicht anlegen"
 
-#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:168
+#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:171
 msgid "Could not update ak track name"
 msgstr "Konnte Namen des AK-Tracks nicht ändern"
 
-#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:174
+#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:177
 msgid "Do you really want to delete this ak track?"
 msgstr "Soll dieser AK-Track wirklich gelöscht werden?"
 
-#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:188
+#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:191
 msgid "Could not delete ak track"
 msgstr "AK-Track konnte nicht gelöscht werden"
 
-#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:200
+#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:203
 msgid "Manage AK Tracks"
 msgstr "AK-Tracks verwalten"
 
-#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:201
+#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:204
 msgid "Add ak track"
 msgstr "AK-Track hinzufügen"
 
-#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:206
+#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:209
 msgid "AKs without track"
 msgstr "AKs ohne Track"
 
-#: AKScheduling/templates/admin/AKScheduling/manage_tracks.html:240
-#: AKScheduling/templates/admin/AKScheduling/scheduling.html:197
-#: AKScheduling/templates/admin/AKScheduling/unscheduled.html:34
-msgid "Event Status"
-msgstr "Event-Status"
-
 #: AKScheduling/templates/admin/AKScheduling/scheduling.html:87
 msgid "Day (Horizontal)"
 msgstr "Tag (horizontal)"
diff --git a/AKScheduling/models.py b/AKScheduling/models.py
index 6b2021999398416a78191ac543b7e0e34d86bc2c..f148da47d1f7b1954bae06cd85d8f39655b63d35 100644
--- a/AKScheduling/models.py
+++ b/AKScheduling/models.py
@@ -1 +1,545 @@
-# Create your models here.
+from django.db.models.signals import post_save, m2m_changed
+from django.dispatch import receiver
+
+from AKModel.availability.models import Availability
+from AKModel.models import AK, AKSlot, Room, Event, AKOwner, ConstraintViolation
+
+
+def update_constraint_violations(new_violations, existing_violations_to_check):
+    """
+    Update existing constraint violations (subset for which new violations were computed) based on these new violations.
+    This will add all new violations without a match, preserve the matching ones
+    and delete the obsolete ones (those without a match from the newly calculated violations).
+
+    :param new_violations: list of new (not yet saved) violations that exist after the last change
+    :type new_violations: list[ConstraintViolation]
+    :param existing_violations_to_check: list of related violations currently in the db
+    :type existing_violations_to_check: list[ConstraintViolation]
+    """
+    for new_violation in new_violations:
+        found_match = False
+        for existing_violation in existing_violations_to_check:
+            if existing_violation.matches(new_violation):
+                # Remove from existing violations set since it should stay in db
+                existing_violations_to_check.remove(existing_violation)
+                found_match = True
+                break
+
+        # Only save new violation if no match was found
+        if not found_match:
+            new_violation.save()
+
+    # Cleanup obsolete violations (ones without matches computed under current conditions)
+    for outdated_violation in existing_violations_to_check:
+        outdated_violation.delete()
+
+
+def update_cv_reso_deadline_for_slot(slot):
+    """
+    Update constraint violation AK_AFTER_RESODEADLINE for given slot
+
+    :param slot: slot to check/update
+    :type slot: AKSlot
+    """
+    event = slot.event
+    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:
+            c = ConstraintViolation(
+                type=violation_type,
+                level=ConstraintViolation.ViolationLevel.VIOLATION,
+                event=event,
+            )
+            c.aks_tmp.add(slot.ak)
+            c.ak_slots_tmp.add(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: Reso intention, Category, Interest
+    # TODO Reso intention changes
+    pass
+
+
+# TODO adapt for Room's reauirements
+@receiver(m2m_changed, sender=AK.owners.through)
+def ak_owners_changed_handler(sender, instance: AK, action: str, **kwargs):
+    """
+    Owners 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
+
+    # Owner(s) changed: Might affect multiple AKs by the same owner(s) at the same time
+    violation_type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS
+    new_violations = []
+
+    slots_of_this_ak: [AKSlot] = instance.akslot_set.filter(start__isnull=False)
+
+    # For all owners (after recent change)...
+    for owner in instance.owners.all():
+        # ...find other slots that might be overlapping...
+
+        for ak in owner.ak_set.all():
+            # ...find overlapping slots...
+            if ak != instance:
+                for slot in slots_of_this_ak:
+                    for other_slot in ak.akslot_set.filter(start__isnull=False):
+                        if slot.overlaps(other_slot):
+                            # ...and create a temporary violation if necessary...
+                            c = ConstraintViolation(
+                                type=violation_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)
+                            new_violations.append(c)
+
+        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!)
+    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(post_save, sender=AKSlot)
+def akslot_changed_handler(sender, instance: AKSlot, **kwargs):
+    # Changes might affect: Duplicate parallel, Two in room, Resodeadline
+    print(f"{sender} changed")
+    event = instance.event
+
+    # == Check for two parallel slots by one of the owners ==
+
+    violation_type = ConstraintViolation.ViolationType.OWNER_TWO_SLOTS
+    new_violations = []
+
+    # For all owners (after recent change)...
+    for owner in instance.ak.owners.all():
+        # ...find other slots that might be overlapping...
+
+        for ak in owner.ak_set.all():
+            # ...find overlapping slots...
+            if ak != instance.ak:
+                for other_slot in ak.akslot_set.filter(start__isnull=False):
+                    if instance.overlaps(other_slot):
+                        # ...and create a temporary violation if necessary...
+                        c = ConstraintViolation(
+                            type=violation_type,
+                            level=ConstraintViolation.ViolationLevel.VIOLATION,
+                            event=event,
+                            ak_owner=owner
+                        )
+                        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"{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)
+
+    # == Check for two aks in the same room at the same time ==
+
+    violation_type = ConstraintViolation.ViolationType.ROOM_TWO_SLOTS
+    new_violations = []
+
+    # For all slots in this room...
+    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):
+                # ...and create a temporary violation if necessary...
+                c = ConstraintViolation(
+                    type=violation_type,
+                    level=ConstraintViolation.ViolationLevel.WARNING,
+                    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)
+
+    # ... 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))
+    update_constraint_violations(new_violations, existing_violations_to_check)
+
+    # == Check for slot outside availability ==
+
+    # An AK's availability changed: Might affect AK slots scheduled outside the permitted time
+    violation_type = ConstraintViolation.ViolationType.SLOT_OUTSIDE_AVAIL
+    new_violations = []
+
+    if instance.start:
+        availabilities_of_this_ak: [Availability] = instance.ak.availabilities.all()
+
+        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 = []
+
+    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.VIOLATION,
+                    event=event,
+                    requirement=requirement,
+                    room=instance.room,
+                )
+                c.aks_tmp.add(instance.ak)
+                c.ak_slots_tmp.add(instance)
+                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)
+
+    # == 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
+    print(f"{sender} changed")
+
+
+@receiver(post_save, sender=Availability)
+def availability_changed_handler(sender, instance: Availability, **kwargs):
+    # Changes might affect: category availability, AK availability, Room availability
+    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 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):
+            update_cv_reso_deadline_for_slot(slot)
diff --git a/AKScheduling/templates/admin/AKScheduling/constraint_violations.html b/AKScheduling/templates/admin/AKScheduling/constraint_violations.html
new file mode 100644
index 0000000000000000000000000000000000000000..8645c192829c9bc58f91b45e78d1be2819b2892e
--- /dev/null
+++ b/AKScheduling/templates/admin/AKScheduling/constraint_violations.html
@@ -0,0 +1,137 @@
+{% extends "admin/base_site.html" %}
+{% load tags_AKModel %}
+
+{% load i18n %}
+{% load l10n %}
+{% load tz %}
+{% load static %}
+{% load tags_AKPlan %}
+{% load fontawesome_5 %}
+
+{% block title %}{% trans "Scheduling for" %} {{event}}{% endblock %}
+
+{% block extrahead %}
+    {{ block.super }}
+
+    <script>
+        document.addEventListener('DOMContentLoaded', function () {
+            // CSRF Protection/Authentication
+            function getCookie(name) {
+                let cookieValue = null;
+                if (document.cookie && document.cookie !== '') {
+                    const cookies = document.cookie.split(';');
+                    for (let i = 0; i < cookies.length; i++) {
+                        const cookie = cookies[i].trim();
+                        // Does this cookie string begin with the name we want?
+                        if (cookie.substring(0, name.length + 1) === (name + '=')) {
+                            cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+                            break;
+                        }
+                    }
+                }
+                return cookieValue;
+            }
+
+            const csrftoken = getCookie('csrftoken');
+
+            function csrfSafeMethod(method) {
+                // these HTTP methods do not require CSRF protection
+                return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
+            }
+
+            $.ajaxSetup({
+                beforeSend: function (xhr, settings) {
+                    if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
+                        xhr.setRequestHeader("X-CSRFToken", csrftoken);
+                    }
+                }
+            });
+
+            // (Re-)Load constraint violations using AJAX and visualize using violation count badge and violation table
+            function reload() {
+                $.ajax({
+                       url: "{% url "model:scheduling-constraint-violations-list" event_slug=event.slug %}",
+                        type: 'GET',
+                        success: function (response) {
+                           console.log(response);
+
+                           let table_html = '';
+
+                           if(response.length > 0) {
+                               // Update violation count badge
+                               $('#violationCountBadge').html(response.length).removeClass('badge-success').addClass('badge-warning');
+
+                               // Update violations table
+                               for(let i=0;i<response.length;i++) {
+                                   table_html += "<tr><td>" + response[i].level_display + "</td><td>" + response[i].type_display + "</td><td>" + response[i].details + "</td><td>" + response[i].timestamp_display + "</td><td></td></tr>";
+                               }
+                           }
+                           else {
+                               // Update violation count badge
+                               $('#violationCountBadge').html(0).removeClass('badge-warning').addClass('badge-success');
+
+                               // Update violations table
+                               table_html ='<tr class="text-muted"><td colspan="5" class="text-center">{% trans "No violations" %}</td></tr>'
+                           }
+
+                           // Show violation list (potentially empty) in violations table
+                           $('#violationsTableBody').html(table_html);
+                        },
+                        error: function (response) {
+                           alert("{% trans 'Cannot load current violations from server' %}");
+                        }
+                   });
+            }
+            reload();
+
+            // Bind reload button
+            $('#btnReloadNow').click(reload);
+
+            // Toggle automatic reloading (every 30 s) based on checkbox
+            let autoReloadInterval = undefined;
+            $('#cbxAutoReload').change(function () {
+                if(this.checked) {
+                    autoReloadInterval = setInterval(reload, 30*1000);
+                }
+                else {
+                    if(autoReloadInterval !== undefined)
+                        clearInterval(autoReloadInterval);
+                }
+            });
+        });
+    </script>
+{% endblock extrahead %}
+
+{% block content %}
+    <h4 class="mt-4 mb-4"><span id="violationCountBadge" class="badge badge-success">0</span> {% trans "Violation(s)" %}</h4>
+
+    <input type="checkbox" id="cbxAutoReload">
+    <label for="cbxAutoReload">{% trans "Auto reload?" %}</label>
+
+    <br>
+
+    <a href="#" id="btnReloadNow" class="btn btn-info">{% fa5_icon "sync-alt" "fas" %} {% trans "Reload now" %}</a>
+
+    <table class="table table-striped mt-4 mb-4">
+        <thead>
+            <tr>
+                <th>{% trans "Violation" %}</th>
+                <th>{% trans "Problem" %}</th>
+                <th>{% trans "Details" %}</th>
+                <th>{% trans "Since" %}</th>
+                <th></th>
+            </tr>
+        </thead>
+        <tbody id="violationsTableBody">
+        <tr class="text-muted">
+            <td colspan="5" class="text-center">
+                {% trans "No violations" %}
+            </td>
+        </tr>
+        </tbody>
+    </table>
+
+    <a href="{% url 'admin:event_status' event.slug %}">{% trans "Event Status" %}</a>
+    &middot;
+    <a href="{% url 'admin:schedule' event.slug %}">{% trans "Scheduling" %}</a>
+{% endblock %}
diff --git a/AKScheduling/urls.py b/AKScheduling/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..93f490950d1b9299a90eb74e3ce0e0b76b2b518f
--- /dev/null
+++ b/AKScheduling/urls.py
@@ -0,0 +1,17 @@
+from django.urls import path
+
+from AKScheduling.views import SchedulingAdminView, UnscheduledSlotsAdminView, TrackAdminView, \
+    ConstraintViolationsAdminView
+
+
+def get_admin_urls_scheduling(admin_site):
+    return [
+        path('<slug:event_slug>/schedule/', admin_site.admin_view(SchedulingAdminView.as_view()),
+             name="schedule"),
+        path('<slug:event_slug>/unscheduled/', admin_site.admin_view(UnscheduledSlotsAdminView.as_view()),
+             name="slots_unscheduled"),
+        path('<slug:slug>/constraint-violations/', admin_site.admin_view(ConstraintViolationsAdminView.as_view()),
+             name="constraint-violations"),
+        path('<slug:event_slug>/tracks/', admin_site.admin_view(TrackAdminView.as_view()),
+             name="tracks_manage"),
+    ]
diff --git a/AKScheduling/views.py b/AKScheduling/views.py
index 86445faccabfb643683794a5e478c1ffaf813fb5..57ec6a211e83a6a6d812c2c8167d45f73350eaf1 100644
--- a/AKScheduling/views.py
+++ b/AKScheduling/views.py
@@ -1,7 +1,7 @@
-from django.views.generic import ListView
 from django.utils.translation import gettext_lazy as _
+from django.views.generic import ListView, DetailView
 
-from AKModel.models import AKSlot, AKTrack
+from AKModel.models import AKSlot, AKTrack, Event
 from AKModel.views import AdminViewMixin, FilterByEventSlugMixin
 
 
@@ -47,3 +47,14 @@ class TrackAdminView(AdminViewMixin, FilterByEventSlugMixin, ListView):
         context = super().get_context_data(object_list=object_list, **kwargs)
         context["aks_without_track"] = self.event.ak_set.filter(track=None)
         return context
+
+
+class ConstraintViolationsAdminView(AdminViewMixin, DetailView):
+    template_name = "admin/AKScheduling/constraint_violations.html"
+    model = Event
+    context_object_name = "event"
+
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context["title"] = f"{_('Constraint violations for')} {context['event']}"
+        return context
diff --git a/INSTALL.md b/INSTALL.md
index 29c9070256f067c320439c4d94f733a9aa83a943..407945e82631fc8b733940804cd5ac0c5075561f 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -2,42 +2,39 @@
 
 This repository contains a Django project with several apps.
 
-
 ## Requirements
 
-AKPlanning has two types of requirements: System requirements are dependent on operating system and need to be installed manually beforehand. Python requirements will be installed inside a virtual environment (strongly recommended) during setup.
-
+AKPlanning has two types of requirements: System requirements are dependent on operating system and need to be installed
+manually beforehand. Python requirements will be installed inside a virtual environment (strongly recommended) during
+setup.
 
 ### System Requirements
 
 * Python 3.7 incl. development tools
 * Virtualenv
-* pdflatex & beamer class (`texlive-latex-base texlive-latex-recommended texlive-latex-extra texlive-fonts-extra texlive-luatex`)
+* pdflatex & beamer
+  class (`texlive-latex-base texlive-latex-recommended texlive-latex-extra texlive-fonts-extra texlive-luatex`)
 * for production using uwsgi:
-  * C compiler e.g. gcc
-  * uwsgi
-  * uwsgi Python3 plugin
+    * C compiler e.g. gcc
+    * uwsgi
+    * uwsgi Python3 plugin
 * for production using Apache (in addition to uwsgi)
-  * the mod proxy uwsgi plugin for apache2
-
+    * the mod proxy uwsgi plugin for apache2
 
 ### Python Requirements
 
 Python requirements are listed in ``requirements.txt``. They can be installed with pip using ``-r requirements.txt``.
 
-
 ## Development Setup
 
 * create a new directory that should contain the files in future, e.g. ``mkdir AKPlanning``
 * change into that directory ``cd AKPlanning``
 * clone this repository ``git clone URL .``
 
-
 ### Automatic Setup
 
 1. execute the setup bash script ``Utils/setup.sh``
 
-
 ### Manual Setup
 
 1. setup a virtual environment using the proper python version ``virtualenv venv -p python3.7``
@@ -49,7 +46,6 @@ Python requirements are listed in ``requirements.txt``. They can be installed wi
 1. create a priviledged user, credentials are entered interactively on CLI ``python manage.py createsuperuser``
 1. deactivate virtualenv ``deactivate``
 
-
 ### Development Server
 
 **Do not use this for deployment!**
@@ -60,11 +56,10 @@ To start the application for development, in the root directory,
 1. start development server ``python manage.py runserver 0:8000``
 1. In your browser, access ``http://127.0.0.1:8000/admin/`` and continue from there.
 
-
 ## Deployment Setup
 
-This application can be deployed using a web server as any other Django application.
-Remember to use a secret key that is not stored in any repository or similar, and disable DEBUG mode (``settings.py``).
+This application can be deployed using a web server as any other Django application. Remember to use a secret key that
+is not stored in any repository or similar, and disable DEBUG mode (``settings.py``).
 
 **Step-by-Step Instructions**
 
@@ -77,9 +72,12 @@ Remember to use a secret key that is not stored in any repository or similar, an
 1. activate virtualenv ``source venv/bin/activate``
 1. update tools ``pip install --upgrade setuptools pip wheel``
 1. install python requirements ``pip install -r requirements.txt``
-1. create the file ``AKPlanning/settings_secrets.py`` (copy from ``settings_secrets.py.sample``) and fill it with the necessary secrets (e.g. generated by ``tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50``) (it is a good idea to restrict read permissions from others)
+1. create the file ``AKPlanning/settings_secrets.py`` (copy from ``settings_secrets.py.sample``) and fill it with the
+   necessary secrets (e.g. generated by ``tr -dc 'a-z0-9!@#$%^&*(-_=+)' < /dev/urandom | head -c50``) (it is a good idea
+   to restrict read permissions from others)
 1. if necessary enable uwsgi proxy plugin for Apache e.g.``a2enmod proxy_uwsgi``
-1. edit the apache config to serve the application and the static files, e.g. on a dedicated system in ``/etc/apache2/sites-enabled/000-default.conf`` within the ``VirtualHost`` tag add:
+1. edit the apache config to serve the application and the static files, e.g. on a dedicated system
+   in ``/etc/apache2/sites-enabled/000-default.conf`` within the ``VirtualHost`` tag add:
 
     ```
     Alias /static /srv/AKPlanning/static
@@ -91,19 +89,25 @@ Remember to use a secret key that is not stored in any repository or similar, an
     ProxyPass / uwsgi://127.0.0.1:3035/
     ```
 
-    or create a new config (.conf) file (similar to ``apache-akplanning.conf``) replacing $SUBDOMAIN with the subdomain the system should be available under, and $MAILADDRESS with the e-mail address of your administrator and $PATHTO with the appropriate paths. Copy or symlink it to ``/etc/apache2/sites-available``. Then symlink it to ``sites-enabled`` e.g. by using ``ln -s /etc/apache2/sites-available/akplanning.conf /etc/apache2/sites-enabled/akplanning.conf``.
+   or create a new config (.conf) file (similar to ``apache-akplanning.conf``) replacing $SUBDOMAIN with the subdomain
+   the system should be available under, and $MAILADDRESS with the e-mail address of your administrator and $PATHTO with
+   the appropriate paths. Copy or symlink it to ``/etc/apache2/sites-available``. Then symlink it to ``sites-enabled``
+   e.g. by using ``ln -s /etc/apache2/sites-available/akplanning.conf /etc/apache2/sites-enabled/akplanning.conf``.
 1. restart Apache ``sudo systemctl restart apache2.service``
 1. create a dedicated user, e.g. ``adduser django``
 1. transfer ownership of the folder to the new user ``chown -R django:django /srv/AKPlanning``
-1. Copy or symlink the uwsgi config in ``uwsgi-akplanning.ini`` to ``/etc/uwsgi/apps-available/`` and then symlink it to ``/etc/uwsgi/apps-enabled/`` using e.g., ``ln -s /srv/AKPlanning/uwsgi-akplanning.ini /etc/uwsgi/apps-available/akplanning.ini`` and ``ln -s /etc/uwsgi/apps-available/akplanning.ini /etc/uwsgi/apps-enabled/akplanning.ini``
-start uwsgi using the configuration file ``uwsgi --ini uwsgi-akplanning.ini``
+1. Copy or symlink the uwsgi config in ``uwsgi-akplanning.ini`` to ``/etc/uwsgi/apps-available/`` and then symlink it
+   to ``/etc/uwsgi/apps-enabled/`` using
+   e.g., ``ln -s /srv/AKPlanning/uwsgi-akplanning.ini /etc/uwsgi/apps-available/akplanning.ini``
+   and ``ln -s /etc/uwsgi/apps-available/akplanning.ini /etc/uwsgi/apps-enabled/akplanning.ini``
+   start uwsgi using the configuration file ``uwsgi --ini uwsgi-akplanning.ini``
 1. restart uwsgi ``sudo systemctl restart uwsgi``
 1. execute the update script ``./Utils/update.sh --prod``
 
-
 ## Updates
 
-To update the setup to the current version on the main branch of the repository use the update script ``Utils/update.sh`` or ``Utils/update.sh --prod`` in production.
+To update the setup to the current version on the main branch of the repository use the update
+script ``Utils/update.sh`` or ``Utils/update.sh --prod`` in production.
 
 Afterwards, you may check your setup by executing ``Utils/check.sh`` or ``Utils/check.sh --prod`` in production.