Add risk status and notification preferences

- Introduced a new `status` field to the `Risk` model with choices for "open", "in_progress", "closed", and "review_required".
- Created a `NotificationPreference` model to manage user notification settings for various events related to risks, controls, residual risks, reviews, users, and incidents.
- Updated the admin interface to include `NotificationPreference` inline with the `User` admin.
- Enhanced signal handlers to send notifications based on user preferences for created, updated, and deleted events for users, risks, controls, and incidents.
- Modified the `check_risk_followups` utility function to update risk status and create notifications for follow-ups.
- Updated serializers and views to accommodate the new `status` field and improved risk listing functionality.
- Added a new section in the risk detail template to display related incidents.
- Removed the unused statistics view from URLs.
This commit is contained in:
Kevin Heyer 2025-09-10 11:54:08 +02:00
parent 79c977f03b
commit ab01841cf2
14 changed files with 910 additions and 368 deletions

View file

@ -216,5 +216,5 @@ if SSO_ENABLED:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
CRONJOBS = [ CRONJOBS = [
("0 8 * * *", "risks.utils.check_risk_followups"), ("0 8 * * *", "risks.utils.check_risk_followups", ">> /var/log/wira_followups.log 2>&1"),
] ]

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 08:16+0200\n" "POT-Creation-Date: 2025-09-10 11:18+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"
@ -20,15 +20,41 @@ msgstr "Verwaltung"
msgid "Admin" msgid "Admin"
msgstr "Admin" msgstr "Admin"
#: risks/admin.py:13 #: risks/admin.py:15 risks/models.py:36 templates/risks/dashboard.html:75
#: templates/risks/dashboard.html:80 templates/risks/dashboard.html:85
#: templates/risks/list_risks.html:76
msgid "Risks"
msgstr "Risiken"
#: risks/admin.py:16 risks/models.py:190 templates/risks/list_risks.html:37
msgid "Controls"
msgstr "Maßnahmen"
#: risks/admin.py:17
msgid "Residual risks"
msgstr "Restrisiken"
#: risks/admin.py:18
msgid "Reviews"
msgstr "Prüfung"
#: risks/admin.py:19 risks/models.py:258
msgid "Incidents"
msgstr "Vorfälle"
#: risks/admin.py:20
msgid "Users"
msgstr "Benutzer"
#: risks/admin.py:26
msgid "SSO Information" msgid "SSO Information"
msgstr "SSO-Informationen" msgstr "SSO-Informationen"
#: risks/admin.py:20 #: risks/admin.py:35
msgid "Risks Owned" msgid "Risks Owned"
msgstr "Eigene Risiken" msgstr "Eigene Risiken"
#: risks/admin.py:24 #: risks/admin.py:39
msgid "Controls Responsible" msgid "Controls Responsible"
msgstr "Verantwortlich für Maßnahmen" msgstr "Verantwortlich für Maßnahmen"
@ -41,160 +67,241 @@ msgstr "Risikomanagement"
msgid "Risk" msgid "Risk"
msgstr "Risiko" msgstr "Risiko"
#: risks/models.py:36 templates/risks/dashboard.html:75
#: templates/risks/dashboard.html:80 templates/risks/dashboard.html:85
#: templates/risks/list_risks.html:76
msgid "Risks"
msgstr "Risiken"
#: risks/models.py:39 #: risks/models.py:39
msgid "Very low occurs less than once every 5 years" msgid "Open"
msgstr "Sehr niedrig tritt seltener als einmal in fünf Jahren auf" msgstr "Offen"
#: risks/models.py:40 #: risks/models.py:40 risks/models.py:262
msgid "Low once every 15 years"
msgstr "Niedrig einmal in 15 Jahren"
#: risks/models.py:41
msgid "Likely once per year or more"
msgstr "Wahrscheinlich einmal pro Jahr oder öfter"
#: risks/models.py:42
msgid "Very likely multiple times per year/monthly"
msgstr "Sehr wahrscheinlich mehrmals pro Jahr/monatlich"
#: risks/models.py:45
msgid "Very Low (< 1,000 € minor operational impact)"
msgstr "Sehr Gering (< 1.000 € geringe betriebliche Auswirkungen)"
#: risks/models.py:46
msgid "Low (1,0005,000 € local impact)"
msgstr "Gering (1.0005.000 € lokale Auswirkungen)"
#: risks/models.py:47
msgid "High (5,00015,000 € team-level impact)"
msgstr "Hoch (5.00015.000 € Auswirkungen auf Teamebene)"
#: risks/models.py:48
msgid "Severe (50,000100,000 € regional impact)"
msgstr "Schwerwiegend (50.000100.000 € regionale Auswirkungen)"
#: risks/models.py:49
msgid "Critical (> 100,000 € existential threat)"
msgstr "Kritisch (> 100.000 € existenzielle Bedrohung)"
#: risks/models.py:52 templates/risks/dashboard.html:74
msgid "Confidentiality"
msgstr "Vertraulichkeit"
#: risks/models.py:53 templates/risks/dashboard.html:79
msgid "Integrity"
msgstr "Integrität"
#: risks/models.py:54 templates/risks/dashboard.html:84
msgid "Availability"
msgstr "Verfügbarkeit"
#: risks/models.py:58 risks/models.py:186 risks/models.py:251
msgid "Title"
msgstr "Titel"
#: risks/models.py:59 risks/models.py:252
msgid "Description"
msgstr "Beschreibung"
#: risks/models.py:60
msgid "Asset"
msgstr "Asset"
#: risks/models.py:61
msgid "Process"
msgstr "Prozess"
#: risks/models.py:62 templates/risks/list_risks.html:85
msgid "Category"
msgstr "Kategorie"
#: risks/models.py:63
msgid "Created at"
msgstr "Erstellt am"
#: risks/models.py:64
msgid "Updated at"
msgstr "Aktualisiert am"
#: risks/models.py:119
msgid "Residual Risk"
msgstr "Restrisiko"
#: risks/models.py:120
msgid "Residual Risks"
msgstr "Restrisiken"
#: risks/models.py:175
msgid "Control"
msgstr "Maßnahme"
#: risks/models.py:176 templates/risks/list_risks.html:37
msgid "Controls"
msgstr "Maßnahmen"
#: risks/models.py:179
msgid "Planned"
msgstr "Geplant"
#: risks/models.py:180
msgid "In progress"
msgstr "In Bearbeitung"
#: risks/models.py:181
msgid "Completed"
msgstr "Abgeschlossen"
#: risks/models.py:182
msgid "Verified"
msgstr "Verifiziert"
#: risks/models.py:183
msgid "Rejected"
msgstr "Abgelehnt"
#: risks/models.py:212
msgid "Auditlog"
msgstr "Audit-Log"
#: risks/models.py:213
msgid "Auditlogs"
msgstr "Audit-Logs"
#: risks/models.py:243
msgid "Incident"
msgstr "Vorfall"
#: risks/models.py:244
msgid "Incidents"
msgstr "Vorfälle"
#: risks/models.py:247
msgid "Opened"
msgstr "Eröffnet"
#: risks/models.py:248
msgid "In Progress" msgid "In Progress"
msgstr "In Bearbeitung" msgstr "In Bearbeitung"
#: risks/models.py:249 #: risks/models.py:41 risks/models.py:263
msgid "Closed" msgid "Closed"
msgstr "Geschlossen" msgstr "Geschlossen"
#: risks/models.py:253 #: risks/models.py:42
msgid "Review required"
msgstr "Prüfung nötig"
#: risks/models.py:45
msgid "Very low occurs less than once every 5 years"
msgstr "Sehr niedrig tritt seltener als einmal in fünf Jahren auf"
#: risks/models.py:46
msgid "Low once every 15 years"
msgstr "Niedrig einmal in 15 Jahren"
#: risks/models.py:47
msgid "Likely once per year or more"
msgstr "Wahrscheinlich einmal pro Jahr oder öfter"
#: risks/models.py:48
msgid "Very likely multiple times per year/monthly"
msgstr "Sehr wahrscheinlich mehrmals pro Jahr/monatlich"
#: risks/models.py:51
msgid "Very Low (< 1,000 € minor operational impact)"
msgstr "Sehr Gering (< 1.000 € geringe betriebliche Auswirkungen)"
#: risks/models.py:52
msgid "Low (1,0005,000 € local impact)"
msgstr "Gering (1.0005.000 € lokale Auswirkungen)"
#: risks/models.py:53
msgid "High (5,00015,000 € team-level impact)"
msgstr "Hoch (5.00015.000 € Auswirkungen auf Teamebene)"
#: risks/models.py:54
msgid "Severe (50,000100,000 € regional impact)"
msgstr "Schwerwiegend (50.000100.000 € regionale Auswirkungen)"
#: risks/models.py:55
msgid "Critical (> 100,000 € existential threat)"
msgstr "Kritisch (> 100.000 € existenzielle Bedrohung)"
#: risks/models.py:58 templates/risks/dashboard.html:74
msgid "Confidentiality"
msgstr "Vertraulichkeit"
#: risks/models.py:59 templates/risks/dashboard.html:79
msgid "Integrity"
msgstr "Integrität"
#: risks/models.py:60 templates/risks/dashboard.html:84
msgid "Availability"
msgstr "Verfügbarkeit"
#: risks/models.py:64 risks/models.py:200 risks/models.py:265
msgid "Title"
msgstr "Titel"
#: risks/models.py:65 risks/models.py:266
msgid "Description"
msgstr "Beschreibung"
#: risks/models.py:66
msgid "Asset"
msgstr "Asset"
#: risks/models.py:67
msgid "Process"
msgstr "Prozess"
#: risks/models.py:68 templates/risks/list_risks.html:85
msgid "Category"
msgstr "Kategorie"
#: risks/models.py:69
msgid "Created at"
msgstr "Erstellt am"
#: risks/models.py:70
msgid "Updated at"
msgstr "Aktualisiert am"
#: risks/models.py:73
msgid "Status"
msgstr "Status"
#: risks/models.py:133
msgid "Residual Risk"
msgstr "Restrisiko"
#: risks/models.py:134
msgid "Residual Risks"
msgstr "Restrisiken"
#: risks/models.py:189
msgid "Control"
msgstr "Maßnahme"
#: risks/models.py:193
msgid "Planned"
msgstr "Geplant"
#: risks/models.py:194
msgid "In progress"
msgstr "In Bearbeitung"
#: risks/models.py:195
msgid "Completed"
msgstr "Abgeschlossen"
#: risks/models.py:196
msgid "Verified"
msgstr "Verifiziert"
#: risks/models.py:197
msgid "Rejected"
msgstr "Abgelehnt"
#: risks/models.py:226
msgid "Auditlog"
msgstr "Audit-Log"
#: risks/models.py:227
msgid "Auditlogs"
msgstr "Audit-Logs"
#: risks/models.py:257
msgid "Incident"
msgstr "Vorfall"
#: risks/models.py:261
msgid "Opened"
msgstr "Eröffnet"
#: risks/models.py:267
msgid "Date reported" msgid "Date reported"
msgstr "Meldedatum" msgstr "Meldedatum"
#: risks/models.py:255 #: risks/models.py:269
msgid "Reported by" msgid "Reported by"
msgstr "Gemeldet von" msgstr "Gemeldet von"
#: risks/models.py:298
msgid "User"
msgstr "Benutzer"
#: risks/signals.py:57
#, python-brace-format
msgid "User '{u}' created"
msgstr "Benutzer '{u}' erstellte"
#: risks/signals.py:62
#, python-brace-format
msgid "User '{u}' deleted"
msgstr "Benutzer '{u}' löschte"
#: risks/signals.py:70
#, python-brace-format
msgid "Risk '{title}' {state}"
msgstr "Risiko '{title}' {state}"
#: risks/signals.py:72 risks/signals.py:147 risks/signals.py:240
#: risks/signals.py:296
msgid "created"
msgstr "erstellt"
#: risks/signals.py:72 risks/signals.py:147 risks/signals.py:240
#: risks/signals.py:296
msgid "updated"
msgstr "Aktualisiert"
#: risks/signals.py:78
#, python-brace-format
msgid "Risk '{title}' deleted"
msgstr "Risiko '{title}' gelöscht"
#: risks/signals.py:145
#, python-brace-format
msgid "Control '{title}' {state}"
msgstr "Maßnahme '{title}' {state}"
#: risks/signals.py:154
#, python-brace-format
msgid "Control '{title}' deleted"
msgstr "Maßnahme '{title}' gelöscht"
#: risks/signals.py:211
#, python-brace-format
msgid "Review required for risk '{t}' due to control change"
msgstr "Prüfung nötig für: '{t}', da Maßnahmen geändert wurden"
#: risks/signals.py:230
#, python-brace-format
msgid "Review required for risk '{t}'"
msgstr "Prüfung benötigt für Risiko '{t}'"
#: risks/signals.py:235
#, python-brace-format
msgid "Review completed for risk '{t}'"
msgstr "Prüfung Abgeschlossen für Risiko '{t}'"
#: risks/signals.py:239
#, python-brace-format
msgid "Residual risk {state} for '{t}'"
msgstr "Restrisiko {state} für '{t}'"
#: risks/signals.py:244
#, python-brace-format
msgid "Residual risk deleted for '{t}'"
msgstr "Restrisiko für '{t}' gelöscht"
#: risks/signals.py:296
msgid "Incident '{t}' {s}"
msgstr "Vorfälle '{t}' {s}"
#: risks/signals.py:301
#, python-brace-format
msgid "Incident '{t}' deleted"
msgstr "Vorfall '{t}' gelöscht"
#: risks/utils.py:48
#, python-brace-format
msgid "Follow-up reached: review required for risk '{t}'"
msgstr "Wiedervorlagedatum erreicht: Prüfung nötig für Risiko '{t}'"
#: templates/risks/dashboard.html:9 #: templates/risks/dashboard.html:9
msgid "Dashboard" msgid "Dashboard"
msgstr "Dashboard" msgstr "Dashboard"
@ -227,6 +334,26 @@ 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/list_risks.html:86
msgid "Likelihood"
msgstr "Eintritt"
#: templates/risks/item_risk.html:77 templates/risks/item_risk.html:123
#: templates/risks/list_risks.html:87
msgid "Impact"
msgstr "Schaden"
#: templates/risks/item_risk.html:86 templates/risks/item_risk.html:132
#: templates/risks/list_risks.html:89
msgid "Level"
msgstr "Stufe"
#: templates/risks/item_risk.html:95 templates/risks/item_risk.html:140
#: templates/risks/list_risks.html:88
msgid "Score"
msgstr "Score"
#: templates/risks/list_risks.html:4 #: templates/risks/list_risks.html:4
msgid "Risk analysis" msgid "Risk analysis"
msgstr "Risikoanalyse" msgstr "Risikoanalyse"
@ -247,19 +374,3 @@ 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/list_risks.html:86
msgid "Likelihood"
msgstr "Eintritt"
#: templates/risks/list_risks.html:87
msgid "Impact"
msgstr "Schaden"
#: templates/risks/list_risks.html:88
msgid "Score"
msgstr "Score"
#: templates/risks/list_risks.html:89
msgid "Level"
msgstr "Stufe"

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 08:16+0200\n" "POT-Creation-Date: 2025-09-10 11:18+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"
@ -26,15 +26,41 @@ msgstr ""
msgid "Admin" msgid "Admin"
msgstr "" msgstr ""
#: risks/admin.py:13 #: risks/admin.py:15 risks/models.py:36 templates/risks/dashboard.html:75
msgid "SSO Information" #: templates/risks/dashboard.html:80 templates/risks/dashboard.html:85
#: templates/risks/list_risks.html:76
msgid "Risks"
msgstr ""
#: risks/admin.py:16 risks/models.py:190 templates/risks/list_risks.html:37
msgid "Controls"
msgstr ""
#: risks/admin.py:17
msgid "Residual risks"
msgstr ""
#: risks/admin.py:18
msgid "Reviews"
msgstr ""
#: risks/admin.py:19 risks/models.py:258
msgid "Incidents"
msgstr "" msgstr ""
#: risks/admin.py:20 #: risks/admin.py:20
msgid "Users"
msgstr ""
#: risks/admin.py:26
msgid "SSO Information"
msgstr ""
#: risks/admin.py:35
msgid "Risks Owned" msgid "Risks Owned"
msgstr "" msgstr ""
#: risks/admin.py:24 #: risks/admin.py:39
msgid "Controls Responsible" msgid "Controls Responsible"
msgstr "" msgstr ""
@ -47,160 +73,242 @@ msgstr ""
msgid "Risk" msgid "Risk"
msgstr "" msgstr ""
#: risks/models.py:36 templates/risks/dashboard.html:75
#: templates/risks/dashboard.html:80 templates/risks/dashboard.html:85
#: templates/risks/list_risks.html:76
msgid "Risks"
msgstr ""
#: risks/models.py:39 #: risks/models.py:39
msgid "Very low occurs less than once every 5 years" msgid "Open"
msgstr "" msgstr ""
#: risks/models.py:40 #: risks/models.py:40 risks/models.py:262
msgid "Low once every 15 years"
msgstr ""
#: risks/models.py:41
msgid "Likely once per year or more"
msgstr ""
#: risks/models.py:42
msgid "Very likely multiple times per year/monthly"
msgstr ""
#: risks/models.py:45
msgid "Very Low (< 1,000 € minor operational impact)"
msgstr ""
#: risks/models.py:46
msgid "Low (1,0005,000 € local impact)"
msgstr ""
#: risks/models.py:47
msgid "High (5,00015,000 € team-level impact)"
msgstr ""
#: risks/models.py:48
msgid "Severe (50,000100,000 € regional impact)"
msgstr ""
#: risks/models.py:49
msgid "Critical (> 100,000 € existential threat)"
msgstr ""
#: risks/models.py:52 templates/risks/dashboard.html:74
msgid "Confidentiality"
msgstr ""
#: risks/models.py:53 templates/risks/dashboard.html:79
msgid "Integrity"
msgstr ""
#: risks/models.py:54 templates/risks/dashboard.html:84
msgid "Availability"
msgstr ""
#: risks/models.py:58 risks/models.py:186 risks/models.py:251
msgid "Title"
msgstr ""
#: risks/models.py:59 risks/models.py:252
msgid "Description"
msgstr ""
#: risks/models.py:60
msgid "Asset"
msgstr ""
#: risks/models.py:61
msgid "Process"
msgstr ""
#: risks/models.py:62 templates/risks/list_risks.html:85
msgid "Category"
msgstr ""
#: risks/models.py:63
msgid "Created at"
msgstr ""
#: risks/models.py:64
msgid "Updated at"
msgstr ""
#: risks/models.py:119
msgid "Residual Risk"
msgstr ""
#: risks/models.py:120
msgid "Residual Risks"
msgstr ""
#: risks/models.py:175
msgid "Control"
msgstr ""
#: risks/models.py:176 templates/risks/list_risks.html:37
msgid "Controls"
msgstr ""
#: risks/models.py:179
msgid "Planned"
msgstr ""
#: risks/models.py:180
msgid "In progress"
msgstr ""
#: risks/models.py:181
msgid "Completed"
msgstr ""
#: risks/models.py:182
msgid "Verified"
msgstr ""
#: risks/models.py:183
msgid "Rejected"
msgstr ""
#: risks/models.py:212
msgid "Auditlog"
msgstr ""
#: risks/models.py:213
msgid "Auditlogs"
msgstr ""
#: risks/models.py:243
msgid "Incident"
msgstr ""
#: risks/models.py:244
msgid "Incidents"
msgstr ""
#: risks/models.py:247
msgid "Opened"
msgstr ""
#: risks/models.py:248
msgid "In Progress" msgid "In Progress"
msgstr "" msgstr ""
#: risks/models.py:249 #: risks/models.py:41 risks/models.py:263
msgid "Closed" msgid "Closed"
msgstr "" msgstr ""
#: risks/models.py:253 #: risks/models.py:42
msgid "Review required"
msgstr ""
#: risks/models.py:45
msgid "Very low occurs less than once every 5 years"
msgstr ""
#: risks/models.py:46
msgid "Low once every 15 years"
msgstr ""
#: risks/models.py:47
msgid "Likely once per year or more"
msgstr ""
#: risks/models.py:48
msgid "Very likely multiple times per year/monthly"
msgstr ""
#: risks/models.py:51
msgid "Very Low (< 1,000 € minor operational impact)"
msgstr ""
#: risks/models.py:52
msgid "Low (1,0005,000 € local impact)"
msgstr ""
#: risks/models.py:53
msgid "High (5,00015,000 € team-level impact)"
msgstr ""
#: risks/models.py:54
msgid "Severe (50,000100,000 € regional impact)"
msgstr ""
#: risks/models.py:55
msgid "Critical (> 100,000 € existential threat)"
msgstr ""
#: risks/models.py:58 templates/risks/dashboard.html:74
msgid "Confidentiality"
msgstr ""
#: risks/models.py:59 templates/risks/dashboard.html:79
msgid "Integrity"
msgstr ""
#: risks/models.py:60 templates/risks/dashboard.html:84
msgid "Availability"
msgstr ""
#: risks/models.py:64 risks/models.py:200 risks/models.py:265
msgid "Title"
msgstr ""
#: risks/models.py:65 risks/models.py:266
msgid "Description"
msgstr ""
#: risks/models.py:66
msgid "Asset"
msgstr ""
#: risks/models.py:67
msgid "Process"
msgstr ""
#: risks/models.py:68 templates/risks/list_risks.html:85
msgid "Category"
msgstr ""
#: risks/models.py:69
msgid "Created at"
msgstr ""
#: risks/models.py:70
msgid "Updated at"
msgstr ""
#: risks/models.py:73
msgid "Status"
msgstr ""
#: risks/models.py:133
msgid "Residual Risk"
msgstr ""
#: risks/models.py:134
msgid "Residual Risks"
msgstr ""
#: risks/models.py:189
msgid "Control"
msgstr ""
#: risks/models.py:193
msgid "Planned"
msgstr ""
#: risks/models.py:194
msgid "In progress"
msgstr ""
#: risks/models.py:195
msgid "Completed"
msgstr ""
#: risks/models.py:196
msgid "Verified"
msgstr ""
#: risks/models.py:197
msgid "Rejected"
msgstr ""
#: risks/models.py:226
msgid "Auditlog"
msgstr ""
#: risks/models.py:227
msgid "Auditlogs"
msgstr ""
#: risks/models.py:257
msgid "Incident"
msgstr ""
#: risks/models.py:261
msgid "Opened"
msgstr ""
#: risks/models.py:267
msgid "Date reported" msgid "Date reported"
msgstr "" msgstr ""
#: risks/models.py:255 #: risks/models.py:269
msgid "Reported by" msgid "Reported by"
msgstr "" msgstr ""
#: risks/models.py:298
msgid "User"
msgstr ""
#: risks/signals.py:57
#, python-brace-format
msgid "User '{u}' created"
msgstr ""
#: risks/signals.py:62
#, python-brace-format
msgid "User '{u}' deleted"
msgstr ""
#: risks/signals.py:70
#, python-brace-format
msgid "Risk '{title}' {state}"
msgstr ""
#: risks/signals.py:72 risks/signals.py:147 risks/signals.py:240
#: risks/signals.py:296
msgid "created"
msgstr ""
#: risks/signals.py:72 risks/signals.py:147 risks/signals.py:240
#: risks/signals.py:296
msgid "updated"
msgstr ""
#: risks/signals.py:78
#, python-brace-format
msgid "Risk '{title}' deleted"
msgstr ""
#: risks/signals.py:145
#, python-brace-format
msgid "Control '{title}' {state}"
msgstr ""
#: risks/signals.py:154
#, python-brace-format
msgid "Control '{title}' deleted"
msgstr ""
#: risks/signals.py:211
#, python-brace-format
msgid "Review required for risk '{t}' due to control change"
msgstr ""
#: risks/signals.py:230
#, python-brace-format
msgid "Review required for risk '{t}'"
msgstr ""
#: risks/signals.py:235
#, python-brace-format
msgid "Review completed for risk '{t}'"
msgstr ""
#: risks/signals.py:239
#, python-brace-format
msgid "Residual risk {state} for '{t}'"
msgstr ""
#: risks/signals.py:244
#, python-brace-format
msgid "Residual risk deleted for '{t}'"
msgstr ""
#: risks/signals.py:296
#, python-brace-format
msgid "Incident '{t}' {s}"
msgstr ""
#: risks/signals.py:301
#, python-brace-format
msgid "Incident '{t}' deleted"
msgstr ""
#: risks/utils.py:48
#, python-brace-format
msgid "Follow-up reached: review required for risk '{t}'"
msgstr ""
#: templates/risks/dashboard.html:9 #: templates/risks/dashboard.html:9
msgid "Dashboard" msgid "Dashboard"
msgstr "" msgstr ""
@ -233,6 +341,26 @@ 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/list_risks.html:86
msgid "Likelihood"
msgstr ""
#: templates/risks/item_risk.html:77 templates/risks/item_risk.html:123
#: templates/risks/list_risks.html:87
msgid "Impact"
msgstr ""
#: templates/risks/item_risk.html:86 templates/risks/item_risk.html:132
#: templates/risks/list_risks.html:89
msgid "Level"
msgstr ""
#: templates/risks/item_risk.html:95 templates/risks/item_risk.html:140
#: templates/risks/list_risks.html:88
msgid "Score"
msgstr ""
#: templates/risks/list_risks.html:4 #: templates/risks/list_risks.html:4
msgid "Risk analysis" msgid "Risk analysis"
msgstr "" msgstr ""
@ -253,19 +381,3 @@ msgstr ""
#: templates/risks/list_risks.html:84 #: templates/risks/list_risks.html:84
msgid "Asset / Process" msgid "Asset / Process"
msgstr "" msgstr ""
#: templates/risks/list_risks.html:86
msgid "Likelihood"
msgstr ""
#: templates/risks/list_risks.html:87
msgid "Impact"
msgstr ""
#: templates/risks/list_risks.html:88
msgid "Score"
msgstr ""
#: templates/risks/list_risks.html:89
msgid "Level"
msgstr ""

View file

@ -1,12 +1,25 @@
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 User, Risk, ResidualRisk, Control, Incident from .models import Control, Incident, NotificationPreference , Risk, ResidualRisk, User
admin.site.site_header = _("Administration") admin.site.site_header = _("Administration")
admin.site.site_title = _("Admin") admin.site.site_title = _("Admin")
admin.site.index_title = _("Administration") admin.site.index_title = _("Administration")
class NotificationPreferenceInline(admin.StackedInline):
model = NotificationPreference
can_delete = False
extra = 0
fieldsets = (
(_("Risks"), {"fields": ("risk_created","risk_updated","risk_deleted")}),
(_("Controls"), {"fields": ("control_created","control_updated","control_deleted")}),
(_("Residual risks"), {"fields": ("residual_created","residual_updated","residual_deleted")}),
(_("Reviews"), {"fields": ("review_required","review_completed")}),
(_("Incidents"), {"fields": ("incident_created","incident_updated","incident_deleted")}),
(_("Users"), {"fields": ("user_created","user_deleted")}),
)
@admin.register(User) @admin.register(User)
class UserAdmin(BaseUserAdmin): class UserAdmin(BaseUserAdmin):
fieldsets = BaseUserAdmin.fieldsets + ( fieldsets = BaseUserAdmin.fieldsets + (
@ -15,6 +28,8 @@ class UserAdmin(BaseUserAdmin):
list_display = ("username", "email", "is_staff", "is_superuser", "is_sso_user", list_display = ("username", "email", "is_staff", "is_superuser", "is_sso_user",
"owned_risks_count", "responsible_controls_count") "owned_risks_count", "responsible_controls_count")
inlines = [NotificationPreferenceInline]
def owned_risks_count(self, obj): def owned_risks_count(self, obj):
return obj.risks_owned.count() return obj.risks_owned.count()
owned_risks_count.short_description = _("Risks Owned") owned_risks_count.short_description = _("Risks Owned")
@ -44,6 +59,7 @@ class RiskAdmin(admin.ModelAdmin):
list_display = ( list_display = (
"title", "title",
"owner_name", "owner_name",
"status",
"score", "score",
"level", "level",
"likelihood", "likelihood",
@ -56,7 +72,7 @@ class RiskAdmin(admin.ModelAdmin):
return "-" return "-"
return obj.owner.get_full_name() or obj.owner.username return obj.owner.get_full_name() or obj.owner.username
list_filter = ("level", "likelihood", "impact", "owner") list_filter = ("status", "level", "likelihood", "impact", "owner")
search_fields = ("title", "asset", "process", "category") search_fields = ("title", "asset", "process", "category")
inlines = [ResidualRiskInline, ControlRisksInline] inlines = [ResidualRiskInline, ControlRisksInline]
@ -118,3 +134,4 @@ class IncidentAdmin(admin.ModelAdmin):
def delete_model(self, request, obj): def delete_model(self, request, obj):
obj._changed_by = request.user obj._changed_by = request.user
super().delete_model(request, obj) super().delete_model(request, obj)

View file

@ -0,0 +1,71 @@
# Generated by Django 5.2.6 on 2025-09-10 09:18
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("risks", "0020_alter_residualrisk_impact_alter_risk_impact"),
]
operations = [
migrations.AddField(
model_name="risk",
name="status",
field=models.CharField(
choices=[
("open", "Open"),
("in_progress", "In Progress"),
("closed", "Closed"),
("review_required", "Review required"),
],
db_index=True,
default="open",
max_length=20,
verbose_name="Status",
),
),
migrations.CreateModel(
name="NotificationPreference",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("risk_created", models.BooleanField(default=True)),
("risk_updated", models.BooleanField(default=True)),
("risk_deleted", models.BooleanField(default=True)),
("control_created", models.BooleanField(default=True)),
("control_updated", models.BooleanField(default=True)),
("control_deleted", models.BooleanField(default=True)),
("residual_created", models.BooleanField(default=True)),
("residual_updated", models.BooleanField(default=True)),
("residual_deleted", models.BooleanField(default=True)),
("review_required", models.BooleanField(default=True)),
("review_completed", models.BooleanField(default=True)),
("user_created", models.BooleanField(default=False)),
("user_deleted", models.BooleanField(default=False)),
("incident_created", models.BooleanField(default=True)),
("incident_updated", models.BooleanField(default=True)),
("incident_deleted", models.BooleanField(default=True)),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="notification_preference",
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
],
),
]

View file

@ -35,6 +35,12 @@ class Risk(models.Model):
verbose_name = _("Risk") verbose_name = _("Risk")
verbose_name_plural = _("Risks") verbose_name_plural = _("Risks")
STATUS_CHOICES = [
("open", _("Open")),
("in_progress", _("In Progress")),
("closed", _("Closed")),
("review_required", _("Review required")),
]
LIKELIHOOD_CHOICES = [ LIKELIHOOD_CHOICES = [
(1, _("Very low occurs less than once every 5 years")), (1, _("Very low occurs less than once every 5 years")),
(2, _("Low once every 15 years")), (2, _("Low once every 15 years")),
@ -63,6 +69,14 @@ class Risk(models.Model):
created_at = models.DateTimeField(_("Created at"), auto_now_add=True) created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
updated_at = models.DateTimeField(_("Updated at"), auto_now=True) updated_at = models.DateTimeField(_("Updated at"), auto_now=True)
status = models.CharField(
_("Status"),
max_length=20,
choices=STATUS_CHOICES,
default="open",
db_index=True,
)
# CIA Protection Goals # CIA Protection Goals
cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True) cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True)
@ -272,3 +286,51 @@ class Notification(models.Model):
def __str__(self): def __str__(self):
user_display = self.user.username if self.user else "System" user_display = self.user.username if self.user else "System"
return f"{user_display}: {self.message[:50]}..." return f"{user_display}: {self.message[:50]}..."
class NotificationPreference(models.Model):
"""
Wich events does the user want to receive as notifications?
"""
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="notification_preference",
verbose_name=_("User"),
)
# Risks
risk_created = models.BooleanField(default=True)
risk_updated = models.BooleanField(default=True)
risk_deleted = models.BooleanField(default=True)
# Controls
control_created = models.BooleanField(default=True)
control_updated = models.BooleanField(default=True)
control_deleted = models.BooleanField(default=True)
# Residual risks
residual_created = models.BooleanField(default=True)
residual_updated = models.BooleanField(default=True)
residual_deleted = models.BooleanField(default=True)
# Reviews
review_required = models.BooleanField(default=True)
review_completed = models.BooleanField(default=True)
# Users
user_created = models.BooleanField(default=False)
user_deleted = models.BooleanField(default=False)
# Incidents
incident_created = models.BooleanField(default=True)
incident_updated = models.BooleanField(default=True)
incident_deleted = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Prefs({self.user})"
def should_notify(self, event_code: str) -> bool:
return bool(getattr(self, event_code, False))

View file

@ -53,6 +53,7 @@ class RiskSerializer(serializers.ModelSerializer):
"impact", "impact",
"score", "score",
"level", "level",
"status",
"owner", "owner",
"follow_up", "follow_up",
"cia", "cia",

View file

@ -1,15 +1,19 @@
from datetime import date, datetime from datetime import date, datetime
from django.contrib.auth import get_user_model
from django.db.models import Model from django.db.models import Model
from django.db.models.signals import post_save, post_delete, m2m_changed from django.db.models.signals import post_save, post_delete, m2m_changed
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from .audit_context import get_current_user from .audit_context import get_current_user
from .models import Control, Risk, ResidualRisk, AuditLog, Incident from .models import Control, Risk, ResidualRisk, AuditLog, Incident, Notification, NotificationPreference
from .utils import model_diff from .utils import model_diff
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# General definitions # General definitions
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
User = get_user_model()
def serialize_value(value): def serialize_value(value):
if isinstance(value, Model): if isinstance(value, Model):
return value.pk # oder str(value), wenn du mehr Infos willst return value.pk # oder str(value), wenn du mehr Infos willst
@ -17,9 +21,65 @@ def serialize_value(value):
return value.isoformat() return value.isoformat()
return value return value
def _pref(user: User) -> NotificationPreference | None:
if not user:
return None
pref = getattr(user, "notification_preference", None)
if not pref:
pref = NotificationPreference.objects.create(user=user)
return pref
def _notify(users, message: str, event_code: str):
"""legt Notification für alle users an, die dieses Event wünschen."""
for u in set(filter(None, users)):
pref = _pref(u)
if pref and pref.should_notify(event_code):
Notification.objects.create(user=u, message=message)
def _risk_stakeholders(risk: Risk):
"""Risikoeigner + alle Verantwortlichen zugehöriger Controls."""
owners = [risk.owner] if risk.owner else []
responsibles = list(
User.objects.filter(responsible_controls__risks=risk).distinct()
)
return set(owners + responsibles)
# ---------------------------------------------------------------------------
# Incidents
# ---------------------------------------------------------------------------
@receiver(post_save, sender=User)
def user_saved(sender, instance: User, created, **kwargs):
# Prefs automatisch anlegen
_pref(instance)
# An Staff, die dieses Event wollen
if created:
staff = User.objects.filter(is_staff=True, notification_preference__user_created=True)
_notify(staff, _("User '{u}' created").format(u=instance.username), "user_created")
@receiver(post_delete, sender=User)
def user_deleted(sender, instance: User, **kwargs):
staff = User.objects.filter(is_staff=True, notification_preference__user_deleted=True)
_notify(staff, _("User '{u}' deleted").format(u=instance.username), "user_deleted")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Risks # Risks
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@receiver(post_save, sender=Risk)
def risk_saved(sender, instance: Risk, created, **kwargs):
event = "risk_created" if created else "risk_updated"
msg = _("Risk '{title}' {state}").format(
title=instance.title,
state=_("created") if created else _("updated"),
)
_notify([instance.owner], msg, event)
@receiver(post_delete, sender=Risk)
def risk_deleted(sender, instance: Risk, **kwargs):
msg = _("Risk '{title}' deleted").format(title=instance.title)
# Owner existiert evtl. nicht mehr -> kein Notify nötig
if instance.owner:
_notify([instance.owner], msg, "risk_deleted")
@receiver(post_save, sender=Risk) @receiver(post_save, sender=Risk)
def log_risk_save(sender, instance, created, **kwargs): def log_risk_save(sender, instance, created, **kwargs):
if created: if created:
@ -68,6 +128,32 @@ def log_risk_delete(sender, instance, **kwargs):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Controls # Controls
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@receiver(post_save, sender=Control)
def control_saved(sender, instance: Control, created, **kwargs):
# Review-Flag für alle betroffenen Residuals setzen
for risk in instance.risks.all():
resid, created = ResidualRisk.objects.get_or_create(risk=risk)
# Statuswechsel auf Review Required
if not resid.review_required:
resid.review_required = True
resid.save()
if risk.status != "review_required":
Risk.objects.filter(pk=risk.pk).update(status="review_required")
# Notifications
event = "control_created" if created else "control_updated"
msg = _("Control '{title}' {state}").format(
title=instance.title,
state=_("created") if created else _("updated"),
)
stakeholders = {instance.responsible} | set(r.owner for r in instance.risks.all() if r.owner)
_notify(stakeholders, msg, event)
@receiver(post_delete, sender=Control)
def control_deleted(sender, instance: Control, **kwargs):
msg = _("Control '{title}' deleted").format(title=instance.title)
stakeholders = {instance.responsible} | set(r.owner for r in instance.risks.all() if r.owner)
_notify(stakeholders, msg, "control_deleted")
@receiver(post_save, sender=Control) @receiver(post_save, sender=Control)
def log_control_save(sender, instance, created, **kwargs): def log_control_save(sender, instance, created, **kwargs):
@ -112,28 +198,51 @@ def log_control_delete(sender, instance, **kwargs):
) )
@receiver(m2m_changed, sender=Control.risks.through) @receiver(m2m_changed, sender=Control.risks.through)
def control_risks_changed(sender, instance, action, reverse, pk_set, **kwargs): def control_risks_changed(sender, instance: Control, action, reverse, pk_set, **kwargs):
if action in {"post_add", "post_remove", "post_clear"}: if action in {"post_add", "post_remove", "post_clear"}:
if action == "post_clear": affected = instance.risks.all() if not pk_set else Risk.objects.filter(pk__in=pk_set)
affected_risks = instance.risks.all() for risk in affected:
elif pk_set: resid, created = ResidualRisk.objects.get_or_create(risk=risk)
if reverse: if not resid.review_required:
from .models import Risk resid.review_required = True
affected_risks = Risk.objects.filter(pk__in=pk_set) resid.save()
else: if risk.status != "review_required":
affected_risks = Risk.objects.filter(pk__in=pk_set) Risk.objects.filter(pk=risk.pk).update(status="review_required")
else: _notify(_risk_stakeholders(risk), _("Review required for risk '{t}' due to control change").format(t=risk.title), "review_required")
affected_risks = instance.risks.all()
from .models import ResidualRisk
for risk in affected_risks:
residual, _ = ResidualRisk.objects.get_or_create(risk=risk)
residual.review_required = True
residual.save()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Residual risks # Residual risks
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@receiver(post_save, sender=ResidualRisk)
def residual_saved(sender, instance: ResidualRisk, created, **kwargs):
# AuditLog erstellst du bereits anderswo hier Fokus auf Status/Notify
risk = instance.risk
old = None
if not created:
try:
old = ResidualRisk.objects.get(pk=instance.pk)
except ResidualRisk.DoesNotExist:
pass
# Review-Logik: wenn review_required=True -> Risk.status = review_required
if instance.review_required and risk.status != "review_required":
Risk.objects.filter(pk=risk.pk).update(status="review_required")
_notify(_risk_stakeholders(risk), _("Review required for risk '{t}'").format(t=risk.title), "review_required")
elif old and old.review_required and not instance.review_required:
# Review abgeschlossen
if risk.status == "review_required":
Risk.objects.filter(pk=risk.pk).update(status="open")
_notify(_risk_stakeholders(risk), _("Review completed for risk '{t}'").format(t=risk.title), "review_completed")
# Standard-Events
event = "residual_created" if created else "residual_updated"
_notify(_risk_stakeholders(risk), _("Residual risk {state} for '{t}'").format(
state=_("created") if created else _("updated"), t=risk.title), event)
@receiver(post_delete, sender=ResidualRisk)
def residual_deleted(sender, instance: ResidualRisk, **kwargs):
_notify(_risk_stakeholders(instance.risk), _("Residual risk deleted for '{t}'").format(t=instance.risk.title), "residual_deleted")
@receiver(post_save, sender=ResidualRisk) @receiver(post_save, sender=ResidualRisk)
def log_residual_save(sender, instance, created, **kwargs): def log_residual_save(sender, instance, created, **kwargs):
@ -180,6 +289,16 @@ def log_residual_delete(sender, instance, **kwargs):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Incidents # Incidents
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@receiver(post_save, sender=Incident)
def incident_saved(sender, instance: Incident, created, **kwargs):
event = "incident_created" if created else "incident_updated"
stakeholders = set([instance.reported_by]) | set(r.owner for r in instance.related_risks.all() if r.owner)
_notify(stakeholders, _("Incident '{t}' {s}").format(t=instance.title, s=_("created") if created else _("updated")), event)
@receiver(post_delete, sender=Incident)
def incident_deleted(sender, instance: Incident, **kwargs):
stakeholders = set([instance.reported_by]) | set(r.owner for r in instance.related_risks.all() if r.owner)
_notify(stakeholders, _("Incident '{t}' deleted").format(t=instance.title), "incident_deleted")
@receiver(post_save, sender=Incident) @receiver(post_save, sender=Incident)
def log_incident_save(sender, instance, created, **kwargs): def log_incident_save(sender, instance, created, **kwargs):

View file

@ -6,7 +6,6 @@ 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/stats", views.stats, name="statistics"),
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"),
path("risks/list_controls", views.list_controls, name="list_controls"), path("risks/list_controls", views.list_controls, name="list_controls"),

View file

@ -1,5 +1,7 @@
from django.contrib.auth import get_user_model
from django.utils.timezone import now from django.utils.timezone import now
from .models import AuditLog, Risk, Notification from django.utils.translation import gettext_lazy as _
from .models import AuditLog, Notification, Risk, ResidualRisk
def model_diff(old, new, fields=None): def model_diff(old, new, fields=None):
""" """
@ -29,21 +31,28 @@ def check_risk_followups():
Ensures no duplicate notifications per risk per day Ensures no duplicate notifications per risk per day
""" """
today = now().date() today = now().date()
risks = Risk.objects.filter(follow_up__lte=today) risks = Risk.objects.filter(follow_up__lte=today).select_related("owner")
for risk in risks: for risk in risks:
if risk.owner: # Risk-Status auf review_required setzen (nicht überschreiben, wenn bereits closed)
if risk.status != "closed" and risk.status != "review_required":
Risk.objects.filter(pk=risk.pk).update(status="review_required")
# ResidualRisk-Objekt sicherstellen und Review-Flag setzen
resid, created = ResidualRisk.objects.get_or_create(risk=risk)
if not resid.review_required:
resid.review_required = True
resid.save()
# Notification an Stakeholder
message = _("Follow-up reached: review required for risk '{t}'").format(t=risk.title)
notification, created = Notification.objects.get_or_create( notification, created = Notification.objects.get_or_create(
user=risk.owner, user=risk.owner,
message=f"Follow-up required for risk: {risk.title}", message=message,
defaults={"read": False, "sent": False}, defaults={"read": False, "sent": False},
) )
if created: if created:
AuditLog.objects.create( AuditLog.objects.create(
user=None, # system action user=None, action="create", model="Notification", object_id=notification.pk,
action="create", changes={"message": notification.message, "user": risk.owner.username if risk.owner else None},
model="Notification",
object_id=notification.pk,
changes={"message": notification.message, "user": risk.owner.username},
) )

View file

@ -50,6 +50,9 @@ class ControlViewSet(viewsets.ModelViewSet):
instance._changed_by = self.request.user instance._changed_by = self.request.user
class ResidualRiskViewSet(viewsets.ModelViewSet): class ResidualRiskViewSet(viewsets.ModelViewSet):
"""
API endpoint for Residual risks.
"""
queryset = ResidualRisk.objects.all() queryset = ResidualRisk.objects.all()
serializer_class = ResidualRiskSerializer serializer_class = ResidualRiskSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@ -98,12 +101,11 @@ class IncidentViewSet(viewsets.ModelViewSet):
# Web => Risks, Controls, Incidents # Web => Risks, Controls, Incidents
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@login_required
def stats(request):
return render(request, "risks/statistics.html")
@login_required @login_required
def list_risks(request): def list_risks(request):
"""
View for listing all Risks
"""
qs = Risk.objects.all().select_related("owner") qs = Risk.objects.all().select_related("owner")
# GET-Parameter lesen # GET-Parameter lesen
@ -131,10 +133,14 @@ def list_risks(request):
@login_required @login_required
def show_risk(request, id): def show_risk(request, id):
"""
View for single risk
"""
risk = get_object_or_404( risk = get_object_or_404(
Risk.objects.select_related("residual_risk", "owner").prefetch_related("controls"), Risk.objects.select_related("residual_risk", "owner").prefetch_related("controls"),
pk=id, pk=id,
) )
ct = ContentType.objects.get_for_model(Risk) ct = ContentType.objects.get_for_model(Risk)
logs = LogEntry.objects.filter(content_type=ct, object_id=risk.pk).order_by("-action_time") logs = LogEntry.objects.filter(content_type=ct, object_id=risk.pk).order_by("-action_time")
@ -142,6 +148,9 @@ def show_risk(request, id):
@login_required @login_required
def list_controls(request): def list_controls(request):
"""
View for listing all Controls
"""
qs = Control.objects.all().select_related("responsible") qs = Control.objects.all().select_related("responsible")
control_id = request.GET.get("control") control_id = request.GET.get("control")
@ -183,6 +192,9 @@ def show_control(request, id):
@login_required @login_required
def list_incidents(request): def list_incidents(request):
"""
View for listing all Incidents
"""
qs = Incident.objects.all().select_related("reported_by").prefetch_related("related_risks") qs = Incident.objects.all().select_related("reported_by").prefetch_related("related_risks")
risk_id = request.GET.get("risk") risk_id = request.GET.get("risk")
@ -218,13 +230,11 @@ def show_incident(request, id):
).order_by("-action_time") ).order_by("-action_time")
return render(request, "risks/item_incident.html", {"incident": incident, "logs": logs}) return render(request, "risks/item_incident.html", {"incident": incident, "logs": logs})
# ---------------------------------------------------------------------------
# Web => Dashboard
# ---------------------------------------------------------------------------
@login_required @login_required
def dashboard(request): def dashboard(request):
"""
Dashboardview with KPIs
"""
# Risikoübersicht # Risikoübersicht
risks_total = Risk.objects.count() risks_total = Risk.objects.count()
risks_by_level = Risk.objects.values('level').annotate(count=Count('id')) risks_by_level = Risk.objects.values('level').annotate(count=Count('id'))

View file

@ -209,6 +209,37 @@
</div> </div>
<!-- Ende Maßnahmen --> <!-- Ende Maßnahmen -->
<!-- Vorfälle -->
<div class="card">
<header class="card-header">
<p class="card-header-title">Vorfälle</p>
</header>
<div class="card-content">
{% if risk.incidents.exists %}
<table class="table is-striped is-fullwidth">
<thead>
<tr>
<th>Vorfall</th>
<th>Status</th>
<th>gemeldet am</th>
</tr>
</thead>
<tbody>
{% for i in risk.incidents.all %}
<tr onclick="window.location.href='/risks/incidents/{{ i.id }}';" style="cursor:pointer;">
<td>{{ i.title }}</td>
<td>{{ i.status }}</td>
<td>{{ i.created_at|date:"d.m.Y H:i" }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="has-text-grey">Keine Vorfälle bekannt.</p>
{% endif %}
</div>
</div> <!-- Ende Vorfälle -->
<!-- Historie --> <!-- Historie -->
<div class="card"> <div class="card">
<header class="card-header"> <header class="card-header">