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:
parent
79c977f03b
commit
ab01841cf2
14 changed files with 910 additions and 368 deletions
|
@ -216,5 +216,5 @@ if SSO_ENABLED:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
CRONJOBS = [
|
||||
("0 8 * * *", "risks.utils.check_risk_followups"),
|
||||
("0 8 * * *", "risks.utils.check_risk_followups", ">> /var/log/wira_followups.log 2>&1"),
|
||||
]
|
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
|
@ -2,7 +2,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: wira-risk-management\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"
|
||||
"Last-Translator: Kevin Heyer <kevin@example.com>\n"
|
||||
"Language-Team: German\n"
|
||||
|
@ -20,15 +20,41 @@ msgstr "Verwaltung"
|
|||
msgid "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"
|
||||
msgstr "SSO-Informationen"
|
||||
|
||||
#: risks/admin.py:20
|
||||
#: risks/admin.py:35
|
||||
msgid "Risks Owned"
|
||||
msgstr "Eigene Risiken"
|
||||
|
||||
#: risks/admin.py:24
|
||||
#: risks/admin.py:39
|
||||
msgid "Controls Responsible"
|
||||
msgstr "Verantwortlich für Maßnahmen"
|
||||
|
||||
|
@ -41,160 +67,241 @@ msgstr "Risikomanagement"
|
|||
msgid "Risk"
|
||||
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
|
||||
msgid "Very low – occurs less than once every 5 years"
|
||||
msgstr "Sehr niedrig – tritt seltener als einmal in fünf Jahren auf"
|
||||
msgid "Open"
|
||||
msgstr "Offen"
|
||||
|
||||
#: risks/models.py:40
|
||||
msgid "Low – once every 1–5 years"
|
||||
msgstr "Niedrig – einmal in 1–5 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,000–5,000 € – local impact)"
|
||||
msgstr "Gering (1.000–5.000 € – lokale Auswirkungen)"
|
||||
|
||||
#: risks/models.py:47
|
||||
msgid "High (5,000–15,000 € – team-level impact)"
|
||||
msgstr "Hoch (5.000–15.000 € – Auswirkungen auf Teamebene)"
|
||||
|
||||
#: risks/models.py:48
|
||||
msgid "Severe (50,000–100,000 € – regional impact)"
|
||||
msgstr "Schwerwiegend (50.000–100.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
|
||||
#: risks/models.py:40 risks/models.py:262
|
||||
msgid "In Progress"
|
||||
msgstr "In Bearbeitung"
|
||||
|
||||
#: risks/models.py:249
|
||||
#: risks/models.py:41 risks/models.py:263
|
||||
msgid "Closed"
|
||||
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 1–5 years"
|
||||
msgstr "Niedrig – einmal in 1–5 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,000–5,000 € – local impact)"
|
||||
msgstr "Gering (1.000–5.000 € – lokale Auswirkungen)"
|
||||
|
||||
#: risks/models.py:53
|
||||
msgid "High (5,000–15,000 € – team-level impact)"
|
||||
msgstr "Hoch (5.000–15.000 € – Auswirkungen auf Teamebene)"
|
||||
|
||||
#: risks/models.py:54
|
||||
msgid "Severe (50,000–100,000 € – regional impact)"
|
||||
msgstr "Schwerwiegend (50.000–100.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"
|
||||
msgstr "Meldedatum"
|
||||
|
||||
#: risks/models.py:255
|
||||
#: risks/models.py:269
|
||||
msgid "Reported by"
|
||||
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
|
||||
msgid "Dashboard"
|
||||
msgstr "Dashboard"
|
||||
|
@ -227,6 +334,26 @@ msgstr "Vorfälle nach Status"
|
|||
msgid "Risks by CIA"
|
||||
msgstr "CIA Risiken"
|
||||
|
||||
#: templates/risks/item_risk.html:68 templates/risks/item_risk.html:114
|
||||
#: templates/risks/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
|
||||
msgid "Risk analysis"
|
||||
msgstr "Risikoanalyse"
|
||||
|
@ -247,19 +374,3 @@ msgstr "Risikoeigner"
|
|||
#: templates/risks/list_risks.html:84
|
||||
msgid "Asset / Process"
|
||||
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"
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\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"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -26,15 +26,41 @@ msgstr ""
|
|||
msgid "Admin"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:13
|
||||
msgid "SSO Information"
|
||||
#: 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 ""
|
||||
|
||||
#: 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 ""
|
||||
|
||||
#: risks/admin.py:20
|
||||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:26
|
||||
msgid "SSO Information"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:35
|
||||
msgid "Risks Owned"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:24
|
||||
#: risks/admin.py:39
|
||||
msgid "Controls Responsible"
|
||||
msgstr ""
|
||||
|
||||
|
@ -47,160 +73,242 @@ msgstr ""
|
|||
msgid "Risk"
|
||||
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
|
||||
msgid "Very low – occurs less than once every 5 years"
|
||||
msgid "Open"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:40
|
||||
msgid "Low – once every 1–5 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,000–5,000 € – local impact)"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:47
|
||||
msgid "High (5,000–15,000 € – team-level impact)"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:48
|
||||
msgid "Severe (50,000–100,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
|
||||
#: risks/models.py:40 risks/models.py:262
|
||||
msgid "In Progress"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:249
|
||||
#: risks/models.py:41 risks/models.py:263
|
||||
msgid "Closed"
|
||||
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 1–5 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,000–5,000 € – local impact)"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:53
|
||||
msgid "High (5,000–15,000 € – team-level impact)"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:54
|
||||
msgid "Severe (50,000–100,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"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:255
|
||||
#: risks/models.py:269
|
||||
msgid "Reported by"
|
||||
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
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
|
@ -233,6 +341,26 @@ msgstr ""
|
|||
msgid "Risks by CIA"
|
||||
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
|
||||
msgid "Risk analysis"
|
||||
msgstr ""
|
||||
|
@ -253,19 +381,3 @@ msgstr ""
|
|||
#: templates/risks/list_risks.html:84
|
||||
msgid "Asset / Process"
|
||||
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 ""
|
||||
|
|
|
@ -1,12 +1,25 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .models import User, Risk, ResidualRisk, Control, Incident
|
||||
from .models import Control, Incident, NotificationPreference , Risk, ResidualRisk, User
|
||||
|
||||
admin.site.site_header = _("Administration")
|
||||
admin.site.site_title = _("Admin")
|
||||
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)
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
fieldsets = BaseUserAdmin.fieldsets + (
|
||||
|
@ -14,6 +27,8 @@ class UserAdmin(BaseUserAdmin):
|
|||
)
|
||||
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()
|
||||
|
@ -44,6 +59,7 @@ class RiskAdmin(admin.ModelAdmin):
|
|||
list_display = (
|
||||
"title",
|
||||
"owner_name",
|
||||
"status",
|
||||
"score",
|
||||
"level",
|
||||
"likelihood",
|
||||
|
@ -56,7 +72,7 @@ class RiskAdmin(admin.ModelAdmin):
|
|||
return "-"
|
||||
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")
|
||||
inlines = [ResidualRiskInline, ControlRisksInline]
|
||||
|
||||
|
@ -117,4 +133,5 @@ class IncidentAdmin(admin.ModelAdmin):
|
|||
|
||||
def delete_model(self, request, obj):
|
||||
obj._changed_by = request.user
|
||||
super().delete_model(request, obj)
|
||||
super().delete_model(request, obj)
|
||||
|
||||
|
|
71
risks/migrations/0021_risk_status_notificationpreference.py
Normal file
71
risks/migrations/0021_risk_status_notificationpreference.py
Normal 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",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -35,6 +35,12 @@ class Risk(models.Model):
|
|||
verbose_name = _("Risk")
|
||||
verbose_name_plural = _("Risks")
|
||||
|
||||
STATUS_CHOICES = [
|
||||
("open", _("Open")),
|
||||
("in_progress", _("In Progress")),
|
||||
("closed", _("Closed")),
|
||||
("review_required", _("Review required")),
|
||||
]
|
||||
LIKELIHOOD_CHOICES = [
|
||||
(1, _("Very low – occurs less than once every 5 years")),
|
||||
(2, _("Low – once every 1–5 years")),
|
||||
|
@ -63,6 +69,14 @@ class Risk(models.Model):
|
|||
created_at = models.DateTimeField(_("Created at"), auto_now_add=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 = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True)
|
||||
|
||||
|
@ -271,4 +285,52 @@ class Notification(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
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))
|
|
@ -53,6 +53,7 @@ class RiskSerializer(serializers.ModelSerializer):
|
|||
"impact",
|
||||
"score",
|
||||
"level",
|
||||
"status",
|
||||
"owner",
|
||||
"follow_up",
|
||||
"cia",
|
||||
|
|
155
risks/signals.py
155
risks/signals.py
|
@ -1,15 +1,19 @@
|
|||
from datetime import date, datetime
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, post_delete, m2m_changed
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
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
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# General definitions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
def serialize_value(value):
|
||||
if isinstance(value, Model):
|
||||
return value.pk # oder str(value), wenn du mehr Infos willst
|
||||
|
@ -17,9 +21,65 @@ def serialize_value(value):
|
|||
return value.isoformat()
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@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)
|
||||
def log_risk_save(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
|
@ -68,6 +128,32 @@ def log_risk_delete(sender, instance, **kwargs):
|
|||
# ---------------------------------------------------------------------------
|
||||
# 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)
|
||||
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)
|
||||
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 == "post_clear":
|
||||
affected_risks = instance.risks.all()
|
||||
elif pk_set:
|
||||
if reverse:
|
||||
from .models import Risk
|
||||
affected_risks = Risk.objects.filter(pk__in=pk_set)
|
||||
else:
|
||||
affected_risks = Risk.objects.filter(pk__in=pk_set)
|
||||
else:
|
||||
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()
|
||||
affected = instance.risks.all() if not pk_set else Risk.objects.filter(pk__in=pk_set)
|
||||
for risk in affected:
|
||||
resid, created = ResidualRisk.objects.get_or_create(risk=risk)
|
||||
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")
|
||||
_notify(_risk_stakeholders(risk), _("Review required for risk '{t}' due to control change").format(t=risk.title), "review_required")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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)
|
||||
def log_residual_save(sender, instance, created, **kwargs):
|
||||
|
@ -180,6 +289,16 @@ def log_residual_delete(sender, instance, **kwargs):
|
|||
# ---------------------------------------------------------------------------
|
||||
# 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)
|
||||
def log_incident_save(sender, instance, created, **kwargs):
|
||||
|
|
|
@ -6,7 +6,6 @@ app_name = "risks"
|
|||
urlpatterns = [
|
||||
path("", views.dashboard, name="dashboard"),
|
||||
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/risks/<int:id>", views.show_risk, name="show_risk"),
|
||||
path("risks/list_controls", views.list_controls, name="list_controls"),
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
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):
|
||||
"""
|
||||
|
@ -29,21 +31,28 @@ def check_risk_followups():
|
|||
Ensures no duplicate notifications per risk per day
|
||||
"""
|
||||
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:
|
||||
if risk.owner:
|
||||
notification, created = Notification.objects.get_or_create(
|
||||
user=risk.owner,
|
||||
message=f"Follow-up required for risk: {risk.title}",
|
||||
defaults={"read": False, "sent": False},
|
||||
)
|
||||
# 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")
|
||||
|
||||
if created:
|
||||
AuditLog.objects.create(
|
||||
user=None, # system action
|
||||
action="create",
|
||||
model="Notification",
|
||||
object_id=notification.pk,
|
||||
changes={"message": notification.message, "user": risk.owner.username},
|
||||
)
|
||||
# ResidualRisk-Objekt sicherstellen und Review-Flag setzen
|
||||
resid, created = ResidualRisk.objects.get_or_create(risk=risk)
|
||||
if not resid.review_required:
|
||||
resid.review_required = True
|
||||
resid.save()
|
||||
|
||||
# Notification an Stakeholder
|
||||
message = _("Follow-up reached: review required for risk '{t}'").format(t=risk.title)
|
||||
notification, created = Notification.objects.get_or_create(
|
||||
user=risk.owner,
|
||||
message=message,
|
||||
defaults={"read": False, "sent": False},
|
||||
)
|
||||
if created:
|
||||
AuditLog.objects.create(
|
||||
user=None, action="create", model="Notification", object_id=notification.pk,
|
||||
changes={"message": notification.message, "user": risk.owner.username if risk.owner else None},
|
||||
)
|
|
@ -50,6 +50,9 @@ class ControlViewSet(viewsets.ModelViewSet):
|
|||
instance._changed_by = self.request.user
|
||||
|
||||
class ResidualRiskViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
API endpoint for Residual risks.
|
||||
"""
|
||||
queryset = ResidualRisk.objects.all()
|
||||
serializer_class = ResidualRiskSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
@ -98,12 +101,11 @@ class IncidentViewSet(viewsets.ModelViewSet):
|
|||
# Web => Risks, Controls, Incidents
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@login_required
|
||||
def stats(request):
|
||||
return render(request, "risks/statistics.html")
|
||||
|
||||
@login_required
|
||||
def list_risks(request):
|
||||
"""
|
||||
View for listing all Risks
|
||||
"""
|
||||
qs = Risk.objects.all().select_related("owner")
|
||||
|
||||
# GET-Parameter lesen
|
||||
|
@ -131,10 +133,14 @@ def list_risks(request):
|
|||
|
||||
@login_required
|
||||
def show_risk(request, id):
|
||||
"""
|
||||
View for single risk
|
||||
"""
|
||||
risk = get_object_or_404(
|
||||
Risk.objects.select_related("residual_risk", "owner").prefetch_related("controls"),
|
||||
pk=id,
|
||||
)
|
||||
|
||||
ct = ContentType.objects.get_for_model(Risk)
|
||||
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
|
||||
def list_controls(request):
|
||||
"""
|
||||
View for listing all Controls
|
||||
"""
|
||||
qs = Control.objects.all().select_related("responsible")
|
||||
|
||||
control_id = request.GET.get("control")
|
||||
|
@ -183,6 +192,9 @@ def show_control(request, id):
|
|||
|
||||
@login_required
|
||||
def list_incidents(request):
|
||||
"""
|
||||
View for listing all Incidents
|
||||
"""
|
||||
qs = Incident.objects.all().select_related("reported_by").prefetch_related("related_risks")
|
||||
|
||||
risk_id = request.GET.get("risk")
|
||||
|
@ -218,13 +230,11 @@ def show_incident(request, id):
|
|||
).order_by("-action_time")
|
||||
|
||||
return render(request, "risks/item_incident.html", {"incident": incident, "logs": logs})
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Web => Dashboard
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@login_required
|
||||
def dashboard(request):
|
||||
"""
|
||||
Dashboardview with KPIs
|
||||
"""
|
||||
# Risikoübersicht
|
||||
risks_total = Risk.objects.count()
|
||||
risks_by_level = Risk.objects.values('level').annotate(count=Count('id'))
|
||||
|
|
|
@ -209,6 +209,37 @@
|
|||
</div>
|
||||
<!-- 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 -->
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
|
|
Loading…
Add table
Reference in a new issue