diff --git a/config/settings.py b/config/settings.py index a8864c4..8b6b5ef 100644 --- a/config/settings.py +++ b/config/settings.py @@ -76,6 +76,7 @@ TEMPLATES = [ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", + "risks.context_processors.unread_notifications_count", ], }, }, diff --git a/db.sqlite3 b/db.sqlite3 index deb65ca..6e58103 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 33d0ec2..b760d04 100644 Binary files a/locale/de/LC_MESSAGES/django.mo and b/locale/de/LC_MESSAGES/django.mo differ diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 13d90de..8db2f14 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: wira-risk-management\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-10 11:18+0200\n" +"POT-Creation-Date: 2025-09-10 12:51+0200\n" "PO-Revision-Date: 2025-09-09 13:45+0200\n" "Last-Translator: Kevin Heyer \n" "Language-Team: German\n" @@ -26,7 +26,8 @@ msgstr "Admin" msgid "Risks" msgstr "Risiken" -#: risks/admin.py:16 risks/models.py:190 templates/risks/list_risks.html:37 +#: risks/admin.py:16 risks/models.py:190 templates/base.html:36 +#: templates/risks/list_risks.html:37 msgid "Controls" msgstr "Maßnahmen" @@ -38,7 +39,7 @@ msgstr "Restrisiken" msgid "Reviews" msgstr "Prüfung" -#: risks/admin.py:19 risks/models.py:258 +#: risks/admin.py:19 risks/models.py:258 templates/base.html:37 msgid "Incidents" msgstr "Vorfälle" @@ -46,22 +47,70 @@ msgstr "Vorfälle" msgid "Users" msgstr "Benutzer" -#: risks/admin.py:26 +#: risks/admin.py:133 risks/models.py:302 +msgid "User" +msgstr "Benutzer" + +#: risks/admin.py:139 +msgid "Message" +msgstr "" + +#: risks/admin.py:147 +msgid "Mark selected as read" +msgstr "Alle als gelesen Markieren" + +#: risks/admin.py:150 +msgid "%(n)d notifications marked as read." +msgstr "%(n)d Benachrichtigungen wurden als gelesen Markiert" + +#: risks/admin.py:152 +msgid "Mark selected as unread" +msgstr "Alle als gelesen Markieren" + +#: risks/admin.py:155 +msgid "%(n)d notifications marked as unread." +msgstr "%(n)d Benachrichtigungen wurden als ungelesen Markiert" + +#: risks/admin.py:157 +msgid "Mark selected as sent" +msgstr "" + +#: risks/admin.py:160 +msgid "%(n)d notifications marked as sent." +msgstr "Alle Benachrichtigungen wurden als gelesen Markiert" + +#: risks/admin.py:162 +msgid "Mark selected as unsent" +msgstr "" + +#: risks/admin.py:165 +msgid "%(n)d notifications marked as unsent." +msgstr "Alle Benachrichtigungen wurden als gelesen Markiert" + +#: risks/admin.py:177 msgid "SSO Information" msgstr "SSO-Informationen" -#: risks/admin.py:35 +#: risks/admin.py:186 msgid "Risks Owned" msgstr "Eigene Risiken" -#: risks/admin.py:39 +#: risks/admin.py:190 msgid "Controls Responsible" msgstr "Verantwortlich für Maßnahmen" -#: risks/apps.py:7 +#: risks/apps.py:7 templates/base.html:7 templates/base.html:32 msgid "Risk Management" msgstr "Risikomanagement" +#: risks/forms.py:9 risks/forms.py:16 risks/forms.py:23 risks/models.py:73 +msgid "Status" +msgstr "Status" + +#: risks/forms.py:30 risks/models.py:42 templates/risks/item_risk.html:136 +msgid "Review required" +msgstr "Prüfung nötig" + #: risks/models.py:35 templates/risks/list_risks.html:18 #: templates/risks/list_risks.html:83 msgid "Risk" @@ -79,10 +128,6 @@ msgstr "In Bearbeitung" msgid "Closed" msgstr "Geschlossen" -#: risks/models.py:42 -msgid "Review required" -msgstr "Prüfung nötig" - #: risks/models.py:45 msgid "Very low – occurs less than once every 5 years" msgstr "Sehr niedrig – tritt seltener als einmal in fünf Jahren auf" @@ -159,10 +204,6 @@ msgstr "Erstellt am" msgid "Updated at" msgstr "Aktualisiert am" -#: risks/models.py:73 -msgid "Status" -msgstr "Status" - #: risks/models.py:133 msgid "Residual Risk" msgstr "Restrisiko" @@ -219,9 +260,14 @@ msgstr "Meldedatum" msgid "Reported by" msgstr "Gemeldet von" -#: risks/models.py:298 -msgid "User" -msgstr "Benutzer" +#: risks/models.py:279 +msgid "Notification" +msgstr "Benachrichtigung" + +#: risks/models.py:280 templates/base.html:88 +#: templates/risks/notifications.html:4 +msgid "Notifications" +msgstr "Nachrichten" #: risks/signals.py:57 #, python-brace-format @@ -289,6 +335,7 @@ msgid "Residual risk deleted for '{t}'" msgstr "Restrisiko für '{t}' gelöscht" #: risks/signals.py:296 +#, python-brace-format msgid "Incident '{t}' {s}" msgstr "Vorfälle '{t}' {s}" @@ -302,10 +349,62 @@ msgstr "Vorfall '{t}' gelöscht" msgid "Follow-up reached: review required for risk '{t}'" msgstr "Wiedervorlagedatum erreicht: Prüfung nötig für Risiko '{t}'" -#: templates/risks/dashboard.html:9 +#: risks/views.py:315 +msgid "Notification marked as read." +msgstr "Nachricht als gelesen markiert" + +#: risks/views.py:323 +msgid "All notifications marked as read." +msgstr "Alle Benachrichtigungen wurden als gelesen Markiert" + +#: risks/views.py:340 +msgid "Risk status updated." +msgstr "Risikostatus Aktualisiert" + +#: risks/views.py:354 +msgid "Control status updated." +msgstr "Maßnahmenstatus Aktualisiert" + +#: risks/views.py:368 +msgid "Incident status updated." +msgstr "Vorfallstatus Aktualisiert" + +#: risks/views.py:384 +msgid "Residual review flag updated." +msgstr "Restrisiko geprüft" + +#: templates/base.html:34 templates/risks/dashboard.html:9 msgid "Dashboard" msgstr "Dashboard" +#: templates/base.html:35 templates/risks/list_risks.html:4 +msgid "Risk analysis" +msgstr "Risikoanalyse" + +#: templates/base.html:70 +msgid "AdminCP" +msgstr "Adminbereich" + +#: templates/base.html:76 +msgid "Derk Mode" +msgstr "Dark Mode" + +#: templates/base.html:82 +msgid "Logout" +msgstr "Logout" + +#: templates/base.html:104 +msgid "Login" +msgstr "Login" + +#: templates/base.html:139 templates/base.html:146 +msgid "Light Mode" +msgstr "Light Mode" + +#: templates/base.html:149 +msgid "Dark Mode" +msgstr "Dark Mode" + #: templates/risks/dashboard.html:12 msgid "Overview of Risks, Controls and Incidents" msgstr "Übersicht der Risiken, Maßnahmen und Vorfälle" @@ -334,36 +433,40 @@ msgstr "Vorfälle nach Status" msgid "Risks by CIA" msgstr "CIA Risiken" -#: templates/risks/item_risk.html:68 templates/risks/item_risk.html:114 +#: templates/risks/item_risk.html:34 +msgid "Update status" +msgstr "Status Aktualisiert" + +#: templates/risks/item_risk.html:89 templates/risks/item_risk.html:147 #: templates/risks/list_risks.html:86 msgid "Likelihood" msgstr "Eintritt" -#: templates/risks/item_risk.html:77 templates/risks/item_risk.html:123 +#: templates/risks/item_risk.html:98 templates/risks/item_risk.html:156 #: templates/risks/list_risks.html:87 msgid "Impact" msgstr "Schaden" -#: templates/risks/item_risk.html:86 templates/risks/item_risk.html:132 +#: templates/risks/item_risk.html:107 templates/risks/item_risk.html:165 #: templates/risks/list_risks.html:89 msgid "Level" msgstr "Stufe" -#: templates/risks/item_risk.html:95 templates/risks/item_risk.html:140 +#: templates/risks/item_risk.html:116 templates/risks/item_risk.html:173 #: templates/risks/list_risks.html:88 msgid "Score" msgstr "Score" -#: templates/risks/list_risks.html:4 -msgid "Risk analysis" -msgstr "Risikoanalyse" +#: templates/risks/item_risk.html:139 +msgid "Save" +msgstr "Speichern" #: templates/risks/list_risks.html:9 msgid "Filter" msgstr "Filter" #: templates/risks/list_risks.html:22 templates/risks/list_risks.html:41 -#: templates/risks/list_risks.html:60 +#: templates/risks/list_risks.html:60 templates/risks/notifications.html:13 msgid "All" msgstr "Alle" @@ -374,3 +477,23 @@ msgstr "Risikoeigner" #: templates/risks/list_risks.html:84 msgid "Asset / Process" msgstr "Asset / Prozess" + +#: templates/risks/notifications.html:12 +msgid "Unread" +msgstr "Ungelesen" + +#: templates/risks/notifications.html:20 +msgid "Mark all as read" +msgstr "Alle als gelesen Markieren" + +#: templates/risks/notifications.html:33 +msgid "New" +msgstr "Neu" + +#: templates/risks/notifications.html:43 +msgid "Mark as read" +msgstr "Als gelesen markieren" + +#: templates/risks/notifications.html:53 +msgid "No notifications." +msgstr "Keine Nachrichten" diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 0fe311a..8c3a626 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-09-10 11:18+0200\n" +"POT-Creation-Date: 2025-09-10 12:51+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -32,7 +32,8 @@ msgstr "" msgid "Risks" msgstr "" -#: risks/admin.py:16 risks/models.py:190 templates/risks/list_risks.html:37 +#: risks/admin.py:16 risks/models.py:190 templates/base.html:36 +#: templates/risks/list_risks.html:37 msgid "Controls" msgstr "" @@ -44,7 +45,7 @@ msgstr "" msgid "Reviews" msgstr "" -#: risks/admin.py:19 risks/models.py:258 +#: risks/admin.py:19 risks/models.py:258 templates/base.html:37 msgid "Incidents" msgstr "" @@ -52,22 +53,74 @@ msgstr "" msgid "Users" msgstr "" -#: risks/admin.py:26 +#: risks/admin.py:133 risks/models.py:302 +msgid "User" +msgstr "" + +#: risks/admin.py:139 +msgid "Message" +msgstr "" + +#: risks/admin.py:147 +msgid "Mark selected as read" +msgstr "" + +#: risks/admin.py:150 +#, python-format +msgid "%(n)d notifications marked as read." +msgstr "" + +#: risks/admin.py:152 +msgid "Mark selected as unread" +msgstr "" + +#: risks/admin.py:155 +#, python-format +msgid "%(n)d notifications marked as unread." +msgstr "" + +#: risks/admin.py:157 +msgid "Mark selected as sent" +msgstr "" + +#: risks/admin.py:160 +#, python-format +msgid "%(n)d notifications marked as sent." +msgstr "" + +#: risks/admin.py:162 +msgid "Mark selected as unsent" +msgstr "" + +#: risks/admin.py:165 +#, python-format +msgid "%(n)d notifications marked as unsent." +msgstr "" + +#: risks/admin.py:177 msgid "SSO Information" msgstr "" -#: risks/admin.py:35 +#: risks/admin.py:186 msgid "Risks Owned" msgstr "" -#: risks/admin.py:39 +#: risks/admin.py:190 msgid "Controls Responsible" msgstr "" -#: risks/apps.py:7 +#: risks/apps.py:7 templates/base.html:7 templates/base.html:32 msgid "Risk Management" msgstr "" +#: risks/forms.py:9 risks/forms.py:16 risks/forms.py:23 risks/models.py:73 +msgid "Status" +msgstr "" + +#: risks/forms.py:30 risks/models.py:42 templates/risks/item_risk.html:136 +msgid "Review required" +msgstr "" + #: risks/models.py:35 templates/risks/list_risks.html:18 #: templates/risks/list_risks.html:83 msgid "Risk" @@ -85,10 +138,6 @@ msgstr "" msgid "Closed" msgstr "" -#: risks/models.py:42 -msgid "Review required" -msgstr "" - #: risks/models.py:45 msgid "Very low – occurs less than once every 5 years" msgstr "" @@ -165,10 +214,6 @@ msgstr "" msgid "Updated at" msgstr "" -#: risks/models.py:73 -msgid "Status" -msgstr "" - #: risks/models.py:133 msgid "Residual Risk" msgstr "" @@ -225,8 +270,13 @@ msgstr "" msgid "Reported by" msgstr "" -#: risks/models.py:298 -msgid "User" +#: risks/models.py:279 +msgid "Notification" +msgstr "" + +#: risks/models.py:280 templates/base.html:88 +#: templates/risks/notifications.html:4 +msgid "Notifications" msgstr "" #: risks/signals.py:57 @@ -309,10 +359,62 @@ msgstr "" msgid "Follow-up reached: review required for risk '{t}'" msgstr "" -#: templates/risks/dashboard.html:9 +#: risks/views.py:315 +msgid "Notification marked as read." +msgstr "" + +#: risks/views.py:323 +msgid "All notifications marked as read." +msgstr "" + +#: risks/views.py:340 +msgid "Risk status updated." +msgstr "" + +#: risks/views.py:354 +msgid "Control status updated." +msgstr "" + +#: risks/views.py:368 +msgid "Incident status updated." +msgstr "" + +#: risks/views.py:384 +msgid "Residual review flag updated." +msgstr "" + +#: templates/base.html:34 templates/risks/dashboard.html:9 msgid "Dashboard" msgstr "" +#: templates/base.html:35 templates/risks/list_risks.html:4 +msgid "Risk analysis" +msgstr "" + +#: templates/base.html:70 +msgid "AdminCP" +msgstr "" + +#: templates/base.html:76 +msgid "Derk Mode" +msgstr "" + +#: templates/base.html:82 +msgid "Logout" +msgstr "" + +#: templates/base.html:104 +msgid "Login" +msgstr "" + +#: templates/base.html:139 templates/base.html:146 +msgid "Light Mode" +msgstr "" + +#: templates/base.html:149 +msgid "Dark Mode" +msgstr "" + #: templates/risks/dashboard.html:12 msgid "Overview of Risks, Controls and Incidents" msgstr "" @@ -341,28 +443,32 @@ msgstr "" msgid "Risks by CIA" msgstr "" -#: templates/risks/item_risk.html:68 templates/risks/item_risk.html:114 +#: templates/risks/item_risk.html:34 +msgid "Update status" +msgstr "" + +#: templates/risks/item_risk.html:89 templates/risks/item_risk.html:147 #: templates/risks/list_risks.html:86 msgid "Likelihood" msgstr "" -#: templates/risks/item_risk.html:77 templates/risks/item_risk.html:123 +#: templates/risks/item_risk.html:98 templates/risks/item_risk.html:156 #: templates/risks/list_risks.html:87 msgid "Impact" msgstr "" -#: templates/risks/item_risk.html:86 templates/risks/item_risk.html:132 +#: templates/risks/item_risk.html:107 templates/risks/item_risk.html:165 #: templates/risks/list_risks.html:89 msgid "Level" msgstr "" -#: templates/risks/item_risk.html:95 templates/risks/item_risk.html:140 +#: templates/risks/item_risk.html:116 templates/risks/item_risk.html:173 #: templates/risks/list_risks.html:88 msgid "Score" msgstr "" -#: templates/risks/list_risks.html:4 -msgid "Risk analysis" +#: templates/risks/item_risk.html:139 +msgid "Save" msgstr "" #: templates/risks/list_risks.html:9 @@ -370,7 +476,7 @@ msgid "Filter" msgstr "" #: templates/risks/list_risks.html:22 templates/risks/list_risks.html:41 -#: templates/risks/list_risks.html:60 +#: templates/risks/list_risks.html:60 templates/risks/notifications.html:13 msgid "All" msgstr "" @@ -381,3 +487,23 @@ msgstr "" #: templates/risks/list_risks.html:84 msgid "Asset / Process" msgstr "" + +#: templates/risks/notifications.html:12 +msgid "Unread" +msgstr "" + +#: templates/risks/notifications.html:20 +msgid "Mark all as read" +msgstr "" + +#: templates/risks/notifications.html:33 +msgid "New" +msgstr "" + +#: templates/risks/notifications.html:43 +msgid "Mark as read" +msgstr "" + +#: templates/risks/notifications.html:53 +msgid "No notifications." +msgstr "" diff --git a/risks/admin.py b/risks/admin.py index b1a783f..1d5f0de 100644 --- a/risks/admin.py +++ b/risks/admin.py @@ -1,7 +1,7 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.utils.translation import gettext_lazy as _ -from .models import Control, Incident, NotificationPreference , Risk, ResidualRisk, User +from .models import Control, Incident, Notification, NotificationPreference , Risk, ResidualRisk, User admin.site.site_header = _("Administration") admin.site.site_title = _("Admin") @@ -20,24 +20,6 @@ class NotificationPreferenceInline(admin.StackedInline): (_("Users"), {"fields": ("user_created","user_deleted")}), ) -@admin.register(User) -class UserAdmin(BaseUserAdmin): - fieldsets = BaseUserAdmin.fieldsets + ( - (_("SSO Information"), {"fields": ("is_sso_user",)}), - ) - list_display = ("username", "email", "is_staff", "is_superuser", "is_sso_user", - "owned_risks_count", "responsible_controls_count") - - inlines = [NotificationPreferenceInline] - - def owned_risks_count(self, obj): - return obj.risks_owned.count() - owned_risks_count.short_description = _("Risks Owned") - - def responsible_controls_count(self, obj): - return obj.controls_responsible.count() - responsible_controls_count.short_description = _("Controls Responsible") - class ResidualRiskInline(admin.StackedInline): """ Inline editor for ResidualRisk, linked one-to-one with Risk @@ -135,3 +117,74 @@ class IncidentAdmin(admin.ModelAdmin): obj._changed_by = request.user super().delete_model(request, obj) +@admin.register(Notification) +class NotificationAdmin(admin.ModelAdmin): + + date_hierarchy = "created_at" + list_display = ("id", "created_at", "user_display", "short_message", "read", "sent") + list_display_links = ("id", "short_message") + list_filter = ("read", "sent", "created_at") + search_fields = ("message", "user__username", "user__first_name", "user__last_name", "user__email") + list_select_related = ("user",) + list_editable = ("read", "sent") + ordering = ("-created_at",) + autocomplete_fields = ("user",) + + @admin.display(description=_("User")) + def user_display(self, obj): + if not obj.user: + return "—" + return obj.user.get_full_name() or obj.user.username + + @admin.display(description=_("Message")) + def short_message(self, obj): + msg = obj.message or "" + return (msg[:80] + "…") if len(msg) > 80 else msg + + # Bulk-Aktionen + actions = ["mark_as_read", "mark_as_unread", "mark_as_sent", "mark_as_unsent"] + + @admin.action(description=_("Mark selected as read")) + def mark_as_read(self, request, queryset): + n = queryset.update(read=True) + self.message_user(request, _("%(n)d notifications marked as read.") % {"n": n}) + + @admin.action(description=_("Mark selected as unread")) + def mark_as_unread(self, request, queryset): + n = queryset.update(read=False) + self.message_user(request, _("%(n)d notifications marked as unread.") % {"n": n}) + + @admin.action(description=_("Mark selected as sent")) + def mark_as_sent(self, request, queryset): + n = queryset.update(sent=True) + self.message_user(request, _("%(n)d notifications marked as sent.") % {"n": n}) + + @admin.action(description=_("Mark selected as unsent")) + def mark_as_unsent(self, request, queryset): + n = queryset.update(sent=False) + self.message_user(request, _("%(n)d notifications marked as unsent.") % {"n": n}) + +class NotificationInline(admin.TabularInline): + model = Notification + fields = ("created_at", "message", "read", "sent") + readonly_fields = ("created_at", "message") + extra = 0 + ordering = ("-created_at",) + +@admin.register(User) +class UserAdmin(BaseUserAdmin): + fieldsets = BaseUserAdmin.fieldsets + ( + (_("SSO Information"), {"fields": ("is_sso_user",)}), + ) + list_display = ("username", "email", "is_staff", "is_superuser", "is_sso_user", + "owned_risks_count", "responsible_controls_count") + + inlines = [NotificationInline, NotificationPreferenceInline] + + def owned_risks_count(self, obj): + return obj.risks_owned.count() + owned_risks_count.short_description = _("Risks Owned") + + def responsible_controls_count(self, obj): + return obj.controls_responsible.count() + responsible_controls_count.short_description = _("Controls Responsible") \ No newline at end of file diff --git a/risks/context_processors.py b/risks/context_processors.py new file mode 100644 index 0000000..aecaaf1 --- /dev/null +++ b/risks/context_processors.py @@ -0,0 +1,7 @@ +def unread_notifications_count(request): + if not request.user.is_authenticated: + return {"notifications_unread_count": 0} + from .models import Notification + return { + "notifications_unread_count": Notification.objects.filter(user=request.user, read=False).count() + } \ No newline at end of file diff --git a/risks/forms.py b/risks/forms.py new file mode 100644 index 0000000..7ec45d0 --- /dev/null +++ b/risks/forms.py @@ -0,0 +1,31 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ +from .models import Risk, Control, Incident, ResidualRisk + +class RiskStatusForm(forms.ModelForm): + class Meta: + model = Risk + fields = ["status"] + labels = {"status": _("Status")} + widgets = {"status": forms.Select(attrs={"class": "select"})} + +class ControlStatusForm(forms.ModelForm): + class Meta: + model = Control + fields = ["status"] + labels = {"status": _("Status")} + widgets = {"status": forms.Select(attrs={"class": "select"})} + +class IncidentStatusForm(forms.ModelForm): + class Meta: + model = Incident + fields = ["status"] + labels = {"status": _("Status")} + widgets = {"status": forms.Select(attrs={"class": "select"})} + +class ResidualReviewForm(forms.ModelForm): + class Meta: + model = ResidualRisk + fields = ["review_required"] + labels = {"review_required": _("Review required")} + widgets = {"review_required": forms.CheckboxInput(attrs={"class": "checkbox"})} \ No newline at end of file diff --git a/risks/migrations/0022_alter_notification_options.py b/risks/migrations/0022_alter_notification_options.py new file mode 100644 index 0000000..1ca8cd6 --- /dev/null +++ b/risks/migrations/0022_alter_notification_options.py @@ -0,0 +1,19 @@ +# Generated by Django 5.2.6 on 2025-09-10 10:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("risks", "0021_risk_status_notificationpreference"), + ] + + operations = [ + migrations.AlterModelOptions( + name="notification", + options={ + "verbose_name": "Notification", + "verbose_name_plural": "Notifications", + }, + ), + ] diff --git a/risks/models.py b/risks/models.py index 426dbd9..9995a56 100644 --- a/risks/models.py +++ b/risks/models.py @@ -275,6 +275,10 @@ class Incident(models.Model): updated_at = models.DateTimeField(auto_now=True) class Notification(models.Model): + class Meta: + verbose_name = _("Notification") + verbose_name_plural = _("Notifications") + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="notifications") message = models.TextField() diff --git a/risks/urls.py b/risks/urls.py index b450e3f..031882e 100644 --- a/risks/urls.py +++ b/risks/urls.py @@ -5,6 +5,7 @@ app_name = "risks" urlpatterns = [ path("", views.dashboard, name="dashboard"), + path("risks/index", views.dashboard, name="index"), path("risks/list_risks", views.list_risks, name="list_risks"), path("risks/risks/", views.show_risk, name="show_risk"), @@ -12,4 +13,15 @@ urlpatterns = [ path("risks/controls/", views.show_control, name="show_control"), path("risks/list_incidents", views.list_incidents, name="list_incidents"), path("risks/incidents/", views.show_incident, name="show_incident"), + + # Notifications + path("notifications/", views.notifications, name="notifications"), + path("notifications//read", views.notification_mark_read, name="notification_mark_read"), + path("notifications/mark_all_read", views.notification_mark_all_read, name="notification_mark_all_read"), + + # Risks status + path("risks//status", views.update_risk_status, name="update_risk_status"), + path("controls//status", views.update_control_status, name="update_control_status"), + path("incidents//status", views.update_incident_status, name="update_incident_status"), + path("residuals//review", views.update_residual_review, name="update_residual_review"), ] \ No newline at end of file diff --git a/risks/views.py b/risks/views.py index e7d3569..e0a4cc6 100644 --- a/risks/views.py +++ b/risks/views.py @@ -2,16 +2,29 @@ from django.contrib.admin.models import LogEntry from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required from django.contrib.contenttypes.models import ContentType +from django.contrib import messages from django.db.models import Count, Q +from django.http import HttpResponseForbidden +from django.shortcuts import redirect, render, get_object_or_404 +from django.utils.translation import gettext_lazy as _ from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated -from django.shortcuts import render, get_object_or_404 from collections import Counter +from .forms import RiskStatusForm, ControlStatusForm, IncidentStatusForm, ResidualReviewForm from .models import Risk, Control, ResidualRisk, AuditLog, Incident, Notification from .serializers import ControlSerializer, RiskSerializer, ResidualRiskSerializer, UserSerializer, AuditSerializer, IncidentSerializer User = get_user_model() +def _can_edit_risk(user, risk: Risk) -> bool: + return bool(user.is_staff or (risk.owner_id and risk.owner_id == user.id)) + +def _can_edit_control(user, control: Control) -> bool: + return bool(user.is_staff or (control.responsible_id and control.responsible_id == user.id)) + +def _can_edit_incident(user, incident: Incident) -> bool: + return bool(user.is_staff or (incident.reported_by_id and incident.reported_by_id == user.id)) + # --------------------------------------------------------------------------- # API # --------------------------------------------------------------------------- @@ -274,3 +287,99 @@ def dashboard(request): 'notifications_unread': notifications_unread, } return render(request, 'risks/dashboard.html', context) + +# --------------------------------------------------------------------------- +# Notifications +# --------------------------------------------------------------------------- + +@login_required +def notifications(request): + """Eigene Benachrichtigungen ansehen + filtern""" + flt = request.GET.get("filter", "unread") + qs = Notification.objects.filter(user=request.user).order_by("-created_at") + if flt == "unread": + qs = qs.filter(read=False) + # Einfache Pagination (optional) + return render(request, "risks/notifications.html", { + "notifications": qs, + "filter": flt, + }) + +@login_required +def notification_mark_read(request, pk): + if request.method != "POST": + return HttpResponseForbidden() + notif = get_object_or_404(Notification, pk=pk, user=request.user) + notif.read = True + notif.save(update_fields=["read"]) + messages.success(request, _("Notification marked as read.")) + return redirect(request.META.get("HTTP_REFERER") or "risks:notifications") + +@login_required +def notification_mark_all_read(request): + if request.method != "POST": + return HttpResponseForbidden() + Notification.objects.filter(user=request.user, read=False).update(read=True) + messages.success(request, _("All notifications marked as read.")) + return redirect("risks:notifications") + +# --------------------------------------------------------------------------- +# Status Updates +# --------------------------------------------------------------------------- +@login_required +def update_risk_status(request, id): + risk = get_object_or_404(Risk, pk=id) + if not _can_edit_risk(request.user, risk): + return HttpResponseForbidden() + if request.method == "POST": + form = RiskStatusForm(request.POST, instance=risk) + if form.is_valid(): + obj = form.save(commit=False) + obj._changed_by = request.user + obj.save(update_fields=["status", "updated_at"]) + messages.success(request, _("Risk status updated.")) + return redirect("risks:show_risk", id=risk.pk) + +@login_required +def update_control_status(request, id): + control = get_object_or_404(Control, pk=id) + if not _can_edit_control(request.user, control): + return HttpResponseForbidden() + if request.method == "POST": + form = ControlStatusForm(request.POST, instance=control) + if form.is_valid(): + obj = form.save(commit=False) + obj._changed_by = request.user + obj.save(update_fields=["status", "updated_at"]) + messages.success(request, _("Control status updated.")) + return redirect("risks:show_control", id=control.pk) + +@login_required +def update_incident_status(request, id): + incident = get_object_or_404(Incident, pk=id) + if not _can_edit_incident(request.user, incident): + return HttpResponseForbidden() + if request.method == "POST": + form = IncidentStatusForm(request.POST, instance=incident) + if form.is_valid(): + obj = form.save(commit=False) + obj._changed_by = request.user + obj.save(update_fields=["status", "updated_at"]) + messages.success(request, _("Incident status updated.")) + return redirect("risks:show_incident", id=incident.pk) + +@login_required +def update_residual_review(request, risk_id): + """Review-Flag (Restrisiko) setzen/lösen""" + risk = get_object_or_404(Risk, pk=risk_id) + if not _can_edit_risk(request.user, risk): + return HttpResponseForbidden() + residual, created_resid = ResidualRisk.objects.get_or_create(risk=risk) + if request.method == "POST": + form = ResidualReviewForm(request.POST, instance=residual) + if form.is_valid(): + obj = form.save(commit=False) + obj._changed_by = request.user + obj.save(update_fields=["review_required", "updated_at"]) + messages.success(request, _("Residual review flag updated.")) + return redirect("risks:show_risk", id=risk.pk) \ No newline at end of file diff --git a/static/css/design.css b/static/css/design.css index 62fe93d..7392c06 100644 --- a/static/css/design.css +++ b/static/css/design.css @@ -243,4 +243,35 @@ body.dark-mode a { @media (max-width: 1215px) { .risk-chip { --chip-w: 100%; width: var(--chip-w); } -} \ No newline at end of file +} + +/* Container für Avatar + Badge */ +.avatar-wrap { + position: relative; + display: inline-block; +} + +.avatar-wrap .badge { + position: absolute; + top: -0.35rem; + right: -0.35rem; + min-width: 1.25rem; + height: 1.25rem; + padding: 0 .25rem; + font-size: 0.75rem; + line-height: 1.25rem; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0 0 2px #fff; +} + +.avatar-wrap .tag.is-medium + .badge { + min-width: 1.15rem; + height: 1.15rem; + font-size: 0.70rem; + line-height: 1.15rem; +} + +/* Dark-Mode/ +.navbar.is-dark .avatar-wrap .badge { box-shadow: 0 0 0 2px hsl(229, 53%, 18%); } \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 7be4056..d872ddb 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,9 +1,10 @@ {% load static %} +{% load i18n %} - Risiko Management + {% trans "Risk Management" %} @@ -28,12 +29,12 @@