diff --git a/AKModel/models.py b/AKModel/models.py
index a49e87f431c7b9395ee2c78ff3027bf786d1a156..e1435c3ef84d57a8385f798a2125a841ed0258d3 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -325,7 +325,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'))
@@ -388,6 +389,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'),
@@ -459,7 +463,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):
         """
@@ -467,10 +476,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)
@@ -496,5 +505,86 @@ class ConstraintViolation(models.Model):
     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/AKScheduling/models.py b/AKScheduling/models.py
index 6b2021999398416a78191ac543b7e0e34d86bc2c..3104d90e35db2d0653dabf83e6b08dc0448cfe77 100644
--- a/AKScheduling/models.py
+++ b/AKScheduling/models.py
@@ -1 +1,238 @@
-# 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:
+        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: Owner(s), Requirements, Conflicts, Prerequisites, Category, Interest
+    pass
+
+
+@receiver(m2m_changed, sender=AK.owners.through)
+def ak_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(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...
+    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.room.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)
+
+
+@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, 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)