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:
Kevin Heyer 2025-09-09 14:25:59 +02:00
parent 686030e4cb
commit bf0a3c22c0
29 changed files with 1174 additions and 123 deletions

View file

@ -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
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View file

@ -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")),
] ]

Binary file not shown.

Binary file not shown.

View 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 15 years"
msgstr "Niedrig einmal in 15 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,0005,000 € local impact)"
msgstr "Gering (1.0005.000 € lokale Auswirkungen)"
#: risks/models.py:47
#, fuzzy
#| msgid "High (5,00015,000 team-level impact)"
msgid "High (5,00015,000 € team-level impact)"
msgstr "Hoch (5.00015.000 € Auswirkungen auf Teamebene)"
#: risks/models.py:48
#, fuzzy
#| msgid "Severe (50,000100,000 regional impact)"
msgid "Severe (50,000100,000 € regional impact)"
msgstr "Schwerwiegend (50.000100.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"

View file

@ -0,0 +1,3 @@
DATE_FORMAT = "d.m.Y"
DATETIME_FORMAT = "d.m.Y H:i"
TIME_FORMAT = "H:i"

Binary file not shown.

View 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 15 years"
msgstr ""
#: risks/models.py:41
msgid "Likely once per year or more"
msgstr ""
#: risks/models.py:42
msgid "Very likely multiple times per year/monthly"
msgstr ""
#: risks/models.py:45
msgid "Very Low (< 1,000 € minor operational impact)"
msgstr ""
#: risks/models.py:46
msgid "Low (1,0005,000 € local impact)"
msgstr ""
#: risks/models.py:47
msgid "High (5,00015,000 € team-level impact)"
msgstr ""
#: risks/models.py:48
msgid "Severe (50,000100,000 € regional impact)"
msgstr ""
#: risks/models.py:49
msgid "Critical (> 100,000 € existential threat)"
msgstr ""
#: risks/models.py:52
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 ""

View file

@ -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

View file

@ -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

View file

@ -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"),
),
]

View 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"},
),
]

View file

@ -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,0005,000 € local impact)"),
(3, "High (5,00015,000 € team-level impact)"),
(4, "Severe (50,000100,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,0005,000 € local impact)"),
(3, "High (5,00015,000 € team-level impact)"),
(4, "Severe (50,000100,000 € regional impact)"),
(5, "Critical (> 100,000 € existential threat)"),
],
default=1,
),
),
]

View file

@ -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 15 years"), (2, _("Low once every 15 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,0005,000 € local impact)"), (2, _("Low (1,0005,000 € local impact)")),
(3, "High (5,00015,000 € team-level impact)"), (3, _("High (5,00015,000 € team-level impact)")),
(4, "Severe (50,000100,000 € regional impact)"), (4, _("Severe (50,000100,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,)

View file

@ -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"

View file

@ -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})

View file

@ -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 */

View 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 %}

View file

@ -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>

View file

@ -0,0 +1 @@
Erfolgreich abgemeldet

View 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>

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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 %}

View file

@ -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>

View file

@ -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 %}