Refactor risk management application with enhanced localization, user authentication, and UI improvements
- Added verbose names for Incident and ResidualRisk models for better clarity in admin interface. - Updated impact choices for ResidualRisk and Risk models to ensure consistency and clarity. - Implemented gettext_lazy for translatable strings in models and choices. - Enhanced the Risk, ResidualRisk, Control, AuditLog, and Incident models with Meta options for better admin representation. - Added login required decorators to views for improved security. - Introduced new CSS variables and classes for better visual representation of risk levels. - Created custom template tags for dynamic CSS class assignment based on risk likelihood and impact. - Improved dashboard and statistics views with user authentication checks. - Updated templates for risks, controls, incidents, and admin interface to include edit and delete options for staff users. - Added new login and logout templates for user authentication. - Enhanced list views for risks, controls, and incidents to include action buttons for staff users. - Improved overall UI/UX with Bulma CSS framework for a more modern look and feel.
This commit is contained in:
parent
686030e4cb
commit
bf0a3c22c0
29 changed files with 1174 additions and 123 deletions
Binary file not shown.
Binary file not shown.
|
@ -47,6 +47,7 @@ INSTALLED_APPS = [
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.locale.LocaleMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
@ -134,14 +135,28 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Login-Flow
|
||||||
|
LOGIN_URL = "login"
|
||||||
|
LOGIN_REDIRECT_URL = "risks:dashboard"
|
||||||
|
LOGOUT_REDIRECT_URL = "login"
|
||||||
|
|
||||||
|
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
LANGUAGE_CODE = "de"
|
LANGUAGE_CODE = "de"
|
||||||
TIME_ZONE = "UTC"
|
TIME_ZONE = "Europe/Berlin"
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
LANGUAGES = [
|
||||||
|
("de", "Deutsch"),
|
||||||
|
("en", "English"),
|
||||||
|
]
|
||||||
|
|
||||||
|
LOCALE_PATHS = [BASE_DIR / "locale"]
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Static files
|
# Static files
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -15,9 +15,11 @@ router.register(r"logs", AuditViewSet)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
|
path("i18n/", include("django.conf.urls.i18n")), # Language Switch
|
||||||
path("api/ping/", ping), # Public healthcheck endpoint
|
path("api/ping/", ping), # Public healthcheck endpoint
|
||||||
path("api/secure-ping/", secure_ping), # Protected API endpoint
|
path("api/secure-ping/", secure_ping), # Protected API endpoint
|
||||||
path("api/", include(router.urls)),
|
path("api/", include(router.urls)),
|
||||||
|
path("accounts/", include("django.contrib.auth.urls")),
|
||||||
path("", include("risks.urls", namespace="risks")),
|
path("", include("risks.urls", namespace="risks")),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
BIN
locale/de/LC_MESSAGES/django.mo
Normal file
BIN
locale/de/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
211
locale/de/LC_MESSAGES/django.po
Normal file
211
locale/de/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: wira-risk-management\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2025-09-09 14:20+0200\n"
|
||||||
|
"PO-Revision-Date: 2025-09-09 13:45+0200\n"
|
||||||
|
"Last-Translator: Kevin Heyer <kevin@example.com>\n"
|
||||||
|
"Language-Team: German\n"
|
||||||
|
"Language: de\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
#: risks/admin.py:6 risks/admin.py:8
|
||||||
|
msgid "Administration"
|
||||||
|
msgstr "Verwaltung"
|
||||||
|
|
||||||
|
#: risks/admin.py:7
|
||||||
|
msgid "Admin"
|
||||||
|
msgstr "Admin"
|
||||||
|
|
||||||
|
#: risks/admin.py:13
|
||||||
|
msgid "SSO Information"
|
||||||
|
msgstr "SSO-Informationen"
|
||||||
|
|
||||||
|
#: risks/admin.py:20
|
||||||
|
msgid "Risks Owned"
|
||||||
|
msgstr "Eigene Risiken"
|
||||||
|
|
||||||
|
#: risks/admin.py:24
|
||||||
|
msgid "Controls Responsible"
|
||||||
|
msgstr "Verantwortlich für Maßnahmen"
|
||||||
|
|
||||||
|
#: risks/apps.py:7
|
||||||
|
msgid "Risk Management"
|
||||||
|
msgstr "Risikomanagement"
|
||||||
|
|
||||||
|
#: risks/models.py:35
|
||||||
|
msgid "Risk"
|
||||||
|
msgstr "Risiko"
|
||||||
|
|
||||||
|
#: risks/models.py:36
|
||||||
|
msgid "Risks"
|
||||||
|
msgstr "Risiken"
|
||||||
|
|
||||||
|
#: risks/models.py:39
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Very low occurs less than once every 5 years"
|
||||||
|
msgid "Very low – occurs less than once every 5 years"
|
||||||
|
msgstr "Sehr niedrig – tritt seltener als einmal in fünf Jahren auf"
|
||||||
|
|
||||||
|
#: risks/models.py:40
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Low once every 15 years"
|
||||||
|
msgid "Low – once every 1–5 years"
|
||||||
|
msgstr "Niedrig – einmal in 1–5 Jahren"
|
||||||
|
|
||||||
|
#: risks/models.py:41
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Likely once per year or more"
|
||||||
|
msgid "Likely – once per year or more"
|
||||||
|
msgstr "Wahrscheinlich – einmal pro Jahr oder öfter"
|
||||||
|
|
||||||
|
#: risks/models.py:42
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Very likely multiple times per year/monthly"
|
||||||
|
msgid "Very likely – multiple times per year/monthly"
|
||||||
|
msgstr "Sehr wahrscheinlich – mehrmals pro Jahr/monatlich"
|
||||||
|
|
||||||
|
#: risks/models.py:45
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Low (< 1,000 minor operational impact)"
|
||||||
|
msgid "Very Low (< 1,000 € – minor operational impact)"
|
||||||
|
msgstr "Sehr Gering (< 1.000 € – geringe betriebliche Auswirkungen)"
|
||||||
|
|
||||||
|
#: risks/models.py:46
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Medium (1,0005,000 local impact)"
|
||||||
|
msgid "Low (1,000–5,000 € – local impact)"
|
||||||
|
msgstr "Gering (1.000–5.000 € – lokale Auswirkungen)"
|
||||||
|
|
||||||
|
#: risks/models.py:47
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "High (5,00015,000 team-level impact)"
|
||||||
|
msgid "High (5,000–15,000 € – team-level impact)"
|
||||||
|
msgstr "Hoch (5.000–15.000 € – Auswirkungen auf Teamebene)"
|
||||||
|
|
||||||
|
#: risks/models.py:48
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Severe (50,000100,000 regional impact)"
|
||||||
|
msgid "Severe (50,000–100,000 € – regional impact)"
|
||||||
|
msgstr "Schwerwiegend (50.000–100.000 € – regionale Auswirkungen)"
|
||||||
|
|
||||||
|
#: risks/models.py:49
|
||||||
|
#, fuzzy
|
||||||
|
#| msgid "Critical (> 100,000 existential threat)"
|
||||||
|
msgid "Critical (> 100,000 € – existential threat)"
|
||||||
|
msgstr "Kritisch (> 100.000 € – existenzielle Bedrohung)"
|
||||||
|
|
||||||
|
#: risks/models.py:52
|
||||||
|
msgid "Confidentiality"
|
||||||
|
msgstr "Vertraulichkeit"
|
||||||
|
|
||||||
|
#: risks/models.py:53
|
||||||
|
msgid "Integrity"
|
||||||
|
msgstr "Integrität"
|
||||||
|
|
||||||
|
#: risks/models.py:54
|
||||||
|
msgid "Availability"
|
||||||
|
msgstr "Verfügbarkeit"
|
||||||
|
|
||||||
|
#: risks/models.py:58 risks/models.py:186 risks/models.py:251
|
||||||
|
msgid "Title"
|
||||||
|
msgstr "Titel"
|
||||||
|
|
||||||
|
#: risks/models.py:59 risks/models.py:252
|
||||||
|
msgid "Description"
|
||||||
|
msgstr "Beschreibung"
|
||||||
|
|
||||||
|
#: risks/models.py:60
|
||||||
|
msgid "Asset"
|
||||||
|
msgstr "Asset"
|
||||||
|
|
||||||
|
#: risks/models.py:61
|
||||||
|
msgid "Process"
|
||||||
|
msgstr "Prozess"
|
||||||
|
|
||||||
|
#: risks/models.py:62
|
||||||
|
msgid "Category"
|
||||||
|
msgstr "Kategorie"
|
||||||
|
|
||||||
|
#: risks/models.py:63
|
||||||
|
msgid "Created at"
|
||||||
|
msgstr "Erstellt am"
|
||||||
|
|
||||||
|
#: risks/models.py:64
|
||||||
|
msgid "Updated at"
|
||||||
|
msgstr "Aktualisiert am"
|
||||||
|
|
||||||
|
#: risks/models.py:119
|
||||||
|
msgid "Residual Risk"
|
||||||
|
msgstr "Restrisiko"
|
||||||
|
|
||||||
|
#: risks/models.py:120
|
||||||
|
msgid "Residual Risks"
|
||||||
|
msgstr "Restrisiken"
|
||||||
|
|
||||||
|
#: risks/models.py:175
|
||||||
|
msgid "Control"
|
||||||
|
msgstr "Maßnahme"
|
||||||
|
|
||||||
|
#: risks/models.py:176
|
||||||
|
msgid "Controls"
|
||||||
|
msgstr "Maßnahmen"
|
||||||
|
|
||||||
|
#: risks/models.py:179
|
||||||
|
msgid "Planned"
|
||||||
|
msgstr "Geplant"
|
||||||
|
|
||||||
|
#: risks/models.py:180
|
||||||
|
msgid "In progress"
|
||||||
|
msgstr "In Bearbeitung"
|
||||||
|
|
||||||
|
#: risks/models.py:181
|
||||||
|
msgid "Completed"
|
||||||
|
msgstr "Abgeschlossen"
|
||||||
|
|
||||||
|
#: risks/models.py:182
|
||||||
|
msgid "Verified"
|
||||||
|
msgstr "Verifiziert"
|
||||||
|
|
||||||
|
#: risks/models.py:183
|
||||||
|
msgid "Rejected"
|
||||||
|
msgstr "Abgelehnt"
|
||||||
|
|
||||||
|
#: risks/models.py:212
|
||||||
|
msgid "Auditlog"
|
||||||
|
msgstr "Audit-Log"
|
||||||
|
|
||||||
|
#: risks/models.py:213
|
||||||
|
msgid "Auditlogs"
|
||||||
|
msgstr "Audit-Logs"
|
||||||
|
|
||||||
|
#: risks/models.py:243
|
||||||
|
msgid "Incident"
|
||||||
|
msgstr "Vorfall"
|
||||||
|
|
||||||
|
#: risks/models.py:244
|
||||||
|
msgid "Incidents"
|
||||||
|
msgstr "Vorfälle"
|
||||||
|
|
||||||
|
#: risks/models.py:247
|
||||||
|
msgid "Opened"
|
||||||
|
msgstr "Eröffnet"
|
||||||
|
|
||||||
|
#: risks/models.py:248
|
||||||
|
msgid "In Progress"
|
||||||
|
msgstr "In Bearbeitung"
|
||||||
|
|
||||||
|
#: risks/models.py:249
|
||||||
|
msgid "Closed"
|
||||||
|
msgstr "Geschlossen"
|
||||||
|
|
||||||
|
#: risks/models.py:253
|
||||||
|
msgid "Date reported"
|
||||||
|
msgstr "Meldedatum"
|
||||||
|
|
||||||
|
#: risks/models.py:255
|
||||||
|
msgid "Reported by"
|
||||||
|
msgstr "Gemeldet von"
|
3
locale/de/LC_MESSAGES/formats.py
Normal file
3
locale/de/LC_MESSAGES/formats.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
DATE_FORMAT = "d.m.Y"
|
||||||
|
DATETIME_FORMAT = "d.m.Y H:i"
|
||||||
|
TIME_FORMAT = "H:i"
|
BIN
locale/en/LC_MESSAGES/django.mo
Normal file
BIN
locale/en/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
199
locale/en/LC_MESSAGES/django.po
Normal file
199
locale/en/LC_MESSAGES/django.po
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
# SOME DESCRIPTIVE TITLE.
|
||||||
|
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||||
|
# This file is distributed under the same license as the PACKAGE package.
|
||||||
|
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2025-09-09 14:20+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"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
#: risks/admin.py:6 risks/admin.py:8
|
||||||
|
msgid "Administration"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/admin.py:7
|
||||||
|
msgid "Admin"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/admin.py:13
|
||||||
|
msgid "SSO Information"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/admin.py:20
|
||||||
|
msgid "Risks Owned"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/admin.py:24
|
||||||
|
msgid "Controls Responsible"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/apps.py:7
|
||||||
|
msgid "Risk Management"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:35
|
||||||
|
msgid "Risk"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:36
|
||||||
|
msgid "Risks"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:39
|
||||||
|
msgid "Very low – occurs less than once every 5 years"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:40
|
||||||
|
msgid "Low – once every 1–5 years"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:41
|
||||||
|
msgid "Likely – once per year or more"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:42
|
||||||
|
msgid "Very likely – multiple times per year/monthly"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:45
|
||||||
|
msgid "Very Low (< 1,000 € – minor operational impact)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:46
|
||||||
|
msgid "Low (1,000–5,000 € – local impact)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:47
|
||||||
|
msgid "High (5,000–15,000 € – team-level impact)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:48
|
||||||
|
msgid "Severe (50,000–100,000 € – regional impact)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:49
|
||||||
|
msgid "Critical (> 100,000 € – existential threat)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:52
|
||||||
|
msgid "Confidentiality"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:53
|
||||||
|
msgid "Integrity"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:54
|
||||||
|
msgid "Availability"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:58 risks/models.py:186 risks/models.py:251
|
||||||
|
msgid "Title"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:59 risks/models.py:252
|
||||||
|
msgid "Description"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:60
|
||||||
|
msgid "Asset"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:61
|
||||||
|
msgid "Process"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:62
|
||||||
|
msgid "Category"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:63
|
||||||
|
msgid "Created at"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:64
|
||||||
|
msgid "Updated at"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:119
|
||||||
|
msgid "Residual Risk"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:120
|
||||||
|
msgid "Residual Risks"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:175
|
||||||
|
msgid "Control"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:176
|
||||||
|
msgid "Controls"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:179
|
||||||
|
msgid "Planned"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:180
|
||||||
|
msgid "In progress"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:181
|
||||||
|
msgid "Completed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:182
|
||||||
|
msgid "Verified"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:183
|
||||||
|
msgid "Rejected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:212
|
||||||
|
msgid "Auditlog"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:213
|
||||||
|
msgid "Auditlogs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:243
|
||||||
|
msgid "Incident"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:244
|
||||||
|
msgid "Incidents"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:247
|
||||||
|
msgid "Opened"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:248
|
||||||
|
msgid "In Progress"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:249
|
||||||
|
msgid "Closed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:253
|
||||||
|
msgid "Date reported"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: risks/models.py:255
|
||||||
|
msgid "Reported by"
|
||||||
|
msgstr ""
|
|
@ -1,23 +1,27 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from .models import User, Risk, ResidualRisk, Control, Incident
|
from .models import User, Risk, ResidualRisk, Control, Incident
|
||||||
|
|
||||||
|
admin.site.site_header = _("Administration")
|
||||||
|
admin.site.site_title = _("Admin")
|
||||||
|
admin.site.index_title = _("Administration")
|
||||||
|
|
||||||
@admin.register(User)
|
@admin.register(User)
|
||||||
class UserAdmin(BaseUserAdmin):
|
class UserAdmin(BaseUserAdmin):
|
||||||
fieldsets = BaseUserAdmin.fieldsets + (
|
fieldsets = BaseUserAdmin.fieldsets + (
|
||||||
("SSO Information", {"fields": ("is_sso_user",)}),
|
(_("SSO Information"), {"fields": ("is_sso_user",)}),
|
||||||
)
|
)
|
||||||
list_display = ("username", "email", "is_staff", "is_superuser", "is_sso_user",
|
list_display = ("username", "email", "is_staff", "is_superuser", "is_sso_user",
|
||||||
"owned_risks_count", "responsible_controls_count")
|
"owned_risks_count", "responsible_controls_count")
|
||||||
|
|
||||||
def owned_risks_count(self, obj):
|
def owned_risks_count(self, obj):
|
||||||
return obj.risks_owned.count()
|
return obj.risks_owned.count()
|
||||||
owned_risks_count.short_description = "Risks Owned"
|
owned_risks_count.short_description = _("Risks Owned")
|
||||||
|
|
||||||
def responsible_controls_count(self, obj):
|
def responsible_controls_count(self, obj):
|
||||||
return obj.controls_responsible.count()
|
return obj.controls_responsible.count()
|
||||||
responsible_controls_count.short_description = "Controls Responsible"
|
responsible_controls_count.short_description = _("Controls Responsible")
|
||||||
|
|
||||||
class ResidualRiskInline(admin.StackedInline):
|
class ResidualRiskInline(admin.StackedInline):
|
||||||
"""
|
"""
|
||||||
|
@ -25,7 +29,7 @@ class ResidualRiskInline(admin.StackedInline):
|
||||||
"""
|
"""
|
||||||
model = ResidualRisk
|
model = ResidualRisk
|
||||||
extra = 0
|
extra = 0
|
||||||
can_delete = False # Since each Risk can have at most one residual risk
|
can_delete = False
|
||||||
readonly_fields = ("score", "level", "review_required")
|
readonly_fields = ("score", "level", "review_required")
|
||||||
fields = ("likelihood", "impact", "score", "level", "review_required")
|
fields = ("likelihood", "impact", "score", "level", "review_required")
|
||||||
|
|
||||||
|
@ -48,7 +52,7 @@ class RiskAdmin(admin.ModelAdmin):
|
||||||
)
|
)
|
||||||
list_filter = ("level", "likelihood", "impact", "owner")
|
list_filter = ("level", "likelihood", "impact", "owner")
|
||||||
search_fields = ("title", "asset", "process", "category")
|
search_fields = ("title", "asset", "process", "category")
|
||||||
inlines = [ResidualRiskInline, ControlRisksInline] # Controls hier verknüpfen
|
inlines = [ResidualRiskInline, ControlRisksInline]
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
obj._changed_by = request.user
|
obj._changed_by = request.user
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
class RisksConfig(AppConfig):
|
class RisksConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = 'risks'
|
name = "risks"
|
||||||
|
verbose_name = _("Risk Management")
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
# Import signals when app is ready
|
|
||||||
import risks.signals
|
import risks.signals
|
|
@ -0,0 +1,108 @@
|
||||||
|
# Generated by Django 5.2.6 on 2025-09-09 11:53
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("risks", "0017_alter_incident_status"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="auditlog",
|
||||||
|
options={"verbose_name": "Auditlog", "verbose_name_plural": "Auditlogs"},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="control",
|
||||||
|
options={"verbose_name": "Control", "verbose_name_plural": "Controls"},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="residualrisk",
|
||||||
|
options={
|
||||||
|
"verbose_name": "Residual Risk",
|
||||||
|
"verbose_name_plural": "Residual Risks",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="risk",
|
||||||
|
options={"verbose_name": "Risk", "verbose_name_plural": "Risks"},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="control",
|
||||||
|
name="title",
|
||||||
|
field=models.CharField(max_length=255, verbose_name="Title"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="incident",
|
||||||
|
name="date_reported",
|
||||||
|
field=models.DateField(blank=True, null=True, verbose_name="Date reported"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="incident",
|
||||||
|
name="description",
|
||||||
|
field=models.TextField(blank=True, null=True, verbose_name="Description"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="incident",
|
||||||
|
name="reported_by",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="incidents",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="Reported by",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="incident",
|
||||||
|
name="title",
|
||||||
|
field=models.CharField(max_length=255, verbose_name="Title"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="risk",
|
||||||
|
name="asset",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, max_length=255, null=True, verbose_name="Asset"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="risk",
|
||||||
|
name="category",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, max_length=255, null=True, verbose_name="Category"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="risk",
|
||||||
|
name="created_at",
|
||||||
|
field=models.DateTimeField(auto_now_add=True, verbose_name="Created at"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="risk",
|
||||||
|
name="description",
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True, max_length=225, null=True, verbose_name="Description"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="risk",
|
||||||
|
name="process",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True, max_length=255, null=True, verbose_name="Process"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="risk",
|
||||||
|
name="title",
|
||||||
|
field=models.CharField(max_length=255, verbose_name="Title"),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="risk",
|
||||||
|
name="updated_at",
|
||||||
|
field=models.DateTimeField(auto_now=True, verbose_name="Updated at"),
|
||||||
|
),
|
||||||
|
]
|
16
risks/migrations/0019_alter_incident_options.py
Normal file
16
risks/migrations/0019_alter_incident_options.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Generated by Django 5.2.6 on 2025-09-09 11:55
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("risks", "0018_alter_auditlog_options_alter_control_options_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="incident",
|
||||||
|
options={"verbose_name": "Incident", "verbose_name_plural": "Incidents"},
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,40 @@
|
||||||
|
# Generated by Django 5.2.6 on 2025-09-09 12:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("risks", "0019_alter_incident_options"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="residualrisk",
|
||||||
|
name="impact",
|
||||||
|
field=models.IntegerField(
|
||||||
|
choices=[
|
||||||
|
(1, "Very Low (< 1,000 € – minor operational impact)"),
|
||||||
|
(2, "Low (1,000–5,000 € – local impact)"),
|
||||||
|
(3, "High (5,000–15,000 € – team-level impact)"),
|
||||||
|
(4, "Severe (50,000–100,000 € – regional impact)"),
|
||||||
|
(5, "Critical (> 100,000 € – existential threat)"),
|
||||||
|
],
|
||||||
|
default=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="risk",
|
||||||
|
name="impact",
|
||||||
|
field=models.IntegerField(
|
||||||
|
choices=[
|
||||||
|
(1, "Very Low (< 1,000 € – minor operational impact)"),
|
||||||
|
(2, "Low (1,000–5,000 € – local impact)"),
|
||||||
|
(3, "High (5,000–15,000 € – team-level impact)"),
|
||||||
|
(4, "Severe (50,000–100,000 € – regional impact)"),
|
||||||
|
(5, "Critical (> 100,000 € – existential threat)"),
|
||||||
|
],
|
||||||
|
default=1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,6 +2,7 @@ from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
from multiselectfield import MultiSelectField
|
from multiselectfield import MultiSelectField
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
@ -29,39 +30,38 @@ class User(AbstractUser):
|
||||||
return self.responsible_controls.all()
|
return self.responsible_controls.all()
|
||||||
|
|
||||||
class Risk(models.Model):
|
class Risk(models.Model):
|
||||||
"""
|
|
||||||
Represents an information security risk.
|
class Meta:
|
||||||
"""
|
verbose_name = _("Risk")
|
||||||
|
verbose_name_plural = _("Risks")
|
||||||
|
|
||||||
LIKELIHOOD_CHOICES = [
|
LIKELIHOOD_CHOICES = [
|
||||||
(1, "Very low – occurs less than once every 5 years"),
|
(1, _("Very low – occurs less than once every 5 years")),
|
||||||
(2, "Low – once every 1–5 years"),
|
(2, _("Low – once every 1–5 years")),
|
||||||
(3, "Likely – once per year or more"),
|
(3, _("Likely – once per year or more")),
|
||||||
(4, "Very likely – multiple times per year/monthly"),
|
(4, _("Very likely – multiple times per year/monthly")),
|
||||||
]
|
]
|
||||||
|
|
||||||
IMPACT_CHOICES = [
|
IMPACT_CHOICES = [
|
||||||
(1, "Low (< 1,000 € – minor operational impact)"),
|
(1, _("Very Low (< 1,000 € – minor operational impact)")),
|
||||||
(2, "Medium (1,000–5,000 € – local impact)"),
|
(2, _("Low (1,000–5,000 € – local impact)")),
|
||||||
(3, "High (5,000–15,000 € – team-level impact)"),
|
(3, _("High (5,000–15,000 € – team-level impact)")),
|
||||||
(4, "Severe (50,000–100,000 € – regional impact)"),
|
(4, _("Severe (50,000–100,000 € – regional impact)")),
|
||||||
(5, "Critical (> 100,000 € – existential threat)"),
|
(5, _("Critical (> 100,000 € – existential threat)")),
|
||||||
]
|
]
|
||||||
|
|
||||||
CIA_CHOICES = [
|
CIA_CHOICES = [
|
||||||
("1", "Confidentiality"),
|
("1", _("Confidentiality")),
|
||||||
("2", "Integrity"),
|
("2", _("Integrity")),
|
||||||
("3", "Availability")
|
("3", _("Availability")),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Basic information
|
# Basic information
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(_("Title"), max_length=255)
|
||||||
description = models.TextField(max_length=225, blank=True, null=True)
|
description = models.TextField(_("Description"), max_length=225, blank=True, null=True)
|
||||||
asset = models.CharField(max_length=255, blank=True, null=True)
|
asset = models.CharField(_("Asset"), max_length=255, blank=True, null=True)
|
||||||
process = models.CharField(max_length=255, blank=True, null=True)
|
process = models.CharField(_("Process"), max_length=255, blank=True, null=True)
|
||||||
category = models.CharField(max_length=255, blank=True, null=True)
|
category = models.CharField(_("Category"), max_length=255, blank=True, null=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True,)
|
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(_("Updated at"), auto_now=True)
|
||||||
|
|
||||||
# CIA Protection Goals
|
# CIA Protection Goals
|
||||||
cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True)
|
cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True)
|
||||||
|
@ -115,6 +115,10 @@ class ResidualRisk(models.Model):
|
||||||
Residual Risk after implementing controls
|
Residual Risk after implementing controls
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Residual Risk")
|
||||||
|
verbose_name_plural = _("Residual Risks")
|
||||||
|
|
||||||
risk = models.OneToOneField(
|
risk = models.OneToOneField(
|
||||||
Risk,
|
Risk,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -167,15 +171,19 @@ class Control(models.Model):
|
||||||
"""
|
"""
|
||||||
A security control/measure linked to a risk.
|
A security control/measure linked to a risk.
|
||||||
"""
|
"""
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Control")
|
||||||
|
verbose_name_plural = _("Controls")
|
||||||
|
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
("planned", "Planned"),
|
("planned", _("Planned")),
|
||||||
("in_progress", "In progress"),
|
("in_progress", _("In progress")),
|
||||||
("completed", "Completed"),
|
("completed", _("Completed")),
|
||||||
("verified", "Verified"),
|
("verified", _("Verified")),
|
||||||
("rejected", "Rejected"),
|
("rejected", _("Rejected")),
|
||||||
]
|
]
|
||||||
|
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(_("Title"), max_length=255)
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="planned")
|
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="planned")
|
||||||
due_date = models.DateField(blank=True, null=True)
|
due_date = models.DateField(blank=True, null=True)
|
||||||
responsible = models.ForeignKey(
|
responsible = models.ForeignKey(
|
||||||
|
@ -199,6 +207,11 @@ class AuditLog(models.Model):
|
||||||
"""
|
"""
|
||||||
Generic audit log entry for tracking changes.
|
Generic audit log entry for tracking changes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Auditlog")
|
||||||
|
verbose_name_plural = _("Auditlogs")
|
||||||
|
|
||||||
ACTION_CHOICES = [
|
ACTION_CHOICES = [
|
||||||
("create", "Created"),
|
("create", "Created"),
|
||||||
("update", "Updated"),
|
("update", "Updated"),
|
||||||
|
@ -225,15 +238,23 @@ class Incident(models.Model):
|
||||||
"""
|
"""
|
||||||
Incidents and related risks
|
Incidents and related risks
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Incident")
|
||||||
|
verbose_name_plural = _("Incidents")
|
||||||
|
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
("open", "Opened"),
|
("open", _("Opened")),
|
||||||
("in_progress", "In Progress"),
|
("in_progress", _("In Progress")),
|
||||||
("closed", "Closed"),
|
("closed", _("Closed")),
|
||||||
]
|
]
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(_("Title"), max_length=255)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(_("Description"), blank=True, null=True)
|
||||||
date_reported = models.DateField(blank=True, null=True)
|
date_reported = models.DateField(_("Date reported"), blank=True, null=True)
|
||||||
reported_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="incidents")
|
reported_by = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, verbose_name=_("Reported by"),
|
||||||
|
null=True, blank=True, on_delete=models.SET_NULL, related_name="incidents"
|
||||||
|
)
|
||||||
status = models.CharField(max_length=12, choices=STATUS_CHOICES)
|
status = models.CharField(max_length=12, choices=STATUS_CHOICES)
|
||||||
related_risks = models.ManyToManyField("Risk", blank=True, related_name="incidents")
|
related_risks = models.ManyToManyField("Risk", blank=True, related_name="incidents")
|
||||||
created_at = models.DateTimeField(auto_now_add=True,)
|
created_at = models.DateTimeField(auto_now_add=True,)
|
||||||
|
|
|
@ -7,3 +7,60 @@ register = template.Library()
|
||||||
def cia_label(value):
|
def cia_label(value):
|
||||||
mapping = dict(Risk.CIA_CHOICES)
|
mapping = dict(Risk.CIA_CHOICES)
|
||||||
return mapping.get(value, value)
|
return mapping.get(value, value)
|
||||||
|
|
||||||
|
LIKELIHOOD_MAP = {
|
||||||
|
1: "is-control-verylow",
|
||||||
|
2: "is-control-low",
|
||||||
|
3: "is-control-mid",
|
||||||
|
4: "is-control-high",
|
||||||
|
}
|
||||||
|
|
||||||
|
IMPACT_MAP = {
|
||||||
|
1: "is-control-verylow",
|
||||||
|
2: "is-control-low",
|
||||||
|
3: "is-control-mid",
|
||||||
|
4: "is-control-high",
|
||||||
|
5: "is-control-veryhigh",
|
||||||
|
}
|
||||||
|
|
||||||
|
LEVEL_MAP = {
|
||||||
|
"Low": "is-control-low",
|
||||||
|
"Medium": "is-control-mid",
|
||||||
|
"High": "is-control-high",
|
||||||
|
"Critical": "is-control-veryhigh",
|
||||||
|
}
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def likelihood_class(val):
|
||||||
|
try:
|
||||||
|
return LIKELIHOOD_MAP.get(int(val), "is-light")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return "is-light"
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def impact_class(val):
|
||||||
|
try:
|
||||||
|
return IMPACT_MAP.get(int(val), "is-light")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return "is-light"
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def level_class(level):
|
||||||
|
return LEVEL_MAP.get(str(level), "is-light")
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def score_class(score):
|
||||||
|
"""Score 1..20 → 5 Stufen"""
|
||||||
|
try:
|
||||||
|
s = int(score)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return "is-light"
|
||||||
|
if s <= 4:
|
||||||
|
return "is-control-verylow"
|
||||||
|
if s <= 8:
|
||||||
|
return "is-control-low"
|
||||||
|
if s <= 12:
|
||||||
|
return "is-control-mid"
|
||||||
|
if s <= 16:
|
||||||
|
return "is-control-high"
|
||||||
|
return "is-control-veryhigh"
|
|
@ -1,5 +1,6 @@
|
||||||
from django.contrib.admin.models import LogEntry
|
from django.contrib.admin.models import LogEntry
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
@ -95,12 +96,15 @@ class IncidentViewSet(viewsets.ModelViewSet):
|
||||||
# Web
|
# Web
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@login_required
|
||||||
def dashboard(request):
|
def dashboard(request):
|
||||||
return render(request, "risks/dashboard.html")
|
return render(request, "risks/dashboard.html")
|
||||||
|
|
||||||
|
@login_required
|
||||||
def stats(request):
|
def stats(request):
|
||||||
return render(request, "risks/statistics.html")
|
return render(request, "risks/statistics.html")
|
||||||
|
|
||||||
|
@login_required
|
||||||
def list_risks(request):
|
def list_risks(request):
|
||||||
qs = Risk.objects.all().select_related("owner")
|
qs = Risk.objects.all().select_related("owner")
|
||||||
|
|
||||||
|
@ -127,16 +131,18 @@ def list_risks(request):
|
||||||
"owners": owners,
|
"owners": owners,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
def show_risk(request, id):
|
def show_risk(request, id):
|
||||||
risk = get_object_or_404(Risk, pk=id)
|
risk = get_object_or_404(
|
||||||
|
Risk.objects.select_related("residual_risk", "owner").prefetch_related("controls"),
|
||||||
|
pk=id,
|
||||||
|
)
|
||||||
ct = ContentType.objects.get_for_model(Risk)
|
ct = ContentType.objects.get_for_model(Risk)
|
||||||
logs = LogEntry.objects.filter(
|
logs = LogEntry.objects.filter(content_type=ct, object_id=risk.pk).order_by("-action_time")
|
||||||
content_type=ct,
|
|
||||||
object_id=risk.pk
|
|
||||||
).order_by("-action_time")
|
|
||||||
|
|
||||||
return render(request, "risks/item_risk.html", {"risk": risk, "logs": logs})
|
return render(request, "risks/item_risk.html", {"risk": risk, "logs": logs})
|
||||||
|
|
||||||
|
@login_required
|
||||||
def list_controls(request):
|
def list_controls(request):
|
||||||
qs = Control.objects.all().select_related("responsible")
|
qs = Control.objects.all().select_related("responsible")
|
||||||
|
|
||||||
|
@ -166,6 +172,7 @@ def list_controls(request):
|
||||||
"status_choices": Control.STATUS_CHOICES,
|
"status_choices": Control.STATUS_CHOICES,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
def show_control(request, id):
|
def show_control(request, id):
|
||||||
control = get_object_or_404(Control, pk=id)
|
control = get_object_or_404(Control, pk=id)
|
||||||
ct = ContentType.objects.get_for_model(Control)
|
ct = ContentType.objects.get_for_model(Control)
|
||||||
|
@ -176,7 +183,7 @@ def show_control(request, id):
|
||||||
|
|
||||||
return render(request, "risks/item_control.html", {"control": control, "logs": logs})
|
return render(request, "risks/item_control.html", {"control": control, "logs": logs})
|
||||||
|
|
||||||
|
@login_required
|
||||||
def list_incidents(request):
|
def list_incidents(request):
|
||||||
qs = Incident.objects.all().select_related("reported_by").prefetch_related("related_risks")
|
qs = Incident.objects.all().select_related("reported_by").prefetch_related("related_risks")
|
||||||
|
|
||||||
|
@ -203,6 +210,7 @@ def list_incidents(request):
|
||||||
"status_choices": Incident.STATUS_CHOICES,
|
"status_choices": Incident.STATUS_CHOICES,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@login_required
|
||||||
def show_incident(request, id):
|
def show_incident(request, id):
|
||||||
incident = get_object_or_404(Incident, pk=id)
|
incident = get_object_or_404(Incident, pk=id)
|
||||||
ct = ContentType.objects.get_for_model(Incident)
|
ct = ContentType.objects.get_for_model(Incident)
|
||||||
|
@ -211,4 +219,4 @@ def show_incident(request, id):
|
||||||
object_id=incident.pk
|
object_id=incident.pk
|
||||||
).order_by("-action_time")
|
).order_by("-action_time")
|
||||||
|
|
||||||
return render(request, "risks/item_incident.html", {"incident": incident, "logs": logs})
|
return render(request, "risks/item_incident.html", {"incident": incident, "logs": logs})
|
||||||
|
|
|
@ -1,3 +1,86 @@
|
||||||
|
/* Base palette */
|
||||||
|
:root{
|
||||||
|
--c-verylow:#22c55e; --c-verylow-100:#dcfce7; --c-verylow-300:#86efac; --c-verylow-inv:#fff;
|
||||||
|
--c-low:#84cc16; --c-low-100:#ecfccb; --c-low-300:#bef264; --c-low-inv:#111;
|
||||||
|
--c-mid:#eab308; --c-mid-100:#fef9c3; --c-mid-300:#fde047; --c-mid-inv:#111;
|
||||||
|
--c-high:#f97316; --c-high-100:#ffedd5; --c-high-300:#fbbf24; --c-high-inv:#111;
|
||||||
|
--c-veryhigh:#dc2626; --c-veryhigh-100:#fee2e2;--c-veryhigh-300:#fca5a5; --c-veryhigh-inv:#fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helpers (wie Bulma) */
|
||||||
|
.has-text-control-verylow{color:var(--c-verylow)!important}
|
||||||
|
.has-text-control-low{color:var(--c-low)!important}
|
||||||
|
.has-text-control-mid{color:var(--c-mid)!important}
|
||||||
|
.has-text-control-high{color:var(--c-high)!important}
|
||||||
|
.has-text-control-veryhigh{color:var(--c-veryhigh)!important}
|
||||||
|
|
||||||
|
.has-background-control-verylow{background:var(--c-verylow)!important;color:var(--c-verylow-inv)!important}
|
||||||
|
.has-background-control-low{background:var(--c-low)!important;color:var(--c-low-inv)!important}
|
||||||
|
.has-background-control-mid{background:var(--c-mid)!important;color:var(--c-mid-inv)!important}
|
||||||
|
.has-background-control-high{background:var(--c-high)!important;color:var(--c-high-inv)!important}
|
||||||
|
.has-background-control-veryhigh{background:var(--c-veryhigh)!important;color:var(--c-veryhigh-inv)!important}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.button.is-control-verylow{background:var(--c-verylow);border-color:transparent;color:var(--c-verylow-inv)}
|
||||||
|
.button.is-control-low{background:var(--c-low);border-color:transparent;color:var(--c-low-inv)}
|
||||||
|
.button.is-control-mid{background:var(--c-mid);border-color:transparent;color:var(--c-mid-inv)}
|
||||||
|
.button.is-control-high{background:var(--c-high);border-color:transparent;color:var(--c-high-inv)}
|
||||||
|
.button.is-control-veryhigh{background:var(--c-veryhigh);border-color:transparent;color:var(--c-veryhigh-inv)}
|
||||||
|
.button.is-control-verylow:hover{filter:brightness(.92)}
|
||||||
|
.button.is-control-low:hover{filter:brightness(.92)}
|
||||||
|
.button.is-control-mid:hover{filter:brightness(.92)}
|
||||||
|
.button.is-control-high:hover{filter:brightness(.92)}
|
||||||
|
.button.is-control-veryhigh:hover{filter:brightness(.92)}
|
||||||
|
.button.is-control-verylow.is-light{background:var(--c-verylow-100);color:var(--c-verylow)}
|
||||||
|
.button.is-control-low.is-light{background:var(--c-low-100);color:var(--c-low)}
|
||||||
|
.button.is-control-mid.is-light{background:var(--c-mid-100);color:var(--c-mid)}
|
||||||
|
.button.is-control-high.is-light{background:var(--c-high-100);color:var(--c-high)}
|
||||||
|
.button.is-control-veryhigh.is-light{background:var(--c-veryhigh-100);color:var(--c-veryhigh)}
|
||||||
|
|
||||||
|
/* Tags */
|
||||||
|
.tag.is-control-verylow{background:var(--c-verylow);color:var(--c-verylow-inv)}
|
||||||
|
.tag.is-control-low{background:var(--c-low);color:var(--c-low-inv)}
|
||||||
|
.tag.is-control-mid{background:var(--c-mid);color:var(--c-mid-inv)}
|
||||||
|
.tag.is-control-high{background:var(--c-high);color:var(--c-high-inv)}
|
||||||
|
.tag.is-control-veryhigh{background:var(--c-veryhigh);color:var(--c-veryhigh-inv)}
|
||||||
|
.tag.is-control-verylow.is-light{background:var(--c-verylow-100);color:var(--c-verylow)}
|
||||||
|
.tag.is-control-low.is-light{background:var(--c-low-100);color:var(--c-low)}
|
||||||
|
.tag.is-control-mid.is-light{background:var(--c-mid-100);color:var(--c-mid)}
|
||||||
|
.tag.is-control-high.is-light{background:var(--c-high-100);color:var(--c-high)}
|
||||||
|
.tag.is-control-veryhigh.is-light{background:var(--c-veryhigh-100);color:var(--c-veryhigh)}
|
||||||
|
|
||||||
|
/* Notifications */
|
||||||
|
.notification.is-control-verylow{background:var(--c-verylow-100);border-left:4px solid var(--c-verylow);color:#111}
|
||||||
|
.notification.is-control-low{background:var(--c-low-100);border-left:4px solid var(--c-low);color:#111}
|
||||||
|
.notification.is-control-mid{background:var(--c-mid-100);border-left:4px solid var(--c-mid);color:#111}
|
||||||
|
.notification.is-control-high{background:var(--c-high-100);border-left:4px solid var(--c-high);color:#111}
|
||||||
|
.notification.is-control-veryhigh{background:var(--c-veryhigh-100);border-left:4px solid var(--c-veryhigh);color:#111}
|
||||||
|
|
||||||
|
/* Messages */
|
||||||
|
.message.is-control-verylow .message-header{background:var(--c-verylow);color:var(--c-verylow-inv)}
|
||||||
|
.message.is-control-low .message-header{background:var(--c-low);color:var(--c-low-inv)}
|
||||||
|
.message.is-control-mid .message-header{background:var(--c-mid);color:var(--c-mid-inv)}
|
||||||
|
.message.is-control-high .message-header{background:var(--c-high);color:var(--c-high-inv)}
|
||||||
|
.message.is-control-veryhigh .message-header{background:var(--c-veryhigh);color:var(--c-veryhigh-inv)}
|
||||||
|
.message.is-control-verylow .message-body{border-color:var(--c-verylow-300)}
|
||||||
|
.message.is-control-low .message-body{border-color:var(--c-low-300)}
|
||||||
|
.message.is-control-mid .message-body{border-color:var(--c-mid-300)}
|
||||||
|
.message.is-control-high .message-body{border-color:var(--c-high-300)}
|
||||||
|
.message.is-control-veryhigh .message-body{border-color:var(--c-veryhigh-300)}
|
||||||
|
|
||||||
|
/* Progress (optional) */
|
||||||
|
.progress.is-control-verylow::-webkit-progress-value{background:var(--c-verylow)}
|
||||||
|
.progress.is-control-low::-webkit-progress-value{background:var(--c-low)}
|
||||||
|
.progress.is-control-mid::-webkit-progress-value{background:var(--c-mid)}
|
||||||
|
.progress.is-control-high::-webkit-progress-value{background:var(--c-high)}
|
||||||
|
.progress.is-control-veryhigh::-webkit-progress-value{background:var(--c-veryhigh)}
|
||||||
|
.progress.is-control-verylow::-moz-progress-bar{background:var(--c-verylow)}
|
||||||
|
.progress.is-control-low::-moz-progress-bar{background:var(--c-low)}
|
||||||
|
.progress.is-control-mid::-moz-progress-bar{background:var(--c-mid)}
|
||||||
|
.progress.is-control-high::-moz-progress-bar{background:var(--c-high)}
|
||||||
|
.progress.is-control-veryhigh::-moz-progress-bar{background:var(--c-veryhigh)}
|
||||||
|
|
||||||
|
|
||||||
/* Topbar-Farbe erzwingen (Bulma überschreibt sonst mit weiß) */
|
/* Topbar-Farbe erzwingen (Bulma überschreibt sonst mit weiß) */
|
||||||
.navbar.topbar-nav {
|
.navbar.topbar-nav {
|
||||||
background-color: #d6801e !important; /* Orange wie im Screenshot */
|
background-color: #d6801e !important; /* Orange wie im Screenshot */
|
||||||
|
|
24
templates/admin/base_site.html
Normal file
24
templates/admin/base_site.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{# templates/admin/base_site.html #}
|
||||||
|
{% extends "admin/base.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}{{ title }} | {{ site_title }}{% endblock %}
|
||||||
|
|
||||||
|
{% block branding %}
|
||||||
|
<h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header }}</a></h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block usertools %}
|
||||||
|
{{ block.super }}
|
||||||
|
<form method="post" action="{% url 'set_language' %}" style="display:inline-block; margin-left: 1rem;">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="next" value="{{ request.get_full_path }}">
|
||||||
|
<select name="language" onchange="this.form.submit();">
|
||||||
|
{% get_current_language as LANGUAGE_CODE %}
|
||||||
|
{% get_available_languages as LANGUAGES %}
|
||||||
|
{% for code, name in LANGUAGES %}
|
||||||
|
<option value="{{ code }}" {% if code == LANGUAGE_CODE %}selected{% endif %}>{{ name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -47,14 +47,60 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Profil -->
|
||||||
<div class="navbar-end">
|
<div class="navbar-end">
|
||||||
<div class="actions">
|
<!-- Suche -->
|
||||||
<input type="text" placeholder="Suchen" class="search">
|
<div class="navbar-item">
|
||||||
<a class="navbar-item" href="/admin">Admin</a>
|
<!--
|
||||||
<div class="profile">KG</div>
|
<div class="field">
|
||||||
<span class="icon">❓</span>
|
<p class="control">
|
||||||
|
<input class="input is-small" type="text" placeholder="Suchen">
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<!-- Profil-Dropdown -->
|
||||||
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
|
<a class="navbar-link">
|
||||||
|
<!-- Initialen-Badge -->
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="navbar-dropdown is-right">
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
<a class="navbar-item" href="/admin/">Admin</a>
|
||||||
|
<hr class="navbar-divider">
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Logout als POST über Hidden-Form -->
|
||||||
|
<a class="navbar-item" href="#"
|
||||||
|
onclick="document.getElementById('logout-form').submit(); return false;">
|
||||||
|
Logout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div><!-- Profil-Dropdown Ende -->
|
||||||
|
|
||||||
|
<!-- Hidden Logout Form (muss im DOM vorhanden sein) -->
|
||||||
|
<form id="logout-form" method="post" action="{% url 'logout' %}" style="display:none;">
|
||||||
|
{% csrf_token %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<!-- Login-Button -->
|
||||||
|
<div class="navbar-item">
|
||||||
|
<div class="buttons">
|
||||||
|
<a class="button is-primary is-light" href="{% url 'login' %}">
|
||||||
|
<strong>Login</strong>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
1
templates/registration/logged_out.html
Normal file
1
templates/registration/logged_out.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Erfolgreich abgemeldet
|
109
templates/registration/login.html
Normal file
109
templates/registration/login.html
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
{% load static %}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Login</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<!-- Wenn Bulma bereits global geladen wird, diesen Link entfernen -->
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.2/css/bulma.min.css">
|
||||||
|
</head>
|
||||||
|
<body class="has-background-light">
|
||||||
|
|
||||||
|
<section class="hero is-fullheight">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns is-centered">
|
||||||
|
<div class="column is-4-widescreen is-5-desktop is-6-tablet">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header">
|
||||||
|
<p class="card-header-title">Bitte anmelden</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
{% if form.non_field_errors %}
|
||||||
|
<div class="notification is-danger is-light">
|
||||||
|
{% for error in form.non_field_errors %}
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="{% url 'login' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<!-- Benutzername -->
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="{{ form.username.id_for_label }}">Benutzername</label>
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<input
|
||||||
|
class="input{% if form.username.errors %} is-danger{% endif %}"
|
||||||
|
type="text"
|
||||||
|
name="{{ form.username.html_name }}"
|
||||||
|
id="{{ form.username.id_for_label }}"
|
||||||
|
value="{{ form.username.value|default:'' }}"
|
||||||
|
placeholder="z. B. maxmustermann"
|
||||||
|
autocomplete="username"
|
||||||
|
required>
|
||||||
|
<span class="icon is-left">👤</span>
|
||||||
|
</div>
|
||||||
|
{% if form.username.errors %}
|
||||||
|
{% for error in form.username.errors %}
|
||||||
|
<p class="help is-danger">{{ error }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Passwort -->
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="{{ form.password.id_for_label }}">Passwort</label>
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<input
|
||||||
|
class="input{% if form.password.errors %} is-danger{% endif %}"
|
||||||
|
type="password"
|
||||||
|
name="{{ form.password.html_name }}"
|
||||||
|
id="{{ form.password.id_for_label }}"
|
||||||
|
placeholder="••••••••"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required>
|
||||||
|
<span class="icon is-left">🔒</span>
|
||||||
|
</div>
|
||||||
|
{% if form.password.errors %}
|
||||||
|
{% for error in form.password.errors %}
|
||||||
|
<p class="help is-danger">{{ error }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
|
||||||
|
<div class="field is-grouped is-justify-content-space-between is-align-items-center">
|
||||||
|
<div class="control">
|
||||||
|
<button type="submit" class="button is-primary">
|
||||||
|
Anmelden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<a class="button is-text" href="{% url 'password_reset' %}">Passwort vergessen?</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="card-footer">
|
||||||
|
<span class="card-footer-item">
|
||||||
|
<span class="is-size-7 has-text-grey">© {% now "Y" %} – WIRA Risk Management</span>
|
||||||
|
</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -15,6 +15,15 @@
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title">Überblick</p>
|
<p class="card-header-title">Überblick</p>
|
||||||
|
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
<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>
|
||||||
|
<a class="card-header-icon has-text-danger" href="{% url 'admin:risks_control_delete' control.pk %}" title="Maßnahme Löschen (WARNUNG!)">
|
||||||
|
<span class="icon"><i class="fas fa-trash" aria-hidden="true"></i></span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
<!-- Inhalt Überblick-->
|
<!-- Inhalt Überblick-->
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
@ -50,6 +59,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
{% for risk in control.risks.all %}
|
{% for risk in control.risks.all %}
|
||||||
<tr onclick="window.location.href='/risks/risks/{{ risk.id }}';" style="cursor:pointer;">
|
<tr onclick="window.location.href='/risks/risks/{{ risk.id }}';" style="cursor:pointer;">
|
||||||
<td>{{ risk.title }}</td>
|
<td>{{ risk.title }}</td>
|
||||||
|
|
|
@ -15,6 +15,15 @@
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title">Überblick</p>
|
<p class="card-header-title">Überblick</p>
|
||||||
|
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
<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>
|
||||||
|
<a class="card-header-icon has-text-danger" href="{% url 'admin:risks_incident_delete' incident.pk %}" title="Vorfall Löschen (WARNUNG!)">
|
||||||
|
<span class="icon"><i class="fas fa-trash" aria-hidden="true"></i></span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
<!-- Inhalt Überblick-->
|
<!-- Inhalt Überblick-->
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
|
|
@ -16,6 +16,15 @@
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title">Überblick</p>
|
<p class="card-header-title">Überblick</p>
|
||||||
|
|
||||||
|
{% 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>
|
||||||
|
<a class="card-header-icon has-text-danger" href="{% url 'admin:risks_risk_delete' risk.pk %}" title="Risiko Löschen (WARNUNG!)">
|
||||||
|
<span class="icon"><i class="fas fa-trash" aria-hidden="true"></i></span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
<!-- Inhalt Überblick-->
|
<!-- Inhalt Überblick-->
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
@ -26,13 +35,9 @@
|
||||||
<p>
|
<p>
|
||||||
<strong>Schutzziele:</strong>
|
<strong>Schutzziele:</strong>
|
||||||
{% if risk.cia %}
|
{% if risk.cia %}
|
||||||
<ul>
|
{{ risk.get_cia_display }}
|
||||||
{% for label in risk.cia %}
|
|
||||||
<li>{{ label|cia_label }}</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>Noch nicht zugewiesen</p>
|
<span class="has-text-grey">Noch nicht zugewiesen</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,33 +58,37 @@
|
||||||
<h4>Brutto (vor Maßnahmen)</h4>
|
<h4>Brutto (vor Maßnahmen)</h4>
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
|
|
||||||
|
<!-- Eintrittswahrscheinlichkeit -->
|
||||||
<div class="column is-half has-text-centered">
|
<div class="column is-half has-text-centered">
|
||||||
<p class="heading">Eintrittswahrscheinlichkeit</p>
|
<p class="heading">Eintrittswahrscheinlichkeit</p>
|
||||||
<button class="button is-small is-info">
|
<button class="button is-small {{ risk.likelihood|likelihood_class }}">
|
||||||
{{ risk.get_likelihood_display }}
|
{{ risk.get_likelihood_display }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div> <!-- Eintrittswahrscheinlichkeit Ende -->
|
||||||
|
|
||||||
|
<!-- Schadensausmaß -->
|
||||||
<div class="column is-half has-text-centered">
|
<div class="column is-half has-text-centered">
|
||||||
<p class="heading">Schadensausmaß</p>
|
<p class="heading">Schadensausmaß</p>
|
||||||
<button class="button is-small is-danger">
|
<button class="button is-small {{ risk.impact|impact_class }}">
|
||||||
{{ risk.get_impact_display }}
|
{{ risk.get_impact_display }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div> <!-- Schadensausmaß Ende -->
|
||||||
|
|
||||||
|
<!-- Stufe -->
|
||||||
<div class="column is-half has-text-centered">
|
<div class="column is-half has-text-centered">
|
||||||
<p class="heading">Stufe</p>
|
<p class="heading">Stufe</p>
|
||||||
<button class="button is-small is-info">
|
<button class="button is-small {{ risk.level|level_class }}">
|
||||||
{{ risk.level }}
|
{{ risk.level }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div> <!-- Stufe Ende -->
|
||||||
|
|
||||||
|
<!-- Score -->
|
||||||
<div class="column is-half has-text-centered">
|
<div class="column is-half has-text-centered">
|
||||||
<p class="heading">Score</p>
|
<p class="heading">Score</p>
|
||||||
<button class="button is-small is-danger">
|
<button class="button is-small {{ risk.score|score_class }}">
|
||||||
{{ risk.score }} / 25
|
{{ risk.score }} / 20
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div> <!-- Score Ende -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -90,36 +99,40 @@
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h4>Netto (nach Maßnahmen)</h4>
|
<h4>Netto (nach Maßnahmen)</h4>
|
||||||
|
|
||||||
{% if risk.residualrisk %}
|
{% if risk.residual_risk %}
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
|
|
||||||
<div class="column is-half has-text-centered">
|
<!-- Eintrittswahrscheinlichkeit -->
|
||||||
<p class="heading">Eintrittswahrscheinlichkeit</p>
|
<div class="column is-half has-text-centered">
|
||||||
<button class="button is-small is-info">
|
<p class="heading">Eintrittswahrscheinlichkeit</p>
|
||||||
{{ risk.residualrisk.get_likelihood_display }}
|
<button class="button is-small {{ risk.likelihood|likelihood_class }}">
|
||||||
</button>
|
{{ risk.residual_risk.get_likelihood_display }}
|
||||||
</div>
|
</button>
|
||||||
|
</div> <!-- Eintrittswahrscheinlichkeit Ende -->
|
||||||
|
|
||||||
<div class="column is-half has-text-centered">
|
<!-- Schadensausmaß -->
|
||||||
<p class="heading">Schadensausmaß</p>
|
<div class="column is-half has-text-centered">
|
||||||
<button class="button is-small is-danger">
|
<p class="heading">Schadensausmaß</p>
|
||||||
{{ risk.residualrisk.get_impact_display }}
|
<button class="button is-small {{ risk.impact|impact_class }}">
|
||||||
</button>
|
{{ risk.residual_risk.get_impact_display }}
|
||||||
</div>
|
</button>
|
||||||
|
</div> <!-- Schadensausmaß Ende -->
|
||||||
|
|
||||||
<div class="column is-half has-text-centered">
|
<!-- Stufe -->
|
||||||
<p class="heading">Stufe</p>
|
<div class="column is-half has-text-centered">
|
||||||
<button class="button is-small is-info">
|
<p class="heading">Stufe</p>
|
||||||
{{ risk.residualrisk.level }}
|
<button class="button is-small {{ risk.level|level_class }}">
|
||||||
</button>
|
{{ risk.residual_risk.level }}
|
||||||
</div>
|
</button>
|
||||||
|
</div> <!-- Stufe Ende -->
|
||||||
|
|
||||||
<div class="column is-half has-text-centered">
|
<!-- Score -->
|
||||||
<p class="heading">Score</p>
|
<div class="column is-half has-text-centered">
|
||||||
<button class="button is-small is-danger">
|
<p class="heading">Score</p>
|
||||||
{{ risk.residualrisk.score }} / 25
|
<button class="button is-small {{ risk.score|score_class }}">
|
||||||
</button>
|
{{ risk.residual_risk.score }} / 20
|
||||||
</div>
|
</button>
|
||||||
|
</div> <!-- Score Ende -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -139,7 +152,7 @@
|
||||||
<p class="card-header-title">Maßnahmen</p>
|
<p class="card-header-title">Maßnahmen</p>
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{% if risk.controls.all %}
|
{% if risk.controls.exists %}
|
||||||
<table class="table is-striped is-hoverable is-fullwidth">
|
<table class="table is-striped is-hoverable is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -96,6 +96,7 @@
|
||||||
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
|
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
{% if request.user.is_staff %}<th></th>{% endif %}
|
||||||
<th>Maßnahme</th>
|
<th>Maßnahme</th>
|
||||||
<th>Risiken</th>
|
<th>Risiken</th>
|
||||||
<th>Verantwortliche/r</th>
|
<th>Verantwortliche/r</th>
|
||||||
|
@ -105,10 +106,32 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
<tr>
|
||||||
|
<td class="has-text-centered">
|
||||||
|
<a class="icon has-text-success" href="{% url 'admin:risks_risk_add' %}" title="Maßnahme hinzufügen">
|
||||||
|
<i class="fas fa-add"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% for c in controls %}
|
{% for c in controls %}
|
||||||
<tr onclick="window.location.href='{% url 'risks:show_control' c.id %}'" style="cursor:pointer;">
|
<tr>
|
||||||
<td>{{ c.title }}</td>
|
{% if request.user.is_staff %}
|
||||||
<td>
|
<td class="has-text-centered">
|
||||||
|
<a class="icon has-text-warning" href="{% url 'admin:risks_control_change' c.id %}" title="Maßnahme bearbeiten">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td onclick="window.location.href='{% url 'risks:show_control' c.id %}'" style="cursor:pointer;">{{ c.title }}</td>
|
||||||
|
<td onclick="window.location.href='{% url 'risks:show_control' c.id %}'" style="cursor:pointer;">
|
||||||
{% if c.risk %}
|
{% if c.risk %}
|
||||||
<a href="{% url 'risks:show_risk' c.risk.id %}" onclick="event.stopPropagation();">
|
<a href="{% url 'risks:show_risk' c.risk.id %}" onclick="event.stopPropagation();">
|
||||||
{{ c.risk.title }}
|
{{ c.risk.title }}
|
||||||
|
@ -117,15 +140,15 @@
|
||||||
–
|
–
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td onclick="window.location.href='{% url 'risks:show_control' c.id %}'" style="cursor:pointer;">
|
||||||
{% if c.responsible %}
|
{% if c.responsible %}
|
||||||
{{ c.responsible.get_full_name|default:c.responsible.username }}
|
{{ c.responsible.get_full_name|default:c.responsible.username }}
|
||||||
{% else %}
|
{% else %}
|
||||||
–
|
–
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ c.get_status_display }}</td>
|
<td onclick="window.location.href='{% url 'risks:show_control' c.id %}'" style="cursor:pointer;">{{ c.get_status_display }}</td>
|
||||||
<td>
|
<td onclick="window.location.href='{% url 'risks:show_control' c.id %}'" style="cursor:pointer;">
|
||||||
{% if c.due_date %}
|
{% if c.due_date %}
|
||||||
{{ c.due_date|date:"d.m.Y" }}
|
{{ c.due_date|date:"d.m.Y" }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -95,6 +95,7 @@
|
||||||
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
|
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
{% if request.user.is_staff %}<th></th>{% endif %}
|
||||||
<th>Vorfall</th>
|
<th>Vorfall</th>
|
||||||
<th>Zugehörige Risiken</th>
|
<th>Zugehörige Risiken</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
@ -103,10 +104,32 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td class="has-text-centered">
|
||||||
|
<a class="icon has-text-success" href="{% url 'admin:risks_incident_add' %}" title="Risiko hinzufügen">
|
||||||
|
<i class="fas fa-add"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% for i in incidents %}
|
{% for i in incidents %}
|
||||||
<tr onclick="window.location.href='{% url 'risks:show_incident' i.id %}'" style="cursor:pointer;">
|
<tr>
|
||||||
<td>{{ i.title }}</td>
|
{% if request.user.is_staff %}
|
||||||
<td>
|
<td class="has-text-centered">
|
||||||
|
<a class="icon has-text-warning" href="{% url 'admin:risks_incident_change' i.id %}" title="Risiko bearbeiten">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td onclick="window.location.href='{% url 'risks:show_incident' i.id %}'" style="cursor:pointer;">{{ i.title }}</td>
|
||||||
|
<td onclick="window.location.href='{% url 'risks:show_incident' i.id %}'" style="cursor:pointer;">
|
||||||
{% if i.related_risks.exists %}
|
{% if i.related_risks.exists %}
|
||||||
<ul>
|
<ul>
|
||||||
{% for r in i.related_risks.all %}
|
{% for r in i.related_risks.all %}
|
||||||
|
@ -117,9 +140,9 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ i.get_status_display }}</td>
|
<td onclick="window.location.href='{% url 'risks:show_incident' i.id %}'" style="cursor:pointer;">{{ i.get_status_display }}</td>
|
||||||
<td>{{ i.date_reported|date:"d.m.Y" }}</td>
|
<td onclick="window.location.href='{% url 'risks:show_incident' i.id %}'" style="cursor:pointer;">{{ i.date_reported|date:"d.m.Y" }}</td>
|
||||||
<td>{{ i.reported_by }}</td>
|
<td onclick="window.location.href='{% url 'risks:show_incident' i.id %}'" style="cursor:pointer;">{{ i.reported_by }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -78,6 +78,7 @@
|
||||||
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
|
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
{% if request.user.is_staff %}<th></th>{% endif %}
|
||||||
<th>Risiko</th>
|
<th>Risiko</th>
|
||||||
<th>Asset / Prozes</th>
|
<th>Asset / Prozes</th>
|
||||||
<th>Kategorie</th>
|
<th>Kategorie</th>
|
||||||
|
@ -89,21 +90,46 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<td class="has-text-centered">
|
||||||
|
<a class="icon has-text-success" href="{% url 'admin:risks_risk_add' %}" title="Risiko hinzufügen">
|
||||||
|
<i class="fas fa-add"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% for r in risks %}
|
{% for r in risks %}
|
||||||
<tr onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">
|
<tr>
|
||||||
<td>{{ r.title }}</td>
|
{% if request.user.is_staff %}
|
||||||
<td>
|
<td class="has-text-centered">
|
||||||
|
<a class="icon has-text-warning" href="{% url 'admin:risks_risk_change' r.id %}" title="Risiko bearbeiten">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">{{ r.title }}</td>
|
||||||
|
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">
|
||||||
{{ r.asset }}
|
{{ r.asset }}
|
||||||
{% if r.process %}
|
{% if r.process %}
|
||||||
<br><small>{{ r.process }}</small>
|
<br><small>{{ r.process }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ r.category }}</td>
|
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">{{ r.category }}</td>
|
||||||
<td>{{ r.get_likelihood_display }}</td>
|
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">{{ r.get_likelihood_display }}</td>
|
||||||
<td>{{ r.get_impact_display }}</td>
|
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">{{ r.get_impact_display }}</td>
|
||||||
<td>{{ r.score }}</td>
|
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">{{ r.score }}</td>
|
||||||
<td>{{ r.level }}</td>
|
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">{{ r.level }}</td>
|
||||||
<td>
|
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">
|
||||||
{% if r.owner %}
|
{% if r.owner %}
|
||||||
{{ r.owner.get_full_name|default:r.owner.username }}
|
{{ r.owner.get_full_name|default:r.owner.username }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
Loading…
Add table
Reference in a new issue