feat: Enhance risk management templates and styles with improved user display, control status labels, and responsive design elements

This commit is contained in:
Kevin Heyer 2025-09-10 10:49:14 +02:00
parent 8afa7363d0
commit f43412b2dd
7 changed files with 262 additions and 86 deletions

Binary file not shown.

View file

@ -43,13 +43,19 @@ class ControlRisksInline(admin.TabularInline):
class RiskAdmin(admin.ModelAdmin):
list_display = (
"title",
"owner",
"owner_name",
"score",
"level",
"likelihood",
"impact",
"follow_up",
)
def owner_name(self, obj):
if not obj.owner:
return "-"
return obj.owner.get_full_name() or obj.owner.username
list_filter = ("level", "likelihood", "impact", "owner")
search_fields = ("title", "asset", "process", "category")
inlines = [ResidualRiskInline, ControlRisksInline]

View file

@ -1,8 +1,64 @@
from django import template
from ..models import Risk
from django.utils.html import format_html
from ..models import Control, Incident, Risk
register = template.Library()
_CONTROL_STATUS_MAP = dict(Control.STATUS_CHOICES)
_INCIDENT_STATUS_MAP = dict(Incident.STATUS_CHOICES)
_LIKELIHOOD_LABELS = dict(Risk.LIKELIHOOD_CHOICES)
_IMPACT_LABELS = dict(Risk.IMPACT_CHOICES)
LEVEL_ID_MAP = {"Low": 1, "Medium": 2, "High": 3, "Critical": 4}
@register.filter
def level_id(level):
return LEVEL_ID_MAP.get(str(level), "")
@register.filter
def user_display(user):
if not user:
return ""
full = user.get_full_name()
return full if full else user.username
def _short(label: str) -> str:
"""First Segment of - from Impact and Likelihood"""
if not label:
return ""
for sep in (" ", " - ", "("):
if sep in label:
return label.split(sep, 1)[0].strip()
return label.strip()
@register.filter
def likelihood_id_label(val):
try:
i = int(val)
except (TypeError, ValueError):
return ""
label = _LIKELIHOOD_LABELS.get(i, "")
short = _short(str(label)) if label else ""
return format_html("{}<br>({})", i, short) if label else format_html("{}", i)
@register.filter
def impact_id_label(val):
try:
i = int(val)
except (TypeError, ValueError):
return ""
label = _IMPACT_LABELS.get(i, "")
short = _short(str(label)) if label else ""
return format_html("{}<br>({})", i, short) if label else format_html("{}", i)
@register.filter
def control_status_label(code):
return _CONTROL_STATUS_MAP.get(code, code)
@register.filter
def incident_status_label(code):
return _INCIDENT_STATUS_MAP.get(code, code)
@register.filter
def cia_label(value):
mapping = dict(Risk.CIA_CHOICES)
@ -63,4 +119,16 @@ def score_class(score):
return "is-control-mid"
if s <= 16:
return "is-control-high"
return "is-control-veryhigh"
return "is-control-veryhigh"
@register.filter
def to_bg(css_class: str):
"""
Wandelt is-control-foo -> has-background-control-foo um.
Für Tabellenzellen-Hintergrundfarben.
"""
try:
css = str(css_class)
except Exception:
return ""
return css.replace("is-control-", "has-background-control-") if css.startswith("is-control-") else css

View file

@ -160,4 +160,87 @@ body.dark-mode .box {
/* Optional: Buttons, Links etc. anpassen */
body.dark-mode a {
color: #bb86fc;
}
/* Ticket-Button (ID links, Text rechts) */
.risk-chip{
--chip-w: 260px;
--chip-id-w: 40px;
width: var(--chip-w);
display: inline-flex;
align-items: stretch;
border: 0;
border-radius: 8px;
overflow: hidden;
font-weight: 600;
box-shadow: 0 4px 14px rgba(0,0,0,.08);
background: var(--chip-bg, #eee);
color: var(--chip-fg, #111);
}
.risk-chip{
display:inline-flex;
flex-direction:column; /* <— neu */
}
.risk-chip .chip-head{
padding:.35rem .6rem;
font-size:.75rem;
font-weight:700;
text-transform:uppercase;
letter-spacing:.02em;
color:var(--chip-fg);
background:rgba(0,0,0,.10);
border-bottom:1px solid rgba(0,0,0,.12);
}
.risk-chip .chip-main{
display:flex;
align-items:stretch;
}
/* linke ID-Spalte mit leichter Textur */
.risk-chip .chip-id{
flex: 0 0 var(--chip-id-w);
display: grid;
place-items: center;
font-size: 1.25rem;
position: relative;
background: rgba(0,0,0,.06);
}
.risk-chip .chip-id::after{
content:"";
position: absolute; inset:0;
background:
linear-gradient( to right, rgba(255,255,255,.15), rgba(0,0,0,.08) 60% ),
repeating-linear-gradient(135deg, rgba(255,255,255,.12) 0 6px, transparent 6px 12px);
mix-blend-mode: soft-light;
opacity:.6;
}
/* rechte Text-Spalte */
.risk-chip .chip-label{
flex: 1 1 auto;
padding: .5rem .75rem;
line-height: 1.25;
display: flex;
align-items: center;
border-left: 1px solid rgba(0,0,0,.08);
min-height: 2.25rem;
}
/* Farbzuteilung aus deinen Custom-Klassen */
.risk-chip.is-control-verylow { --chip-bg: var(--c-verylow); --chip-fg: var(--c-verylow-inv); }
.risk-chip.is-control-low { --chip-bg: var(--c-low); --chip-fg: var(--c-low-inv); }
.risk-chip.is-control-mid { --chip-bg: var(--c-mid); --chip-fg: var(--c-mid-inv); }
.risk-chip.is-control-high { --chip-bg: var(--c-high); --chip-fg: var(--c-high-inv); }
.risk-chip.is-control-veryhigh{ --chip-bg: var(--c-veryhigh); --chip-fg: var(--c-veryhigh-inv); }
/* Responsiv: auf schmalen Screens volle Breite */
@media (max-width: 480px){
.risk-chip{ width: 100%; }
}
@media (max-width: 1215px) {
.risk-chip { --chip-w: 100%; width: var(--chip-w); }
}

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% load i18n %}
{% load i18n risk_extras %}
{% block content %}
<!-- Hero Section -->
<section class="hero is-small is-bold">
@ -26,7 +26,7 @@
</div>
</div>
<!-- Residual-Risiken -->
<!-- Restrisiken -->
<div class="column is-one-quarter">
<div class="box has-text-centered {% if residual_review_required > 0 %}has-background-control-high{% else %}has-background-control-low{% endif %}">
<h2 class="title is-4">{{ residual_review_required }}</h2>
@ -47,8 +47,8 @@
<div class="box">
<h2 class="subtitle">{% trans "Controls by Status" %}</h2>
<ul>
{% for status in controls_by_status %}
<li>{{ status.status }}: {{ status.count }}</li>
{% for row in controls_by_status %}
<li>{{ row.status|control_status_label }}: {{ row.count }}</li>
{% endfor %}
</ul>
</div>
@ -57,8 +57,8 @@
<div class="box">
<h2 class="subtitle">{% trans "Incidents by Status" %}</h2>
<ul>
{% for incident in incidents_status %}
<li>{{ incident.status }}: {{ incident.count }}</li>
{% for row in incidents_status %}
<li>{{ row.status|incident_status_label }}: {{ row.count }}</li>
{% endfor %}
</ul>
</div>
@ -72,12 +72,12 @@
{% if cia == '1' %}
<div class="box has-background-control-verylow has-text-centered">
<h3 class="title is-5">{% trans "Confidentiality" %}</h3>
<p>{{ count }} Risks</p>
<p>{{ count }} {% trans "Risks" %}</p>
</div>
{% elif cia == '2' %}
<div class="box has-background-control-mid has-text-centered">
<h3 class="title is-5">{% trans "Integrity" %}</h3>
<p>{{ count }} Risks</p>
<p>{{ count }} {% trans "Risks" %}</p>
</div>
{% elif cia == '3' %}
<div class="box has-background-control-high has-text-centered">

View file

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% load risk_extras %}
{% load i18n risk_extras %}
{% block crumbs %}
<li><a href="{% url 'risks:list_risks' %}">Risikoanalyse</a></li>
<li><a href="{% url 'risks:show_risk' risk.id %}">{{ risk.title }}</a></li>
@ -32,6 +32,7 @@
<div class="column is-half">
<p><strong>Asset:</strong> {{ risk.asset|default:"-" }}</p>
<p><strong>Prozess:</strong> {{ risk.process|default:"-" }}</p>
<p><strong>Kategorie:</strong> {{ risk.category|default:"-" }}</p>
<p>
<strong>Schutzziele:</strong>
{% if risk.cia %}
@ -42,103 +43,111 @@
</p>
</div>
<div class="column is-half">
<p><strong>Kategorie:</strong> {{ risk.category|default:"-" }}</p>
<p><strong>Risikoeigner:</strong> {{ risk.owner|default:"-" }}</p>
<p><strong>Risikoeigner:</strong> {{ risk.owner|user_display|default:"-" }}</p>
<p><strong>Erstellt am:</strong> {{ risk.created_at|date:'d.m.Y H:i' }}</p>
<p><strong>Aktualisiert am:</strong> {{ risk.updated_at|date:'d.m.Y H:i' }}</p>
<p><strong>Wiedervorlage am:</strong> {{ risk.follow_up|date:'d.m.Y' }}</p>
</div>
</div>
<!-- Risikobewertung -->
<h3>Risikobewertung</h3>
<section class="hero has-text-centered is-small">
<div class="hero-body">
<p class="title">Risikobewertung</p>
</div>
</section>
<div class="columns is-multiline">
<!-- Bruttorisiko -->
<div class="column is-half">
<div class="box">
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered is-flex">
<div class="box is-flex is-flex-direction-column is-flex-grow-1">
<h4>Brutto (vor Maßnahmen)</h4>
<div class="columns is-multiline">
<!-- Eintrittswahrscheinlichkeit -->
<div class="column is-half has-text-centered">
<p class="heading">Eintrittswahrscheinlichkeit</p>
<button class="button is-small {{ risk.likelihood|likelihood_class }}">
{{ risk.get_likelihood_display }}
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.likelihood|likelihood_class }}" type="button" aria-label="{% trans 'Likelihood' as likelihood_long_name %}">
<span class="chip-head">Eintrittswahrscheinlichkeit</span>
<span class="chip-id">{{ risk.likelihood }}</span>
<span class="chip-label">{{ risk.get_likelihood_display }}</span>
</button>
</div> <!-- Eintrittswahrscheinlichkeit Ende -->
</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.get_impact_display }}
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.impact|impact_class }}" type="button" aria-label="{% trans 'Impact' as impact_long_name %}">
<span class="chip-head">Schadensausmaß</span>
<span class="chip-id">{{ risk.impact }}</span>
<span class="chip-label">{{ risk.get_impact_display }}</span>
</button>
</div> <!-- Schadensausmaß Ende -->
</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.level }}
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.level|level_class }}" type="button" aria-label="{% trans 'Level' %}">
<span class="chip-head">Stufe</span>
<span class="chip-id">{{ risk.level|level_id }}</span>
<span class="chip-label">{{ risk.level }}</span>
</button>
</div> <!-- Stufe Ende -->
</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.score }} / 20
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.score|score_class }}" type="button" aria-label="{% trans 'Score' %}">
<span class="chip-head">Score</span>
<span class="chip-id">{{ risk.score }}</span>
<span class="chip-label">Score (max. 20)</span>
</button>
</div> <!-- Score Ende -->
</div>
</div>
</div>
</div> <!-- Ende Bruttorisiko -->
<!-- Nettorisiko -->
<div class="column is-half">
<div class="box">
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered is-flex">
<div class="box is-flex is-flex-direction-column is-flex-grow-1">
<h4>Netto (nach Maßnahmen)</h4>
<div class="columns is-multiline">
{% if risk.residual_risk %}
<div class="columns is-multiline">
<!-- 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 }}
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.residual_risk.likelihood|likelihood_class }}" type="button" aria-label="{% trans 'Likelihood' as likelihood_long_name %}">
<span class="chip-head">Eintrittswahrscheinlichkeit</span>
<span class="chip-id">{{ risk.residual_risk.likelihood }}</span>
<span class="chip-label">{{ risk.residual_risk.get_likelihood_display }}</span>
</button>
</div> <!-- Eintrittswahrscheinlichkeit Ende -->
</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 }}
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.residual_risk.impact|impact_class }}" type="button" aria-label="{% trans 'Impact' as impact_long_name %}">
<span class="chip-head">Schadensausmaß</span>
<span class="chip-id">{{ risk.residual_risk.impact }}</span>
<span class="chip-label">{{ risk.residual_risk.get_impact_display }}</span>
</button>
</div> <!-- Schadensausmaß Ende -->
</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 }}
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered">
<button class="risk-chip {{ risk.residual_risk.level|level_class }}" type="button" aria-label="{% trans 'Level' %}">
<span class="chip-head">Stufe</span>
<span class="chip-id">{{ risk.residual_risk.level|level_id }}</span>
<span class="chip-label">{{ risk.residual_risk.level }}</span>
</button>
</div> <!-- Stufe Ende -->
</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
<div class="column is-full-mobile is-full-tablet is-half-desktop has-text-centered" aria-label="{% trans 'Score' %}">
<button class="risk-chip {{ risk.residual_risk.score|score_class }}" type="button">
<span class="chip-head">Score</span>
<span class="chip-id">{{ risk.residual_risk.score }}</span>
<span class="chip-label">(max. 20)</span>
</button>
</div> <!-- Score Ende -->
</div>
{% else %}
<p class="has-text-grey">Noch kein Nettorisiko erfasst.</p>
{% endif %}
</div>
</div>
</div> <!-- Ende Nettorisiko -->

View file

@ -1,11 +1,12 @@
{% extends "base.html" %}
{% load i18n risk_extras %}
{% block crumbs %}
<li><a href="{% url 'risks:list_risks' %}">Risikoanalyse</a></li>
<li><a href="{% url 'risks:list_risks' %}">{% trans "Risk analysis" %}</a></li>
{% endblock %}
{% block content %}
<section class="section">
<div class="box">
<h2 class="title is-5">Auswahl</h2>
<h2 class="title is-5">{% trans "Filter" %}</h2>
<!-- Filter -->
<form method="get">
@ -14,11 +15,11 @@
<!-- Risiko Filter -->
<div class="column is-3">
<div class="field">
<label class="label">Risiko</label>
<label class="label">{% trans "Risk" %}</label>
<div class="control">
<div class="select is-fullwidth">
<select name="risk" onchange="this.form.submit()">
<option value="">Alle</option>
<option value="">{% trans "All" %}</option>
{% for r in risks %}
<option value="{{ r.id }}" {% if request.GET.risk == r.id|stringformat:"s" %}selected{% endif %}>
{{ r.title }}
@ -33,11 +34,11 @@
<!-- Maßnahmen Filter -->
<div class="column is-3">
<div class="field">
<label class="label">Maßnahmen</label>
<label class="label">{% trans "Controls" %}</label>
<div class="control">
<div class="select is-fullwidth">
<select name="control" onchange="this.form.submit()">
<option value="">Alle</option>
<option value="">{% trans "All" %}</option>
{% for c in controls %}
<option value="{{ c.id }}" {% if request.GET.control == c.id|stringformat:"s" %}selected{% endif %}>
{{ c.title }}
@ -52,11 +53,11 @@
<!-- Risikoeigner Filter -->
<div class="column is-3">
<div class="field">
<label class="label">Risikoeigner</label>
<label class="label">{% trans "Risk Owner" %}</label>
<div class="control">
<div class="select is-fullwidth">
<select name="owner" onchange="this.form.submit()">
<option value="">Alle</option>
<option value="">{% trans "All" %}</option>
{% for u in owners %}
<option value="{{ u.id }}" {% if request.GET.owner == u.id|stringformat:"s" %}selected{% endif %}>
{{ u.get_full_name|default:u.username }}
@ -72,21 +73,21 @@
</form>
<h2 class="title is-5">Risiken</h2>
<h2 class="title is-5">{% trans "Risks" %}</h2>
<!-- Risiken -->
<div class="table-container">
<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>
<th>Eintritt</th>
<th>Schaden</th>
<th>Score</th>
<th>Stufe</th>
<th>Risikoeigner</th>
<th>{% trans "Risk" %}</th>
<th>{% trans "Asset / Process" %}</th>
<th>{% trans "Category" %}</th>
<th>{% trans "Likelihood" %}</th>
<th>{% trans "Impact" %}</th>
<th>{% trans "Score" %}</th>
<th>{% trans "Level" %}</th>
<th>{% trans "Risk Owner" %}</th>
</tr>
</thead>
<tbody>
@ -125,13 +126,22 @@
{% endif %}
</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 %}'" class="has-text-centered {{ r.likelihood|likelihood_class|to_bg }}" style="cursor:pointer;">
{{ r.likelihood|likelihood_id_label }}
</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" class="has-text-centered {{ r.impact|impact_class|to_bg }}" style="cursor:pointer;">
{{ r.impact|impact_id_label }}
</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" class="has-text-centered {{ r.score|score_class|to_bg }}" style="cursor:pointer;">
{{ r.score }}
</td>
<td onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" class="has-text-centered {{ r.level|level_class|to_bg }}" 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 }}
{{ r.owner|user_display }}
{% else %}
{% endif %}