from datetime import date, datetime from typing import Any, Iterable, Optional from django.conf import settings from django.contrib.auth import get_user_model from django.core.mail import send_mail from django.urls import reverse from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from .models import ( AuditLog, Notification, NotificationRule, NotificationKind, Risk, ResidualRisk, ) User = get_user_model() # --------------------------------------------------------------------------- # notify_event() # --------------------------------------------------------------------------- def get_entity_url(obj): if obj is None: return None model_name = obj.__class__.__name__.lower() mapping = { "risk": "risks:show_risk", "control": "risks:show_control", "residualrisk": "risks:show_risk", "incident": "risks:show_incident", "user": "admin:user_detail", } view_name = mapping.get(model_name) if view_name: return reverse(view_name, args=[obj.pk]) return None def notify_event(kind: str, *, message: str, users: Optional[Iterable[Any]] = None, obj=None): """ Generates in-app notifications and/or emails depending on the NotificationRule. - users: Basic recipients (owner/responsible/reporter) – can be None. - staff/extra recipients are added from the rule. """ rule = NotificationRule.objects.filter(kind=kind).first() # Defaults (no rule → in-app only) enabled_in_app = True enabled_email = False recipients_users = set() extra_emails = [] # Base recipients if users: recipients_users.update(u for u in users if u and getattr(u, "is_active", False)) # Rule overrides if rule: enabled_in_app = rule.enabled_in_app enabled_email = rule.enabled_email if rule.to_staff: recipients_users.update(User.objects.filter(is_staff=True, is_active=True)) extra_emails = _split_emails(rule.extra_recipients) url = get_entity_url(obj) # In-App Notifications if enabled_in_app: for u in recipients_users: Notification.objects.create(user=u, message=message, target_url=url) # Email Notifications if enabled_email: emails = [u.email for u in recipients_users if u and u.email] + extra_emails emails = list(dict.fromkeys(emails)) if emails: body = f"{message}\n\n{url}" if url else message send_mail( _("Notification"), body, getattr(settings, "DEFAULT_FROM_EMAIL", "webmaster@localhost"), emails, fail_silently=True, ) # --------------------------------------------------------------------------- # model_diff() # --------------------------------------------------------------------------- def model_diff(old, new, fields=None): """ Compare two model instances and return a dict of changed fields. - old: previous model instance (from DB) - new: updated model instance (unsaved) - fields: optional list of fields to check """ changes = {} opts = new._meta if fields is None: fields = [f.name for f in opts.fields] for field_name in fields: old_value = getattr(old, field_name, None) new_value = getattr(new, field_name, None) if old_value != new_value: changes[field_name] = {"old": old_value, "new": new_value} return changes # --------------------------------------------------------------------------- # check_risk_followups() # --------------------------------------------------------------------------- def check_risk_followups(): """ Check if follow-ups need attention and create notifications. Ensures no duplicate notifications per risk per day. """ today = now().date() risks = Risk.objects.filter(follow_up__lte=today).select_related("owner") for risk in risks: # Status aktualisieren (außer wenn bereits closed/review_required) if risk.status not in ("closed", "review_required"): Risk.objects.filter(pk=risk.pk).update(status="review_required") # ResidualRisk sicherstellen + Review-Flag setzen resid, _ = ResidualRisk.objects.get_or_create(risk=risk) if not resid.review_required: resid.review_required = True resid.save() # Notification (einmalig pro Risk/Tag) message = _("Follow-up reached: review required for risk '{t}'").format(t=risk.title) notification, created = Notification.objects.get_or_create( user=risk.owner, message=message, defaults={"read": False, "sent": False}, ) if created: AuditLog.objects.create( user=None, action="create", model="Notification", object_id=notification.pk, changes={ "message": notification.message, "user": risk.owner.username if risk.owner else None, }, ) notify_event( NotificationKind.RISK_REVIEW_REQUIRED, message=message, users=[risk.owner] if risk.owner_id else None, obj=risk, ) # --------------------------------------------------------------------------- # _split_emails() # --------------------------------------------------------------------------- def _split_emails(value: str) -> list[str]: """Normalize a comma/newline-separated list of emails into a clean list.""" if not value: return [] raw = value.replace("\n", ",").split(",") return [e.strip() for e in raw if "@" in e and e.strip()]