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:
parent
ab01841cf2
commit
ebfcbddd5c
19 changed files with 797 additions and 142 deletions
|
@ -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",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
|
@ -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"
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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")
|
7
risks/context_processors.py
Normal file
7
risks/context_processors.py
Normal 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
31
risks/forms.py
Normal 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"})}
|
19
risks/migrations/0022_alter_notification_options.py
Normal file
19
risks/migrations/0022_alter_notification_options.py
Normal 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",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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()
|
||||||
|
|
|
@ -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"),
|
||||||
]
|
]
|
111
risks/views.py
111
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 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)
|
|
@ -243,4 +243,35 @@ 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%); }
|
|
@ -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" %}';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 -->
|
||||||
|
|
|
@ -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 %}
|
57
templates/risks/notifications.html
Normal file
57
templates/risks/notifications.html
Normal 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 %}
|
Loading…
Add table
Reference in a new issue