from datetime import date, datetime from django.contrib.auth import get_user_model from django.db.models import Model from django.db.models.signals import post_save, post_delete, m2m_changed from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from .audit_context import get_current_user from .models import Control, Risk, ResidualRisk, AuditLog, Incident, Notification, NotificationKind, NotificationPreference from .utils import model_diff, notify_event # --------------------------------------------------------------------------- # General definitions # --------------------------------------------------------------------------- User = get_user_model() def serialize_value(value): if isinstance(value, Model): return value.pk # oder str(value), wenn du mehr Infos willst if isinstance(value, (datetime, date)): return value.isoformat() return value def _pref(user: User) -> NotificationPreference | None: if not user: return None pref = getattr(user, "notification_preference", None) if not pref: pref = NotificationPreference.objects.create(user=user) return pref def _notify(users, message: str, event_code: str): """legt Notification für alle users an, die dieses Event wünschen.""" for u in set(filter(None, users)): pref = _pref(u) if pref and pref.should_notify(event_code): Notification.objects.create(user=u, message=message) def _risk_stakeholders(risk: Risk): """Risikoeigner + alle Verantwortlichen zugehöriger Controls.""" owners = [risk.owner] if risk.owner else [] responsibles = list( User.objects.filter(responsible_controls__risks=risk).distinct() ) return set(owners + responsibles) # --------------------------------------------------------------------------- # Incidents # --------------------------------------------------------------------------- @receiver(post_save, sender=User) def user_saved(sender, instance: User, created, **kwargs): # Prefs automatisch anlegen _pref(instance) # An Staff, die dieses Event wollen if created: staff = User.objects.filter(is_staff=True, notification_preference__user_created=True) _notify(staff, _("User '{u}' created").format(u=instance.username), "user_created") @receiver(post_delete, sender=User) def user_deleted(sender, instance: User, **kwargs): staff = User.objects.filter(is_staff=True, notification_preference__user_deleted=True) _notify(staff, _("User '{u}' deleted").format(u=instance.username), "user_deleted") # --------------------------------------------------------------------------- # Risks # --------------------------------------------------------------------------- @receiver(post_save, sender=Risk) def risk_saved(sender, instance: Risk, created, **kwargs): event = "risk_created" if created else "risk_updated" msg = _("Risk '{title}' {state}").format( title=instance.title, state=_("created") if created else _("updated"), ) _notify([instance.owner], msg, event) @receiver(post_delete, sender=Risk) def risk_deleted(sender, instance: Risk, **kwargs): msg = _("Risk '{title}' deleted").format(title=instance.title) # Owner existiert evtl. nicht mehr -> kein Notify nötig if instance.owner: _notify([instance.owner], msg, "risk_deleted") @receiver(post_save, sender=Risk) def log_risk_save(sender, instance, created, **kwargs): if created: AuditLog.objects.create( user=getattr(instance, "_changed_by", None), action="create", model="Risk", object_id=instance.pk, changes={ f.name: { "old": None, "new": serialize_value(getattr(instance, f.name)) } for f in instance._meta.fields }, ) else: old = Risk.objects.get(pk=instance.pk) changes = model_diff(old, instance) if changes: clean_changes = { field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])} for field, vals in changes.items() } AuditLog.objects.create( user=getattr(instance, "_changed_by", None), action="update", model="Risk", object_id=instance.pk, changes=clean_changes, ) if created: notify_event( NotificationKind.RISK_CREATED, message=_("Risk created: {t}").format(t=instance.title), users=[instance.owner] if instance.owner_id else None, ) else: notify_event( NotificationKind.RISK_UPDATED, message=_("Risk updated: {t}").format(t=instance.title), users=[instance.owner] if instance.owner_id else None, ) @receiver(post_delete, sender=Risk) def log_risk_delete(sender, instance, **kwargs): """ Signal that runs after a Risk is deleted. """ user = getattr(instance, "_changed_by", None) or get_current_user() AuditLog.objects.create( user=user, action="delete", model="Risk", object_id=instance.pk, changes=None, # no fields to track on deletion ) notify_event( NotificationKind.RISK_DELETED, message=_("Risk deleted: {t}").format(t=instance.title), users=[instance.owner] if instance.owner_id else None, ) # --------------------------------------------------------------------------- # Controls # --------------------------------------------------------------------------- @receiver(post_save, sender=Control) def control_saved(sender, instance: Control, created, **kwargs): # Review-Flag für alle betroffenen Residuals setzen for risk in instance.risks.all(): resid, created = ResidualRisk.objects.get_or_create(risk=risk) # Statuswechsel auf Review Required if not resid.review_required: resid.review_required = True resid.save() if risk.status != "review_required": Risk.objects.filter(pk=risk.pk).update(status="review_required") # Notifications event = "control_created" if created else "control_updated" msg = _("Control '{title}' {state}").format( title=instance.title, state=_("created") if created else _("updated"), ) stakeholders = {instance.responsible} | set(r.owner for r in instance.risks.all() if r.owner) _notify(stakeholders, msg, event) @receiver(post_delete, sender=Control) def control_deleted(sender, instance: Control, **kwargs): msg = _("Control '{title}' deleted").format(title=instance.title) stakeholders = {instance.responsible} | set(r.owner for r in instance.risks.all() if r.owner) _notify(stakeholders, msg, "control_deleted") @receiver(post_save, sender=Control) def log_control_save(sender, instance, created, **kwargs): if created: AuditLog.objects.create( user=getattr(instance, "_changed_by", None), action="create", model="Control", object_id=instance.pk, changes={ f.name: { "old": None, "new": serialize_value(getattr(instance, f.name)) } for f in instance._meta.fields }, ) else: old = Control.objects.get(pk=instance.pk) changes = model_diff(old, instance) if changes: clean_changes = { field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])} for field, vals in changes.items() } AuditLog.objects.create( user=getattr(instance, "_changed_by", None), action="update", model="Control", object_id=instance.pk, changes=clean_changes, ) kind = NotificationKind.CONTROL_CREATED if created else NotificationKind.CONTROL_UPDATED notify_event( kind, message=_("Control {event}: {t}").format( event=_("created") if created else _("updated"), t=instance.title, ), users=[instance.responsible] if instance.responsible_id else None, ) @receiver(post_delete, sender=Control) def log_control_delete(sender, instance, **kwargs): user = getattr(instance, "_changed_by", None) or get_current_user() AuditLog.objects.create( user=user, action="delete", model="Control", object_id=instance.pk, changes=None, ) notify_event( NotificationKind.CONTROL_DELETED, message=_("Control deleted: {t}").format(t=instance.title), users=[instance.responsible] if instance.responsible_id else None, ) @receiver(m2m_changed, sender=Control.risks.through) def control_risks_changed(sender, instance: Control, action, reverse, pk_set, **kwargs): if action in {"post_add", "post_remove", "post_clear"}: affected = instance.risks.all() if not pk_set else Risk.objects.filter(pk__in=pk_set) for risk in affected: resid, created = ResidualRisk.objects.get_or_create(risk=risk) if not resid.review_required: resid.review_required = True resid.save() if risk.status != "review_required": Risk.objects.filter(pk=risk.pk).update(status="review_required") _notify(_risk_stakeholders(risk), _("Review required for risk '{t}' due to control change").format(t=risk.title), "review_required") # --------------------------------------------------------------------------- # Residual risks # --------------------------------------------------------------------------- @receiver(post_save, sender=ResidualRisk) def residual_saved(sender, instance: ResidualRisk, created, **kwargs): # AuditLog erstellst du bereits anderswo – hier Fokus auf Status/Notify risk = instance.risk old = None if not created: try: old = ResidualRisk.objects.get(pk=instance.pk) except ResidualRisk.DoesNotExist: pass # Review-Logik: wenn review_required=True -> Risk.status = review_required if instance.review_required and risk.status != "review_required": Risk.objects.filter(pk=risk.pk).update(status="review_required") _notify(_risk_stakeholders(risk), _("Review required for risk '{t}'").format(t=risk.title), "review_required") elif old and old.review_required and not instance.review_required: # Review abgeschlossen if risk.status == "review_required": Risk.objects.filter(pk=risk.pk).update(status="open") _notify(_risk_stakeholders(risk), _("Review completed for risk '{t}'").format(t=risk.title), "review_completed") # Standard-Events event = "residual_created" if created else "residual_updated" _notify(_risk_stakeholders(risk), _("Residual risk {state} for '{t}'").format( state=_("created") if created else _("updated"), t=risk.title), event) @receiver(post_delete, sender=ResidualRisk) def residual_deleted(sender, instance: ResidualRisk, **kwargs): _notify(_risk_stakeholders(instance.risk), _("Residual risk deleted for '{t}'").format(t=instance.risk.title), "residual_deleted") @receiver(post_save, sender=ResidualRisk) def log_residual_save(sender, instance, created, **kwargs): if created: AuditLog.objects.create( user=getattr(instance, "_changed_by", None), action="create", model="ResidualRisk", object_id=instance.pk, changes={ f.name: { "old": None, "new": serialize_value(getattr(instance, f.name)) } for f in instance._meta.fields }, ) else: old = ResidualRisk.objects.get(pk=instance.pk) changes = model_diff(old, instance) if changes: clean_changes = { field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])} for field, vals in changes.items() } AuditLog.objects.create( user=getattr(instance, "_changed_by", None), action="update", model="ResidualRisk", object_id=instance.pk, changes=clean_changes, ) if created: notify_event( NotificationKind.RESIDUAL_CREATED, message=_("Residual created for risk: {t}").format(t=instance.risk.title), users=[instance.risk.owner] if instance.risk.owner_id else None, ) else: # Änderungen prüfen old = ResidualRisk.objects.get(pk=instance.pk) changes = model_diff(old, instance) # Review-Flag Wechsel gezielt melden: if "review_required" in changes: if getattr(instance, "review_required", False): notify_event( NotificationKind.RESIDUAL_REVIEW_REQUIRED, message=_("Residual review required for risk: {t}").format(t=instance.risk.title), users=[instance.risk.owner] if instance.risk.owner_id else None, ) else: notify_event( NotificationKind.RESIDUAL_REVIEW_COMPLETED, message=_("Residual review completed for risk: {t}").format(t=instance.risk.title), users=[instance.risk.owner] if instance.risk.owner_id else None, ) else: notify_event( NotificationKind.RESIDUAL_UPDATED, message=_("Residual updated for risk: {t}").format(t=instance.risk.title), users=[instance.risk.owner] if instance.risk.owner_id else None, ) @receiver(post_delete, sender=ResidualRisk) def log_residual_delete(sender, instance, **kwargs): user = getattr(instance, "_changed_by", None) or get_current_user() AuditLog.objects.create( user=user, action="delete", model="ResidualRisk", object_id=instance.pk, changes=None, ) notify_event( NotificationKind.RESIDUAL_DELETED, message=_("Residual deleted for risk: {t}").format(t=instance.risk.title), users=[instance.risk.owner] if instance.risk.owner_id else None, ) # --------------------------------------------------------------------------- # Incidents # --------------------------------------------------------------------------- @receiver(post_save, sender=Incident) def incident_saved(sender, instance: Incident, created, **kwargs): event = "incident_created" if created else "incident_updated" stakeholders = set([instance.reported_by]) | set(r.owner for r in instance.related_risks.all() if r.owner) _notify(stakeholders, _("Incident '{t}' {s}").format(t=instance.title, s=_("created") if created else _("updated")), event) @receiver(post_delete, sender=Incident) def incident_deleted(sender, instance: Incident, **kwargs): stakeholders = set([instance.reported_by]) | set(r.owner for r in instance.related_risks.all() if r.owner) _notify(stakeholders, _("Incident '{t}' deleted").format(t=instance.title), "incident_deleted") @receiver(post_save, sender=Incident) def log_incident_save(sender, instance, created, **kwargs): if created: AuditLog.objects.create( user=getattr(instance, "_changed_by", None), action="create", model="Incident", object_id=instance.pk, changes={ f.name: { "old": None, "new": serialize_value(getattr(instance, f.name)) } for f in instance._meta.fields }, ) else: old = Incident.objects.get(pk=instance.pk) changes = model_diff(old, instance) if changes: clean_changes = { field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])} for field, vals in changes.items() } AuditLog.objects.create( user=getattr(instance, "_changed_by", None), action="update", model="Incident", object_id=instance.pk, changes=clean_changes, ) kind = NotificationKind.INCIDENT_CREATED if created else NotificationKind.INCIDENT_UPDATED notify_event( kind, message=_("Incident {event}: {t}").format( event=_("created") if created else _("updated"), t=instance.title, ), users=[instance.reported_by] if instance.reported_by_id else None, ) @receiver(m2m_changed, sender=Incident.related_risks.through) def log_incident_risks_change(sender, instance, action, reverse, model, pk_set, **kwargs): if action in ["post_add", "post_remove", "post_clear"]: user = getattr(instance, "_changed_by", None) or get_current_user() AuditLog.objects.create( user=user, action="update", model="Incident", object_id=instance.pk, changes={"related_risks": {"action": action, "ids": list(pk_set)}}, ) @receiver(post_delete, sender=Incident) def log_incident_delete(sender, instance, **kwargs): user = getattr(instance, "_changed_by", None) or get_current_user() AuditLog.objects.create( user=user, action="delete", model="Incident", object_id=instance.pk, changes=None, ) notify_event( NotificationKind.INCIDENT_DELETED, message=_("Incident deleted: {t}").format(t=instance.title), users=[instance.reported_by] if instance.reported_by_id else None, )