2025-09-10 14:26:29 +02:00
|
|
|
|
from django.conf import settings
|
Add risk status and notification preferences
- Introduced a new `status` field to the `Risk` model with choices for "open", "in_progress", "closed", and "review_required".
- Created a `NotificationPreference` model to manage user notification settings for various events related to risks, controls, residual risks, reviews, users, and incidents.
- Updated the admin interface to include `NotificationPreference` inline with the `User` admin.
- Enhanced signal handlers to send notifications based on user preferences for created, updated, and deleted events for users, risks, controls, and incidents.
- Modified the `check_risk_followups` utility function to update risk status and create notifications for follow-ups.
- Updated serializers and views to accommodate the new `status` field and improved risk listing functionality.
- Added a new section in the risk detail template to display related incidents.
- Removed the unused statistics view from URLs.
2025-09-10 11:54:08 +02:00
|
|
|
|
from django.contrib.auth import get_user_model
|
2025-09-10 14:26:29 +02:00
|
|
|
|
from django.core.mail import send_mail
|
2025-09-07 20:52:19 +02:00
|
|
|
|
from django.utils.timezone import now
|
Add risk status and notification preferences
- Introduced a new `status` field to the `Risk` model with choices for "open", "in_progress", "closed", and "review_required".
- Created a `NotificationPreference` model to manage user notification settings for various events related to risks, controls, residual risks, reviews, users, and incidents.
- Updated the admin interface to include `NotificationPreference` inline with the `User` admin.
- Enhanced signal handlers to send notifications based on user preferences for created, updated, and deleted events for users, risks, controls, and incidents.
- Modified the `check_risk_followups` utility function to update risk status and create notifications for follow-ups.
- Updated serializers and views to accommodate the new `status` field and improved risk listing functionality.
- Added a new section in the risk detail template to display related incidents.
- Removed the unused statistics view from URLs.
2025-09-10 11:54:08 +02:00
|
|
|
|
from django.utils.translation import gettext_lazy as _
|
2025-09-10 14:26:29 +02:00
|
|
|
|
from .models import AuditLog, Notification,NotificationRule, NotificationKind, Risk, ResidualRisk
|
|
|
|
|
from typing import Iterable, Optional
|
|
|
|
|
|
|
|
|
|
User = get_user_model()
|
2025-09-07 20:52:19 +02:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
def check_risk_followups():
|
|
|
|
|
"""
|
|
|
|
|
Check if follow ups need attention and create notifications.
|
|
|
|
|
Ensures no duplicate notifications per risk per day
|
|
|
|
|
"""
|
|
|
|
|
today = now().date()
|
Add risk status and notification preferences
- Introduced a new `status` field to the `Risk` model with choices for "open", "in_progress", "closed", and "review_required".
- Created a `NotificationPreference` model to manage user notification settings for various events related to risks, controls, residual risks, reviews, users, and incidents.
- Updated the admin interface to include `NotificationPreference` inline with the `User` admin.
- Enhanced signal handlers to send notifications based on user preferences for created, updated, and deleted events for users, risks, controls, and incidents.
- Modified the `check_risk_followups` utility function to update risk status and create notifications for follow-ups.
- Updated serializers and views to accommodate the new `status` field and improved risk listing functionality.
- Added a new section in the risk detail template to display related incidents.
- Removed the unused statistics view from URLs.
2025-09-10 11:54:08 +02:00
|
|
|
|
risks = Risk.objects.filter(follow_up__lte=today).select_related("owner")
|
2025-09-07 20:52:19 +02:00
|
|
|
|
|
|
|
|
|
for risk in risks:
|
Add risk status and notification preferences
- Introduced a new `status` field to the `Risk` model with choices for "open", "in_progress", "closed", and "review_required".
- Created a `NotificationPreference` model to manage user notification settings for various events related to risks, controls, residual risks, reviews, users, and incidents.
- Updated the admin interface to include `NotificationPreference` inline with the `User` admin.
- Enhanced signal handlers to send notifications based on user preferences for created, updated, and deleted events for users, risks, controls, and incidents.
- Modified the `check_risk_followups` utility function to update risk status and create notifications for follow-ups.
- Updated serializers and views to accommodate the new `status` field and improved risk listing functionality.
- Added a new section in the risk detail template to display related incidents.
- Removed the unused statistics view from URLs.
2025-09-10 11:54:08 +02:00
|
|
|
|
# Risk-Status auf review_required setzen (nicht überschreiben, wenn bereits closed)
|
|
|
|
|
if risk.status != "closed" and risk.status != "review_required":
|
|
|
|
|
Risk.objects.filter(pk=risk.pk).update(status="review_required")
|
|
|
|
|
|
|
|
|
|
# ResidualRisk-Objekt sicherstellen und Review-Flag setzen
|
|
|
|
|
resid, created = ResidualRisk.objects.get_or_create(risk=risk)
|
|
|
|
|
if not resid.review_required:
|
|
|
|
|
resid.review_required = True
|
|
|
|
|
resid.save()
|
|
|
|
|
|
|
|
|
|
# Notification an Stakeholder
|
|
|
|
|
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},
|
2025-09-10 14:26:29 +02:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
notify_event(
|
|
|
|
|
NotificationKind.RISK_REVIEW_REQUIRED,
|
|
|
|
|
message=_("Follow-up reached: review required for risk '{t}'").format(t=risk.title),
|
|
|
|
|
users=[risk.owner] if risk.owner_id else None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _split_emails(value: str) -> list[str]:
|
|
|
|
|
if not value:
|
|
|
|
|
return []
|
|
|
|
|
raw = value.replace("\n", ",").split(",")
|
|
|
|
|
return [e.strip() for e in raw if "@" in e and e.strip()]
|
|
|
|
|
|
|
|
|
|
def notify_event(kind: str, *, message: str, users: Optional[Iterable[User]] = None):
|
|
|
|
|
"""
|
|
|
|
|
Generates in-app notifications and/or emails depending on the rule.
|
|
|
|
|
- users: Basic recipients (owner/responsible/reporter) – can be None.
|
|
|
|
|
- staff/extra recipients are added from the rule.
|
|
|
|
|
"""
|
|
|
|
|
rule = NotificationRule.objects.filter(kind=kind).first()
|
|
|
|
|
|
|
|
|
|
# Fallback: without rule → only in-app
|
|
|
|
|
enabled_in_app = True
|
|
|
|
|
enabled_email = False
|
|
|
|
|
to_staff = False
|
|
|
|
|
extra_emails = []
|
|
|
|
|
|
|
|
|
|
recipients_users = set()
|
|
|
|
|
|
|
|
|
|
if users:
|
|
|
|
|
for u in users:
|
|
|
|
|
if u and getattr(u, "is_active", False):
|
|
|
|
|
recipients_users.add(u)
|
|
|
|
|
|
|
|
|
|
if rule:
|
|
|
|
|
enabled_in_app = rule.enabled_in_app
|
|
|
|
|
enabled_email = rule.enabled_email
|
|
|
|
|
if rule.to_staff:
|
|
|
|
|
to_staff = True
|
|
|
|
|
extra_emails = _split_emails(rule.extra_recipients)
|
|
|
|
|
|
|
|
|
|
if to_staff:
|
|
|
|
|
for u in User.objects.filter(is_staff=True, is_active=True):
|
|
|
|
|
recipients_users.add(u)
|
|
|
|
|
|
|
|
|
|
# In-App
|
|
|
|
|
if enabled_in_app:
|
|
|
|
|
for u in recipients_users:
|
|
|
|
|
Notification.objects.create(user=u, message=message)
|
|
|
|
|
|
|
|
|
|
# E-Mail
|
|
|
|
|
if enabled_email:
|
|
|
|
|
emails = [u.email for u in recipients_users if u and u.email] + extra_emails
|
|
|
|
|
emails = list(dict.fromkeys(emails)) # de-dupe, Reihenfolge erhalten
|
|
|
|
|
if emails:
|
|
|
|
|
subject = _("Notification")
|
|
|
|
|
body = message
|
|
|
|
|
send_mail(
|
|
|
|
|
subject,
|
|
|
|
|
body,
|
|
|
|
|
getattr(settings, "DEFAULT_FROM_EMAIL", "webmaster@localhost"),
|
|
|
|
|
emails,
|
|
|
|
|
fail_silently=True, # im Zweifel nicht crashen
|
|
|
|
|
)
|