feat: Enhance Risk Management Module
- Updated Risk model to include description, created_at, and updated_at fields. - Modified RiskSerializer to include created_at and updated_at in serialized output. - Improved logging in signals for Risk and Control models, including serialization of values. - Added new template tags for CIA label mapping. - Refactored URL patterns for better clarity and added detail views for risks, controls, and incidents. - Implemented list and detail views for risks, controls, and incidents with filtering options. - Enhanced CSS for better UI/UX, including breadcrumbs and table styling. - Created new templates for displaying individual risks, controls, and incidents with detailed information.
This commit is contained in:
parent
ee78caa3d1
commit
43e86d0357
27 changed files with 1073 additions and 107 deletions
54
TODO
54
TODO
|
@ -1,66 +1,12 @@
|
|||
✅Risiken
|
||||
✅-Titel
|
||||
✅-Asset
|
||||
✅-Prozess
|
||||
✅-Kategorie
|
||||
✅-Eintrittswahrscheinlichkeit vor Maßnahmen
|
||||
✅-Schadenshöhe vor Maßnahmen
|
||||
✅-Score vor Maßnahmen (Berechnet sich aus Eintrittswahrscheinlichkeit und Schadenshöhe)
|
||||
✅-Stufe vor Maßnahmen (Errechnet sich aus dem Score)
|
||||
✅-Risikoeigner
|
||||
✅-Maßnahmen (Ein Risiko kann mehrere Maßnahmen haben)
|
||||
✅-Restrisiko
|
||||
✅-Wiedervorlage
|
||||
✅-Schutzziele
|
||||
|
||||
✅Eintrittswahrscheinlichkeiten
|
||||
✅-1, sehr gering, Voraussichtliches Auftreten seltener als einmal in 5 Jahren.
|
||||
✅-2, gering, Voraussichtliches Auftreten einmal in 1 bis 5 Jahren.
|
||||
✅-3, wahrscheinlich, Voraussichtliches Auftreten einmal pro Jahr oder häufiger.
|
||||
✅-4, sehr wahrscheinlich, Voraussichtliches Auftreten mehrmals pro Jahr oder monatlich.
|
||||
|
||||
✅Schadenshöhen
|
||||
✅-1, niedrig, (z.B. Schaden < 1.000 €, geringer operativer Einfluss)
|
||||
✅-2, mittel, (z.B. Schaden 1.000 € -5.000 €, lokaler Einfluss)
|
||||
✅-3, hoch, (z.B. Schaden 5.000 € -15.000 €, Einfluss auf ein Team)
|
||||
✅-4, erheblich, (z.B. Schaden 50.000 € -100.000 €, regionaler Einfluss)
|
||||
✅-5, kritisch, (z.B. Schaden > 100.000 €, existenzbedrohend)
|
||||
|
||||
✅Maßnahmen
|
||||
✅-Titel
|
||||
✅-Status
|
||||
✅-Frist
|
||||
✅-Verantwortlicher
|
||||
✅-Beschreibung
|
||||
✅-Wiki-Link
|
||||
|
||||
✅Maßnahmenstatus
|
||||
✅-Geplant, Die Maßnahme wurde identifiziert und im Risikoregister erfasst, die Umsetzung hat jedoch noch nicht begonnen. Dies ist der Ausgangsstatus für jede neue Maßnahme.
|
||||
✅-In Bearbeitung, Die Umsetzung der Maßnahme hat begonnen.
|
||||
✅-Abgeschlossen, Die Maßnahme wurde vollständig umgesetzt (Triggert Neubewertung durch Risikoeigner)
|
||||
✅-Überprüft, Die Wirksamkeit der abgeschlossenen Maßnahme wurde verifiziert und bestätigt.
|
||||
✅-Abgelehnt/Verworfen, Eine geplante Maßnahme wird nicht umgesetzt, weil sie entweder nicht mehr relevant ist, die Kosten zu hoch sind oder eine alternative, effektivere Maßnahme gefunden wurde. Dies muss gut dokumentiert und begründet werden.
|
||||
|
||||
✅Restrisiko
|
||||
✅-Risiko identifikation
|
||||
✅-Eintrittswahrscheinlichkeit nach Maßnahmen
|
||||
✅-Schadenshöhe nach Maßnahmen
|
||||
✅-Score nach Maßnahmen (Berechnet sich aus Eintrittswahrscheinlichkeit und Schadenshöhe)
|
||||
✅-Stufe nach Maßnahmen (Errechnet sich aus dem Score)
|
||||
|
||||
✅Schutzziele (CIA)
|
||||
✅-Verfügbarkeit
|
||||
✅-Integrität
|
||||
✅-Vertraulichkeit
|
||||
|
||||
✅Benutzer
|
||||
✅-Benutzer ist Risikoverantwortlicher
|
||||
✅-Benutzer ist Maßnahmenverantwortlicher
|
||||
|
||||
✅Audit
|
||||
✅-Logging
|
||||
✅-Audit-Trail
|
||||
|
||||
✅Vorfälle
|
||||
|
||||
Benachrichtigungen
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
25
risks/migrations/0009_risk_created_at_risk_updatet_at.py
Normal file
25
risks/migrations/0009_risk_created_at_risk_updatet_at.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-08 09:15
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0008_rename_send_notification_sent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='risk',
|
||||
name='created_at',
|
||||
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='risk',
|
||||
name='updatet_at',
|
||||
field=models.DateTimeField(auto_now=True),
|
||||
),
|
||||
]
|
39
risks/migrations/0010_alter_residualrisk_impact_and_more.py
Normal file
39
risks/migrations/0010_alter_residualrisk_impact_and_more.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-08 09:44
|
||||
|
||||
import multiselectfield.db.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0009_risk_created_at_risk_updatet_at'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='residualrisk',
|
||||
name='impact',
|
||||
field=models.IntegerField(choices=[('1', 'Low (< 1,000 € – minor operational impact)'), ('2', 'Medium (1,000–5,000 € – local impact)'), ('3', 'High (5,000–15,000 € – team-level impact)'), ('4', 'Severe (50,000–100,000 € – regional impact)'), ('5', 'Critical (> 100,000 € – existential threat)')], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='residualrisk',
|
||||
name='likelihood',
|
||||
field=models.IntegerField(choices=[('1', 'Very low – occurs less than once every 5 years'), ('2', 'Low – once every 1–5 years'), ('3', 'Likely – once per year or more'), ('4', 'Very likely – multiple times per year/monthly')], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='risk',
|
||||
name='cia',
|
||||
field=multiselectfield.db.fields.MultiSelectField(blank=True, choices=[('1', 'Confidentiality'), ('2', 'Integrity'), ('3', 'Availability')], max_length=100, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='risk',
|
||||
name='impact',
|
||||
field=models.IntegerField(choices=[('1', 'Low (< 1,000 € – minor operational impact)'), ('2', 'Medium (1,000–5,000 € – local impact)'), ('3', 'High (5,000–15,000 € – team-level impact)'), ('4', 'Severe (50,000–100,000 € – regional impact)'), ('5', 'Critical (> 100,000 € – existential threat)')], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='risk',
|
||||
name='likelihood',
|
||||
field=models.IntegerField(choices=[('1', 'Very low – occurs less than once every 5 years'), ('2', 'Low – once every 1–5 years'), ('3', 'Likely – once per year or more'), ('4', 'Very likely – multiple times per year/monthly')], default=1),
|
||||
),
|
||||
]
|
18
risks/migrations/0011_risk_description.py
Normal file
18
risks/migrations/0011_risk_description.py
Normal file
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-08 09:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0010_alter_residualrisk_impact_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='risk',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, max_length=225, null=True),
|
||||
),
|
||||
]
|
33
risks/migrations/0012_alter_residualrisk_impact_and_more.py
Normal file
33
risks/migrations/0012_alter_residualrisk_impact_and_more.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 5.2.6 on 2025-09-08 09:53
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('risks', '0011_risk_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='residualrisk',
|
||||
name='impact',
|
||||
field=models.IntegerField(choices=[(1, 'Low (< 1,000 € – minor operational impact)'), (2, 'Medium (1,000–5,000 € – local impact)'), (3, 'High (5,000–15,000 € – team-level impact)'), (4, 'Severe (50,000–100,000 € – regional impact)'), (5, 'Critical (> 100,000 € – existential threat)')], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='residualrisk',
|
||||
name='likelihood',
|
||||
field=models.IntegerField(choices=[(1, 'Very low – occurs less than once every 5 years'), (2, 'Low – once every 1–5 years'), (3, 'Likely – once per year or more'), (4, 'Very likely – multiple times per year/monthly')], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='risk',
|
||||
name='impact',
|
||||
field=models.IntegerField(choices=[(1, 'Low (< 1,000 € – minor operational impact)'), (2, 'Medium (1,000–5,000 € – local impact)'), (3, 'High (5,000–15,000 € – team-level impact)'), (4, 'Severe (50,000–100,000 € – regional impact)'), (5, 'Critical (> 100,000 € – existential threat)')], default=1),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='risk',
|
||||
name='likelihood',
|
||||
field=models.IntegerField(choices=[(1, 'Very low – occurs less than once every 5 years'), (2, 'Low – once every 1–5 years'), (3, 'Likely – once per year or more'), (4, 'Very likely – multiple times per year/monthly')], default=1),
|
||||
),
|
||||
]
|
|
@ -40,16 +40,19 @@ class Risk(models.Model):
|
|||
]
|
||||
|
||||
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,)
|
||||
updatet_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
# CIA Protection Goals
|
||||
cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True)
|
||||
|
|
|
@ -43,6 +43,8 @@ class RiskSerializer(serializers.ModelSerializer):
|
|||
"asset",
|
||||
"process",
|
||||
"category",
|
||||
"created_at",
|
||||
"updatet_at",
|
||||
"likelihood",
|
||||
"impact",
|
||||
"score",
|
||||
|
|
|
@ -1,41 +1,50 @@
|
|||
from datetime import date, datetime
|
||||
from django.db.models import Model
|
||||
from django.db.models.signals import post_save, post_delete, m2m_changed
|
||||
from django.dispatch import receiver
|
||||
from .models import Control, Risk, ResidualRisk, AuditLog, Incident
|
||||
from .utils import model_diff
|
||||
|
||||
@receiver(post_save, sender=Control)
|
||||
def update_residual_risk_on_control_change(sender, instance, **kwargs):
|
||||
"""
|
||||
Whenever a control is saved, check if the related risk has a residual risk.
|
||||
If a control is completed or verified, flag the residual risk for review.
|
||||
"""
|
||||
|
||||
risk = instance.risk
|
||||
|
||||
# Ensure residual risk exists
|
||||
residual, created = ResidualRisk.objects.get_or_create(risk=risk)
|
||||
|
||||
# If a control is marked as completed or verified, we mark residual risk for review
|
||||
if instance.status in ["completed", "verified"]:
|
||||
residual.review_required = True
|
||||
residual.save()
|
||||
# ---------------------------------------------------------------------------
|
||||
# General definitions
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def serialize_value(value):
|
||||
if isinstance(value, Model):
|
||||
return value.pk # oder str(value), wenn du mehr Infos willst
|
||||
if isinstance(value, (datetime, date)):
|
||||
return value.isoformat()
|
||||
return value
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Risks
|
||||
# ---------------------------------------------------------------------------
|
||||
@receiver(post_save, sender=Risk)
|
||||
def log_risk_save(sender, instance, created, **kwargs):
|
||||
|
||||
|
||||
if created:
|
||||
AuditLog.objects.create(
|
||||
user=getattr(instance, "_changed_by", None),
|
||||
action="create",
|
||||
model="Risk",
|
||||
object_id=instance.pk,
|
||||
changes={f.name: {"old": None, "new": getattr(instance, f.name)} for f in instance._meta.fields},
|
||||
changes={
|
||||
f.name: {
|
||||
"old": None,
|
||||
"new": serialize_value(getattr(instance, f.name))
|
||||
} for f in instance._meta.fields
|
||||
},
|
||||
)
|
||||
else:
|
||||
old = Risk.objects.get(pk=instance.pk)
|
||||
changes = model_diff(old, instance)
|
||||
|
||||
if changes:
|
||||
clean_changes = {
|
||||
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
|
||||
for field, vals in changes.items()
|
||||
}
|
||||
AuditLog.objects.create(
|
||||
user=getattr(instance, "_changed_by", None),
|
||||
action="update",
|
||||
|
@ -57,6 +66,9 @@ def log_risk_delete(sender, instance, **kwargs):
|
|||
changes=None, # no fields to track on deletion
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Controls
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@receiver(post_save, sender=Control)
|
||||
def log_control_save(sender, instance, created, **kwargs):
|
||||
|
@ -66,13 +78,22 @@ def log_control_save(sender, instance, created, **kwargs):
|
|||
action="create",
|
||||
model="Control",
|
||||
object_id=instance.pk,
|
||||
changes={f.name: {"old": None, "new": getattr(instance, f.name)} for f in instance._meta.fields},
|
||||
changes={
|
||||
f.name: {
|
||||
"old": None,
|
||||
"new": serialize_value(getattr(instance, f.name))
|
||||
} for f in instance._meta.fields
|
||||
},
|
||||
)
|
||||
else:
|
||||
old = Control.objects.get(pk=instance.pk)
|
||||
changes = model_diff(old, instance)
|
||||
|
||||
if changes:
|
||||
clean_changes = {
|
||||
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
|
||||
for field, vals in changes.items()
|
||||
}
|
||||
AuditLog.objects.create(
|
||||
user=getattr(instance, "_changed_by", None),
|
||||
action="update",
|
||||
|
@ -91,6 +112,26 @@ def log_control_delete(sender, instance, **kwargs):
|
|||
changes=None,
|
||||
)
|
||||
|
||||
@receiver(post_save, sender=Control)
|
||||
def update_residual_risk_on_control_change(sender, instance, **kwargs):
|
||||
"""
|
||||
Whenever a control is saved, check if the related risk has a residual risk.
|
||||
If a control is completed or verified, flag the residual risk for review.
|
||||
"""
|
||||
|
||||
risk = instance.risk
|
||||
|
||||
# Ensure residual risk exists
|
||||
residual, created = ResidualRisk.objects.get_or_create(risk=risk)
|
||||
|
||||
# If a control is marked as completed or verified, we mark residual risk for review
|
||||
if instance.status in ["completed", "verified"]:
|
||||
residual.review_required = True
|
||||
residual.save()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Residual risks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@receiver(post_save, sender=ResidualRisk)
|
||||
def log_residual_save(sender, instance, created, **kwargs):
|
||||
|
@ -100,19 +141,28 @@ def log_residual_save(sender, instance, created, **kwargs):
|
|||
action="create",
|
||||
model="ResidualRisk",
|
||||
object_id=instance.pk,
|
||||
changes={f.name: {"old": None, "new": getattr(instance, f.name)} for f in instance._meta.fields},
|
||||
changes={
|
||||
f.name: {
|
||||
"old": None,
|
||||
"new": serialize_value(getattr(instance, f.name))
|
||||
} for f in instance._meta.fields
|
||||
},
|
||||
)
|
||||
else:
|
||||
old = ResidualRisk.objects.get(pk=instance.pk)
|
||||
changes = model_diff(old, instance)
|
||||
|
||||
if changes:
|
||||
clean_changes = {
|
||||
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
|
||||
for field, vals in changes.items()
|
||||
}
|
||||
AuditLog.objects.create(
|
||||
user=getattr(instance, "_changed_by", None),
|
||||
action="update",
|
||||
model="ResidualRisk",
|
||||
object_id=instance.pk,
|
||||
changes=changes,
|
||||
changes=clean_changes,
|
||||
)
|
||||
|
||||
@receiver(post_delete, sender=ResidualRisk)
|
||||
|
@ -125,6 +175,9 @@ def log_residual_delete(sender, instance, **kwargs):
|
|||
changes=None,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Incidents
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@receiver(post_save, sender=Incident)
|
||||
def log_incident_save(sender, instance, created, **kwargs):
|
||||
|
@ -141,6 +194,10 @@ def log_incident_save(sender, instance, created, **kwargs):
|
|||
changes = model_diff(old, instance)
|
||||
|
||||
if changes:
|
||||
clean_changes = {
|
||||
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
|
||||
for field, vals in changes.items()
|
||||
}
|
||||
AuditLog.objects.create(
|
||||
user=getattr(instance, "_changed_by", None),
|
||||
action="update",
|
||||
|
|
0
risks/templatetags/___init__.py
Normal file
0
risks/templatetags/___init__.py
Normal file
9
risks/templatetags/risk_extras.py
Normal file
9
risks/templatetags/risk_extras.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django import template
|
||||
from ..models import Risk
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter
|
||||
def cia_label(value):
|
||||
mapping = dict(Risk.CIA_CHOICES)
|
||||
return mapping.get(value, value)
|
|
@ -7,7 +7,10 @@ urlpatterns = [
|
|||
path("", views.dashboard, name="dashboard"),
|
||||
path("risks/index", views.dashboard, name="index"),
|
||||
path("risks/stats", views.stats, name="statistics"),
|
||||
path("risks/risks", views.risks, name="risks"),
|
||||
path("risks/controls", views.controls, name="controls"),
|
||||
path("risks/incidents", views.incidents, name="incidents"),
|
||||
path("risks/list_risks", views.list_risks, name="list_risks"),
|
||||
path("risks/risks/<int:id>", views.show_risk, name="show_risk"),
|
||||
path("risks/list_controls", views.list_controls, name="list_controls"),
|
||||
path("risks/controls/<int:id>", views.show_control, name="show_control"),
|
||||
path("risks/list_incidents", views.list_incidents, name="list_incidents"),
|
||||
path("risks/incidents/<int:id>", views.show_incident, name="show_incident"),
|
||||
]
|
|
@ -1,10 +1,14 @@
|
|||
from django.contrib.admin.models import LogEntry
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from .models import Risk, Control, ResidualRisk, AuditLog, Incident
|
||||
from .serializers import ControlSerializer, RiskSerializer, ResidualRiskSerializer, UserSerializer, AuditSerializer, IncidentSerializer
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
@ -51,8 +55,6 @@ class ResidualRiskViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = ResidualRiskSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
API endpoint for listing users and their responsibilities.
|
||||
|
@ -107,11 +109,85 @@ def dashboard(request):
|
|||
def stats(request):
|
||||
return render(request, "risks/statistics.html")
|
||||
|
||||
def risks(request):
|
||||
return render(request, "risks/list_risks.html")
|
||||
def list_risks(request):
|
||||
qs = Risk.objects.all().select_related("owner")
|
||||
|
||||
def controls(request):
|
||||
return render(request, "risks/list_controls.html")
|
||||
# GET-Parameter lesen
|
||||
risk_id = request.GET.get("risk")
|
||||
control_id = request.GET.get("control")
|
||||
owner_id = request.GET.get("owner")
|
||||
|
||||
def incidents(request):
|
||||
if risk_id:
|
||||
qs = qs.filter(id=risk_id)
|
||||
if control_id:
|
||||
qs = qs.filter(controls__id=control_id)
|
||||
if owner_id:
|
||||
qs = qs.filter(owner_id=owner_id)
|
||||
|
||||
risks = qs.order_by("title").distinct()
|
||||
|
||||
controls = Control.objects.all().order_by("title")
|
||||
owners = User.objects.filter(owned_risks__isnull=False).distinct().order_by("username")
|
||||
|
||||
return render(request, "risks/list_risks.html", {
|
||||
"risks": risks,
|
||||
"controls": controls,
|
||||
"owners": owners,
|
||||
})
|
||||
|
||||
def show_risk(request, id):
|
||||
risk = get_object_or_404(Risk, pk=id)
|
||||
ct = ContentType.objects.get_for_model(Risk)
|
||||
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})
|
||||
|
||||
def list_controls(request):
|
||||
qs = Control.objects.all().select_related("risk", "responsible")
|
||||
|
||||
# Filter
|
||||
control_id = request.GET.get("control")
|
||||
risk_id = request.GET.get("risk")
|
||||
status = request.GET.get("status")
|
||||
responsible_id = request.GET.get("responsible")
|
||||
|
||||
if control_id:
|
||||
qs = qs.filter(id=control_id)
|
||||
if risk_id:
|
||||
qs = qs.filter(risk_id=risk_id)
|
||||
if status:
|
||||
qs = qs.filter(status=status)
|
||||
if responsible_id:
|
||||
qs = qs.filter(responsible_id=responsible_id)
|
||||
|
||||
controls = qs.order_by("title")
|
||||
|
||||
risks = Risk.objects.all().order_by("title")
|
||||
users = User.objects.filter(responsible_controls__isnull=False).distinct().order_by("username")
|
||||
|
||||
return render(request, "risks/list_controls.html", {
|
||||
"controls": controls,
|
||||
"risks": risks,
|
||||
"users": users,
|
||||
"status_choices": Control.STATUS_CHOICES,
|
||||
})
|
||||
|
||||
def show_control(request, id):
|
||||
control = get_object_or_404(Control, pk=id)
|
||||
ct = ContentType.objects.get_for_model(Control)
|
||||
logs = LogEntry.objects.filter(
|
||||
content_type=ct,
|
||||
object_id=control.pk
|
||||
).order_by("-action_time")
|
||||
|
||||
return render(request, "risks/item_control.html", {"control": control, "logs": logs})
|
||||
|
||||
def list_incidents(request):
|
||||
return render(request, "risks/list_incidents.html")
|
||||
|
||||
def show_incident(request, id):
|
||||
incident = Incident.objects.get(pk=id)
|
||||
return render(request, "risks/item_incident.html", {"incident": incident })
|
|
@ -43,3 +43,19 @@
|
|||
|
||||
/* Dropdown optisch näher am Screenshot */
|
||||
.navbar-dropdown { border-top: none; box-shadow: 0 8px 16px rgba(0,0,0,.1); }
|
||||
|
||||
/* Breadcrumbs */
|
||||
.top-breadcrumb { padding: 10px 0;}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 20px;
|
||||
background-color: #f0ebeb;
|
||||
}
|
||||
|
||||
.content li{
|
||||
margin-top: 5px !important;
|
||||
}
|
||||
|
||||
.content li+li {
|
||||
margin: 0 !important;
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
<nav class="navbar topbar-nav" role="navigation" aria-label="main navigation">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item logo" href="/risks/index">
|
||||
<strong class="logo-text">R</strong>
|
||||
<strong class="logo-text">RM</strong>
|
||||
</a>
|
||||
|
||||
<!-- Burger Menu für Mobile -->
|
||||
|
@ -38,17 +38,19 @@
|
|||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">Risikomanagement</a>
|
||||
<div class="navbar-dropdown">
|
||||
<a class="navbar-item" href="/risks/risks">Risikoanalyse</a>
|
||||
<a class="navbar-item" href="/risks/controls">Maßnahmen</a>
|
||||
<a class="navbar-item" href="/risks/incidents">Vorfälle</a>
|
||||
<a class="navbar-item" href="/risks/list_risks">Risikoanalyse</a>
|
||||
<a class="navbar-item" href="/risks/list_controls">Maßnahmen</a>
|
||||
<a class="navbar-item" href="/risks/list_incidents">Vorfälle</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
@ -57,13 +59,22 @@
|
|||
</header>
|
||||
|
||||
<main class="content">
|
||||
<div class="home-icon">
|
||||
<a href="{% url 'risks:index' %}">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-home" aria-hidden="true" style="color: #6b2bbd"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% block breadcrumbs %}
|
||||
<nav class="breadcrumb top-breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/risks/index">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-home" aria-hidden="true" style="color: #6b2bbd"></i>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% block crumbs %}
|
||||
{% if title %} › {{ title }}{% endif %}
|
||||
{% endblock %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
|
|
125
templates/risks/item_control.html
Normal file
125
templates/risks/item_control.html
Normal file
|
@ -0,0 +1,125 @@
|
|||
{% extends "base.html" %}
|
||||
{% block crumbs %}
|
||||
<li><a href="{% url 'risks:list_controls' %}">Maßnahmen</a></li>
|
||||
<li><a href="{% url 'risks:show_control' control.id %}">{{ control.title }}</a></li>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<section class="hero is-small">
|
||||
<div class="hero-body">
|
||||
<p class="title">Maßnahme: {{ control.title }}</p>
|
||||
<p class="subtitle is-6">{{ control.description }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Überblick-->
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">Überblick</p>
|
||||
</header>
|
||||
<!-- Inhalt Überblick-->
|
||||
<div class="card-content">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-half">
|
||||
<p>
|
||||
<strong>Verknüpfte Risiken:</strong>
|
||||
</p>
|
||||
<p><strong><a>Zum Wiki Eintrag</a></strong></p>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<p><strong>Verantwortliche/r:</strong> {{ control.owner|default:"-" }}</p>
|
||||
<p><strong>Erstellt am:</strong> {{ control.created_at|date:'d.m.Y H:i' }}</p>
|
||||
<p><strong>Aktualisiert am:</strong> {{ control.updatet_at|date:'d.m.Y H:i' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- Ende Inhalt Überblick -->
|
||||
</div> <!-- Ende Überblick -->
|
||||
|
||||
<!-- Maßnahmen -->
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">Maßnahmen</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
{% if control.controls.all %}
|
||||
<table class="table is-striped is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titel</th>
|
||||
<th>Status</th>
|
||||
<th>Frist</th>
|
||||
<th>Verantwortlicher</th>
|
||||
<th>Link</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for control in control.controls.all %}
|
||||
<tr onclick="window.location.href='/risks/controls/{{ control.id }}';" style="cursor:pointer;">
|
||||
<td>{{ control.title }}</td>
|
||||
<td>{{ control.get_status_display }}</td>
|
||||
<td>
|
||||
{% if control.due_date %}
|
||||
{{ control.due_date|date:"d.m.Y" }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if control.responsible %}
|
||||
{{ control.responsible.get_full_name|default:control.responsible.username }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if control.wiki_link %}
|
||||
<a href="{{ control.wiki_link }}" target="_blank">🔗</a>
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="has-text-grey">Keine Maßnahmen erfasst.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Ende Maßnahmen -->
|
||||
|
||||
<!-- Historie -->
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">Historie</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
{% if logs %}
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zeitpunkt</th>
|
||||
<th>Benutzer</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
<tr>
|
||||
<td>{{ log.action_time|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ log.user.get_full_name|default:log.user.username }}</td>
|
||||
<td>{{ log.get_change_message }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="has-text-grey">Keine Historie vorhanden.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div> <!-- Ende Historie -->
|
||||
|
||||
<br><br>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
8
templates/risks/item_incident.html
Normal file
8
templates/risks/item_incident.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
{% extends "base.html" %}
|
||||
{% block crumbs %}
|
||||
<li><a href="{% url 'risks:list_incidents' %}">Vorfälle</a></li>
|
||||
<li><a href="{% url 'risks:show_incident' incident.id %}">{{ incident.title }}</a></li>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{% endblock %}
|
224
templates/risks/item_risk.html
Normal file
224
templates/risks/item_risk.html
Normal file
|
@ -0,0 +1,224 @@
|
|||
{% extends "base.html" %}
|
||||
{% load 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>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<section class="hero is-small">
|
||||
<div class="hero-body">
|
||||
<p class="title">Risiko: {{ risk.title }}</p>
|
||||
<p class="subtitle is-6">{{ risk.description }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Überblick-->
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">Überblick</p>
|
||||
</header>
|
||||
<!-- Inhalt Überblick-->
|
||||
<div class="card-content">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-half">
|
||||
<p><strong>Asset:</strong> {{ risk.asset|default:"-" }}</p>
|
||||
<p><strong>Prozess:</strong> {{ risk.process|default:"-" }}</p>
|
||||
<p>
|
||||
<strong>Schutzziele:</strong>
|
||||
{% if risk.cia %}
|
||||
<ul>
|
||||
{% for label in risk.cia %}
|
||||
<li>{{ label|cia_label }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>Noch nicht zugewiesen</p>
|
||||
{% endif %}
|
||||
</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>Erstellt am:</strong> {{ risk.created_at|date:'d.m.Y H:i' }}</p>
|
||||
<p><strong>Aktualisiert am:</strong> {{ risk.updatet_at|date:'d.m.Y H:i' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Risikobewertung -->
|
||||
<h3>Risikobewertung</h3>
|
||||
<div class="columns is-multiline">
|
||||
|
||||
<!-- Bruttorisiko -->
|
||||
<div class="column is-half">
|
||||
<div class="box">
|
||||
<h4>Brutto (vor Maßnahmen)</h4>
|
||||
<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.get_likelihood_display }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="column is-half has-text-centered">
|
||||
<p class="heading">Schadensausmaß</p>
|
||||
<button class="button is-small is-danger">
|
||||
{{ risk.get_impact_display }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="column is-half has-text-centered">
|
||||
<p class="heading">Stufe</p>
|
||||
<button class="button is-small is-info">
|
||||
{{ risk.level }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="column is-half has-text-centered">
|
||||
<p class="heading">Score</p>
|
||||
<button class="button is-small is-danger">
|
||||
{{ risk.score }} / 25
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- Ende Bruttorisiko -->
|
||||
|
||||
<!-- Nettorisiko -->
|
||||
<div class="column is-half">
|
||||
<div class="box">
|
||||
<h4>Netto (nach Maßnahmen)</h4>
|
||||
|
||||
{% if risk.residualrisk %}
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="has-text-grey">Noch kein Nettorisiko erfasst.</p>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div> <!-- Ende Nettorisiko -->
|
||||
|
||||
</div> <!-- Ende Risikobewertung -->
|
||||
</div> <!-- Ende Inhalt Überblick -->
|
||||
</div> <!-- Ende Überblick -->
|
||||
|
||||
<!-- Maßnahmen -->
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">Maßnahmen</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
{% if risk.controls.all %}
|
||||
<table class="table is-striped is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Titel</th>
|
||||
<th>Status</th>
|
||||
<th>Frist</th>
|
||||
<th>Verantwortlicher</th>
|
||||
<th>Link</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for control in risk.controls.all %}
|
||||
<tr onclick="window.location.href='/risks/controls/{{ control.id }}';" style="cursor:pointer;">
|
||||
<td>{{ control.title }}</td>
|
||||
<td>{{ control.get_status_display }}</td>
|
||||
<td>
|
||||
{% if control.due_date %}
|
||||
{{ control.due_date|date:"d.m.Y" }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if control.responsible %}
|
||||
{{ control.responsible.get_full_name|default:control.responsible.username }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if control.wiki_link %}
|
||||
<a href="{{ control.wiki_link }}" target="_blank">🔗</a>
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="has-text-grey">Keine Maßnahmen erfasst.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Ende Maßnahmen -->
|
||||
|
||||
<!-- Historie -->
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">Historie</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
{% if logs %}
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zeitpunkt</th>
|
||||
<th>Benutzer</th>
|
||||
<th>Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
<tr>
|
||||
<td>{{ log.action_time|date:"d.m.Y H:i" }}</td>
|
||||
<td>{{ log.user.get_full_name|default:log.user.username }}</td>
|
||||
<td>{{ log.get_change_message }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="has-text-grey">Keine Historie vorhanden.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div> <!-- Ende Historie -->
|
||||
|
||||
<br><br>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,7 +1,154 @@
|
|||
{% extends "base.html" %}
|
||||
{% block crumbs %}
|
||||
<li><a href="{% url 'risks:controls' %}">Maßnahmen</a></li>
|
||||
<li><a href="{% url 'risks:list_controls' %}">Maßnahmen</a></li>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
Maßnahmen
|
||||
<!-- Filter -->
|
||||
<section class="section">
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Auswahl</h2>
|
||||
|
||||
<form method="get">
|
||||
<div class="columns is-multiline">
|
||||
|
||||
<!-- Maßnahmen -->
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Maßnahme</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="control" onchange="this.form.submit()">
|
||||
<option value="">Alle</option>
|
||||
{% for c in controls %}
|
||||
<option value="{{ c.id }}" {% if request.GET.control == c.id|stringformat:"s" %}selected{% endif %}>
|
||||
{{ c.title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Risiko -->
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Risiko</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="risk" onchange="this.form.submit()">
|
||||
<option value="">Alle</option>
|
||||
{% for r in risks %}
|
||||
<option value="{{ r.id }}" {% if request.GET.risk == r.id|stringformat:"s" %}selected{% endif %}>
|
||||
{{ r.title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Status</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="status" onchange="this.form.submit()">
|
||||
<option value="">Alle</option>
|
||||
{% for key,label in status_choices %}
|
||||
<option value="{{ key }}" {% if request.GET.status == key %}selected{% endif %}>
|
||||
{{ label }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Verantwortliche/r -->
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Verantwortliche/r</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="responsible" onchange="this.form.submit()">
|
||||
<option value="">Alle</option>
|
||||
{% for u in users %}
|
||||
<option value="{{ u.id }}" {% if request.GET.responsible == u.id|stringformat:"s" %}selected{% endif %}>
|
||||
{{ u.get_full_name|default:u.username }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h2 class="title is-5">Maßnahmen</h2>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Maßnahme</th>
|
||||
<th>Risiken</th>
|
||||
<th>Verantwortliche/r</th>
|
||||
<th>Status</th>
|
||||
<th>Frist</th>
|
||||
<th>Link</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in controls %}
|
||||
<tr onclick="window.location.href='{% url 'risks:show_control' c.id %}'" style="cursor:pointer;">
|
||||
<td>{{ c.title }}</td>
|
||||
<td>
|
||||
{% if c.risk %}
|
||||
<a href="{% url 'risks:show_risk' c.risk.id %}" onclick="event.stopPropagation();">
|
||||
{{ c.risk.title }}
|
||||
</a>
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.responsible %}
|
||||
{{ c.responsible.get_full_name|default:c.responsible.username }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ c.get_status_display }}</td>
|
||||
<td>
|
||||
{% if c.due_date %}
|
||||
{{ c.due_date|date:"d.m.Y" }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if c.wiki_link %}
|
||||
<a href="{{ c.wiki_link }}" target="_blank">🔗</a>
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="has-text-centered has-text-grey">Keine Maßnahmen gefunden</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
|
@ -1,7 +1,113 @@
|
|||
{% extends "base.html" %}
|
||||
{% block crumbs %}
|
||||
<li><a href="{% url 'risks:incidents' %}">Vorfälle</a></li>
|
||||
<li><a href="{% url 'risks:list_incidents' %}">Vorfälle</a></li>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
Vorfälle
|
||||
<!-- Filter -->
|
||||
<section class="section">
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Auswahl</h2>
|
||||
|
||||
<div class="columns is-multiline">
|
||||
|
||||
<!-- Vorfälle -->
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Vorfall</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select>
|
||||
<option>Alle</option>
|
||||
<option>Vorfall A</option>
|
||||
<option>Vorfall B</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Risiko -->
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Risiko</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select>
|
||||
<option>Alle</option>
|
||||
<option>Risiko 1</option>
|
||||
<option>Risiko 2</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status -->
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Status</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select>
|
||||
<option>All</option>
|
||||
<option>Opened</option>
|
||||
<option>In progress</option>
|
||||
<option>Closed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Melder -->
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Meldende Person</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select>
|
||||
<option>Alle</option>
|
||||
<option>Kevin Heyer</option>
|
||||
<option>Stefan Lange</option>
|
||||
<option>Kirsten Herzhoff</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="title is-5">Vorfälle</h2>
|
||||
|
||||
<div class="table-container">
|
||||
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Vorfall</th>
|
||||
<th>Zugehörige Risiken</th>
|
||||
<th>Status</th>
|
||||
<th>Gemeldet am</th>
|
||||
<th>Gemeldet von</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr onclick="window.location.href='/risks/incidents/1';" style="cursor:pointer;">
|
||||
<td>Switch entwendet</td>
|
||||
<td>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/risks/risks/1" onclick="event.stopPropagation();">Hardware Diebstahl</a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<td>Closed</td>
|
||||
<td>08.09.2025</td>
|
||||
<td>Kevin Heyer</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
|
@ -1,7 +1,125 @@
|
|||
{% extends "base.html" %}
|
||||
{% block crumbs %}
|
||||
<li><a href="{% url 'risks:risks' %}">Risiken</a></li>
|
||||
<li><a href="{% url 'risks:list_risks' %}">Risikoanalyse</a></li>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
Risiken
|
||||
<section class="section">
|
||||
<div class="box">
|
||||
<h2 class="title is-5">Auswahl</h2>
|
||||
|
||||
<!-- Filter -->
|
||||
<form method="get">
|
||||
<div class="columns is-multiline">
|
||||
|
||||
<!-- Risiko Filter -->
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Risiko</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="risk" onchange="this.form.submit()">
|
||||
<option value="">Alle</option>
|
||||
{% for r in risks %}
|
||||
<option value="{{ r.id }}" {% if request.GET.risk == r.id|stringformat:"s" %}selected{% endif %}>
|
||||
{{ r.title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Maßnahmen Filter -->
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Maßnahmen</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="control" onchange="this.form.submit()">
|
||||
<option value="">Alle</option>
|
||||
{% for c in controls %}
|
||||
<option value="{{ c.id }}" {% if request.GET.control == c.id|stringformat:"s" %}selected{% endif %}>
|
||||
{{ c.title }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Risikoeigner Filter -->
|
||||
<div class="column is-3">
|
||||
<div class="field">
|
||||
<label class="label">Risikoeigner</label>
|
||||
<div class="control">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="owner" onchange="this.form.submit()">
|
||||
<option value="">Alle</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 }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<h2 class="title is-5">Risiken</h2>
|
||||
<!-- Risiken -->
|
||||
<div class="table-container">
|
||||
<table class="table is-bordered is-striped is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in risks %}
|
||||
<tr onclick="window.location.href='{% url 'risks:show_risk' r.id %}'" style="cursor:pointer;">
|
||||
<td>{{ r.title }}</td>
|
||||
<td>
|
||||
{{ 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>
|
||||
{% if r.owner %}
|
||||
{{ r.owner.get_full_name|default:r.owner.username }}
|
||||
{% else %}
|
||||
–
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="8" class="has-text-centered has-text-grey">Keine Risiken vorhanden</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div> <!-- Ende Risiken -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
Loading…
Add table
Reference in a new issue