Implement notification system and status update forms

- Added Notification model with admin interface for managing notifications.
- Created context processor to count unread notifications for the user.
- Introduced forms for updating the status of Risk, Control, Incident, and ResidualRisk.
- Added views for displaying and managing notifications, including marking them as read.
- Updated URLs to include routes for notifications and status updates.
- Enhanced templates to support notifications display and status update forms.
- Improved CSS for avatar and badge display in the navbar.
- Translated various static texts to support internationalization.
This commit is contained in:
Kevin Heyer 2025-09-10 13:44:03 +02:00
parent ab01841cf2
commit ebfcbddd5c
19 changed files with 797 additions and 142 deletions

View file

@ -76,6 +76,7 @@ TEMPLATES = [
"django.template.context_processors.request", "django.template.context_processors.request",
"django.contrib.auth.context_processors.auth", "django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
"risks.context_processors.unread_notifications_count",
], ],
}, },
}, },

Binary file not shown.

Binary file not shown.

View file

@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: wira-risk-management\n" "Project-Id-Version: wira-risk-management\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2025-09-09 13:45+0200\n"
"Last-Translator: Kevin Heyer <kevin@example.com>\n" "Last-Translator: Kevin Heyer <kevin@example.com>\n"
"Language-Team: German\n" "Language-Team: German\n"
@ -26,7 +26,8 @@ msgstr "Admin"
msgid "Risks" msgid "Risks"
msgstr "Risiken" 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" msgid "Controls"
msgstr "Maßnahmen" msgstr "Maßnahmen"
@ -38,7 +39,7 @@ msgstr "Restrisiken"
msgid "Reviews" msgid "Reviews"
msgstr "Prüfung" 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" msgid "Incidents"
msgstr "Vorfälle" msgstr "Vorfälle"
@ -46,22 +47,70 @@ msgstr "Vorfälle"
msgid "Users" msgid "Users"
msgstr "Benutzer" 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" msgid "SSO Information"
msgstr "SSO-Informationen" msgstr "SSO-Informationen"
#: risks/admin.py:35 #: risks/admin.py:186
msgid "Risks Owned" msgid "Risks Owned"
msgstr "Eigene Risiken" msgstr "Eigene Risiken"
#: risks/admin.py:39 #: risks/admin.py:190
msgid "Controls Responsible" msgid "Controls Responsible"
msgstr "Verantwortlich für Maßnahmen" 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" msgid "Risk Management"
msgstr "Risikomanagement" 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 #: risks/models.py:35 templates/risks/list_risks.html:18
#: templates/risks/list_risks.html:83 #: templates/risks/list_risks.html:83
msgid "Risk" msgid "Risk"
@ -79,10 +128,6 @@ msgstr "In Bearbeitung"
msgid "Closed" msgid "Closed"
msgstr "Geschlossen" msgstr "Geschlossen"
#: risks/models.py:42
msgid "Review required"
msgstr "Prüfung nötig"
#: risks/models.py:45 #: risks/models.py:45
msgid "Very low occurs less than once every 5 years" msgid "Very low occurs less than once every 5 years"
msgstr "Sehr niedrig tritt seltener als einmal in fünf Jahren auf" msgstr "Sehr niedrig tritt seltener als einmal in fünf Jahren auf"
@ -159,10 +204,6 @@ msgstr "Erstellt am"
msgid "Updated at" msgid "Updated at"
msgstr "Aktualisiert am" msgstr "Aktualisiert am"
#: risks/models.py:73
msgid "Status"
msgstr "Status"
#: risks/models.py:133 #: risks/models.py:133
msgid "Residual Risk" msgid "Residual Risk"
msgstr "Restrisiko" msgstr "Restrisiko"
@ -219,9 +260,14 @@ msgstr "Meldedatum"
msgid "Reported by" msgid "Reported by"
msgstr "Gemeldet von" msgstr "Gemeldet von"
#: risks/models.py:298 #: risks/models.py:279
msgid "User" msgid "Notification"
msgstr "Benutzer" msgstr "Benachrichtigung"
#: risks/models.py:280 templates/base.html:88
#: templates/risks/notifications.html:4
msgid "Notifications"
msgstr "Nachrichten"
#: risks/signals.py:57 #: risks/signals.py:57
#, python-brace-format #, python-brace-format
@ -289,6 +335,7 @@ msgid "Residual risk deleted for '{t}'"
msgstr "Restrisiko für '{t}' gelöscht" msgstr "Restrisiko für '{t}' gelöscht"
#: risks/signals.py:296 #: risks/signals.py:296
#, python-brace-format
msgid "Incident '{t}' {s}" msgid "Incident '{t}' {s}"
msgstr "Vorfälle '{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}'" msgid "Follow-up reached: review required for risk '{t}'"
msgstr "Wiedervorlagedatum erreicht: Prüfung nötig für Risiko '{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" msgid "Dashboard"
msgstr "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 #: templates/risks/dashboard.html:12
msgid "Overview of Risks, Controls and Incidents" msgid "Overview of Risks, Controls and Incidents"
msgstr "Übersicht der Risiken, Maßnahmen und Vorfälle" msgstr "Übersicht der Risiken, Maßnahmen und Vorfälle"
@ -334,36 +433,40 @@ msgstr "Vorfälle nach Status"
msgid "Risks by CIA" msgid "Risks by CIA"
msgstr "CIA Risiken" 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 #: templates/risks/list_risks.html:86
msgid "Likelihood" msgid "Likelihood"
msgstr "Eintritt" 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 #: templates/risks/list_risks.html:87
msgid "Impact" msgid "Impact"
msgstr "Schaden" 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 #: templates/risks/list_risks.html:89
msgid "Level" msgid "Level"
msgstr "Stufe" 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 #: templates/risks/list_risks.html:88
msgid "Score" msgid "Score"
msgstr "Score" msgstr "Score"
#: templates/risks/list_risks.html:4 #: templates/risks/item_risk.html:139
msgid "Risk analysis" msgid "Save"
msgstr "Risikoanalyse" msgstr "Speichern"
#: templates/risks/list_risks.html:9 #: templates/risks/list_risks.html:9
msgid "Filter" msgid "Filter"
msgstr "Filter" msgstr "Filter"
#: templates/risks/list_risks.html:22 templates/risks/list_risks.html:41 #: 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" msgid "All"
msgstr "Alle" msgstr "Alle"
@ -374,3 +477,23 @@ msgstr "Risikoeigner"
#: templates/risks/list_risks.html:84 #: templates/risks/list_risks.html:84
msgid "Asset / Process" msgid "Asset / Process"
msgstr "Asset / Prozess" 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"

View file

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -32,7 +32,8 @@ msgstr ""
msgid "Risks" msgid "Risks"
msgstr "" 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" msgid "Controls"
msgstr "" msgstr ""
@ -44,7 +45,7 @@ msgstr ""
msgid "Reviews" msgid "Reviews"
msgstr "" msgstr ""
#: risks/admin.py:19 risks/models.py:258 #: risks/admin.py:19 risks/models.py:258 templates/base.html:37
msgid "Incidents" msgid "Incidents"
msgstr "" msgstr ""
@ -52,22 +53,74 @@ msgstr ""
msgid "Users" msgid "Users"
msgstr "" 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" msgid "SSO Information"
msgstr "" msgstr ""
#: risks/admin.py:35 #: risks/admin.py:186
msgid "Risks Owned" msgid "Risks Owned"
msgstr "" msgstr ""
#: risks/admin.py:39 #: risks/admin.py:190
msgid "Controls Responsible" msgid "Controls Responsible"
msgstr "" msgstr ""
#: risks/apps.py:7 #: risks/apps.py:7 templates/base.html:7 templates/base.html:32
msgid "Risk Management" msgid "Risk Management"
msgstr "" 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 #: risks/models.py:35 templates/risks/list_risks.html:18
#: templates/risks/list_risks.html:83 #: templates/risks/list_risks.html:83
msgid "Risk" msgid "Risk"
@ -85,10 +138,6 @@ msgstr ""
msgid "Closed" msgid "Closed"
msgstr "" msgstr ""
#: risks/models.py:42
msgid "Review required"
msgstr ""
#: risks/models.py:45 #: risks/models.py:45
msgid "Very low occurs less than once every 5 years" msgid "Very low occurs less than once every 5 years"
msgstr "" msgstr ""
@ -165,10 +214,6 @@ msgstr ""
msgid "Updated at" msgid "Updated at"
msgstr "" msgstr ""
#: risks/models.py:73
msgid "Status"
msgstr ""
#: risks/models.py:133 #: risks/models.py:133
msgid "Residual Risk" msgid "Residual Risk"
msgstr "" msgstr ""
@ -225,8 +270,13 @@ msgstr ""
msgid "Reported by" msgid "Reported by"
msgstr "" msgstr ""
#: risks/models.py:298 #: risks/models.py:279
msgid "User" msgid "Notification"
msgstr ""
#: risks/models.py:280 templates/base.html:88
#: templates/risks/notifications.html:4
msgid "Notifications"
msgstr "" msgstr ""
#: risks/signals.py:57 #: risks/signals.py:57
@ -309,10 +359,62 @@ msgstr ""
msgid "Follow-up reached: review required for risk '{t}'" msgid "Follow-up reached: review required for risk '{t}'"
msgstr "" 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" msgid "Dashboard"
msgstr "" 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 #: templates/risks/dashboard.html:12
msgid "Overview of Risks, Controls and Incidents" msgid "Overview of Risks, Controls and Incidents"
msgstr "" msgstr ""
@ -341,28 +443,32 @@ msgstr ""
msgid "Risks by CIA" msgid "Risks by CIA"
msgstr "" 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 #: templates/risks/list_risks.html:86
msgid "Likelihood" msgid "Likelihood"
msgstr "" 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 #: templates/risks/list_risks.html:87
msgid "Impact" msgid "Impact"
msgstr "" 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 #: templates/risks/list_risks.html:89
msgid "Level" msgid "Level"
msgstr "" 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 #: templates/risks/list_risks.html:88
msgid "Score" msgid "Score"
msgstr "" msgstr ""
#: templates/risks/list_risks.html:4 #: templates/risks/item_risk.html:139
msgid "Risk analysis" msgid "Save"
msgstr "" msgstr ""
#: templates/risks/list_risks.html:9 #: templates/risks/list_risks.html:9
@ -370,7 +476,7 @@ msgid "Filter"
msgstr "" msgstr ""
#: templates/risks/list_risks.html:22 templates/risks/list_risks.html:41 #: 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" msgid "All"
msgstr "" msgstr ""
@ -381,3 +487,23 @@ msgstr ""
#: templates/risks/list_risks.html:84 #: templates/risks/list_risks.html:84
msgid "Asset / Process" msgid "Asset / Process"
msgstr "" 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 ""

View file

@ -1,7 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext_lazy as _ 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_header = _("Administration")
admin.site.site_title = _("Admin") admin.site.site_title = _("Admin")
@ -20,24 +20,6 @@ class NotificationPreferenceInline(admin.StackedInline):
(_("Users"), {"fields": ("user_created","user_deleted")}), (_("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): class ResidualRiskInline(admin.StackedInline):
""" """
Inline editor for ResidualRisk, linked one-to-one with Risk Inline editor for ResidualRisk, linked one-to-one with Risk
@ -135,3 +117,74 @@ class IncidentAdmin(admin.ModelAdmin):
obj._changed_by = request.user obj._changed_by = request.user
super().delete_model(request, obj) 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")

View file

@ -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()
}

31
risks/forms.py Normal file
View file

@ -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"})}

View file

@ -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",
},
),
]

View file

@ -275,6 +275,10 @@ class Incident(models.Model):
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
class Notification(models.Model): 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") user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="notifications")
message = models.TextField() message = models.TextField()

View file

@ -5,6 +5,7 @@ app_name = "risks"
urlpatterns = [ urlpatterns = [
path("", views.dashboard, name="dashboard"), path("", views.dashboard, name="dashboard"),
path("risks/index", views.dashboard, name="index"), path("risks/index", views.dashboard, name="index"),
path("risks/list_risks", views.list_risks, name="list_risks"), path("risks/list_risks", views.list_risks, name="list_risks"),
path("risks/risks/<int:id>", views.show_risk, name="show_risk"), path("risks/risks/<int:id>", views.show_risk, name="show_risk"),
@ -12,4 +13,15 @@ urlpatterns = [
path("risks/controls/<int:id>", views.show_control, name="show_control"), path("risks/controls/<int:id>", views.show_control, name="show_control"),
path("risks/list_incidents", views.list_incidents, name="list_incidents"), path("risks/list_incidents", views.list_incidents, name="list_incidents"),
path("risks/incidents/<int:id>", views.show_incident, name="show_incident"), path("risks/incidents/<int:id>", views.show_incident, name="show_incident"),
# Notifications
path("notifications/", views.notifications, name="notifications"),
path("notifications/<int:pk>/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/<int:id>/status", views.update_risk_status, name="update_risk_status"),
path("controls/<int:id>/status", views.update_control_status, name="update_control_status"),
path("incidents/<int:id>/status", views.update_incident_status, name="update_incident_status"),
path("residuals/<int:risk_id>/review", views.update_residual_review, name="update_residual_review"),
] ]

View file

@ -2,16 +2,29 @@ from django.contrib.admin.models import LogEntry
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib import messages
from django.db.models import Count, Q 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 import viewsets
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from django.shortcuts import render, get_object_or_404
from collections import Counter from collections import Counter
from .forms import RiskStatusForm, ControlStatusForm, IncidentStatusForm, ResidualReviewForm
from .models import Risk, Control, ResidualRisk, AuditLog, Incident, Notification from .models import Risk, Control, ResidualRisk, AuditLog, Incident, Notification
from .serializers import ControlSerializer, RiskSerializer, ResidualRiskSerializer, UserSerializer, AuditSerializer, IncidentSerializer from .serializers import ControlSerializer, RiskSerializer, ResidualRiskSerializer, UserSerializer, AuditSerializer, IncidentSerializer
User = get_user_model() 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 # API
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -274,3 +287,99 @@ def dashboard(request):
'notifications_unread': notifications_unread, 'notifications_unread': notifications_unread,
} }
return render(request, 'risks/dashboard.html', context) 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)

View file

@ -244,3 +244,34 @@ body.dark-mode a {
@media (max-width: 1215px) { @media (max-width: 1215px) {
.risk-chip { --chip-w: 100%; width: var(--chip-w); } .risk-chip { --chip-w: 100%; width: var(--chip-w); }
} }
/* 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%); }

View file

@ -1,9 +1,10 @@
{% load static %} {% load static %}
{% load i18n %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Risiko Management</title> <title>{% trans "Risk Management" %}</title>
<link rel="stylesheet" href="{% static 'css/bulma.min.css' %}"> <link rel="stylesheet" href="{% static 'css/bulma.min.css' %}">
<link rel="stylesheet" href="{% static 'css/design.css' %}"> <link rel="stylesheet" href="{% static 'css/design.css' %}">
<link rel="stylesheet" href="{% static 'css/fontawesome.min.css' %}"> <link rel="stylesheet" href="{% static 'css/fontawesome.min.css' %}">
@ -28,12 +29,12 @@
<div id="mainNavbar" class="navbar-menu"> <div id="mainNavbar" class="navbar-menu">
<div class="navbar-start"> <div class="navbar-start">
<div class="navbar-item has-dropdown is-hoverable"> <div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">Risikomanagement</a> <a class="navbar-link">{% trans "Risk Management" %}</a>
<div class="navbar-dropdown"> <div class="navbar-dropdown">
<a class="navbar-item" href="/risks/index">Dashboard</a> <a class="navbar-item" href="/risks/index">{% trans "Dashboard" %}</a>
<a class="navbar-item" href="/risks/list_risks">Risikoanalyse</a> <a class="navbar-item" href="/risks/list_risks">{% trans "Risk analysis" %}</a>
<a class="navbar-item" href="/risks/list_controls">Maßnahmen</a> <a class="navbar-item" href="/risks/list_controls">{% trans "Controls" %}</a>
<a class="navbar-item" href="/risks/list_incidents">Vorfälle</a> <a class="navbar-item" href="/risks/list_incidents">{% trans "Incidents" %}</a>
</div> </div>
</div> </div>
@ -44,13 +45,6 @@
<div class="navbar-end"> <div class="navbar-end">
<!-- Suche --> <!-- Suche -->
<div class="navbar-item"> <div class="navbar-item">
<!--
<div class="field">
<p class="control">
<input class="input is-small" type="text" placeholder="Suchen">
</p>
</div>
-->
</div> </div>
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
@ -58,27 +52,44 @@
<div class="navbar-item has-dropdown is-hoverable"> <div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link"> <a class="navbar-link">
<!-- Initialen-Badge --> <!-- Initialen-Badge -->
<span class="avatar-wrap">
<span class="tag is-link is-light is-medium is-rounded" <span class="tag is-link is-light is-medium is-rounded"
style="width:2.25rem;height:2.25rem;display:inline-flex;align-items:center;justify-content:center;"> style="width:2.25rem;height:2.25rem;display:inline-flex;align-items:center;justify-content:center;">
{{ request.user.username|slice:":2"|upper }} {{ request.user.username|slice:":2"|upper }}
</span> </span>
{% if notifications_unread_count %}
<span class="badge tag is-danger is-rounded">
{% if notifications_unread_count > 99 %}99+{% else %}{{ notifications_unread_count }}{% endif %}
</span>
{% endif %}
</span>
</a> </a>
<div class="navbar-dropdown is-right"> <div class="navbar-dropdown is-right">
{% if request.user.is_staff %} {% if request.user.is_staff %}
<a class="navbar-item" href="/admin/">Admin</a> <a class="navbar-item" href="/admin/">{% trans "AdminCP" %}</a>
<hr class="navbar-divider"> <hr class="navbar-divider">
{% endif %} {% endif %}
<a class="navbar-item" href="{% url 'risks:notifications' %}">
{% trans "Notifications" %}
{% if notifications_unread_count %}
<span class="tag is-danger is-rounded" style="margin-left:.5rem;">{{ notifications_unread_count }}</span>
{% endif %}
</a>
<!-- Dark Mode Toggle --> <!-- Dark Mode Toggle -->
<button id="dark-mode-toggle" class="button is-small is-light"> <a id="dark-mode-toggle" class="navbar-item is-small is-light">
🌙 Dark Mode 🌙 {% trans "Derk Mode" %}
</button> </a>
<!-- Logout als POST über Hidden-Form --> <!-- Logout als POST über Hidden-Form -->
<a class="navbar-item" href="#" <a class="navbar-item" href="#"
onclick="document.getElementById('logout-form').submit(); return false;"> onclick="document.getElementById('logout-form').submit(); return false;">
Logout {% trans "Logout" %}
</a> </a>
</div> </div>
</div><!-- Profil-Dropdown Ende --> </div><!-- Profil-Dropdown Ende -->
@ -93,7 +104,7 @@
<div class="navbar-item"> <div class="navbar-item">
<div class="buttons"> <div class="buttons">
<a class="button is-primary is-light" href="{% url 'login' %}"> <a class="button is-primary is-light" href="{% url 'login' %}">
<strong>Login</strong> <strong>{% trans "Login" %}</strong>
</a> </a>
</div> </div>
</div> </div>
@ -128,17 +139,17 @@
// Dark Mode aus localStorage laden // Dark Mode aus localStorage laden
if (localStorage.getItem('darkMode') === 'enabled') { if (localStorage.getItem('darkMode') === 'enabled') {
document.body.classList.add('dark-mode'); document.body.classList.add('dark-mode');
toggleButton.textContent = '☀️ Light Mode'; toggleButton.textContent = '☀️ {% trans "Light Mode" %}';
} }
toggleButton.addEventListener('click', () => { toggleButton.addEventListener('click', () => {
document.body.classList.toggle('dark-mode'); document.body.classList.toggle('dark-mode');
if (document.body.classList.contains('dark-mode')) { if (document.body.classList.contains('dark-mode')) {
localStorage.setItem('darkMode', 'enabled'); localStorage.setItem('darkMode', 'enabled');
toggleButton.textContent = '☀️ Light Mode'; toggleButton.textContent = '☀️ {% trans "Light Mode" %}';
} else { } else {
localStorage.setItem('darkMode', 'disabled'); localStorage.setItem('darkMode', 'disabled');
toggleButton.textContent = '🌙 Dark Mode'; toggleButton.textContent = '🌙 {% trans "Dark Mode" %}';
} }
}); });

View file

@ -1,4 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n risk_extras %}
{% block crumbs %} {% block crumbs %}
<li><a href="{% url 'risks:list_controls' %}">Maßnahmen</a></li> <li><a href="{% url 'risks:list_controls' %}">Maßnahmen</a></li>
<li><a href="{% url 'risks:show_control' control.id %}">{{ control.title }}</a></li> <li><a href="{% url 'risks:show_control' control.id %}">{{ control.title }}</a></li>
@ -16,7 +17,26 @@
<header class="card-header"> <header class="card-header">
<p class="card-header-title">Überblick</p> <p class="card-header-title">Überblick</p>
{% if request.user.is_staff %} {% if request.user.is_staff or control.responsible.id == request.user.id %}
<form method="post" action="{% url 'risks:update_control_status' control.id %}" class="card-header-icon" style="margin-left:auto;">
{% csrf_token %}
<div class="field has-addons">
<div class="control">
<div class="select is-small">
<select name="status">
{% for value,label in control.STATUS_CHOICES %}
<option value="{{ value }}" {% if control.status == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="control">
<button class="button is-small is-link">
<span class="icon"><i class="fas fa-save"></i></span>
</button>
</div>
</div>
</form>
<a class="card-header-icon has-text-warning" href="{% url 'admin:risks_control_change' control.pk %}" title="Maßnahme bearbeiten"> <a class="card-header-icon has-text-warning" href="{% url 'admin:risks_control_change' control.pk %}" title="Maßnahme bearbeiten">
<span class="icon"><i class="fas fa-edit" aria-hidden="true"></i></span> <span class="icon"><i class="fas fa-edit" aria-hidden="true"></i></span>
</a> </a>

View file

@ -16,7 +16,26 @@
<header class="card-header"> <header class="card-header">
<p class="card-header-title">Überblick</p> <p class="card-header-title">Überblick</p>
{% if request.user.is_staff %} {% if request.user.is_staff or incident.reported_by_id == request.user.id %}
<form method="post" action="{% url 'risks:update_incident_status' incident.id %}" class="card-header-icon" style="margin-left:auto;">
{% csrf_token %}
<div class="field has-addons">
<div class="control">
<div class="select is-small">
<select name="status">
{% for value,label in incident.STATUS_CHOICES %}
<option value="{{ value }}" {% if incident.status == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="control">
<button class="button is-small is-link">
<span class="icon"><i class="fas fa-save"></i></span>
</button>
</div>
</div>
</form>
<a class="card-header-icon has-text-warning" href="{% url 'admin:risks_incident_change' incident.pk %}" title="Vorfall bearbeiten"> <a class="card-header-icon has-text-warning" href="{% url 'admin:risks_incident_change' incident.pk %}" title="Vorfall bearbeiten">
<span class="icon"><i class="fas fa-edit" aria-hidden="true"></i></span> <span class="icon"><i class="fas fa-edit" aria-hidden="true"></i></span>
</a> </a>

View file

@ -1,23 +1,43 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n risk_extras %} {% load i18n risk_extras %}
{% block crumbs %} {% block crumbs %}
<li><a href="{% url 'risks:list_risks' %}">Risikoanalyse</a></li> <li><a href="{% url 'risks:list_risks' %}">{% trans "Risk analysis" %}</a></li>
<li><a href="{% url 'risks:show_risk' risk.id %}">{{ risk.title }}</a></li> <li><a href="{% url 'risks:show_risk' risk.id %}">{{ risk.title }}</a></li>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<section class="hero is-small"> <section class="hero is-small">
<div class="hero-body"> <div class="hero-body">
<p class="title">Risiko: {{ risk.title }}</p> <p class="title">{% trans "Risk" %}: {{ risk.title }}</p>
<p class="subtitle is-6">{{ risk.description }}</p> <p class="subtitle is-6">{{ risk.description }}</p>
</div> </div>
</section> </section>
<!-- Überblick--> <!-- Überblick-->
<div class="card"> <div class="card">
<header class="card-header"> <header class="card-header">
<p class="card-header-title">Überblick</p> <p class="card-header-title">{% trans "Overview" %}</p>
{% if request.user.is_staff or risk.owner_id == request.user.id %}
<form method="post" action="{% url 'risks:update_risk_status' risk.id %}" class="card-header-icon" style="margin-left:auto;">
{% csrf_token %}
<div class="field has-addons">
<div class="control">
<div class="select is-small">
<select name="status">
{% for value,label in risk.STATUS_CHOICES %}
<option value="{{ value }}" {% if risk.status == value %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="control">
<button class="button is-small is-link" title="{% trans 'Update status' %}">
<span class="icon"><i class="fas fa-save"></i></span>
</button>
</div>
</div>
</form>
{% if request.user.is_staff %}
<a class="card-header-icon has-text-warning" href="{% url 'admin:risks_risk_change' risk.pk %}" title="Risiko bearbeiten"> <a class="card-header-icon has-text-warning" href="{% url 'admin:risks_risk_change' risk.pk %}" title="Risiko bearbeiten">
<span class="icon"><i class="fas fa-edit" aria-hidden="true"></i></span> <span class="icon"><i class="fas fa-edit" aria-hidden="true"></i></span>
</a> </a>
@ -30,29 +50,30 @@
<div class="card-content"> <div class="card-content">
<div class="columns is-multiline"> <div class="columns is-multiline">
<div class="column is-half"> <div class="column is-half">
<p><strong>Asset:</strong> {{ risk.asset|default:"-" }}</p> <p><strong>{% trans "Asset" %}:</strong> {{ risk.asset|default:"-" }}</p>
<p><strong>Prozess:</strong> {{ risk.process|default:"-" }}</p> <p><strong>{% trans "Process" %}:</strong> {{ risk.process|default:"-" }}</p>
<p><strong>Kategorie:</strong> {{ risk.category|default:"-" }}</p> <p><strong>{% trans "Category" %}:</strong> {{ risk.category|default:"-" }}</p>
<p> <p>
<strong>Schutzziele:</strong> <strong>{% trans "Protection goals" %}:</strong>
{% if risk.cia %} {% if risk.cia %}
{{ risk.get_cia_display }} {{ risk.get_cia_display }}
{% else %} {% else %}
<span class="has-text-grey">Noch nicht zugewiesen</span> <span class="has-text-grey">{% trans "Not yet assigned" %}</span>
{% endif %} {% endif %}
</p> </p>
<p><strong>{% trans "Status" %}:</strong> {{ risk.status }}</p>
</div> </div>
<div class="column is-half"> <div class="column is-half">
<p><strong>Risikoeigner:</strong> {{ risk.owner|user_display|default:"-" }}</p> <p><strong>{% trans "Risk owner" %}:</strong> {{ risk.owner|user_display|default:"-" }}</p>
<p><strong>Erstellt am:</strong> {{ risk.created_at|date:'d.m.Y H:i' }}</p> <p><strong>{% trans "Created at" %}:</strong> {{ risk.created_at|date:'d.m.Y H:i' }}</p>
<p><strong>Aktualisiert am:</strong> {{ risk.updated_at|date:'d.m.Y H:i' }}</p> <p><strong>{% trans "updated at" %}:</strong> {{ risk.updated_at|date:'d.m.Y H:i' }}</p>
<p><strong>Wiedervorlage am:</strong> {{ risk.follow_up|date:'d.m.Y' }}</p> <p><strong>{% trans "Resubmission" %}:</strong> {{ risk.follow_up|date:'d.m.Y' }}</p>
</div> </div>
</div> </div>
<!-- Risikobewertung --> <!-- Risikobewertung -->
<section class="hero has-text-centered is-small"> <section class="hero has-text-centered is-small">
<div class="hero-body"> <div class="hero-body">
<p class="title">Risikobewertung</p> <p class="title">{% trans "Risk assessment" %}</p>
</div> </div>
</section> </section>
<div class="columns is-multiline"> <div class="columns is-multiline">
@ -60,13 +81,13 @@
<!-- Bruttorisiko --> <!-- Bruttorisiko -->
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered is-flex"> <div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered is-flex">
<div class="box is-flex is-flex-direction-column is-flex-grow-1"> <div class="box is-flex is-flex-direction-column is-flex-grow-1">
<h4>Brutto (vor Maßnahmen)</h4> <h4>{% trans "Gross (before measures)" %}</h4>
<div class="columns is-multiline"> <div class="columns is-multiline">
<!-- Eintrittswahrscheinlichkeit --> <!-- Eintrittswahrscheinlichkeit -->
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered"> <div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.likelihood|likelihood_class }}" type="button" aria-label="{% trans 'Likelihood' as likelihood_long_name %}"> <button class="risk-chip {{ risk.likelihood|likelihood_class }}" type="button" aria-label="{% trans 'Likelihood' as likelihood_long_name %}">
<span class="chip-head">Eintrittswahrscheinlichkeit</span> <span class="chip-head">{% trans "Probability of occurrence" %}</span>
<span class="chip-id">{{ risk.likelihood }}</span> <span class="chip-id">{{ risk.likelihood }}</span>
<span class="chip-label">{{ risk.get_likelihood_display }}</span> <span class="chip-label">{{ risk.get_likelihood_display }}</span>
</button> </button>
@ -75,7 +96,7 @@
<!-- Schadensausmaß --> <!-- Schadensausmaß -->
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered"> <div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.impact|impact_class }}" type="button" aria-label="{% trans 'Impact' as impact_long_name %}"> <button class="risk-chip {{ risk.impact|impact_class }}" type="button" aria-label="{% trans 'Impact' as impact_long_name %}">
<span class="chip-head">Schadensausmaß</span> <span class="chip-head">{% trans "Extent of damage" %}</span>
<span class="chip-id">{{ risk.impact }}</span> <span class="chip-id">{{ risk.impact }}</span>
<span class="chip-label">{{ risk.get_impact_display }}</span> <span class="chip-label">{{ risk.get_impact_display }}</span>
</button> </button>
@ -84,7 +105,7 @@
<!-- Stufe --> <!-- Stufe -->
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered"> <div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.level|level_class }}" type="button" aria-label="{% trans 'Level' %}"> <button class="risk-chip {{ risk.level|level_class }}" type="button" aria-label="{% trans 'Level' %}">
<span class="chip-head">Stufe</span> <span class="chip-head">{% trans "Level" %}</span>
<span class="chip-id">{{ risk.level|level_id }}</span> <span class="chip-id">{{ risk.level|level_id }}</span>
<span class="chip-label">{{ risk.level }}</span> <span class="chip-label">{{ risk.level }}</span>
</button> </button>
@ -93,7 +114,7 @@
<!-- Score --> <!-- Score -->
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered"> <div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.score|score_class }}" type="button" aria-label="{% trans 'Score' %}"> <button class="risk-chip {{ risk.score|score_class }}" type="button" aria-label="{% trans 'Score' %}">
<span class="chip-head">Score</span> <span class="chip-head">{% trans "Score" %}</span>
<span class="chip-id">{{ risk.score }}</span> <span class="chip-id">{{ risk.score }}</span>
<span class="chip-label">Score (max. 20)</span> <span class="chip-label">Score (max. 20)</span>
</button> </button>
@ -106,13 +127,13 @@
<!-- Nettorisiko --> <!-- Nettorisiko -->
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered is-flex"> <div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered is-flex">
<div class="box is-flex is-flex-direction-column is-flex-grow-1"> <div class="box is-flex is-flex-direction-column is-flex-grow-1">
<h4>Netto (nach Maßnahmen)</h4> <h4>{% trans "Net (after measures)" %}</h4>
<div class="columns is-multiline"> <div class="columns is-multiline">
{% if risk.residual_risk %} {% if risk.residual_risk %}
<!-- Eintrittswahrscheinlichkeit --> <!-- Eintrittswahrscheinlichkeit -->
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered"> <div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.residual_risk.likelihood|likelihood_class }}" type="button" aria-label="{% trans 'Likelihood' as likelihood_long_name %}"> <button class="risk-chip {{ risk.residual_risk.likelihood|likelihood_class }}" type="button" aria-label="{% trans 'Likelihood' as likelihood_long_name %}">
<span class="chip-head">Eintrittswahrscheinlichkeit</span> <span class="chip-head">{% trans "Probability of occurrence" %}</span>
<span class="chip-id">{{ risk.residual_risk.likelihood }}</span> <span class="chip-id">{{ risk.residual_risk.likelihood }}</span>
<span class="chip-label">{{ risk.residual_risk.get_likelihood_display }}</span> <span class="chip-label">{{ risk.residual_risk.get_likelihood_display }}</span>
</button> </button>
@ -121,7 +142,7 @@
<!-- Schadensausmaß --> <!-- Schadensausmaß -->
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered"> <div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.residual_risk.impact|impact_class }}" type="button" aria-label="{% trans 'Impact' as impact_long_name %}"> <button class="risk-chip {{ risk.residual_risk.impact|impact_class }}" type="button" aria-label="{% trans 'Impact' as impact_long_name %}">
<span class="chip-head">Schadensausmaß</span> <span class="chip-head">{% trans "Extent of damage" %}</span>
<span class="chip-id">{{ risk.residual_risk.impact }}</span> <span class="chip-id">{{ risk.residual_risk.impact }}</span>
<span class="chip-label">{{ risk.residual_risk.get_impact_display }}</span> <span class="chip-label">{{ risk.residual_risk.get_impact_display }}</span>
</button> </button>
@ -130,7 +151,7 @@
<!-- Stufe --> <!-- Stufe -->
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered"> <div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.residual_risk.level|level_class }}" type="button" aria-label="{% trans 'Level' %}"> <button class="risk-chip {{ risk.residual_risk.level|level_class }}" type="button" aria-label="{% trans 'Level' %}">
<span class="chip-head">Stufe</span> <span class="chip-head">{% trans "Level" %}</span>
<span class="chip-id">{{ risk.residual_risk.level|level_id }}</span> <span class="chip-id">{{ risk.residual_risk.level|level_id }}</span>
<span class="chip-label">{{ risk.residual_risk.level }}</span> <span class="chip-label">{{ risk.residual_risk.level }}</span>
</button> </button>
@ -139,15 +160,27 @@
<!-- Score --> <!-- Score -->
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered" aria-label="{% trans 'Score' %}"> <div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered" aria-label="{% trans 'Score' %}">
<button class="risk-chip {{ risk.residual_risk.score|score_class }}" type="button"> <button class="risk-chip {{ risk.residual_risk.score|score_class }}" type="button">
<span class="chip-head">Score</span> <span class="chip-head">{% trans "Score" %}</span>
<span class="chip-id">{{ risk.residual_risk.score }}</span> <span class="chip-id">{{ risk.residual_risk.score }}</span>
<span class="chip-label">(max. 20)</span> <span class="chip-label">(max. 20)</span>
</button> </button>
</div> </div>
{% else %} {% else %}
<p class="has-text-grey">Noch kein Nettorisiko erfasst.</p> <p class="has-text-grey">{% trans "No net risk recorded yet." %}</p>
{% endif %}
</div><br>
{% if request.user.is_staff or risk.owner_id == request.user.id %}
<form method="post" action="{% url 'risks:update_residual_review' risk.id %}">
{% csrf_token %}
<label class="checkbox">
<input type="checkbox" name="review_required" value="on" {% if risk.residual_risk and risk.residual_risk.review_required %}checked{% endif %}>
{% trans "Review required" %}
</label>
<button class="button is-small is-link" style="margin-left:.5rem;">
{% trans "Save" %}
</button>
</form>
{% endif %} {% endif %}
</div>
</div> </div>
</div> <!-- Ende Nettorisiko --> </div> <!-- Ende Nettorisiko -->
@ -158,18 +191,18 @@
<!-- Maßnahmen --> <!-- Maßnahmen -->
<div class="card"> <div class="card">
<header class="card-header"> <header class="card-header">
<p class="card-header-title">Maßnahmen</p> <p class="card-header-title">{% trans "Measures" %}</p>
</header> </header>
<div class="card-content"> <div class="card-content">
{% if risk.controls.exists %} {% if risk.controls.exists %}
<table class="table is-striped is-hoverable is-fullwidth"> <table class="table is-striped is-hoverable is-fullwidth">
<thead> <thead>
<tr> <tr>
<th>Titel</th> <th>{% trans "Title" %}</th>
<th>Status</th> <th>{% trans "Status" %}</th>
<th>Frist</th> <th>{% trans "Deadline" %}</th>
<th>Verantwortlicher</th> <th>{% trans "Responsible" %}</th>
<th>Link</th> <th>{% trans "Link" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -203,7 +236,7 @@
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
<p class="has-text-grey">Keine Maßnahmen erfasst.</p> <p class="has-text-grey">{% trans "No measures recorded." %}</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -212,16 +245,16 @@
<!-- Vorfälle --> <!-- Vorfälle -->
<div class="card"> <div class="card">
<header class="card-header"> <header class="card-header">
<p class="card-header-title">Vorfälle</p> <p class="card-header-title">{% trans "Incidents" %}</p>
</header> </header>
<div class="card-content"> <div class="card-content">
{% if risk.incidents.exists %} {% if risk.incidents.exists %}
<table class="table is-striped is-fullwidth"> <table class="table is-striped is-fullwidth">
<thead> <thead>
<tr> <tr>
<th>Vorfall</th> <th>{% trans "Incident" %}</th>
<th>Status</th> <th>{% trans "Status" %}</th>
<th>gemeldet am</th> <th>{% trans "Reported on" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -235,7 +268,7 @@
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
<p class="has-text-grey">Keine Vorfälle bekannt.</p> <p class="has-text-grey">{% trans "No incidents recorded." %}</p>
{% endif %} {% endif %}
</div> </div>
</div> <!-- Ende Vorfälle --> </div> <!-- Ende Vorfälle -->
@ -243,16 +276,16 @@
<!-- Historie --> <!-- Historie -->
<div class="card"> <div class="card">
<header class="card-header"> <header class="card-header">
<p class="card-header-title">Historie</p> <p class="card-header-title">{% trans "History" %}</p>
</header> </header>
<div class="card-content"> <div class="card-content">
{% if logs %} {% if logs %}
<table class="table is-striped is-fullwidth"> <table class="table is-striped is-fullwidth">
<thead> <thead>
<tr> <tr>
<th>Zeitpunkt</th> <th>{% trans "Time" %}</th>
<th>Benutzer</th> <th>{% trans "User" %}</th>
<th>Aktion</th> <th>{% trans "Action" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -266,7 +299,7 @@
</tbody> </tbody>
</table> </table>
{% else %} {% else %}
<p class="has-text-grey">Keine Historie vorhanden.</p> <p class="has-text-grey">{% trans "No History found." %}</p>
{% endif %} {% endif %}
</div> </div>
</div> <!-- Ende Historie --> </div> <!-- Ende Historie -->

View file

@ -149,7 +149,7 @@
</tr> </tr>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="8" class="has-text-centered has-text-grey">Keine Risiken vorhanden</td> <td colspan="8" class="has-text-centered has-text-grey">{% trans "No risks present" %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -157,5 +157,4 @@
</div> <!-- Ende Risiken --> </div> <!-- Ende Risiken -->
</div> </div>
</section> </section>
{% endblock %} {% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% load i18n %}
{% block crumbs %}
<li><a href="{% url 'risks:notifications' %}">{% trans "Notifications" %}</a></li>
{% endblock %}
{% block content %}
<section class="section">
<div class="container">
<div class="level">
<div class="level-left">
<div class="buttons has-addons">
<a class="button {% if filter == 'unread' %}is-link{% endif %}" href="?filter=unread">{% trans "Unread" %}</a>
<a class="button {% if filter == 'all' %}is-link{% endif %}" href="?filter=all">{% trans "All" %}</a>
</div>
</div>
<div class="level-right">
<form method="post" action="{% url 'risks:notification_mark_all_read' %}">
{% csrf_token %}
<button class="button is-small is-success" {% if not notifications|length %}disabled{% endif %}>
{% trans "Mark all as read" %}
</button>
</form>
</div>
</div>
{% if notifications %}
<div class="box">
{% for n in notifications %}
<article class="media" style="border-bottom:1px solid #eee; padding-bottom:.75rem; margin-bottom:.75rem;">
<div class="media-content">
<p>
{% if not n.read %}
<span class="tag is-warning is-light" style="margin-right:.5rem;">{% trans "New" %}</span>
{% endif %}
{{ n.message }}
</p>
<p class="is-size-7 has-text-grey">{{ n.created_at|date:"d.m.Y H:i" }}</p>
</div>
<div class="media-right">
{% if not n.read %}
<form method="post" action="{% url 'risks:notification_mark_read' n.pk %}">
{% csrf_token %}
<button class="button is-small is-light" title="{% trans 'Mark as read' %}">
<span class="icon"><i class="fas fa-check"></i></span>
</button>
</form>
{% endif %}
</div>
</article>
{% endfor %}
</div>
{% else %}
<p class="has-text-grey">{% trans "No notifications." %}</p>
{% endif %}
</div>
</section>
{% endblock %}