feat: Enhance risk management application with user auditing and improved incident handling

- Added AuditUserMiddleware to track the current user for auditing purposes.
- Introduced audit_context for managing the current user in thread-local storage.
- Updated Control and Incident models to include created_at and updated_at timestamps.
- Refactored Control and Incident serializers to handle related risks and timestamps.
- Modified views to set the _changed_by attribute for user actions.
- Enhanced incident listing and detail views to display related risks and user actions.
- Updated templates for better presentation of risks and incidents.
- Added migrations for new fields and relationships in the database.
- Improved filtering options in the incident list view.
This commit is contained in:
Kevin Heyer 2025-09-09 12:00:29 +02:00
parent 43e86d0357
commit 686030e4cb
25 changed files with 540 additions and 171 deletions

View file

@ -50,6 +50,7 @@ MIDDLEWARE = [
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"risks.middleware.AuditUserMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]

View file

@ -18,8 +18,7 @@ urlpatterns = [
path("api/ping/", ping), # Public healthcheck endpoint
path("api/secure-ping/", secure_ping), # Protected API endpoint
path("api/", include(router.urls)),
path("", include("risks.urls")),
path("risks/", include("risks.urls")),
path("", include("risks.urls", namespace="risks")),
]
# Add OIDC routes only if Single Sign-On is enabled

Binary file not shown.

View file

@ -19,13 +19,6 @@ class UserAdmin(BaseUserAdmin):
return obj.controls_responsible.count()
responsible_controls_count.short_description = "Controls Responsible"
class ControlInline(admin.TabularInline):
model = Control
extra = 1
fields = ("title", "status", "due_date", "responsible", "wiki_link")
autocomplete_fields = ("responsible",)
class ResidualRiskInline(admin.StackedInline):
"""
Inline editor for ResidualRisk, linked one-to-one with Risk
@ -36,6 +29,11 @@ class ResidualRiskInline(admin.StackedInline):
readonly_fields = ("score", "level", "review_required")
fields = ("likelihood", "impact", "score", "level", "review_required")
class ControlRisksInline(admin.TabularInline):
model = Control.risks.through
fk_name = "risk"
extra = 1
autocomplete_fields = ("control",)
@admin.register(Risk)
class RiskAdmin(admin.ModelAdmin):
@ -50,7 +48,7 @@ class RiskAdmin(admin.ModelAdmin):
)
list_filter = ("level", "likelihood", "impact", "owner")
search_fields = ("title", "asset", "process", "category")
inlines = [ControlInline, ResidualRiskInline]
inlines = [ResidualRiskInline, ControlRisksInline] # Controls hier verknüpfen
def save_model(self, request, obj, form, change):
obj._changed_by = request.user
@ -82,10 +80,10 @@ class ResidualRiskAdmin(admin.ModelAdmin):
@admin.register(Control)
class ControlAdmin(admin.ModelAdmin):
list_display = ("title", "status", "due_date", "responsible", "risk")
list_display = ("title", "status", "due_date", "responsible")
list_filter = ("status", "due_date")
autocomplete_fields = ("risks", "responsible",)
search_fields = ("title", "description")
autocomplete_fields = ("responsible", "risk")
def save_model(self, request, obj, form, change):
obj._changed_by = request.user

8
risks/audit_context.py Normal file
View file

@ -0,0 +1,8 @@
import threading
_local = threading.local()
def set_current_user(user):
_local.user = user
def get_current_user():
return getattr(_local, "user", None)

9
risks/middleware.py Normal file
View file

@ -0,0 +1,9 @@
from .audit_context import set_current_user
class AuditUserMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
set_current_user(getattr(request, "user", None))
return self.get_response(request)

View file

@ -0,0 +1,52 @@
# Generated by Django 5.2.6 on 2025-09-09 07:00
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("risks", "0012_alter_residualrisk_impact_and_more"),
]
operations = [
migrations.AddField(
model_name="control",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="control",
name="updatet_at",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="incident",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="incident",
name="updatet_at",
field=models.DateTimeField(auto_now=True),
),
migrations.AddField(
model_name="residualrisk",
name="created_at",
field=models.DateTimeField(
auto_now_add=True, default=django.utils.timezone.now
),
preserve_default=False,
),
migrations.AddField(
model_name="residualrisk",
name="updatet_at",
field=models.DateTimeField(auto_now=True),
),
]

View file

@ -0,0 +1,21 @@
# Generated by Django 5.2.6 on 2025-09-09 07:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("risks", "0013_control_created_at_control_updatet_at_and_more"),
]
operations = [
migrations.RemoveField(
model_name="control",
name="risk",
),
migrations.AddField(
model_name="control",
name="risks",
field=models.ManyToManyField(related_name="controls", to="risks.risk"),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 5.2.6 on 2025-09-09 08:37
import risks.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("risks", "0014_remove_control_risk_control_risks"),
]
operations = [
migrations.AlterField(
model_name="auditlog",
name="changes",
field=models.JSONField(
blank=True, encoder=risks.models.SafeJSONEncoder, null=True
),
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 5.2.6 on 2025-09-09 09:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("risks", "0015_alter_auditlog_changes"),
]
operations = [
migrations.RenameField(
model_name="control",
old_name="updatet_at",
new_name="updated_at",
),
migrations.RenameField(
model_name="incident",
old_name="updatet_at",
new_name="updated_at",
),
migrations.RenameField(
model_name="residualrisk",
old_name="updatet_at",
new_name="updated_at",
),
migrations.RenameField(
model_name="risk",
old_name="updatet_at",
new_name="updated_at",
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 5.2.6 on 2025-09-09 09:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("risks", "0016_rename_updatet_at_control_updated_at_and_more"),
]
operations = [
migrations.AlterField(
model_name="incident",
name="status",
field=models.CharField(
choices=[
("open", "Opened"),
("in_progress", "In Progress"),
("closed", "Closed"),
],
max_length=12,
),
),
]

View file

@ -1,7 +1,16 @@
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 multiselectfield import MultiSelectField
import datetime
import json
class SafeJSONEncoder(DjangoJSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.date):
return obj.isoformat()
return super().default(obj)
class User(AbstractUser):
"""
@ -52,7 +61,7 @@ class Risk(models.Model):
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)
updated_at = models.DateTimeField(auto_now=True)
# CIA Protection Goals
cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True)
@ -127,12 +136,15 @@ class ResidualRisk(models.Model):
review_required = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True,)
updated_at = models.DateTimeField(auto_now=True)
def save(self, *args, **kwargs):
# Load previous state (if it exists)
if self.pk:
old = ResidualRisk.objects.get(pk=self.pk)
if old.likelihood != self.likelihood or old.impact != self.impact:
self.review_required = False
self.review_required = True
self.score = self.likelihood * self.impact
@ -174,9 +186,11 @@ class Control(models.Model):
)
description = models.TextField(blank=True, null=True)
wiki_link = models.URLField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True,)
updated_at = models.DateTimeField(auto_now=True)
# Relation to risk
risk = models.ForeignKey(Risk, on_delete=models.CASCADE, related_name="controls")
risks = models.ManyToManyField("Risk", related_name="controls")
def __str__(self):
return f"{self.title} ({self.get_status_display()})"
@ -201,7 +215,7 @@ class AuditLog(models.Model):
action = models.CharField(max_length=10, choices=ACTION_CHOICES)
model = models.CharField(max_length=100)
object_id = models.CharField(max_length=50)
changes = models.JSONField(null=True, blank=True)
changes = models.JSONField(null=True, blank=True, encoder=SafeJSONEncoder)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
@ -214,7 +228,7 @@ class Incident(models.Model):
STATUS_CHOICES = [
("open", "Opened"),
("in_progress", "In Progress"),
("close", "Closed"),
("closed", "Closed"),
]
title = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
@ -222,6 +236,8 @@ class Incident(models.Model):
reported_by = models.ForeignKey(settings.AUTH_USER_MODEL, 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,)
updated_at = models.DateTimeField(auto_now=True)
class Notification(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="notifications")

View file

@ -18,17 +18,21 @@ class ResidualRiskSerializer(serializers.ModelSerializer):
class ControlSerializer(serializers.ModelSerializer):
risks = serializers.PrimaryKeyRelatedField(many=True, queryset=Risk.objects.all())
class Meta:
model = Control
fields = [
"id",
"title",
"status",
"created_at",
"updated_at",
"due_date",
"responsible",
"description",
"wiki_link",
"risk",
"risks",
]
class RiskSerializer(serializers.ModelSerializer):
@ -44,16 +48,14 @@ class RiskSerializer(serializers.ModelSerializer):
"process",
"category",
"created_at",
"updatet_at",
"updated_at",
"likelihood",
"impact",
"score",
"level",
"owner",
"follow_up",
"confidentiality",
"integrity",
"availability",
"cia",
"controls",
]
@ -93,15 +95,30 @@ class RiskSummarySerializer(serializers.ModelSerializer):
fields = ["id", "title", "score", "level"]
class IncidentSerializer(serializers.ModelSerializer):
related_risks = RiskSummarySerializer(many=True, read_only=True)
related_risks = serializers.PrimaryKeyRelatedField(
many=True, queryset=Risk.objects.all()
)
date_reported = serializers.DateField(format="%Y-%m-%d", required=False)
created_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
updated_at = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", read_only=True)
class Meta:
model = Incident
fields = [
"id",
"title",
"description",
"date_reported",
"status",
"related_risks",
]
"id", "title", "description", "date_reported",
"created_at", "updated_at", "status", "related_risks",
]
def create(self, validated_data):
risks = validated_data.pop("related_risks", [])
obj = super().create(validated_data)
if risks:
obj.related_risks.set(risks)
return obj
def update(self, instance, validated_data):
risks = validated_data.pop("related_risks", None)
obj = super().update(instance, validated_data)
if risks is not None:
obj.related_risks.set(risks)
return obj

View file

@ -2,6 +2,7 @@ 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 .audit_context import get_current_user
from .models import Control, Risk, ResidualRisk, AuditLog, Incident
from .utils import model_diff
@ -21,8 +22,6 @@ def serialize_value(value):
# ---------------------------------------------------------------------------
@receiver(post_save, sender=Risk)
def log_risk_save(sender, instance, created, **kwargs):
if created:
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
@ -39,7 +38,6 @@ def log_risk_save(sender, instance, created, **kwargs):
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"])}
@ -50,7 +48,7 @@ def log_risk_save(sender, instance, created, **kwargs):
action="update",
model="Risk",
object_id=instance.pk,
changes=changes,
changes=clean_changes,
)
@receiver(post_delete, sender=Risk)
@ -58,8 +56,9 @@ def log_risk_delete(sender, instance, **kwargs):
"""
Signal that runs after a Risk is deleted.
"""
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
user=user,
action="delete",
model="Risk",
object_id=instance.pk,
@ -88,7 +87,6 @@ def log_control_save(sender, instance, created, **kwargs):
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"])}
@ -99,35 +97,39 @@ def log_control_save(sender, instance, created, **kwargs):
action="update",
model="Control",
object_id=instance.pk,
changes=changes,
changes=clean_changes,
)
@receiver(post_delete, sender=Control)
def log_control_delete(sender, instance, **kwargs):
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
user=user,
action="delete",
model="Control",
object_id=instance.pk,
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.
"""
@receiver(m2m_changed, sender=Control.risks.through)
def control_risks_changed(sender, instance, action, reverse, pk_set, **kwargs):
if action in {"post_add", "post_remove", "post_clear"}:
if action == "post_clear":
affected_risks = instance.risks.all()
elif pk_set:
if reverse:
from .models import Risk
affected_risks = Risk.objects.filter(pk__in=pk_set)
else:
affected_risks = Risk.objects.filter(pk__in=pk_set)
else:
affected_risks = instance.risks.all()
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()
from .models import ResidualRisk
for risk in affected_risks:
residual, _ = ResidualRisk.objects.get_or_create(risk=risk)
residual.review_required = True
residual.save()
# ---------------------------------------------------------------------------
# Residual risks
@ -151,7 +153,6 @@ def log_residual_save(sender, instance, created, **kwargs):
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"])}
@ -167,8 +168,9 @@ def log_residual_save(sender, instance, created, **kwargs):
@receiver(post_delete, sender=ResidualRisk)
def log_residual_delete(sender, instance, **kwargs):
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
user=user,
action="delete",
model="ResidualRisk",
object_id=instance.pk,
@ -187,12 +189,16 @@ def log_incident_save(sender, instance, created, **kwargs):
action="create",
model="Incident",
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 = Incident.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"])}
@ -203,14 +209,15 @@ def log_incident_save(sender, instance, created, **kwargs):
action="update",
model="Incident",
object_id=instance.pk,
changes=changes,
changes=clean_changes,
)
@receiver(m2m_changed, sender=Incident.related_risks.through)
def log_incident_risks_change(sender, instance, action, reverse, model, pk_set, **kwargs):
if action in ["post_add", "post_remove", "post_clear"]:
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
user=user,
action="update",
model="Incident",
object_id=instance.pk,
@ -219,8 +226,9 @@ def log_incident_risks_change(sender, instance, action, reverse, model, pk_set,
@receiver(post_delete, sender=Incident)
def log_incident_delete(sender, instance, **kwargs):
user = getattr(instance, "_changed_by", None) or get_current_user()
AuditLog.objects.create(
user=getattr(instance, "_changed_by", None),
user=user,
action="delete",
model="Incident",
object_id=instance.pk,

View file

@ -24,12 +24,10 @@ class RiskViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
instance = serializer.save()
instance._changed_by = self.request.user
instance.save()
def perform_update(self, serializer):
instance = serializer.save()
instance._changed_by = self.request.user
instance.save()
class ControlViewSet(viewsets.ModelViewSet):
"""
@ -43,12 +41,10 @@ class ControlViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
instance = serializer.save()
instance._changed_by = self.request.user
instance.save()
def perform_update(self, serializer):
instance = serializer.save()
instance._changed_by = self.request.user
instance.save()
class ResidualRiskViewSet(viewsets.ModelViewSet):
queryset = ResidualRisk.objects.all()
@ -66,12 +62,10 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
def perform_create(self, serializer):
instance = serializer.save()
instance._changed_by = self.request.user
instance.save()
def perform_update(self, serializer):
instance = serializer.save()
instance._changed_by = self.request.user
instance.save()
class AuditViewSet(viewsets.ReadOnlyModelViewSet):
"""
@ -92,12 +86,10 @@ class IncidentViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
instance = serializer.save(reported_by=self.request.user)
instance._changed_by = self.request.user
instance.save()
def perform_update(self, serializer):
instance = serializer.save()
instance._changed_by = self.request.user
instance.save()
# ---------------------------------------------------------------------------
# Web
@ -146,9 +138,8 @@ def show_risk(request, id):
return render(request, "risks/item_risk.html", {"risk": risk, "logs": logs})
def list_controls(request):
qs = Control.objects.all().select_related("risk", "responsible")
qs = Control.objects.all().select_related("responsible")
# Filter
control_id = request.GET.get("control")
risk_id = request.GET.get("risk")
status = request.GET.get("status")
@ -157,13 +148,13 @@ def list_controls(request):
if control_id:
qs = qs.filter(id=control_id)
if risk_id:
qs = qs.filter(risk_id=risk_id)
qs = qs.filter(risks__id=risk_id) # FIX
if status:
qs = qs.filter(status=status)
if responsible_id:
qs = qs.filter(responsible_id=responsible_id)
controls = qs.order_by("title")
controls = qs.order_by("title").distinct()
risks = Risk.objects.all().order_by("title")
users = User.objects.filter(responsible_controls__isnull=False).distinct().order_by("username")
@ -185,9 +176,39 @@ def show_control(request, id):
return render(request, "risks/item_control.html", {"control": control, "logs": logs})
def list_incidents(request):
return render(request, "risks/list_incidents.html")
qs = Incident.objects.all().select_related("reported_by").prefetch_related("related_risks")
risk_id = request.GET.get("risk")
status = request.GET.get("status")
reported_by = request.GET.get("reported_by")
if risk_id:
qs = qs.filter(related_risks__id=risk_id) # FIX
if status:
qs = qs.filter(status=status)
if reported_by:
qs = qs.filter(reported_by=reported_by)
incidents = qs.order_by("title").distinct()
risks = Risk.objects.all().order_by("title")
users = User.objects.filter(incidents__isnull=False).distinct().order_by("username") # sinnvoller
return render(request, "risks/list_incidents.html", {
"incidents": incidents,
"risks": risks,
"users": users,
"status_choices": Incident.STATUS_CHOICES,
})
def show_incident(request, id):
incident = Incident.objects.get(pk=id)
return render(request, "risks/item_incident.html", {"incident": incident })
incident = get_object_or_404(Incident, pk=id)
ct = ContentType.objects.get_for_model(Incident)
logs = LogEntry.objects.filter(
content_type=ct,
object_id=incident.pk
).order_by("-action_time")
return render(request, "risks/item_incident.html", {"incident": incident, "logs": logs})

View file

@ -20,59 +20,63 @@
<div class="card-content">
<div class="columns is-multiline">
<div class="column is-half">
<p>
<strong>Verknüpfte Risiken:</strong>
</p>
<p><strong>Verantwortliche/r:</strong> {{ control.responsible|default:"-" }}</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>
<p><strong>Aktualisiert am:</strong> {{ control.updated_at|date:'d.m.Y H:i' }}</p>
</div>
</div>
</div> <!-- Ende Inhalt Überblick -->
</div> <!-- Ende Überblick -->
<!-- Maßnahmen -->
<!-- Risiken -->
<div class="card">
<header class="card-header">
<p class="card-header-title">Maßnahmen</p>
<p class="card-header-title">Verknüpfte Risiken</p>
</header>
<div class="card-content">
{% if control.controls.all %}
{% if control.risks %}
<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>
<th>Risikoeigner</th>
<th>Kategorie</th>
<th>Asset</th>
<th>Prozess</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>
{% for risk in control.risks.all %}
<tr onclick="window.location.href='/risks/risks/{{ risk.id }}';" style="cursor:pointer;">
<td>{{ risk.title }}</td>
<td>
{% if control.due_date %}
{{ control.due_date|date:"d.m.Y" }}
{% if risk.owner %}
{{ risk.owner }}
{% else %}
{% endif %}
</td>
<td>
{% if control.responsible %}
{{ control.responsible.get_full_name|default:control.responsible.username }}
{% if risk.category %}
{{ risk.category }}
{% else %}
{% endif %}
</td>
<td>
{% if control.wiki_link %}
<a href="{{ control.wiki_link }}" target="_blank">🔗</a>
{% if risk.asset %}
{{ risk.asset }}
{% else %}
{% endif %}
</td>
<td>
{% if risk.process %}
{{ risk.process }}
{% else %}
{% endif %}
@ -82,7 +86,7 @@
</tbody>
</table>
{% else %}
<p class="has-text-grey">Keine Maßnahmen erfasst.</p>
<p class="has-text-grey">Keine Verknüpften Risiken.</p>
{% endif %}
</div>
</div>

View file

@ -4,5 +4,126 @@
<li><a href="{% url 'risks:show_incident' incident.id %}">{{ incident.title }}</a></li>
{% endblock %}
{% block content %}
<div class="container">
<section class="hero is-small">
<div class="hero-body">
<p class="title">Vorfall: {{ incident.title }}</p>
<p class="subtitle is-6">{{ incident.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>Gemeldet von:</strong> {{ incident.reported_by|default:"-" }}</p>
<p><strong>Gemeldet am:</strong> {{ incident.date_reported|date:'d.m.Y' }}</p>
<p><strong>Status:</strong> {{ incident.status }}</p>
</div>
<div class="column is-half">
<p><strong>Erstellt am:</strong> {{ incident.created_at|date:'d.m.Y H:i' }}</p>
<p><strong>Aktualisiert am:</strong> {{ incident.updated_at|date:'d.m.Y H:i' }}</p>
</div>
</div>
</div> <!-- Ende Inhalt Überblick -->
</div> <!-- Ende Überblick -->
<!-- Risiken -->
<div class="card">
<header class="card-header">
<p class="card-header-title">Zugehörige Risiken</p>
</header>
<div class="card-content">
{% if incident.related_risks %}
<table class="table is-striped is-hoverable is-fullwidth">
<thead>
<tr>
<th>Titel</th>
<th>Risikoeigner</th>
<th>Kategorie</th>
<th>Asset</th>
<th>Prozess</th>
</tr>
</thead>
<tbody>
{% for risk in incident.related_risks.all %}
<tr onclick="window.location.href='/risks/risks/{{ risk.id }}';" style="cursor:pointer;">
<td>{{ risk.title }}</td>
<td>
{% if risk.owner %}
{{ risk.owner }}
{% else %}
{% endif %}
</td>
<td>
{% if risk.category %}
{{ risk.category }}
{% else %}
{% endif %}
</td>
<td>
{% if risk.asset %}
{{ risk.asset }}
{% else %}
{% endif %}
</td>
<td>
{% if risk.process %}
{{ risk.process }}
{% else %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="has-text-grey">Keine Verknüpften Risiken.</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 %}

View file

@ -40,7 +40,7 @@
<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>
<p><strong>Aktualisiert am:</strong> {{ risk.updated_at|date:'d.m.Y H:i' }}</p>
</div>
</div>
<!-- Risikobewertung -->

View file

@ -8,74 +8,86 @@
<div class="box">
<h2 class="title is-5">Auswahl</h2>
<div class="columns is-multiline">
<form method="get">
<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>
<!-- 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>
{% for i in incidents %}
<option value="{{ i.id }}" {% if request.GET.risk == i.id|stringformat:"s" %}selected{% endif %}>
{{ i.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>
<!-- 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>
{% 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>
<!-- 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>
</form>
<h2 class="title is-5">Vorfälle</h2>
@ -91,19 +103,25 @@
</tr>
</thead>
<tbody>
<tr onclick="window.location.href='/risks/incidents/1';" style="cursor:pointer;">
<td>Switch entwendet</td>
{% for i in incidents %}
<tr onclick="window.location.href='{% url 'risks:show_incident' i.id %}'" style="cursor:pointer;">
<td>{{ i.title }}</td>
<td>
{% if i.related_risks.exists %}
<ul>
<li>
<a href="/risks/risks/1" onclick="event.stopPropagation();">Hardware Diebstahl</a>
</li>
{% for r in i.related_risks.all %}
<li>{{ r.title }}</li>
{% endfor %}
{% else %}
Noch kein Risiko zugeordnet
{% endif %}
</ul>
</td>
<td>Closed</td>
<td>08.09.2025</td>
<td>Kevin Heyer</td>
<td>{{ i.get_status_display }}</td>
<td>{{ i.date_reported|date:"d.m.Y" }}</td>
<td>{{ i.reported_by }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>