Implement notification system and status update forms
- Added Notification model with admin interface for managing notifications. - Created context processor to count unread notifications for the user. - Introduced forms for updating the status of Risk, Control, Incident, and ResidualRisk. - Added views for displaying and managing notifications, including marking them as read. - Updated URLs to include routes for notifications and status updates. - Enhanced templates to support notifications display and status update forms. - Improved CSS for avatar and badge display in the navbar. - Translated various static texts to support internationalization.
This commit is contained in:
parent
ab01841cf2
commit
ebfcbddd5c
19 changed files with 797 additions and 142 deletions
|
@ -76,6 +76,7 @@ TEMPLATES = [
|
|||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
"risks.context_processors.unread_notifications_count",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
|
@ -2,7 +2,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: wira-risk-management\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 11:18+0200\n"
|
||||
"POT-Creation-Date: 2025-09-10 12:51+0200\n"
|
||||
"PO-Revision-Date: 2025-09-09 13:45+0200\n"
|
||||
"Last-Translator: Kevin Heyer <kevin@example.com>\n"
|
||||
"Language-Team: German\n"
|
||||
|
@ -26,7 +26,8 @@ msgstr "Admin"
|
|||
msgid "Risks"
|
||||
msgstr "Risiken"
|
||||
|
||||
#: risks/admin.py:16 risks/models.py:190 templates/risks/list_risks.html:37
|
||||
#: risks/admin.py:16 risks/models.py:190 templates/base.html:36
|
||||
#: templates/risks/list_risks.html:37
|
||||
msgid "Controls"
|
||||
msgstr "Maßnahmen"
|
||||
|
||||
|
@ -38,7 +39,7 @@ msgstr "Restrisiken"
|
|||
msgid "Reviews"
|
||||
msgstr "Prüfung"
|
||||
|
||||
#: risks/admin.py:19 risks/models.py:258
|
||||
#: risks/admin.py:19 risks/models.py:258 templates/base.html:37
|
||||
msgid "Incidents"
|
||||
msgstr "Vorfälle"
|
||||
|
||||
|
@ -46,22 +47,70 @@ msgstr "Vorfälle"
|
|||
msgid "Users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: risks/admin.py:26
|
||||
#: risks/admin.py:133 risks/models.py:302
|
||||
msgid "User"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: risks/admin.py:139
|
||||
msgid "Message"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:147
|
||||
msgid "Mark selected as read"
|
||||
msgstr "Alle als gelesen Markieren"
|
||||
|
||||
#: risks/admin.py:150
|
||||
msgid "%(n)d notifications marked as read."
|
||||
msgstr "%(n)d Benachrichtigungen wurden als gelesen Markiert"
|
||||
|
||||
#: risks/admin.py:152
|
||||
msgid "Mark selected as unread"
|
||||
msgstr "Alle als gelesen Markieren"
|
||||
|
||||
#: risks/admin.py:155
|
||||
msgid "%(n)d notifications marked as unread."
|
||||
msgstr "%(n)d Benachrichtigungen wurden als ungelesen Markiert"
|
||||
|
||||
#: risks/admin.py:157
|
||||
msgid "Mark selected as sent"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:160
|
||||
msgid "%(n)d notifications marked as sent."
|
||||
msgstr "Alle Benachrichtigungen wurden als gelesen Markiert"
|
||||
|
||||
#: risks/admin.py:162
|
||||
msgid "Mark selected as unsent"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:165
|
||||
msgid "%(n)d notifications marked as unsent."
|
||||
msgstr "Alle Benachrichtigungen wurden als gelesen Markiert"
|
||||
|
||||
#: risks/admin.py:177
|
||||
msgid "SSO Information"
|
||||
msgstr "SSO-Informationen"
|
||||
|
||||
#: risks/admin.py:35
|
||||
#: risks/admin.py:186
|
||||
msgid "Risks Owned"
|
||||
msgstr "Eigene Risiken"
|
||||
|
||||
#: risks/admin.py:39
|
||||
#: risks/admin.py:190
|
||||
msgid "Controls Responsible"
|
||||
msgstr "Verantwortlich für Maßnahmen"
|
||||
|
||||
#: risks/apps.py:7
|
||||
#: risks/apps.py:7 templates/base.html:7 templates/base.html:32
|
||||
msgid "Risk Management"
|
||||
msgstr "Risikomanagement"
|
||||
|
||||
#: risks/forms.py:9 risks/forms.py:16 risks/forms.py:23 risks/models.py:73
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
#: risks/forms.py:30 risks/models.py:42 templates/risks/item_risk.html:136
|
||||
msgid "Review required"
|
||||
msgstr "Prüfung nötig"
|
||||
|
||||
#: risks/models.py:35 templates/risks/list_risks.html:18
|
||||
#: templates/risks/list_risks.html:83
|
||||
msgid "Risk"
|
||||
|
@ -79,10 +128,6 @@ msgstr "In Bearbeitung"
|
|||
msgid "Closed"
|
||||
msgstr "Geschlossen"
|
||||
|
||||
#: 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"
|
||||
|
@ -159,10 +204,6 @@ msgstr "Erstellt am"
|
|||
msgid "Updated at"
|
||||
msgstr "Aktualisiert am"
|
||||
|
||||
#: risks/models.py:73
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
#: risks/models.py:133
|
||||
msgid "Residual Risk"
|
||||
msgstr "Restrisiko"
|
||||
|
@ -219,9 +260,14 @@ msgstr "Meldedatum"
|
|||
msgid "Reported by"
|
||||
msgstr "Gemeldet von"
|
||||
|
||||
#: risks/models.py:298
|
||||
msgid "User"
|
||||
msgstr "Benutzer"
|
||||
#: risks/models.py:279
|
||||
msgid "Notification"
|
||||
msgstr "Benachrichtigung"
|
||||
|
||||
#: risks/models.py:280 templates/base.html:88
|
||||
#: templates/risks/notifications.html:4
|
||||
msgid "Notifications"
|
||||
msgstr "Nachrichten"
|
||||
|
||||
#: risks/signals.py:57
|
||||
#, python-brace-format
|
||||
|
@ -289,6 +335,7 @@ msgid "Residual risk deleted for '{t}'"
|
|||
msgstr "Restrisiko für '{t}' gelöscht"
|
||||
|
||||
#: risks/signals.py:296
|
||||
#, python-brace-format
|
||||
msgid "Incident '{t}' {s}"
|
||||
msgstr "Vorfälle '{t}' {s}"
|
||||
|
||||
|
@ -302,10 +349,62 @@ msgstr "Vorfall '{t}' gelöscht"
|
|||
msgid "Follow-up reached: review required for risk '{t}'"
|
||||
msgstr "Wiedervorlagedatum erreicht: Prüfung nötig für Risiko '{t}'"
|
||||
|
||||
#: templates/risks/dashboard.html:9
|
||||
#: risks/views.py:315
|
||||
msgid "Notification marked as read."
|
||||
msgstr "Nachricht als gelesen markiert"
|
||||
|
||||
#: risks/views.py:323
|
||||
msgid "All notifications marked as read."
|
||||
msgstr "Alle Benachrichtigungen wurden als gelesen Markiert"
|
||||
|
||||
#: risks/views.py:340
|
||||
msgid "Risk status updated."
|
||||
msgstr "Risikostatus Aktualisiert"
|
||||
|
||||
#: risks/views.py:354
|
||||
msgid "Control status updated."
|
||||
msgstr "Maßnahmenstatus Aktualisiert"
|
||||
|
||||
#: risks/views.py:368
|
||||
msgid "Incident status updated."
|
||||
msgstr "Vorfallstatus Aktualisiert"
|
||||
|
||||
#: risks/views.py:384
|
||||
msgid "Residual review flag updated."
|
||||
msgstr "Restrisiko geprüft"
|
||||
|
||||
#: templates/base.html:34 templates/risks/dashboard.html:9
|
||||
msgid "Dashboard"
|
||||
msgstr "Dashboard"
|
||||
|
||||
#: templates/base.html:35 templates/risks/list_risks.html:4
|
||||
msgid "Risk analysis"
|
||||
msgstr "Risikoanalyse"
|
||||
|
||||
#: templates/base.html:70
|
||||
msgid "AdminCP"
|
||||
msgstr "Adminbereich"
|
||||
|
||||
#: templates/base.html:76
|
||||
msgid "Derk Mode"
|
||||
msgstr "Dark Mode"
|
||||
|
||||
#: templates/base.html:82
|
||||
msgid "Logout"
|
||||
msgstr "Logout"
|
||||
|
||||
#: templates/base.html:104
|
||||
msgid "Login"
|
||||
msgstr "Login"
|
||||
|
||||
#: templates/base.html:139 templates/base.html:146
|
||||
msgid "Light Mode"
|
||||
msgstr "Light Mode"
|
||||
|
||||
#: templates/base.html:149
|
||||
msgid "Dark Mode"
|
||||
msgstr "Dark Mode"
|
||||
|
||||
#: templates/risks/dashboard.html:12
|
||||
msgid "Overview of Risks, Controls and Incidents"
|
||||
msgstr "Übersicht der Risiken, Maßnahmen und Vorfälle"
|
||||
|
@ -334,36 +433,40 @@ 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/item_risk.html:34
|
||||
msgid "Update status"
|
||||
msgstr "Status Aktualisiert"
|
||||
|
||||
#: templates/risks/item_risk.html:89 templates/risks/item_risk.html:147
|
||||
#: templates/risks/list_risks.html:86
|
||||
msgid "Likelihood"
|
||||
msgstr "Eintritt"
|
||||
|
||||
#: templates/risks/item_risk.html:77 templates/risks/item_risk.html:123
|
||||
#: templates/risks/item_risk.html:98 templates/risks/item_risk.html:156
|
||||
#: templates/risks/list_risks.html:87
|
||||
msgid "Impact"
|
||||
msgstr "Schaden"
|
||||
|
||||
#: templates/risks/item_risk.html:86 templates/risks/item_risk.html:132
|
||||
#: templates/risks/item_risk.html:107 templates/risks/item_risk.html:165
|
||||
#: templates/risks/list_risks.html:89
|
||||
msgid "Level"
|
||||
msgstr "Stufe"
|
||||
|
||||
#: templates/risks/item_risk.html:95 templates/risks/item_risk.html:140
|
||||
#: templates/risks/item_risk.html:116 templates/risks/item_risk.html:173
|
||||
#: templates/risks/list_risks.html:88
|
||||
msgid "Score"
|
||||
msgstr "Score"
|
||||
|
||||
#: templates/risks/list_risks.html:4
|
||||
msgid "Risk analysis"
|
||||
msgstr "Risikoanalyse"
|
||||
#: templates/risks/item_risk.html:139
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#: templates/risks/list_risks.html:9
|
||||
msgid "Filter"
|
||||
msgstr "Filter"
|
||||
|
||||
#: templates/risks/list_risks.html:22 templates/risks/list_risks.html:41
|
||||
#: templates/risks/list_risks.html:60
|
||||
#: templates/risks/list_risks.html:60 templates/risks/notifications.html:13
|
||||
msgid "All"
|
||||
msgstr "Alle"
|
||||
|
||||
|
@ -374,3 +477,23 @@ msgstr "Risikoeigner"
|
|||
#: templates/risks/list_risks.html:84
|
||||
msgid "Asset / Process"
|
||||
msgstr "Asset / Prozess"
|
||||
|
||||
#: templates/risks/notifications.html:12
|
||||
msgid "Unread"
|
||||
msgstr "Ungelesen"
|
||||
|
||||
#: templates/risks/notifications.html:20
|
||||
msgid "Mark all as read"
|
||||
msgstr "Alle als gelesen Markieren"
|
||||
|
||||
#: templates/risks/notifications.html:33
|
||||
msgid "New"
|
||||
msgstr "Neu"
|
||||
|
||||
#: templates/risks/notifications.html:43
|
||||
msgid "Mark as read"
|
||||
msgstr "Als gelesen markieren"
|
||||
|
||||
#: templates/risks/notifications.html:53
|
||||
msgid "No notifications."
|
||||
msgstr "Keine Nachrichten"
|
||||
|
|
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-09-10 11:18+0200\n"
|
||||
"POT-Creation-Date: 2025-09-10 12:51+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
@ -32,7 +32,8 @@ msgstr ""
|
|||
msgid "Risks"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:16 risks/models.py:190 templates/risks/list_risks.html:37
|
||||
#: risks/admin.py:16 risks/models.py:190 templates/base.html:36
|
||||
#: templates/risks/list_risks.html:37
|
||||
msgid "Controls"
|
||||
msgstr ""
|
||||
|
||||
|
@ -44,7 +45,7 @@ msgstr ""
|
|||
msgid "Reviews"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:19 risks/models.py:258
|
||||
#: risks/admin.py:19 risks/models.py:258 templates/base.html:37
|
||||
msgid "Incidents"
|
||||
msgstr ""
|
||||
|
||||
|
@ -52,22 +53,74 @@ msgstr ""
|
|||
msgid "Users"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:26
|
||||
#: risks/admin.py:133 risks/models.py:302
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:139
|
||||
msgid "Message"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:147
|
||||
msgid "Mark selected as read"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:150
|
||||
#, python-format
|
||||
msgid "%(n)d notifications marked as read."
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:152
|
||||
msgid "Mark selected as unread"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:155
|
||||
#, python-format
|
||||
msgid "%(n)d notifications marked as unread."
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:157
|
||||
msgid "Mark selected as sent"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:160
|
||||
#, python-format
|
||||
msgid "%(n)d notifications marked as sent."
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:162
|
||||
msgid "Mark selected as unsent"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:165
|
||||
#, python-format
|
||||
msgid "%(n)d notifications marked as unsent."
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:177
|
||||
msgid "SSO Information"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:35
|
||||
#: risks/admin.py:186
|
||||
msgid "Risks Owned"
|
||||
msgstr ""
|
||||
|
||||
#: risks/admin.py:39
|
||||
#: risks/admin.py:190
|
||||
msgid "Controls Responsible"
|
||||
msgstr ""
|
||||
|
||||
#: risks/apps.py:7
|
||||
#: risks/apps.py:7 templates/base.html:7 templates/base.html:32
|
||||
msgid "Risk Management"
|
||||
msgstr ""
|
||||
|
||||
#: risks/forms.py:9 risks/forms.py:16 risks/forms.py:23 risks/models.py:73
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: risks/forms.py:30 risks/models.py:42 templates/risks/item_risk.html:136
|
||||
msgid "Review required"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:35 templates/risks/list_risks.html:18
|
||||
#: templates/risks/list_risks.html:83
|
||||
msgid "Risk"
|
||||
|
@ -85,10 +138,6 @@ msgstr ""
|
|||
msgid "Closed"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:42
|
||||
msgid "Review required"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:45
|
||||
msgid "Very low – occurs less than once every 5 years"
|
||||
msgstr ""
|
||||
|
@ -165,10 +214,6 @@ msgstr ""
|
|||
msgid "Updated at"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:73
|
||||
msgid "Status"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:133
|
||||
msgid "Residual Risk"
|
||||
msgstr ""
|
||||
|
@ -225,8 +270,13 @@ msgstr ""
|
|||
msgid "Reported by"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:298
|
||||
msgid "User"
|
||||
#: risks/models.py:279
|
||||
msgid "Notification"
|
||||
msgstr ""
|
||||
|
||||
#: risks/models.py:280 templates/base.html:88
|
||||
#: templates/risks/notifications.html:4
|
||||
msgid "Notifications"
|
||||
msgstr ""
|
||||
|
||||
#: risks/signals.py:57
|
||||
|
@ -309,10 +359,62 @@ msgstr ""
|
|||
msgid "Follow-up reached: review required for risk '{t}'"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/dashboard.html:9
|
||||
#: risks/views.py:315
|
||||
msgid "Notification marked as read."
|
||||
msgstr ""
|
||||
|
||||
#: risks/views.py:323
|
||||
msgid "All notifications marked as read."
|
||||
msgstr ""
|
||||
|
||||
#: risks/views.py:340
|
||||
msgid "Risk status updated."
|
||||
msgstr ""
|
||||
|
||||
#: risks/views.py:354
|
||||
msgid "Control status updated."
|
||||
msgstr ""
|
||||
|
||||
#: risks/views.py:368
|
||||
msgid "Incident status updated."
|
||||
msgstr ""
|
||||
|
||||
#: risks/views.py:384
|
||||
msgid "Residual review flag updated."
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:34 templates/risks/dashboard.html:9
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:35 templates/risks/list_risks.html:4
|
||||
msgid "Risk analysis"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:70
|
||||
msgid "AdminCP"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:76
|
||||
msgid "Derk Mode"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:82
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:104
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:139 templates/base.html:146
|
||||
msgid "Light Mode"
|
||||
msgstr ""
|
||||
|
||||
#: templates/base.html:149
|
||||
msgid "Dark Mode"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/dashboard.html:12
|
||||
msgid "Overview of Risks, Controls and Incidents"
|
||||
msgstr ""
|
||||
|
@ -341,28 +443,32 @@ msgstr ""
|
|||
msgid "Risks by CIA"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:68 templates/risks/item_risk.html:114
|
||||
#: templates/risks/item_risk.html:34
|
||||
msgid "Update status"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:89 templates/risks/item_risk.html:147
|
||||
#: templates/risks/list_risks.html:86
|
||||
msgid "Likelihood"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:77 templates/risks/item_risk.html:123
|
||||
#: templates/risks/item_risk.html:98 templates/risks/item_risk.html:156
|
||||
#: templates/risks/list_risks.html:87
|
||||
msgid "Impact"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:86 templates/risks/item_risk.html:132
|
||||
#: templates/risks/item_risk.html:107 templates/risks/item_risk.html:165
|
||||
#: templates/risks/list_risks.html:89
|
||||
msgid "Level"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/item_risk.html:95 templates/risks/item_risk.html:140
|
||||
#: templates/risks/item_risk.html:116 templates/risks/item_risk.html:173
|
||||
#: templates/risks/list_risks.html:88
|
||||
msgid "Score"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/list_risks.html:4
|
||||
msgid "Risk analysis"
|
||||
#: templates/risks/item_risk.html:139
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/list_risks.html:9
|
||||
|
@ -370,7 +476,7 @@ msgid "Filter"
|
|||
msgstr ""
|
||||
|
||||
#: templates/risks/list_risks.html:22 templates/risks/list_risks.html:41
|
||||
#: templates/risks/list_risks.html:60
|
||||
#: templates/risks/list_risks.html:60 templates/risks/notifications.html:13
|
||||
msgid "All"
|
||||
msgstr ""
|
||||
|
||||
|
@ -381,3 +487,23 @@ msgstr ""
|
|||
#: templates/risks/list_risks.html:84
|
||||
msgid "Asset / Process"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/notifications.html:12
|
||||
msgid "Unread"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/notifications.html:20
|
||||
msgid "Mark all as read"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/notifications.html:33
|
||||
msgid "New"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/notifications.html:43
|
||||
msgid "Mark as read"
|
||||
msgstr ""
|
||||
|
||||
#: templates/risks/notifications.html:53
|
||||
msgid "No notifications."
|
||||
msgstr ""
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .models import Control, Incident, NotificationPreference , Risk, ResidualRisk, User
|
||||
from .models import Control, Incident, Notification, NotificationPreference , Risk, ResidualRisk, User
|
||||
|
||||
admin.site.site_header = _("Administration")
|
||||
admin.site.site_title = _("Admin")
|
||||
|
@ -20,24 +20,6 @@ class NotificationPreferenceInline(admin.StackedInline):
|
|||
(_("Users"), {"fields": ("user_created","user_deleted")}),
|
||||
)
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
fieldsets = BaseUserAdmin.fieldsets + (
|
||||
(_("SSO Information"), {"fields": ("is_sso_user",)}),
|
||||
)
|
||||
list_display = ("username", "email", "is_staff", "is_superuser", "is_sso_user",
|
||||
"owned_risks_count", "responsible_controls_count")
|
||||
|
||||
inlines = [NotificationPreferenceInline]
|
||||
|
||||
def owned_risks_count(self, obj):
|
||||
return obj.risks_owned.count()
|
||||
owned_risks_count.short_description = _("Risks Owned")
|
||||
|
||||
def responsible_controls_count(self, obj):
|
||||
return obj.controls_responsible.count()
|
||||
responsible_controls_count.short_description = _("Controls Responsible")
|
||||
|
||||
class ResidualRiskInline(admin.StackedInline):
|
||||
"""
|
||||
Inline editor for ResidualRisk, linked one-to-one with Risk
|
||||
|
@ -135,3 +117,74 @@ class IncidentAdmin(admin.ModelAdmin):
|
|||
obj._changed_by = request.user
|
||||
super().delete_model(request, obj)
|
||||
|
||||
@admin.register(Notification)
|
||||
class NotificationAdmin(admin.ModelAdmin):
|
||||
|
||||
date_hierarchy = "created_at"
|
||||
list_display = ("id", "created_at", "user_display", "short_message", "read", "sent")
|
||||
list_display_links = ("id", "short_message")
|
||||
list_filter = ("read", "sent", "created_at")
|
||||
search_fields = ("message", "user__username", "user__first_name", "user__last_name", "user__email")
|
||||
list_select_related = ("user",)
|
||||
list_editable = ("read", "sent")
|
||||
ordering = ("-created_at",)
|
||||
autocomplete_fields = ("user",)
|
||||
|
||||
@admin.display(description=_("User"))
|
||||
def user_display(self, obj):
|
||||
if not obj.user:
|
||||
return "—"
|
||||
return obj.user.get_full_name() or obj.user.username
|
||||
|
||||
@admin.display(description=_("Message"))
|
||||
def short_message(self, obj):
|
||||
msg = obj.message or ""
|
||||
return (msg[:80] + "…") if len(msg) > 80 else msg
|
||||
|
||||
# Bulk-Aktionen
|
||||
actions = ["mark_as_read", "mark_as_unread", "mark_as_sent", "mark_as_unsent"]
|
||||
|
||||
@admin.action(description=_("Mark selected as read"))
|
||||
def mark_as_read(self, request, queryset):
|
||||
n = queryset.update(read=True)
|
||||
self.message_user(request, _("%(n)d notifications marked as read.") % {"n": n})
|
||||
|
||||
@admin.action(description=_("Mark selected as unread"))
|
||||
def mark_as_unread(self, request, queryset):
|
||||
n = queryset.update(read=False)
|
||||
self.message_user(request, _("%(n)d notifications marked as unread.") % {"n": n})
|
||||
|
||||
@admin.action(description=_("Mark selected as sent"))
|
||||
def mark_as_sent(self, request, queryset):
|
||||
n = queryset.update(sent=True)
|
||||
self.message_user(request, _("%(n)d notifications marked as sent.") % {"n": n})
|
||||
|
||||
@admin.action(description=_("Mark selected as unsent"))
|
||||
def mark_as_unsent(self, request, queryset):
|
||||
n = queryset.update(sent=False)
|
||||
self.message_user(request, _("%(n)d notifications marked as unsent.") % {"n": n})
|
||||
|
||||
class NotificationInline(admin.TabularInline):
|
||||
model = Notification
|
||||
fields = ("created_at", "message", "read", "sent")
|
||||
readonly_fields = ("created_at", "message")
|
||||
extra = 0
|
||||
ordering = ("-created_at",)
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
fieldsets = BaseUserAdmin.fieldsets + (
|
||||
(_("SSO Information"), {"fields": ("is_sso_user",)}),
|
||||
)
|
||||
list_display = ("username", "email", "is_staff", "is_superuser", "is_sso_user",
|
||||
"owned_risks_count", "responsible_controls_count")
|
||||
|
||||
inlines = [NotificationInline, NotificationPreferenceInline]
|
||||
|
||||
def owned_risks_count(self, obj):
|
||||
return obj.risks_owned.count()
|
||||
owned_risks_count.short_description = _("Risks Owned")
|
||||
|
||||
def responsible_controls_count(self, obj):
|
||||
return obj.controls_responsible.count()
|
||||
responsible_controls_count.short_description = _("Controls Responsible")
|
7
risks/context_processors.py
Normal file
7
risks/context_processors.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
def unread_notifications_count(request):
|
||||
if not request.user.is_authenticated:
|
||||
return {"notifications_unread_count": 0}
|
||||
from .models import Notification
|
||||
return {
|
||||
"notifications_unread_count": Notification.objects.filter(user=request.user, read=False).count()
|
||||
}
|
31
risks/forms.py
Normal file
31
risks/forms.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .models import Risk, Control, Incident, ResidualRisk
|
||||
|
||||
class RiskStatusForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Risk
|
||||
fields = ["status"]
|
||||
labels = {"status": _("Status")}
|
||||
widgets = {"status": forms.Select(attrs={"class": "select"})}
|
||||
|
||||
class ControlStatusForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Control
|
||||
fields = ["status"]
|
||||
labels = {"status": _("Status")}
|
||||
widgets = {"status": forms.Select(attrs={"class": "select"})}
|
||||
|
||||
class IncidentStatusForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Incident
|
||||
fields = ["status"]
|
||||
labels = {"status": _("Status")}
|
||||
widgets = {"status": forms.Select(attrs={"class": "select"})}
|
||||
|
||||
class ResidualReviewForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = ResidualRisk
|
||||
fields = ["review_required"]
|
||||
labels = {"review_required": _("Review required")}
|
||||
widgets = {"review_required": forms.CheckboxInput(attrs={"class": "checkbox"})}
|
19
risks/migrations/0022_alter_notification_options.py
Normal file
19
risks/migrations/0022_alter_notification_options.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-10 10:53
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("risks", "0021_risk_status_notificationpreference"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="notification",
|
||||
options={
|
||||
"verbose_name": "Notification",
|
||||
"verbose_name_plural": "Notifications",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -275,6 +275,10 @@ class Incident(models.Model):
|
|||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Notification(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Notification")
|
||||
verbose_name_plural = _("Notifications")
|
||||
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="notifications")
|
||||
|
||||
message = models.TextField()
|
||||
|
|
|
@ -5,6 +5,7 @@ app_name = "risks"
|
|||
|
||||
urlpatterns = [
|
||||
path("", views.dashboard, name="dashboard"),
|
||||
|
||||
path("risks/index", views.dashboard, name="index"),
|
||||
path("risks/list_risks", views.list_risks, name="list_risks"),
|
||||
path("risks/risks/<int:id>", views.show_risk, name="show_risk"),
|
||||
|
@ -12,4 +13,15 @@ urlpatterns = [
|
|||
path("risks/controls/<int:id>", views.show_control, name="show_control"),
|
||||
path("risks/list_incidents", views.list_incidents, name="list_incidents"),
|
||||
path("risks/incidents/<int:id>", views.show_incident, name="show_incident"),
|
||||
|
||||
# Notifications
|
||||
path("notifications/", views.notifications, name="notifications"),
|
||||
path("notifications/<int:pk>/read", views.notification_mark_read, name="notification_mark_read"),
|
||||
path("notifications/mark_all_read", views.notification_mark_all_read, name="notification_mark_all_read"),
|
||||
|
||||
# Risks status
|
||||
path("risks/<int:id>/status", views.update_risk_status, name="update_risk_status"),
|
||||
path("controls/<int:id>/status", views.update_control_status, name="update_control_status"),
|
||||
path("incidents/<int:id>/status", views.update_incident_status, name="update_incident_status"),
|
||||
path("residuals/<int:risk_id>/review", views.update_residual_review, name="update_residual_review"),
|
||||
]
|
111
risks/views.py
111
risks/views.py
|
@ -2,16 +2,29 @@ from django.contrib.admin.models import LogEntry
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib import messages
|
||||
from django.db.models import Count, Q
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.shortcuts import redirect, render, get_object_or_404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from collections import Counter
|
||||
from .forms import RiskStatusForm, ControlStatusForm, IncidentStatusForm, ResidualReviewForm
|
||||
from .models import Risk, Control, ResidualRisk, AuditLog, Incident, Notification
|
||||
from .serializers import ControlSerializer, RiskSerializer, ResidualRiskSerializer, UserSerializer, AuditSerializer, IncidentSerializer
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
def _can_edit_risk(user, risk: Risk) -> bool:
|
||||
return bool(user.is_staff or (risk.owner_id and risk.owner_id == user.id))
|
||||
|
||||
def _can_edit_control(user, control: Control) -> bool:
|
||||
return bool(user.is_staff or (control.responsible_id and control.responsible_id == user.id))
|
||||
|
||||
def _can_edit_incident(user, incident: Incident) -> bool:
|
||||
return bool(user.is_staff or (incident.reported_by_id and incident.reported_by_id == user.id))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
@ -274,3 +287,99 @@ def dashboard(request):
|
|||
'notifications_unread': notifications_unread,
|
||||
}
|
||||
return render(request, 'risks/dashboard.html', context)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Notifications
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@login_required
|
||||
def notifications(request):
|
||||
"""Eigene Benachrichtigungen ansehen + filtern"""
|
||||
flt = request.GET.get("filter", "unread")
|
||||
qs = Notification.objects.filter(user=request.user).order_by("-created_at")
|
||||
if flt == "unread":
|
||||
qs = qs.filter(read=False)
|
||||
# Einfache Pagination (optional)
|
||||
return render(request, "risks/notifications.html", {
|
||||
"notifications": qs,
|
||||
"filter": flt,
|
||||
})
|
||||
|
||||
@login_required
|
||||
def notification_mark_read(request, pk):
|
||||
if request.method != "POST":
|
||||
return HttpResponseForbidden()
|
||||
notif = get_object_or_404(Notification, pk=pk, user=request.user)
|
||||
notif.read = True
|
||||
notif.save(update_fields=["read"])
|
||||
messages.success(request, _("Notification marked as read."))
|
||||
return redirect(request.META.get("HTTP_REFERER") or "risks:notifications")
|
||||
|
||||
@login_required
|
||||
def notification_mark_all_read(request):
|
||||
if request.method != "POST":
|
||||
return HttpResponseForbidden()
|
||||
Notification.objects.filter(user=request.user, read=False).update(read=True)
|
||||
messages.success(request, _("All notifications marked as read."))
|
||||
return redirect("risks:notifications")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Status Updates
|
||||
# ---------------------------------------------------------------------------
|
||||
@login_required
|
||||
def update_risk_status(request, id):
|
||||
risk = get_object_or_404(Risk, pk=id)
|
||||
if not _can_edit_risk(request.user, risk):
|
||||
return HttpResponseForbidden()
|
||||
if request.method == "POST":
|
||||
form = RiskStatusForm(request.POST, instance=risk)
|
||||
if form.is_valid():
|
||||
obj = form.save(commit=False)
|
||||
obj._changed_by = request.user
|
||||
obj.save(update_fields=["status", "updated_at"])
|
||||
messages.success(request, _("Risk status updated."))
|
||||
return redirect("risks:show_risk", id=risk.pk)
|
||||
|
||||
@login_required
|
||||
def update_control_status(request, id):
|
||||
control = get_object_or_404(Control, pk=id)
|
||||
if not _can_edit_control(request.user, control):
|
||||
return HttpResponseForbidden()
|
||||
if request.method == "POST":
|
||||
form = ControlStatusForm(request.POST, instance=control)
|
||||
if form.is_valid():
|
||||
obj = form.save(commit=False)
|
||||
obj._changed_by = request.user
|
||||
obj.save(update_fields=["status", "updated_at"])
|
||||
messages.success(request, _("Control status updated."))
|
||||
return redirect("risks:show_control", id=control.pk)
|
||||
|
||||
@login_required
|
||||
def update_incident_status(request, id):
|
||||
incident = get_object_or_404(Incident, pk=id)
|
||||
if not _can_edit_incident(request.user, incident):
|
||||
return HttpResponseForbidden()
|
||||
if request.method == "POST":
|
||||
form = IncidentStatusForm(request.POST, instance=incident)
|
||||
if form.is_valid():
|
||||
obj = form.save(commit=False)
|
||||
obj._changed_by = request.user
|
||||
obj.save(update_fields=["status", "updated_at"])
|
||||
messages.success(request, _("Incident status updated."))
|
||||
return redirect("risks:show_incident", id=incident.pk)
|
||||
|
||||
@login_required
|
||||
def update_residual_review(request, risk_id):
|
||||
"""Review-Flag (Restrisiko) setzen/lösen"""
|
||||
risk = get_object_or_404(Risk, pk=risk_id)
|
||||
if not _can_edit_risk(request.user, risk):
|
||||
return HttpResponseForbidden()
|
||||
residual, created_resid = ResidualRisk.objects.get_or_create(risk=risk)
|
||||
if request.method == "POST":
|
||||
form = ResidualReviewForm(request.POST, instance=residual)
|
||||
if form.is_valid():
|
||||
obj = form.save(commit=False)
|
||||
obj._changed_by = request.user
|
||||
obj.save(update_fields=["review_required", "updated_at"])
|
||||
messages.success(request, _("Residual review flag updated."))
|
||||
return redirect("risks:show_risk", id=risk.pk)
|
|
@ -244,3 +244,34 @@ body.dark-mode a {
|
|||
@media (max-width: 1215px) {
|
||||
.risk-chip { --chip-w: 100%; width: var(--chip-w); }
|
||||
}
|
||||
|
||||
/* Container für Avatar + Badge */
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.avatar-wrap .badge {
|
||||
position: absolute;
|
||||
top: -0.35rem;
|
||||
right: -0.35rem;
|
||||
min-width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
padding: 0 .25rem;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 0 2px #fff;
|
||||
}
|
||||
|
||||
.avatar-wrap .tag.is-medium + .badge {
|
||||
min-width: 1.15rem;
|
||||
height: 1.15rem;
|
||||
font-size: 0.70rem;
|
||||
line-height: 1.15rem;
|
||||
}
|
||||
|
||||
/* Dark-Mode/
|
||||
.navbar.is-dark .avatar-wrap .badge { box-shadow: 0 0 0 2px hsl(229, 53%, 18%); }
|
|
@ -1,9 +1,10 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Risiko Management</title>
|
||||
<title>{% trans "Risk Management" %}</title>
|
||||
<link rel="stylesheet" href="{% static 'css/bulma.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/design.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/fontawesome.min.css' %}">
|
||||
|
@ -28,12 +29,12 @@
|
|||
<div id="mainNavbar" class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">Risikomanagement</a>
|
||||
<a class="navbar-link">{% trans "Risk Management" %}</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="/risks/index">Dashboard</a>
|
||||
<a class="navbar-item" href="/risks/list_risks">Risikoanalyse</a>
|
||||
<a class="navbar-item" href="/risks/list_controls">Maßnahmen</a>
|
||||
<a class="navbar-item" href="/risks/list_incidents">Vorfälle</a>
|
||||
<a class="navbar-item" href="/risks/index">{% trans "Dashboard" %}</a>
|
||||
<a class="navbar-item" href="/risks/list_risks">{% trans "Risk analysis" %}</a>
|
||||
<a class="navbar-item" href="/risks/list_controls">{% trans "Controls" %}</a>
|
||||
<a class="navbar-item" href="/risks/list_incidents">{% trans "Incidents" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -44,13 +45,6 @@
|
|||
<div class="navbar-end">
|
||||
<!-- Suche -->
|
||||
<div class="navbar-item">
|
||||
<!--
|
||||
<div class="field">
|
||||
<p class="control">
|
||||
<input class="input is-small" type="text" placeholder="Suchen">
|
||||
</p>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
|
||||
{% if request.user.is_authenticated %}
|
||||
|
@ -58,27 +52,44 @@
|
|||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
<!-- Initialen-Badge -->
|
||||
<span class="avatar-wrap">
|
||||
<span class="tag is-link is-light is-medium is-rounded"
|
||||
style="width:2.25rem;height:2.25rem;display:inline-flex;align-items:center;justify-content:center;">
|
||||
{{ request.user.username|slice:":2"|upper }}
|
||||
</span>
|
||||
|
||||
{% if notifications_unread_count %}
|
||||
<span class="badge tag is-danger is-rounded">
|
||||
{% if notifications_unread_count > 99 %}99+{% else %}{{ notifications_unread_count }}{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
|
||||
</a>
|
||||
|
||||
<div class="navbar-dropdown is-right">
|
||||
{% if request.user.is_staff %}
|
||||
<a class="navbar-item" href="/admin/">Admin</a>
|
||||
<a class="navbar-item" href="/admin/">{% trans "AdminCP" %}</a>
|
||||
<hr class="navbar-divider">
|
||||
{% endif %}
|
||||
|
||||
<a class="navbar-item" href="{% url 'risks:notifications' %}">
|
||||
{% trans "Notifications" %}
|
||||
{% if notifications_unread_count %}
|
||||
<span class="tag is-danger is-rounded" style="margin-left:.5rem;">{{ notifications_unread_count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
<!-- Dark Mode Toggle -->
|
||||
<button id="dark-mode-toggle" class="button is-small is-light">
|
||||
🌙 Dark Mode
|
||||
</button>
|
||||
<a id="dark-mode-toggle" class="navbar-item is-small is-light">
|
||||
🌙 {% trans "Derk Mode" %}
|
||||
</a>
|
||||
|
||||
<!-- Logout als POST über Hidden-Form -->
|
||||
<a class="navbar-item" href="#"
|
||||
onclick="document.getElementById('logout-form').submit(); return false;">
|
||||
Logout
|
||||
{% trans "Logout" %}
|
||||
</a>
|
||||
</div>
|
||||
</div><!-- Profil-Dropdown Ende -->
|
||||
|
@ -93,7 +104,7 @@
|
|||
<div class="navbar-item">
|
||||
<div class="buttons">
|
||||
<a class="button is-primary is-light" href="{% url 'login' %}">
|
||||
<strong>Login</strong>
|
||||
<strong>{% trans "Login" %}</strong>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -128,17 +139,17 @@
|
|||
// Dark Mode aus localStorage laden
|
||||
if (localStorage.getItem('darkMode') === 'enabled') {
|
||||
document.body.classList.add('dark-mode');
|
||||
toggleButton.textContent = '☀️ Light Mode';
|
||||
toggleButton.textContent = '☀️ {% trans "Light Mode" %}';
|
||||
}
|
||||
|
||||
toggleButton.addEventListener('click', () => {
|
||||
document.body.classList.toggle('dark-mode');
|
||||
if (document.body.classList.contains('dark-mode')) {
|
||||
localStorage.setItem('darkMode', 'enabled');
|
||||
toggleButton.textContent = '☀️ Light Mode';
|
||||
toggleButton.textContent = '☀️ {% trans "Light Mode" %}';
|
||||
} else {
|
||||
localStorage.setItem('darkMode', 'disabled');
|
||||
toggleButton.textContent = '🌙 Dark Mode';
|
||||
toggleButton.textContent = '🌙 {% trans "Dark Mode" %}';
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n risk_extras %}
|
||||
{% block crumbs %}
|
||||
<li><a href="{% url 'risks:list_controls' %}">Maßnahmen</a></li>
|
||||
<li><a href="{% url 'risks:show_control' control.id %}">{{ control.title }}</a></li>
|
||||
|
@ -16,7 +17,26 @@
|
|||
<header class="card-header">
|
||||
<p class="card-header-title">Überblick</p>
|
||||
|
||||
{% if request.user.is_staff %}
|
||||
{% if request.user.is_staff or control.responsible.id == request.user.id %}
|
||||
<form method="post" action="{% url 'risks:update_control_status' control.id %}" class="card-header-icon" style="margin-left:auto;">
|
||||
{% csrf_token %}
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<div class="select is-small">
|
||||
<select name="status">
|
||||
{% for value,label in control.STATUS_CHOICES %}
|
||||
<option value="{{ value }}" {% if control.status == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-small is-link">
|
||||
<span class="icon"><i class="fas fa-save"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<a class="card-header-icon has-text-warning" href="{% url 'admin:risks_control_change' control.pk %}" title="Maßnahme bearbeiten">
|
||||
<span class="icon"><i class="fas fa-edit" aria-hidden="true"></i></span>
|
||||
</a>
|
||||
|
|
|
@ -16,7 +16,26 @@
|
|||
<header class="card-header">
|
||||
<p class="card-header-title">Überblick</p>
|
||||
|
||||
{% if request.user.is_staff %}
|
||||
{% if request.user.is_staff or incident.reported_by_id == request.user.id %}
|
||||
<form method="post" action="{% url 'risks:update_incident_status' incident.id %}" class="card-header-icon" style="margin-left:auto;">
|
||||
{% csrf_token %}
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<div class="select is-small">
|
||||
<select name="status">
|
||||
{% for value,label in incident.STATUS_CHOICES %}
|
||||
<option value="{{ value }}" {% if incident.status == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-small is-link">
|
||||
<span class="icon"><i class="fas fa-save"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<a class="card-header-icon has-text-warning" href="{% url 'admin:risks_incident_change' incident.pk %}" title="Vorfall bearbeiten">
|
||||
<span class="icon"><i class="fas fa-edit" aria-hidden="true"></i></span>
|
||||
</a>
|
||||
|
|
|
@ -1,23 +1,43 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n risk_extras %}
|
||||
{% block crumbs %}
|
||||
<li><a href="{% url 'risks:list_risks' %}">Risikoanalyse</a></li>
|
||||
<li><a href="{% url 'risks:list_risks' %}">{% trans "Risk analysis" %}</a></li>
|
||||
<li><a href="{% url 'risks:show_risk' risk.id %}">{{ risk.title }}</a></li>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<section class="hero is-small">
|
||||
<div class="hero-body">
|
||||
<p class="title">Risiko: {{ risk.title }}</p>
|
||||
<p class="title">{% trans "Risk" %}: {{ risk.title }}</p>
|
||||
<p class="subtitle is-6">{{ risk.description }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Überblick-->
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">Überblick</p>
|
||||
<p class="card-header-title">{% trans "Overview" %}</p>
|
||||
|
||||
{% if request.user.is_staff or risk.owner_id == request.user.id %}
|
||||
<form method="post" action="{% url 'risks:update_risk_status' risk.id %}" class="card-header-icon" style="margin-left:auto;">
|
||||
{% csrf_token %}
|
||||
<div class="field has-addons">
|
||||
<div class="control">
|
||||
<div class="select is-small">
|
||||
<select name="status">
|
||||
{% for value,label in risk.STATUS_CHOICES %}
|
||||
<option value="{{ value }}" {% if risk.status == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-small is-link" title="{% trans 'Update status' %}">
|
||||
<span class="icon"><i class="fas fa-save"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if request.user.is_staff %}
|
||||
<a class="card-header-icon has-text-warning" href="{% url 'admin:risks_risk_change' risk.pk %}" title="Risiko bearbeiten">
|
||||
<span class="icon"><i class="fas fa-edit" aria-hidden="true"></i></span>
|
||||
</a>
|
||||
|
@ -30,29 +50,30 @@
|
|||
<div class="card-content">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-half">
|
||||
<p><strong>Asset:</strong> {{ risk.asset|default:"-" }}</p>
|
||||
<p><strong>Prozess:</strong> {{ risk.process|default:"-" }}</p>
|
||||
<p><strong>Kategorie:</strong> {{ risk.category|default:"-" }}</p>
|
||||
<p><strong>{% trans "Asset" %}:</strong> {{ risk.asset|default:"-" }}</p>
|
||||
<p><strong>{% trans "Process" %}:</strong> {{ risk.process|default:"-" }}</p>
|
||||
<p><strong>{% trans "Category" %}:</strong> {{ risk.category|default:"-" }}</p>
|
||||
<p>
|
||||
<strong>Schutzziele:</strong>
|
||||
<strong>{% trans "Protection goals" %}:</strong>
|
||||
{% if risk.cia %}
|
||||
{{ risk.get_cia_display }}
|
||||
{% else %}
|
||||
<span class="has-text-grey">Noch nicht zugewiesen</span>
|
||||
<span class="has-text-grey">{% trans "Not yet assigned" %}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p><strong>{% trans "Status" %}:</strong> {{ risk.status }}</p>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<p><strong>Risikoeigner:</strong> {{ risk.owner|user_display|default:"-" }}</p>
|
||||
<p><strong>Erstellt am:</strong> {{ risk.created_at|date:'d.m.Y H:i' }}</p>
|
||||
<p><strong>Aktualisiert am:</strong> {{ risk.updated_at|date:'d.m.Y H:i' }}</p>
|
||||
<p><strong>Wiedervorlage am:</strong> {{ risk.follow_up|date:'d.m.Y' }}</p>
|
||||
<p><strong>{% trans "Risk owner" %}:</strong> {{ risk.owner|user_display|default:"-" }}</p>
|
||||
<p><strong>{% trans "Created at" %}:</strong> {{ risk.created_at|date:'d.m.Y H:i' }}</p>
|
||||
<p><strong>{% trans "updated at" %}:</strong> {{ risk.updated_at|date:'d.m.Y H:i' }}</p>
|
||||
<p><strong>{% trans "Resubmission" %}:</strong> {{ risk.follow_up|date:'d.m.Y' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Risikobewertung -->
|
||||
<section class="hero has-text-centered is-small">
|
||||
<div class="hero-body">
|
||||
<p class="title">Risikobewertung</p>
|
||||
<p class="title">{% trans "Risk assessment" %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<div class="columns is-multiline">
|
||||
|
@ -60,13 +81,13 @@
|
|||
<!-- Bruttorisiko -->
|
||||
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered is-flex">
|
||||
<div class="box is-flex is-flex-direction-column is-flex-grow-1">
|
||||
<h4>Brutto (vor Maßnahmen)</h4>
|
||||
<h4>{% trans "Gross (before measures)" %}</h4>
|
||||
<div class="columns is-multiline">
|
||||
|
||||
<!-- Eintrittswahrscheinlichkeit -->
|
||||
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
|
||||
<button class="risk-chip {{ risk.likelihood|likelihood_class }}" type="button" aria-label="{% trans 'Likelihood' as likelihood_long_name %}">
|
||||
<span class="chip-head">Eintrittswahrscheinlichkeit</span>
|
||||
<span class="chip-head">{% trans "Probability of occurrence" %}</span>
|
||||
<span class="chip-id">{{ risk.likelihood }}</span>
|
||||
<span class="chip-label">{{ risk.get_likelihood_display }}</span>
|
||||
</button>
|
||||
|
@ -75,7 +96,7 @@
|
|||
<!-- Schadensausmaß -->
|
||||
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
|
||||
<button class="risk-chip {{ risk.impact|impact_class }}" type="button" aria-label="{% trans 'Impact' as impact_long_name %}">
|
||||
<span class="chip-head">Schadensausmaß</span>
|
||||
<span class="chip-head">{% trans "Extent of damage" %}</span>
|
||||
<span class="chip-id">{{ risk.impact }}</span>
|
||||
<span class="chip-label">{{ risk.get_impact_display }}</span>
|
||||
</button>
|
||||
|
@ -84,7 +105,7 @@
|
|||
<!-- Stufe -->
|
||||
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
|
||||
<button class="risk-chip {{ risk.level|level_class }}" type="button" aria-label="{% trans 'Level' %}">
|
||||
<span class="chip-head">Stufe</span>
|
||||
<span class="chip-head">{% trans "Level" %}</span>
|
||||
<span class="chip-id">{{ risk.level|level_id }}</span>
|
||||
<span class="chip-label">{{ risk.level }}</span>
|
||||
</button>
|
||||
|
@ -93,7 +114,7 @@
|
|||
<!-- Score -->
|
||||
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
|
||||
<button class="risk-chip {{ risk.score|score_class }}" type="button" aria-label="{% trans 'Score' %}">
|
||||
<span class="chip-head">Score</span>
|
||||
<span class="chip-head">{% trans "Score" %}</span>
|
||||
<span class="chip-id">{{ risk.score }}</span>
|
||||
<span class="chip-label">Score (max. 20)</span>
|
||||
</button>
|
||||
|
@ -106,13 +127,13 @@
|
|||
<!-- Nettorisiko -->
|
||||
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered is-flex">
|
||||
<div class="box is-flex is-flex-direction-column is-flex-grow-1">
|
||||
<h4>Netto (nach Maßnahmen)</h4>
|
||||
<h4>{% trans "Net (after measures)" %}</h4>
|
||||
<div class="columns is-multiline">
|
||||
{% if risk.residual_risk %}
|
||||
<!-- Eintrittswahrscheinlichkeit -->
|
||||
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
|
||||
<button class="risk-chip {{ risk.residual_risk.likelihood|likelihood_class }}" type="button" aria-label="{% trans 'Likelihood' as likelihood_long_name %}">
|
||||
<span class="chip-head">Eintrittswahrscheinlichkeit</span>
|
||||
<span class="chip-head">{% trans "Probability of occurrence" %}</span>
|
||||
<span class="chip-id">{{ risk.residual_risk.likelihood }}</span>
|
||||
<span class="chip-label">{{ risk.residual_risk.get_likelihood_display }}</span>
|
||||
</button>
|
||||
|
@ -121,7 +142,7 @@
|
|||
<!-- Schadensausmaß -->
|
||||
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
|
||||
<button class="risk-chip {{ risk.residual_risk.impact|impact_class }}" type="button" aria-label="{% trans 'Impact' as impact_long_name %}">
|
||||
<span class="chip-head">Schadensausmaß</span>
|
||||
<span class="chip-head">{% trans "Extent of damage" %}</span>
|
||||
<span class="chip-id">{{ risk.residual_risk.impact }}</span>
|
||||
<span class="chip-label">{{ risk.residual_risk.get_impact_display }}</span>
|
||||
</button>
|
||||
|
@ -130,7 +151,7 @@
|
|||
<!-- Stufe -->
|
||||
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
|
||||
<button class="risk-chip {{ risk.residual_risk.level|level_class }}" type="button" aria-label="{% trans 'Level' %}">
|
||||
<span class="chip-head">Stufe</span>
|
||||
<span class="chip-head">{% trans "Level" %}</span>
|
||||
<span class="chip-id">{{ risk.residual_risk.level|level_id }}</span>
|
||||
<span class="chip-label">{{ risk.residual_risk.level }}</span>
|
||||
</button>
|
||||
|
@ -139,15 +160,27 @@
|
|||
<!-- Score -->
|
||||
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered" aria-label="{% trans 'Score' %}">
|
||||
<button class="risk-chip {{ risk.residual_risk.score|score_class }}" type="button">
|
||||
<span class="chip-head">Score</span>
|
||||
<span class="chip-head">{% trans "Score" %}</span>
|
||||
<span class="chip-id">{{ risk.residual_risk.score }}</span>
|
||||
<span class="chip-label">(max. 20)</span>
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="has-text-grey">Noch kein Nettorisiko erfasst.</p>
|
||||
<p class="has-text-grey">{% trans "No net risk recorded yet." %}</p>
|
||||
{% endif %}
|
||||
</div><br>
|
||||
{% if request.user.is_staff or risk.owner_id == request.user.id %}
|
||||
<form method="post" action="{% url 'risks:update_residual_review' risk.id %}">
|
||||
{% csrf_token %}
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="review_required" value="on" {% if risk.residual_risk and risk.residual_risk.review_required %}checked{% endif %}>
|
||||
{% trans "Review required" %}
|
||||
</label>
|
||||
<button class="button is-small is-link" style="margin-left:.5rem;">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- Ende Nettorisiko -->
|
||||
|
||||
|
@ -158,18 +191,18 @@
|
|||
<!-- Maßnahmen -->
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">Maßnahmen</p>
|
||||
<p class="card-header-title">{% trans "Measures" %}</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
{% if risk.controls.exists %}
|
||||
<table class="table is-striped is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titel</th>
|
||||
<th>Status</th>
|
||||
<th>Frist</th>
|
||||
<th>Verantwortlicher</th>
|
||||
<th>Link</th>
|
||||
<th>{% trans "Title" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Deadline" %}</th>
|
||||
<th>{% trans "Responsible" %}</th>
|
||||
<th>{% trans "Link" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -203,7 +236,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="has-text-grey">Keine Maßnahmen erfasst.</p>
|
||||
<p class="has-text-grey">{% trans "No measures recorded." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -212,16 +245,16 @@
|
|||
<!-- Vorfälle -->
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">Vorfälle</p>
|
||||
<p class="card-header-title">{% trans "Incidents" %}</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>
|
||||
<th>{% trans "Incident" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>{% trans "Reported on" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -235,7 +268,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="has-text-grey">Keine Vorfälle bekannt.</p>
|
||||
<p class="has-text-grey">{% trans "No incidents recorded." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div> <!-- Ende Vorfälle -->
|
||||
|
@ -243,16 +276,16 @@
|
|||
<!-- Historie -->
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">Historie</p>
|
||||
<p class="card-header-title">{% trans "History" %}</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
{% if logs %}
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zeitpunkt</th>
|
||||
<th>Benutzer</th>
|
||||
<th>Aktion</th>
|
||||
<th>{% trans "Time" %}</th>
|
||||
<th>{% trans "User" %}</th>
|
||||
<th>{% trans "Action" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -266,7 +299,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="has-text-grey">Keine Historie vorhanden.</p>
|
||||
<p class="has-text-grey">{% trans "No History found." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div> <!-- Ende Historie -->
|
||||
|
|
|
@ -149,7 +149,7 @@
|
|||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="8" class="has-text-centered has-text-grey">Keine Risiken vorhanden</td>
|
||||
<td colspan="8" class="has-text-centered has-text-grey">{% trans "No risks present" %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -157,5 +157,4 @@
|
|||
</div> <!-- Ende Risiken -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
57
templates/risks/notifications.html
Normal file
57
templates/risks/notifications.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% block crumbs %}
|
||||
<li><a href="{% url 'risks:notifications' %}">{% trans "Notifications" %}</a></li>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="buttons has-addons">
|
||||
<a class="button {% if filter == 'unread' %}is-link{% endif %}" href="?filter=unread">{% trans "Unread" %}</a>
|
||||
<a class="button {% if filter == 'all' %}is-link{% endif %}" href="?filter=all">{% trans "All" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="level-right">
|
||||
<form method="post" action="{% url 'risks:notification_mark_all_read' %}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-small is-success" {% if not notifications|length %}disabled{% endif %}>
|
||||
{% trans "Mark all as read" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if notifications %}
|
||||
<div class="box">
|
||||
{% for n in notifications %}
|
||||
<article class="media" style="border-bottom:1px solid #eee; padding-bottom:.75rem; margin-bottom:.75rem;">
|
||||
<div class="media-content">
|
||||
<p>
|
||||
{% if not n.read %}
|
||||
<span class="tag is-warning is-light" style="margin-right:.5rem;">{% trans "New" %}</span>
|
||||
{% endif %}
|
||||
{{ n.message }}
|
||||
</p>
|
||||
<p class="is-size-7 has-text-grey">{{ n.created_at|date:"d.m.Y H:i" }}</p>
|
||||
</div>
|
||||
<div class="media-right">
|
||||
{% if not n.read %}
|
||||
<form method="post" action="{% url 'risks:notification_mark_read' n.pk %}">
|
||||
{% csrf_token %}
|
||||
<button class="button is-small is-light" title="{% trans 'Mark as read' %}">
|
||||
<span class="icon"><i class="fas fa-check"></i></span>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="has-text-grey">{% trans "No notifications." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
Loading…
Add table
Reference in a new issue