feat: Implement notification rules and email notifications for risk events
This commit is contained in:
parent
ebfcbddd5c
commit
86525d9ab0
11 changed files with 626 additions and 56 deletions
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 12:51+0200\n"
|
||||
"POT-Creation-Date: 2025-09-10 13:44+0200\n"
|
||||
"PO-Revision-Date: 2025-09-09 13:45+0200\n"
|
||||
"Last-Translator: Kevin Heyer <kevin@example.com>\n"
|
||||
"Language-Team: German\n"
|
||||
|
@ -40,6 +40,7 @@ msgid "Reviews"
|
|||
msgstr "Prüfung"
|
||||
|
||||
#: risks/admin.py:19 risks/models.py:258 templates/base.html:37
|
||||
#: templates/risks/item_risk.html:248
|
||||
msgid "Incidents"
|
||||
msgstr "Vorfälle"
|
||||
|
||||
|
@ -47,7 +48,7 @@ msgstr "Vorfälle"
|
|||
msgid "Users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: risks/admin.py:133 risks/models.py:302
|
||||
#: risks/admin.py:133 risks/models.py:302 templates/risks/item_risk.html:287
|
||||
msgid "User"
|
||||
msgstr "Benutzer"
|
||||
|
||||
|
@ -60,6 +61,7 @@ msgid "Mark selected as read"
|
|||
msgstr "Alle als gelesen Markieren"
|
||||
|
||||
#: risks/admin.py:150
|
||||
#, python-format
|
||||
msgid "%(n)d notifications marked as read."
|
||||
msgstr "%(n)d Benachrichtigungen wurden als gelesen Markiert"
|
||||
|
||||
|
@ -68,6 +70,7 @@ msgid "Mark selected as unread"
|
|||
msgstr "Alle als gelesen Markieren"
|
||||
|
||||
#: risks/admin.py:155
|
||||
#, python-format
|
||||
msgid "%(n)d notifications marked as unread."
|
||||
msgstr "%(n)d Benachrichtigungen wurden als ungelesen Markiert"
|
||||
|
||||
|
@ -77,15 +80,15 @@ msgstr ""
|
|||
|
||||
#: risks/admin.py:160
|
||||
msgid "%(n)d notifications marked as sent."
|
||||
msgstr "Alle Benachrichtigungen wurden als gelesen Markiert"
|
||||
msgstr "%(n)d Benachrichtigungen wurden als gelesen Markiert"
|
||||
|
||||
#: risks/admin.py:162
|
||||
msgid "Mark selected as unsent"
|
||||
msgstr ""
|
||||
msgstr "Auswahl als ungesendet markieren"
|
||||
|
||||
#: risks/admin.py:165
|
||||
msgid "%(n)d notifications marked as unsent."
|
||||
msgstr "Alle Benachrichtigungen wurden als gelesen Markiert"
|
||||
msgstr "%(n)d Benachrichtigungen wurden als gelesen Markiert"
|
||||
|
||||
#: risks/admin.py:177
|
||||
msgid "SSO Information"
|
||||
|
@ -104,15 +107,17 @@ msgid "Risk Management"
|
|||
msgstr "Risikomanagement"
|
||||
|
||||
#: risks/forms.py:9 risks/forms.py:16 risks/forms.py:23 risks/models.py:73
|
||||
#: templates/risks/item_risk.html:64 templates/risks/item_risk.html:202
|
||||
#: templates/risks/item_risk.html:256
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
#: risks/forms.py:30 risks/models.py:42 templates/risks/item_risk.html:136
|
||||
#: risks/forms.py:30 risks/models.py:42 templates/risks/item_risk.html:177
|
||||
msgid "Review required"
|
||||
msgstr "Prüfung nötig"
|
||||
|
||||
#: risks/models.py:35 templates/risks/list_risks.html:18
|
||||
#: templates/risks/list_risks.html:83
|
||||
#: risks/models.py:35 templates/risks/item_risk.html:11
|
||||
#: templates/risks/list_risks.html:18 templates/risks/list_risks.html:83
|
||||
msgid "Risk"
|
||||
msgstr "Risiko"
|
||||
|
||||
|
@ -177,6 +182,7 @@ msgid "Availability"
|
|||
msgstr "Verfügbarkeit"
|
||||
|
||||
#: risks/models.py:64 risks/models.py:200 risks/models.py:265
|
||||
#: templates/risks/item_risk.html:201
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
|
@ -184,19 +190,20 @@ msgstr "Titel"
|
|||
msgid "Description"
|
||||
msgstr "Beschreibung"
|
||||
|
||||
#: risks/models.py:66
|
||||
#: risks/models.py:66 templates/risks/item_risk.html:53
|
||||
msgid "Asset"
|
||||
msgstr "Asset"
|
||||
|
||||
#: risks/models.py:67
|
||||
#: risks/models.py:67 templates/risks/item_risk.html:54
|
||||
msgid "Process"
|
||||
msgstr "Prozess"
|
||||
|
||||
#: risks/models.py:68 templates/risks/list_risks.html:85
|
||||
#: risks/models.py:68 templates/risks/item_risk.html:55
|
||||
#: templates/risks/list_risks.html:85
|
||||
msgid "Category"
|
||||
msgstr "Kategorie"
|
||||
|
||||
#: risks/models.py:69
|
||||
#: risks/models.py:69 templates/risks/item_risk.html:68
|
||||
msgid "Created at"
|
||||
msgstr "Erstellt am"
|
||||
|
||||
|
@ -244,7 +251,7 @@ msgstr "Audit-Log"
|
|||
msgid "Auditlogs"
|
||||
msgstr "Audit-Logs"
|
||||
|
||||
#: risks/models.py:257
|
||||
#: risks/models.py:257 templates/risks/item_risk.html:255
|
||||
msgid "Incident"
|
||||
msgstr "Vorfall"
|
||||
|
||||
|
@ -264,7 +271,7 @@ msgstr "Gemeldet von"
|
|||
msgid "Notification"
|
||||
msgstr "Benachrichtigung"
|
||||
|
||||
#: risks/models.py:280 templates/base.html:88
|
||||
#: risks/models.py:280 templates/base.html:78
|
||||
#: templates/risks/notifications.html:4
|
||||
msgid "Notifications"
|
||||
msgstr "Nachrichten"
|
||||
|
@ -377,31 +384,32 @@ msgstr "Restrisiko geprüft"
|
|||
msgid "Dashboard"
|
||||
msgstr "Dashboard"
|
||||
|
||||
#: templates/base.html:35 templates/risks/list_risks.html:4
|
||||
#: templates/base.html:35 templates/risks/item_risk.html:4
|
||||
#: templates/risks/list_risks.html:4
|
||||
msgid "Risk analysis"
|
||||
msgstr "Risikoanalyse"
|
||||
|
||||
#: templates/base.html:70
|
||||
#: templates/base.html:73
|
||||
msgid "AdminCP"
|
||||
msgstr "Adminbereich"
|
||||
|
||||
#: templates/base.html:76
|
||||
#: templates/base.html:86
|
||||
msgid "Derk Mode"
|
||||
msgstr "Dark Mode"
|
||||
|
||||
#: templates/base.html:82
|
||||
#: templates/base.html:92
|
||||
msgid "Logout"
|
||||
msgstr "Logout"
|
||||
|
||||
#: templates/base.html:104
|
||||
#: templates/base.html:107
|
||||
msgid "Login"
|
||||
msgstr "Login"
|
||||
|
||||
#: templates/base.html:139 templates/base.html:146
|
||||
#: templates/base.html:142 templates/base.html:149
|
||||
msgid "Light Mode"
|
||||
msgstr "Light Mode"
|
||||
|
||||
#: templates/base.html:149
|
||||
#: templates/base.html:152
|
||||
msgid "Dark Mode"
|
||||
msgstr "Dark Mode"
|
||||
|
||||
|
@ -433,34 +441,132 @@ msgstr "Vorfälle nach Status"
|
|||
msgid "Risks by CIA"
|
||||
msgstr "CIA Risiken"
|
||||
|
||||
#: templates/risks/item_risk.html:18
|
||||
#, fuzzy
|
||||
#| msgid "Reviews"
|
||||
msgid "Overview"
|
||||
msgstr "Prüfung"
|
||||
|
||||
#: templates/risks/item_risk.html:34
|
||||
msgid "Update status"
|
||||
msgstr "Status Aktualisiert"
|
||||
|
||||
#: templates/risks/item_risk.html:89 templates/risks/item_risk.html:147
|
||||
#: templates/risks/item_risk.html:57
|
||||
msgid "Protection goals"
|
||||
msgstr "Schutzziele"
|
||||
|
||||
#: templates/risks/item_risk.html:61
|
||||
msgid "Not yet assigned"
|
||||
msgstr "Keine Zugewiesenen Ziele"
|
||||
|
||||
#: templates/risks/item_risk.html:67
|
||||
msgid "Risk owner"
|
||||
msgstr "Risikoeigner"
|
||||
|
||||
#: templates/risks/item_risk.html:69
|
||||
msgid "updated at"
|
||||
msgstr "Aktualisiert am"
|
||||
|
||||
#: templates/risks/item_risk.html:70
|
||||
msgid "Resubmission"
|
||||
msgstr "Wiedervorlagedatum"
|
||||
|
||||
#: templates/risks/item_risk.html:76
|
||||
msgid "Risk assessment"
|
||||
msgstr "Risikomanagement"
|
||||
|
||||
#: templates/risks/item_risk.html:84
|
||||
msgid "Gross (before measures)"
|
||||
msgstr "Brutto (vor Maßnahmen)"
|
||||
|
||||
#: templates/risks/item_risk.html:89 templates/risks/item_risk.html:135
|
||||
#: templates/risks/list_risks.html:86
|
||||
msgid "Likelihood"
|
||||
msgstr "Eintritt"
|
||||
|
||||
#: templates/risks/item_risk.html:98 templates/risks/item_risk.html:156
|
||||
#: templates/risks/item_risk.html:90 templates/risks/item_risk.html:136
|
||||
msgid "Probability of occurrence"
|
||||
msgstr "Eintrittswahrscheinlichkeit"
|
||||
|
||||
#: templates/risks/item_risk.html:98 templates/risks/item_risk.html:144
|
||||
#: templates/risks/list_risks.html:87
|
||||
msgid "Impact"
|
||||
msgstr "Schaden"
|
||||
|
||||
#: templates/risks/item_risk.html:107 templates/risks/item_risk.html:165
|
||||
#: templates/risks/item_risk.html:99 templates/risks/item_risk.html:145
|
||||
msgid "Extent of damage"
|
||||
msgstr "Schadensausmaß"
|
||||
|
||||
#: templates/risks/item_risk.html:107 templates/risks/item_risk.html:108
|
||||
#: templates/risks/item_risk.html:153 templates/risks/item_risk.html:154
|
||||
#: templates/risks/list_risks.html:89
|
||||
msgid "Level"
|
||||
msgstr "Stufe"
|
||||
|
||||
#: templates/risks/item_risk.html:116 templates/risks/item_risk.html:173
|
||||
#: templates/risks/item_risk.html:116 templates/risks/item_risk.html:117
|
||||
#: templates/risks/item_risk.html:161 templates/risks/item_risk.html:163
|
||||
#: templates/risks/list_risks.html:88
|
||||
msgid "Score"
|
||||
msgstr "Score"
|
||||
|
||||
#: templates/risks/item_risk.html:139
|
||||
#: templates/risks/item_risk.html:130
|
||||
msgid "Net (after measures)"
|
||||
msgstr "Netto (nach Maßnahmen)"
|
||||
|
||||
#: templates/risks/item_risk.html:169
|
||||
msgid "No net risk recorded yet."
|
||||
msgstr "Kein Restrisiko vergeben"
|
||||
|
||||
#: templates/risks/item_risk.html:180
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#: templates/risks/item_risk.html:194
|
||||
msgid "Measures"
|
||||
msgstr "Maßnahmen"
|
||||
|
||||
#: templates/risks/item_risk.html:203
|
||||
msgid "Deadline"
|
||||
msgstr "Frist"
|
||||
|
||||
#: templates/risks/item_risk.html:204
|
||||
msgid "Responsible"
|
||||
msgstr "Verantwortliche/r"
|
||||
|
||||
#: templates/risks/item_risk.html:205
|
||||
msgid "Link"
|
||||
msgstr "Link"
|
||||
|
||||
#: templates/risks/item_risk.html:239
|
||||
msgid "No measures recorded."
|
||||
msgstr "Keine Maßnahmen gefunden."
|
||||
|
||||
#: templates/risks/item_risk.html:257
|
||||
#, fuzzy
|
||||
#| msgid "Reported by"
|
||||
msgid "Reported on"
|
||||
msgstr "Gemeldet von"
|
||||
|
||||
#: templates/risks/item_risk.html:271
|
||||
msgid "No incidents recorded."
|
||||
msgstr "Keine Vorfälle gefunden."
|
||||
|
||||
#: templates/risks/item_risk.html:279
|
||||
msgid "History"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:286
|
||||
msgid "Time"
|
||||
msgstr "Zeitpunkt"
|
||||
|
||||
#: templates/risks/item_risk.html:288
|
||||
msgid "Action"
|
||||
msgstr "Aktion"
|
||||
|
||||
#: templates/risks/item_risk.html:302
|
||||
msgid "No History found."
|
||||
msgstr "Keine Historie vorhanden"
|
||||
|
||||
#: templates/risks/list_risks.html:9
|
||||
msgid "Filter"
|
||||
msgstr "Filter"
|
||||
|
@ -478,6 +584,10 @@ msgstr "Risikoeigner"
|
|||
msgid "Asset / Process"
|
||||
msgstr "Asset / Prozess"
|
||||
|
||||
#: templates/risks/list_risks.html:152
|
||||
msgid "No risks present"
|
||||
msgstr "Aktuell keine Risiken"
|
||||
|
||||
#: templates/risks/notifications.html:12
|
||||
msgid "Unread"
|
||||
msgstr "Ungelesen"
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 12:51+0200\n"
|
||||
"POT-Creation-Date: 2025-09-10 13:44+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"
|
||||
|
@ -46,6 +46,7 @@ msgid "Reviews"
|
|||
msgstr ""
|
||||
|
||||
#: risks/admin.py:19 risks/models.py:258 templates/base.html:37
|
||||
#: templates/risks/item_risk.html:248
|
||||
msgid "Incidents"
|
||||
msgstr ""
|
||||
|
||||
|
@ -53,7 +54,7 @@ msgstr ""
|
|||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:133 risks/models.py:302
|
||||
#: risks/admin.py:133 risks/models.py:302 templates/risks/item_risk.html:287
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
|
@ -114,15 +115,17 @@ msgid "Risk Management"
|
|||
msgstr ""
|
||||
|
||||
#: risks/forms.py:9 risks/forms.py:16 risks/forms.py:23 risks/models.py:73
|
||||
#: templates/risks/item_risk.html:64 templates/risks/item_risk.html:202
|
||||
#: templates/risks/item_risk.html:256
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: risks/forms.py:30 risks/models.py:42 templates/risks/item_risk.html:136
|
||||
#: risks/forms.py:30 risks/models.py:42 templates/risks/item_risk.html:177
|
||||
msgid "Review required"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:35 templates/risks/list_risks.html:18
|
||||
#: templates/risks/list_risks.html:83
|
||||
#: risks/models.py:35 templates/risks/item_risk.html:11
|
||||
#: templates/risks/list_risks.html:18 templates/risks/list_risks.html:83
|
||||
msgid "Risk"
|
||||
msgstr ""
|
||||
|
||||
|
@ -187,6 +190,7 @@ msgid "Availability"
|
|||
msgstr ""
|
||||
|
||||
#: risks/models.py:64 risks/models.py:200 risks/models.py:265
|
||||
#: templates/risks/item_risk.html:201
|
||||
msgid "Title"
|
||||
msgstr ""
|
||||
|
||||
|
@ -194,19 +198,20 @@ msgstr ""
|
|||
msgid "Description"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:66
|
||||
#: risks/models.py:66 templates/risks/item_risk.html:53
|
||||
msgid "Asset"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:67
|
||||
#: risks/models.py:67 templates/risks/item_risk.html:54
|
||||
msgid "Process"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:68 templates/risks/list_risks.html:85
|
||||
#: risks/models.py:68 templates/risks/item_risk.html:55
|
||||
#: templates/risks/list_risks.html:85
|
||||
msgid "Category"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:69
|
||||
#: risks/models.py:69 templates/risks/item_risk.html:68
|
||||
msgid "Created at"
|
||||
msgstr ""
|
||||
|
||||
|
@ -254,7 +259,7 @@ msgstr ""
|
|||
msgid "Auditlogs"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:257
|
||||
#: risks/models.py:257 templates/risks/item_risk.html:255
|
||||
msgid "Incident"
|
||||
msgstr ""
|
||||
|
||||
|
@ -274,7 +279,7 @@ msgstr ""
|
|||
msgid "Notification"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:280 templates/base.html:88
|
||||
#: risks/models.py:280 templates/base.html:78
|
||||
#: templates/risks/notifications.html:4
|
||||
msgid "Notifications"
|
||||
msgstr ""
|
||||
|
@ -387,31 +392,32 @@ msgstr ""
|
|||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:35 templates/risks/list_risks.html:4
|
||||
#: templates/base.html:35 templates/risks/item_risk.html:4
|
||||
#: templates/risks/list_risks.html:4
|
||||
msgid "Risk analysis"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:70
|
||||
#: templates/base.html:73
|
||||
msgid "AdminCP"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:76
|
||||
#: templates/base.html:86
|
||||
msgid "Derk Mode"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:82
|
||||
#: templates/base.html:92
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:104
|
||||
#: templates/base.html:107
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:139 templates/base.html:146
|
||||
#: templates/base.html:142 templates/base.html:149
|
||||
msgid "Light Mode"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:149
|
||||
#: templates/base.html:152
|
||||
msgid "Dark Mode"
|
||||
msgstr ""
|
||||
|
||||
|
@ -443,34 +449,128 @@ msgstr ""
|
|||
msgid "Risks by CIA"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:18
|
||||
msgid "Overview"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:34
|
||||
msgid "Update status"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:89 templates/risks/item_risk.html:147
|
||||
#: templates/risks/item_risk.html:57
|
||||
msgid "Protection goals"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:61
|
||||
msgid "Not yet assigned"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:67
|
||||
msgid "Risk owner"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:69
|
||||
msgid "updated at"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:70
|
||||
msgid "Resubmission"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:76
|
||||
msgid "Risk assessment"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:84
|
||||
msgid "Gross (before measures)"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:89 templates/risks/item_risk.html:135
|
||||
#: templates/risks/list_risks.html:86
|
||||
msgid "Likelihood"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:98 templates/risks/item_risk.html:156
|
||||
#: templates/risks/item_risk.html:90 templates/risks/item_risk.html:136
|
||||
msgid "Probability of occurrence"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:98 templates/risks/item_risk.html:144
|
||||
#: templates/risks/list_risks.html:87
|
||||
msgid "Impact"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:107 templates/risks/item_risk.html:165
|
||||
#: templates/risks/item_risk.html:99 templates/risks/item_risk.html:145
|
||||
msgid "Extent of damage"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:107 templates/risks/item_risk.html:108
|
||||
#: templates/risks/item_risk.html:153 templates/risks/item_risk.html:154
|
||||
#: templates/risks/list_risks.html:89
|
||||
msgid "Level"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:116 templates/risks/item_risk.html:173
|
||||
#: templates/risks/item_risk.html:116 templates/risks/item_risk.html:117
|
||||
#: templates/risks/item_risk.html:161 templates/risks/item_risk.html:163
|
||||
#: templates/risks/list_risks.html:88
|
||||
msgid "Score"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:139
|
||||
#: templates/risks/item_risk.html:130
|
||||
msgid "Net (after measures)"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:169
|
||||
msgid "No net risk recorded yet."
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:180
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:194
|
||||
msgid "Measures"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:203
|
||||
msgid "Deadline"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:204
|
||||
msgid "Responsible"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:205
|
||||
msgid "Link"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:239
|
||||
msgid "No measures recorded."
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:257
|
||||
msgid "Reported on"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:271
|
||||
msgid "No incidents recorded."
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:279
|
||||
msgid "History"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:286
|
||||
msgid "Time"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:288
|
||||
msgid "Action"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:302
|
||||
msgid "No History found."
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/list_risks.html:9
|
||||
msgid "Filter"
|
||||
msgstr ""
|
||||
|
@ -488,6 +588,10 @@ msgstr ""
|
|||
msgid "Asset / Process"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/list_risks.html:152
|
||||
msgid "No risks present"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/notifications.html:12
|
||||
msgid "Unread"
|
||||
msgstr ""
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .models import Control, Incident, Notification, NotificationPreference , Risk, ResidualRisk, User
|
||||
from .models import Control, Incident, Notification, NotificationPreference, NotificationRule, Risk, ResidualRisk, User
|
||||
|
||||
admin.site.site_header = _("Administration")
|
||||
admin.site.site_title = _("Admin")
|
||||
|
@ -171,6 +171,19 @@ class NotificationInline(admin.TabularInline):
|
|||
extra = 0
|
||||
ordering = ("-created_at",)
|
||||
|
||||
@admin.register(NotificationRule)
|
||||
class NotificationRuleAdmin(admin.ModelAdmin):
|
||||
list_display = ("kind", "enabled_in_app", "enabled_email", "to_owner", "to_staff", "short_extras")
|
||||
list_editable = ("enabled_in_app", "enabled_email", "to_owner", "to_staff")
|
||||
list_filter = ("enabled_in_app", "enabled_email", "to_owner", "to_staff")
|
||||
search_fields = ("kind", "extra_recipients")
|
||||
ordering = ("kind",)
|
||||
|
||||
@admin.display(description=_("Extra recipients"))
|
||||
def short_extras(self, obj):
|
||||
txt = (obj.extra_recipients or "").replace("\n", ", ")
|
||||
return (txt[:50] + "…") if len(txt) > 50 else txt
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
fieldsets = BaseUserAdmin.fieldsets + (
|
||||
|
|
|
@ -7,4 +7,15 @@ class RisksConfig(AppConfig):
|
|||
verbose_name = _("Risk Management")
|
||||
|
||||
def ready(self):
|
||||
import risks.signals
|
||||
import risks.signals
|
||||
|
||||
try:
|
||||
from django.db.utils import OperationalError, ProgrammingError
|
||||
from .models import NotificationRule, NotificationKind
|
||||
NotificationRule.objects.count()
|
||||
except (OperationalError, ProgrammingError):
|
||||
return
|
||||
existing = set(NotificationRule.objects.values_list("kind", flat=True))
|
||||
for kind, _label in NotificationKind.choices:
|
||||
if kind not in existing:
|
||||
NotificationRule.objects.create(kind=kind)
|
30
risks/email_utils.py
Normal file
30
risks/email_utils.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
from django.conf import settings
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
def send_notification_email(user, subject, template_txt, context, template_html=None):
|
||||
"""
|
||||
Versendet nur, wenn EMAIL_ENABLED=True und user.email vorhanden.
|
||||
template_txt: Pfad zu Plaintext-Template
|
||||
template_html: optional Pfad zu HTML-Template
|
||||
"""
|
||||
if not settings.EMAIL_ENABLED:
|
||||
return False
|
||||
if not user or not user.email:
|
||||
return False
|
||||
|
||||
subject_full = f"{settings.EMAIL_SUBJECT_PREFIX}{subject}"
|
||||
body_txt = render_to_string(template_txt, context)
|
||||
|
||||
msg = EmailMultiAlternatives(
|
||||
subject=subject_full,
|
||||
body=body_txt,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[user.email],
|
||||
)
|
||||
if template_html:
|
||||
body_html = render_to_string(template_html, context)
|
||||
msg.attach_alternative(body_html, "text/html")
|
||||
|
||||
msg.send(fail_silently=False)
|
||||
return True
|
86
risks/migrations/0023_notificationrule.py
Normal file
86
risks/migrations/0023_notificationrule.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-10 12:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("risks", "0022_alter_notification_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="NotificationRule",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"kind",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("risk.created", "Risk created"),
|
||||
("risk.updated", "Risk updated"),
|
||||
("risk.deleted", "Risk deleted"),
|
||||
("risk.review_required", "Risk review required"),
|
||||
("risk.review_completed", "Risk review completed"),
|
||||
("control.created", "Control created"),
|
||||
("control.updated", "Control updated"),
|
||||
("control.deleted", "Control deleted"),
|
||||
("residual.created", "Residual created"),
|
||||
("residual.updated", "Residual updated"),
|
||||
("residual.deleted", "Residual deleted"),
|
||||
("residual.review_required", "Residual review required"),
|
||||
("residual.review_completed", "Residual review completed"),
|
||||
("incident.created", "Incident created"),
|
||||
("incident.updated", "Incident updated"),
|
||||
("incident.deleted", "Incident deleted"),
|
||||
("user.created", "User created"),
|
||||
("user.deleted", "User deleted"),
|
||||
],
|
||||
max_length=40,
|
||||
unique=True,
|
||||
verbose_name="Event",
|
||||
),
|
||||
),
|
||||
(
|
||||
"enabled_in_app",
|
||||
models.BooleanField(default=True, verbose_name="Show in app"),
|
||||
),
|
||||
(
|
||||
"enabled_email",
|
||||
models.BooleanField(default=False, verbose_name="Send via email"),
|
||||
),
|
||||
(
|
||||
"to_owner",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
verbose_name="Send to owner/responsible/reporter (if available)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"to_staff",
|
||||
models.BooleanField(
|
||||
default=False, verbose_name="Send to all staff"
|
||||
),
|
||||
),
|
||||
(
|
||||
"extra_recipients",
|
||||
models.TextField(
|
||||
blank=True,
|
||||
verbose_name="Extra recipients (emails, comma or newline separated)",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Notification rule",
|
||||
"verbose_name_plural": "Notification rules",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -291,6 +291,30 @@ class Notification(models.Model):
|
|||
user_display = self.user.username if self.user else "System"
|
||||
return f"{user_display}: {self.message[:50]}..."
|
||||
|
||||
class NotificationKind(models.TextChoices):
|
||||
RISK_CREATED = "risk.created", _("Risk created")
|
||||
RISK_UPDATED = "risk.updated", _("Risk updated")
|
||||
RISK_DELETED = "risk.deleted", _("Risk deleted")
|
||||
RISK_REVIEW_REQUIRED = "risk.review_required", _("Risk review required")
|
||||
RISK_REVIEW_COMPLETED = "risk.review_completed", _("Risk review completed")
|
||||
|
||||
CONTROL_CREATED = "control.created", _("Control created")
|
||||
CONTROL_UPDATED = "control.updated", _("Control updated")
|
||||
CONTROL_DELETED = "control.deleted", _("Control deleted")
|
||||
|
||||
RESIDUAL_CREATED = "residual.created", _("Residual created")
|
||||
RESIDUAL_UPDATED = "residual.updated", _("Residual updated")
|
||||
RESIDUAL_DELETED = "residual.deleted", _("Residual deleted")
|
||||
RESIDUAL_REVIEW_REQUIRED = "residual.review_required", _("Residual review required")
|
||||
RESIDUAL_REVIEW_COMPLETED = "residual.review_completed", _("Residual review completed")
|
||||
|
||||
INCIDENT_CREATED = "incident.created", _("Incident created")
|
||||
INCIDENT_UPDATED = "incident.updated", _("Incident updated")
|
||||
INCIDENT_DELETED = "incident.deleted", _("Incident deleted")
|
||||
|
||||
USER_CREATED = "user.created", _("User created")
|
||||
USER_DELETED = "user.deleted", _("User deleted")
|
||||
|
||||
class NotificationPreference(models.Model):
|
||||
"""
|
||||
Wich events does the user want to receive as notifications?
|
||||
|
@ -337,4 +361,38 @@ class NotificationPreference(models.Model):
|
|||
return f"Prefs({self.user})"
|
||||
|
||||
def should_notify(self, event_code: str) -> bool:
|
||||
return bool(getattr(self, event_code, False))
|
||||
return bool(getattr(self, event_code, False))
|
||||
|
||||
class NotificationRule(models.Model):
|
||||
"""
|
||||
Global Rules: Wich Event sends In-App- and/or Mail-Notifications?
|
||||
"""
|
||||
class Meta:
|
||||
verbose_name = _("Notification rule")
|
||||
verbose_name_plural = _("Notification rules")
|
||||
|
||||
kind = models.CharField(
|
||||
_("Event"),
|
||||
max_length=40,
|
||||
choices=NotificationKind.choices,
|
||||
unique=True,
|
||||
)
|
||||
enabled_in_app = models.BooleanField(_("Show in app"), default=True)
|
||||
enabled_email = models.BooleanField(_("Send via email"), default=False)
|
||||
|
||||
# Empfängerkreise
|
||||
to_owner = models.BooleanField(
|
||||
_("Send to owner/responsible/reporter (if available)"),
|
||||
default=True
|
||||
)
|
||||
to_staff = models.BooleanField(
|
||||
_("Send to all staff"),
|
||||
default=False
|
||||
)
|
||||
extra_recipients = models.TextField(
|
||||
_("Extra recipients (emails, comma or newline separated)"),
|
||||
blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.get_kind_display() or self.kind
|
||||
|
|
|
@ -5,8 +5,8 @@ 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, Notification, NotificationPreference
|
||||
from .utils import model_diff
|
||||
from .models import Control, Risk, ResidualRisk, AuditLog, Incident, Notification, NotificationKind, NotificationPreference
|
||||
from .utils import model_diff, notify_event
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# General definitions
|
||||
|
@ -111,6 +111,19 @@ def log_risk_save(sender, instance, created, **kwargs):
|
|||
changes=clean_changes,
|
||||
)
|
||||
|
||||
if created:
|
||||
notify_event(
|
||||
NotificationKind.RISK_CREATED,
|
||||
message=_("Risk created: {t}").format(t=instance.title),
|
||||
users=[instance.owner] if instance.owner_id else None,
|
||||
)
|
||||
else:
|
||||
notify_event(
|
||||
NotificationKind.RISK_UPDATED,
|
||||
message=_("Risk updated: {t}").format(t=instance.title),
|
||||
users=[instance.owner] if instance.owner_id else None,
|
||||
)
|
||||
|
||||
@receiver(post_delete, sender=Risk)
|
||||
def log_risk_delete(sender, instance, **kwargs):
|
||||
"""
|
||||
|
@ -125,6 +138,12 @@ def log_risk_delete(sender, instance, **kwargs):
|
|||
changes=None, # no fields to track on deletion
|
||||
)
|
||||
|
||||
notify_event(
|
||||
NotificationKind.RISK_DELETED,
|
||||
message=_("Risk deleted: {t}").format(t=instance.title),
|
||||
users=[instance.owner] if instance.owner_id else None,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Controls
|
||||
# ---------------------------------------------------------------------------
|
||||
|
@ -186,6 +205,16 @@ def log_control_save(sender, instance, created, **kwargs):
|
|||
changes=clean_changes,
|
||||
)
|
||||
|
||||
kind = NotificationKind.CONTROL_CREATED if created else NotificationKind.CONTROL_UPDATED
|
||||
notify_event(
|
||||
kind,
|
||||
message=_("Control {event}: {t}").format(
|
||||
event=_("created") if created else _("updated"),
|
||||
t=instance.title,
|
||||
),
|
||||
users=[instance.responsible] if instance.responsible_id else None,
|
||||
)
|
||||
|
||||
@receiver(post_delete, sender=Control)
|
||||
def log_control_delete(sender, instance, **kwargs):
|
||||
user = getattr(instance, "_changed_by", None) or get_current_user()
|
||||
|
@ -197,6 +226,12 @@ def log_control_delete(sender, instance, **kwargs):
|
|||
changes=None,
|
||||
)
|
||||
|
||||
notify_event(
|
||||
NotificationKind.CONTROL_DELETED,
|
||||
message=_("Control deleted: {t}").format(t=instance.title),
|
||||
users=[instance.responsible] if instance.responsible_id else None,
|
||||
)
|
||||
|
||||
@receiver(m2m_changed, sender=Control.risks.through)
|
||||
def control_risks_changed(sender, instance: Control, action, reverse, pk_set, **kwargs):
|
||||
if action in {"post_add", "post_remove", "post_clear"}:
|
||||
|
@ -275,6 +310,38 @@ def log_residual_save(sender, instance, created, **kwargs):
|
|||
changes=clean_changes,
|
||||
)
|
||||
|
||||
if created:
|
||||
notify_event(
|
||||
NotificationKind.RESIDUAL_CREATED,
|
||||
message=_("Residual created for risk: {t}").format(t=instance.risk.title),
|
||||
users=[instance.risk.owner] if instance.risk.owner_id else None,
|
||||
)
|
||||
else:
|
||||
# Änderungen prüfen
|
||||
old = ResidualRisk.objects.get(pk=instance.pk)
|
||||
changes = model_diff(old, instance)
|
||||
# Review-Flag Wechsel gezielt melden:
|
||||
if "review_required" in changes:
|
||||
if getattr(instance, "review_required", False):
|
||||
notify_event(
|
||||
NotificationKind.RESIDUAL_REVIEW_REQUIRED,
|
||||
message=_("Residual review required for risk: {t}").format(t=instance.risk.title),
|
||||
users=[instance.risk.owner] if instance.risk.owner_id else None,
|
||||
)
|
||||
else:
|
||||
notify_event(
|
||||
NotificationKind.RESIDUAL_REVIEW_COMPLETED,
|
||||
message=_("Residual review completed for risk: {t}").format(t=instance.risk.title),
|
||||
users=[instance.risk.owner] if instance.risk.owner_id else None,
|
||||
)
|
||||
else:
|
||||
notify_event(
|
||||
NotificationKind.RESIDUAL_UPDATED,
|
||||
message=_("Residual updated for risk: {t}").format(t=instance.risk.title),
|
||||
users=[instance.risk.owner] if instance.risk.owner_id else None,
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=ResidualRisk)
|
||||
def log_residual_delete(sender, instance, **kwargs):
|
||||
user = getattr(instance, "_changed_by", None) or get_current_user()
|
||||
|
@ -286,6 +353,12 @@ def log_residual_delete(sender, instance, **kwargs):
|
|||
changes=None,
|
||||
)
|
||||
|
||||
notify_event(
|
||||
NotificationKind.RESIDUAL_DELETED,
|
||||
message=_("Residual deleted for risk: {t}").format(t=instance.risk.title),
|
||||
users=[instance.risk.owner] if instance.risk.owner_id else None,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Incidents
|
||||
# ---------------------------------------------------------------------------
|
||||
|
@ -331,6 +404,16 @@ def log_incident_save(sender, instance, created, **kwargs):
|
|||
changes=clean_changes,
|
||||
)
|
||||
|
||||
kind = NotificationKind.INCIDENT_CREATED if created else NotificationKind.INCIDENT_UPDATED
|
||||
notify_event(
|
||||
kind,
|
||||
message=_("Incident {event}: {t}").format(
|
||||
event=_("created") if created else _("updated"),
|
||||
t=instance.title,
|
||||
),
|
||||
users=[instance.reported_by] if instance.reported_by_id else None,
|
||||
)
|
||||
|
||||
@receiver(m2m_changed, sender=Incident.related_risks.through)
|
||||
def log_incident_risks_change(sender, instance, action, reverse, model, pk_set, **kwargs):
|
||||
if action in ["post_add", "post_remove", "post_clear"]:
|
||||
|
@ -352,4 +435,10 @@ def log_incident_delete(sender, instance, **kwargs):
|
|||
model="Incident",
|
||||
object_id=instance.pk,
|
||||
changes=None,
|
||||
)
|
||||
|
||||
notify_event(
|
||||
NotificationKind.INCIDENT_DELETED,
|
||||
message=_("Incident deleted: {t}").format(t=instance.title),
|
||||
users=[instance.reported_by] if instance.reported_by_id else None,
|
||||
)
|
|
@ -1,7 +1,12 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.mail import send_mail
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .models import AuditLog, Notification, Risk, ResidualRisk
|
||||
from .models import AuditLog, Notification,NotificationRule, NotificationKind, Risk, ResidualRisk
|
||||
from typing import Iterable, Optional
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
def model_diff(old, new, fields=None):
|
||||
"""
|
||||
|
@ -55,4 +60,68 @@ def check_risk_followups():
|
|||
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},
|
||||
)
|
||||
)
|
||||
|
||||
notify_event(
|
||||
NotificationKind.RISK_REVIEW_REQUIRED,
|
||||
message=_("Follow-up reached: review required for risk '{t}'").format(t=risk.title),
|
||||
users=[risk.owner] if risk.owner_id else None,
|
||||
)
|
||||
|
||||
def _split_emails(value: str) -> list[str]:
|
||||
if not value:
|
||||
return []
|
||||
raw = value.replace("\n", ",").split(",")
|
||||
return [e.strip() for e in raw if "@" in e and e.strip()]
|
||||
|
||||
def notify_event(kind: str, *, message: str, users: Optional[Iterable[User]] = None):
|
||||
"""
|
||||
Generates in-app notifications and/or emails depending on the rule.
|
||||
- users: Basic recipients (owner/responsible/reporter) – can be None.
|
||||
- staff/extra recipients are added from the rule.
|
||||
"""
|
||||
rule = NotificationRule.objects.filter(kind=kind).first()
|
||||
|
||||
# Fallback: without rule → only in-app
|
||||
enabled_in_app = True
|
||||
enabled_email = False
|
||||
to_staff = False
|
||||
extra_emails = []
|
||||
|
||||
recipients_users = set()
|
||||
|
||||
if users:
|
||||
for u in users:
|
||||
if u and getattr(u, "is_active", False):
|
||||
recipients_users.add(u)
|
||||
|
||||
if rule:
|
||||
enabled_in_app = rule.enabled_in_app
|
||||
enabled_email = rule.enabled_email
|
||||
if rule.to_staff:
|
||||
to_staff = True
|
||||
extra_emails = _split_emails(rule.extra_recipients)
|
||||
|
||||
if to_staff:
|
||||
for u in User.objects.filter(is_staff=True, is_active=True):
|
||||
recipients_users.add(u)
|
||||
|
||||
# In-App
|
||||
if enabled_in_app:
|
||||
for u in recipients_users:
|
||||
Notification.objects.create(user=u, message=message)
|
||||
|
||||
# E-Mail
|
||||
if enabled_email:
|
||||
emails = [u.email for u in recipients_users if u and u.email] + extra_emails
|
||||
emails = list(dict.fromkeys(emails)) # de-dupe, Reihenfolge erhalten
|
||||
if emails:
|
||||
subject = _("Notification")
|
||||
body = message
|
||||
send_mail(
|
||||
subject,
|
||||
body,
|
||||
getattr(settings, "DEFAULT_FROM_EMAIL", "webmaster@localhost"),
|
||||
emails,
|
||||
fail_silently=True, # im Zweifel nicht crashen
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue