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 = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.locale.LocaleMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
@ -134,14 +135,28 @@ AUTH_PASSWORD_VALIDATORS = [
{"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
# ---------------------------------------------------------------------------
LANGUAGE_CODE = "de"
TIME_ZONE = "UTC"
TIME_ZONE = "Europe/Berlin"
USE_I18N = True
USE_TZ = True
LANGUAGES = [
("de", "Deutsch"),
("en", "English"),
]
LOCALE_PATHS = [BASE_DIR / "locale"]
# ---------------------------------------------------------------------------
# Static files
# ---------------------------------------------------------------------------

View file

@ -15,9 +15,11 @@ router.register(r"logs", AuditViewSet)
urlpatterns = [
path("admin/", admin.site.urls),
path("i18n/", include("django.conf.urls.i18n")), # Language Switch
path("api/ping/", ping), # Public healthcheck endpoint
path("api/secure-ping/", secure_ping), # Protected API endpoint
path("api/", include(router.urls)),
path("accounts/", include("django.contrib.auth.urls")),
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.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import gettext_lazy as _
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)
class UserAdmin(BaseUserAdmin):
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",
"owned_risks_count", "responsible_controls_count")
def owned_risks_count(self, obj):
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):
return obj.controls_responsible.count()
responsible_controls_count.short_description = "Controls Responsible"
responsible_controls_count.short_description = _("Controls Responsible")
class ResidualRiskInline(admin.StackedInline):
"""
@ -25,7 +29,7 @@ class ResidualRiskInline(admin.StackedInline):
"""
model = ResidualRisk
extra = 0
can_delete = False # Since each Risk can have at most one residual risk
can_delete = False
readonly_fields = ("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")
search_fields = ("title", "asset", "process", "category")
inlines = [ResidualRiskInline, ControlRisksInline] # Controls hier verknüpfen
inlines = [ResidualRiskInline, ControlRisksInline]
def save_model(self, request, obj, form, change):
obj._changed_by = request.user

View file

@ -1,10 +1,10 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class RisksConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'risks'
default_auto_field = "django.db.models.BigAutoField"
name = "risks"
verbose_name = _("Risk Management")
def ready(self):
# Import signals when app is ready
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.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.utils.translation import gettext_lazy as _
from multiselectfield import MultiSelectField
import datetime
import json
@ -29,39 +30,38 @@ class User(AbstractUser):
return self.responsible_controls.all()
class Risk(models.Model):
"""
Represents an information security risk.
"""
class Meta:
verbose_name = _("Risk")
verbose_name_plural = _("Risks")
LIKELIHOOD_CHOICES = [
(1, "Very low occurs less than once every 5 years"),
(2, "Low once every 15 years"),
(3, "Likely once per year or more"),
(4, "Very likely multiple times per year/monthly"),
(1, _("Very low occurs less than once every 5 years")),
(2, _("Low once every 15 years")),
(3, _("Likely once per year or more")),
(4, _("Very likely multiple times per year/monthly")),
]
IMPACT_CHOICES = [
(1, "Low (< 1,000 € minor operational impact)"),
(2, "Medium (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)"),
(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)")),
]
CIA_CHOICES = [
("1", "Confidentiality"),
("2", "Integrity"),
("3", "Availability")
("1", _("Confidentiality")),
("2", _("Integrity")),
("3", _("Availability")),
]
# Basic information
title = models.CharField(max_length=255)
description = models.TextField(max_length=225, blank=True, null=True)
asset = models.CharField(max_length=255, blank=True, null=True)
process = models.CharField(max_length=255, blank=True, null=True)
category = models.CharField(max_length=255, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True,)
updated_at = models.DateTimeField(auto_now=True)
title = models.CharField(_("Title"), max_length=255)
description = models.TextField(_("Description"), max_length=225, blank=True, null=True)
asset = models.CharField(_("Asset"), max_length=255, blank=True, null=True)
process = models.CharField(_("Process"), max_length=255, blank=True, null=True)
category = models.CharField(_("Category"), max_length=255, blank=True, null=True)
created_at = models.DateTimeField(_("Created at"), auto_now_add=True)
updated_at = models.DateTimeField(_("Updated at"), auto_now=True)
# CIA Protection Goals
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
"""
class Meta:
verbose_name = _("Residual Risk")
verbose_name_plural = _("Residual Risks")
risk = models.OneToOneField(
Risk,
on_delete=models.CASCADE,
@ -167,15 +171,19 @@ class Control(models.Model):
"""
A security control/measure linked to a risk.
"""
class Meta:
verbose_name = _("Control")
verbose_name_plural = _("Controls")
STATUS_CHOICES = [
("planned", "Planned"),
("in_progress", "In progress"),
("completed", "Completed"),
("verified", "Verified"),
("rejected", "Rejected"),
("planned", _("Planned")),
("in_progress", _("In progress")),
("completed", _("Completed")),
("verified", _("Verified")),
("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")
due_date = models.DateField(blank=True, null=True)
responsible = models.ForeignKey(
@ -199,6 +207,11 @@ class AuditLog(models.Model):
"""
Generic audit log entry for tracking changes.
"""
class Meta:
verbose_name = _("Auditlog")
verbose_name_plural = _("Auditlogs")
ACTION_CHOICES = [
("create", "Created"),
("update", "Updated"),
@ -225,15 +238,23 @@ class Incident(models.Model):
"""
Incidents and related risks
"""
class Meta:
verbose_name = _("Incident")
verbose_name_plural = _("Incidents")
STATUS_CHOICES = [
("open", "Opened"),
("in_progress", "In Progress"),
("closed", "Closed"),
("open", _("Opened")),
("in_progress", _("In Progress")),
("closed", _("Closed")),
]
title = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
date_reported = models.DateField(blank=True, null=True)
reported_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="incidents")
title = models.CharField(_("Title"), max_length=255)
description = models.TextField(_("Description"), blank=True, null=True)
date_reported = models.DateField(_("Date reported"), blank=True, null=True)
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)
related_risks = models.ManyToManyField("Risk", blank=True, related_name="incidents")
created_at = models.DateTimeField(auto_now_add=True,)

View file

@ -7,3 +7,60 @@ register = template.Library()
def cia_label(value):
mapping = dict(Risk.CIA_CHOICES)
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.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
@ -95,12 +96,15 @@ class IncidentViewSet(viewsets.ModelViewSet):
# Web
# ---------------------------------------------------------------------------
@login_required
def dashboard(request):
return render(request, "risks/dashboard.html")
@login_required
def stats(request):
return render(request, "risks/statistics.html")
@login_required
def list_risks(request):
qs = Risk.objects.all().select_related("owner")
@ -127,16 +131,18 @@ def list_risks(request):
"owners": owners,
})
@login_required
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)
logs = LogEntry.objects.filter(
content_type=ct,
object_id=risk.pk
).order_by("-action_time")
logs = LogEntry.objects.filter(content_type=ct, object_id=risk.pk).order_by("-action_time")
return render(request, "risks/item_risk.html", {"risk": risk, "logs": logs})
@login_required
def list_controls(request):
qs = Control.objects.all().select_related("responsible")
@ -166,6 +172,7 @@ def list_controls(request):
"status_choices": Control.STATUS_CHOICES,
})
@login_required
def show_control(request, id):
control = get_object_or_404(Control, pk=id)
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})
@login_required
def list_incidents(request):
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,
})
@login_required
def show_incident(request, id):
incident = get_object_or_404(Incident, pk=id)
ct = ContentType.objects.get_for_model(Incident)
@ -211,4 +219,4 @@ def show_incident(request, id):
object_id=incident.pk
).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ß) */
.navbar.topbar-nav {
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>
<!-- Profil -->
<div class="navbar-end">
<div class="actions">
<input type="text" placeholder="Suchen" class="search">
<a class="navbar-item" href="/admin">Admin</a>
<div class="profile">KG</div>
<span class="icon"></span>
<!-- Suche -->
<div class="navbar-item">
<!--
<div class="field">
<p class="control">
<input class="input is-small" type="text" placeholder="Suchen">
</p>
</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>
</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">
<header class="card-header">
<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>
<!-- Inhalt Überblick-->
<div class="card-content">
@ -50,6 +59,7 @@
</tr>
</thead>
<tbody>
{% for risk in control.risks.all %}
<tr onclick="window.location.href='/risks/risks/{{ risk.id }}';" style="cursor:pointer;">
<td>{{ risk.title }}</td>

View file

@ -15,6 +15,15 @@
<div class="card">
<header class="card-header">
<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>
<!-- Inhalt Überblick-->
<div class="card-content">

View file

@ -16,6 +16,15 @@
<div class="card">
<header class="card-header">
<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>
<!-- Inhalt Überblick-->
<div class="card-content">
@ -26,13 +35,9 @@
<p>
<strong>Schutzziele:</strong>
{% if risk.cia %}
<ul>
{% for label in risk.cia %}
<li>{{ label|cia_label }}</li>
{% endfor %}
</ul>
{{ risk.get_cia_display }}
{% else %}
<p>Noch nicht zugewiesen</p>
<span class="has-text-grey">Noch nicht zugewiesen</span>
{% endif %}
</p>
</div>
@ -53,33 +58,37 @@
<h4>Brutto (vor Maßnahmen)</h4>
<div class="columns is-multiline">
<!-- Eintrittswahrscheinlichkeit -->
<div class="column is-half has-text-centered">
<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 }}
</button>
</div>
</div> <!-- Eintrittswahrscheinlichkeit Ende -->
<!-- Schadensausmaß -->
<div class="column is-half has-text-centered">
<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 }}
</button>
</div>
</div> <!-- Schadensausmaß Ende -->
<!-- Stufe -->
<div class="column is-half has-text-centered">
<p class="heading">Stufe</p>
<button class="button is-small is-info">
<button class="button is-small {{ risk.level|level_class }}">
{{ risk.level }}
</button>
</div>
</div> <!-- Stufe Ende -->
<!-- Score -->
<div class="column is-half has-text-centered">
<p class="heading">Score</p>
<button class="button is-small is-danger">
{{ risk.score }} / 25
<button class="button is-small {{ risk.score|score_class }}">
{{ risk.score }} / 20
</button>
</div>
</div> <!-- Score Ende -->
</div>
</div>
@ -90,36 +99,40 @@
<div class="box">
<h4>Netto (nach Maßnahmen)</h4>
{% if risk.residualrisk %}
{% if risk.residual_risk %}
<div class="columns is-multiline">
<div class="column is-half has-text-centered">
<p class="heading">Eintrittswahrscheinlichkeit</p>
<button class="button is-small is-info">
{{ risk.residualrisk.get_likelihood_display }}
</button>
</div>
<!-- Eintrittswahrscheinlichkeit -->
<div class="column is-half has-text-centered">
<p class="heading">Eintrittswahrscheinlichkeit</p>
<button class="button is-small {{ risk.likelihood|likelihood_class }}">
{{ risk.residual_risk.get_likelihood_display }}
</button>
</div> <!-- Eintrittswahrscheinlichkeit Ende -->
<div class="column is-half has-text-centered">
<p class="heading">Schadensausmaß</p>
<button class="button is-small is-danger">
{{ risk.residualrisk.get_impact_display }}
</button>
</div>
<!-- Schadensausmaß -->
<div class="column is-half has-text-centered">
<p class="heading">Schadensausmaß</p>
<button class="button is-small {{ risk.impact|impact_class }}">
{{ risk.residual_risk.get_impact_display }}
</button>
</div> <!-- Schadensausmaß Ende -->
<div class="column is-half has-text-centered">
<p class="heading">Stufe</p>
<button class="button is-small is-info">
{{ risk.residualrisk.level }}
</button>
</div>
<!-- Stufe -->
<div class="column is-half has-text-centered">
<p class="heading">Stufe</p>
<button class="button is-small {{ risk.level|level_class }}">
{{ risk.residual_risk.level }}
</button>
</div> <!-- Stufe Ende -->
<div class="column is-half has-text-centered">
<p class="heading">Score</p>
<button class="button is-small is-danger">
{{ risk.residualrisk.score }} / 25
</button>
</div>
<!-- Score -->
<div class="column is-half has-text-centered">
<p class="heading">Score</p>
<button class="button is-small {{ risk.score|score_class }}">
{{ risk.residual_risk.score }} / 20
</button>
</div> <!-- Score Ende -->
</div>
{% else %}
@ -139,7 +152,7 @@
<p class="card-header-title">Maßnahmen</p>
</header>
<div class="card-content">
{% if risk.controls.all %}
{% if risk.controls.exists %}
<table class="table is-striped is-hoverable is-fullwidth">
<thead>
<tr>

View file

@ -96,6 +96,7 @@
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
<thead>
<tr>
{% if request.user.is_staff %}<th></th>{% endif %}
<th>Maßnahme</th>
<th>Risiken</th>
<th>Verantwortliche/r</th>
@ -105,10 +106,32 @@
</tr>
</thead>
<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 %}
<tr onclick="window.location.href='{% url 'risks:show_control' c.id %}'" style="cursor:pointer;">
<td>{{ c.title }}</td>
<td>
<tr>
{% if request.user.is_staff %}
<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 %}
<a href="{% url 'risks:show_risk' c.risk.id %}" onclick="event.stopPropagation();">
{{ c.risk.title }}
@ -117,15 +140,15 @@
{% endif %}
</td>
<td>
<td onclick="window.location.href='{% url 'risks:show_control' c.id %}'" style="cursor:pointer;">
{% if c.responsible %}
{{ c.responsible.get_full_name|default:c.responsible.username }}
{% else %}
{% endif %}
</td>
<td>{{ c.get_status_display }}</td>
<td>
<td onclick="window.location.href='{% url 'risks:show_control' c.id %}'" style="cursor:pointer;">{{ c.get_status_display }}</td>
<td onclick="window.location.href='{% url 'risks:show_control' c.id %}'" style="cursor:pointer;">
{% if c.due_date %}
{{ c.due_date|date:"d.m.Y" }}
{% else %}

View file

@ -95,6 +95,7 @@
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
<thead>
<tr>
{% if request.user.is_staff %}<th></th>{% endif %}
<th>Vorfall</th>
<th>Zugehörige Risiken</th>
<th>Status</th>
@ -103,10 +104,32 @@
</tr>
</thead>
<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 %}
<tr onclick="window.location.href='{% url 'risks:show_incident' i.id %}'" style="cursor:pointer;">
<td>{{ i.title }}</td>
<td>
<tr>
{% if request.user.is_staff %}
<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 %}
<ul>
{% for r in i.related_risks.all %}
@ -117,9 +140,9 @@
{% endif %}
</ul>
</td>
<td>{{ i.get_status_display }}</td>
<td>{{ 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.get_status_display }}</td>
<td onclick="window.location.href='{% url 'risks:show_incident' i.id %}'" style="cursor:pointer;">{{ i.date_reported|date:"d.m.Y" }}</td>
<td onclick="window.location.href='{% url 'risks:show_incident' i.id %}'" style="cursor:pointer;">{{ i.reported_by }}</td>
</tr>
{% endfor %}
</tbody>

View file

@ -78,6 +78,7 @@
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
<thead>
<tr>
{% if request.user.is_staff %}<th></th>{% endif %}
<th>Risiko</th>
<th>Asset / Prozes</th>
<th>Kategorie</th>
@ -89,21 +90,46 @@
</tr>
</thead>
<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 %}
<tr onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">
<td>{{ r.title }}</td>
<td>
<tr>
{% if request.user.is_staff %}
<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 }}
{% if r.process %}
<br><small>{{ r.process }}</small>
{% endif %}
</td>
<td>{{ r.category }}</td>
<td>{{ r.get_likelihood_display }}</td>
<td>{{ r.get_impact_display }}</td>
<td>{{ r.score }}</td>
<td>{{ r.level }}</td>
<td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">{{ r.category }}</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">{{ r.get_likelihood_display }}</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">{{ r.get_impact_display }}</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">{{ r.score }}</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">{{ r.level }}</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">
{% if r.owner %}
{{ r.owner.get_full_name|default:r.owner.username }}
{% else %}