diff --git a/api/__pycache__/__init__.cpython-311.pyc b/api/__pycache__/__init__.cpython-311.pyc index 6ba2218..9a9a706 100644 Binary files a/api/__pycache__/__init__.cpython-311.pyc and b/api/__pycache__/__init__.cpython-311.pyc differ diff --git a/api/__pycache__/views.cpython-311.pyc b/api/__pycache__/views.cpython-311.pyc index b7efb64..5515e00 100644 Binary files a/api/__pycache__/views.cpython-311.pyc and b/api/__pycache__/views.cpython-311.pyc differ diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc index 48eb40b..2579686 100644 Binary files a/config/__pycache__/__init__.cpython-311.pyc and b/config/__pycache__/__init__.cpython-311.pyc differ diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index 0cf6574..d2e5ac0 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 85d305b..31827bc 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc index b976614..01cbb7e 100644 Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index f85427c..899d0f9 100644 --- a/config/settings.py +++ b/config/settings.py @@ -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", ] diff --git a/config/urls.py b/config/urls.py index b247a85..4802609 100644 --- a/config/urls.py +++ b/config/urls.py @@ -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 diff --git a/db.sqlite3 b/db.sqlite3 index c5adb26..16f0f61 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/risks/admin.py b/risks/admin.py index 955afb1..32ee87d 100644 --- a/risks/admin.py +++ b/risks/admin.py @@ -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 diff --git a/risks/audit_context.py b/risks/audit_context.py new file mode 100644 index 0000000..5e5b04e --- /dev/null +++ b/risks/audit_context.py @@ -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) \ No newline at end of file diff --git a/risks/middleware.py b/risks/middleware.py new file mode 100644 index 0000000..cad4512 --- /dev/null +++ b/risks/middleware.py @@ -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) diff --git a/risks/migrations/0013_control_created_at_control_updatet_at_and_more.py b/risks/migrations/0013_control_created_at_control_updatet_at_and_more.py new file mode 100644 index 0000000..d1043ea --- /dev/null +++ b/risks/migrations/0013_control_created_at_control_updatet_at_and_more.py @@ -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), + ), + ] diff --git a/risks/migrations/0014_remove_control_risk_control_risks.py b/risks/migrations/0014_remove_control_risk_control_risks.py new file mode 100644 index 0000000..bf2a0b9 --- /dev/null +++ b/risks/migrations/0014_remove_control_risk_control_risks.py @@ -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"), + ), + ] diff --git a/risks/migrations/0015_alter_auditlog_changes.py b/risks/migrations/0015_alter_auditlog_changes.py new file mode 100644 index 0000000..96b4bc9 --- /dev/null +++ b/risks/migrations/0015_alter_auditlog_changes.py @@ -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 + ), + ), + ] diff --git a/risks/migrations/0016_rename_updatet_at_control_updated_at_and_more.py b/risks/migrations/0016_rename_updatet_at_control_updated_at_and_more.py new file mode 100644 index 0000000..9c7a9eb --- /dev/null +++ b/risks/migrations/0016_rename_updatet_at_control_updated_at_and_more.py @@ -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", + ), + ] diff --git a/risks/migrations/0017_alter_incident_status.py b/risks/migrations/0017_alter_incident_status.py new file mode 100644 index 0000000..a1a3c8c --- /dev/null +++ b/risks/migrations/0017_alter_incident_status.py @@ -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, + ), + ), + ] diff --git a/risks/models.py b/risks/models.py index ced4069..6d5ace5 100644 --- a/risks/models.py +++ b/risks/models.py @@ -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") diff --git a/risks/serializers.py b/risks/serializers.py index 0570c3e..fb12561 100644 --- a/risks/serializers.py +++ b/risks/serializers.py @@ -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", - ] \ No newline at end of file + "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 diff --git a/risks/signals.py b/risks/signals.py index c8ccef2..599a838 100644 --- a/risks/signals.py +++ b/risks/signals.py @@ -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, diff --git a/risks/views.py b/risks/views.py index 0b1a62d..f3dc6f0 100644 --- a/risks/views.py +++ b/risks/views.py @@ -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 }) \ No newline at end of file + 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}) \ No newline at end of file diff --git a/templates/risks/item_control.html b/templates/risks/item_control.html index b171e41..21540e5 100644 --- a/templates/risks/item_control.html +++ b/templates/risks/item_control.html @@ -20,59 +20,63 @@
-

- Verknüpfte Risiken: -

+

Verantwortliche/r: {{ control.responsible|default:"-" }}

Zum Wiki Eintrag

-

Verantwortliche/r: {{ control.owner|default:"-" }}

+

Erstellt am: {{ control.created_at|date:'d.m.Y H:i' }}

-

Aktualisiert am: {{ control.updatet_at|date:'d.m.Y H:i' }}

+

Aktualisiert am: {{ control.updated_at|date:'d.m.Y H:i' }}

- +
-

Maßnahmen

+

Verknüpfte Risiken

- {% if control.controls.all %} + {% if control.risks %} - - - - + + + + - {% for control in control.controls.all %} - - - + {% for risk in control.risks.all %} + + +
TitelStatusFristVerantwortlicherLinkRisikoeignerKategorieAssetProzess
{{ control.title }}{{ control.get_status_display }}
{{ risk.title }} - {% if control.due_date %} - {{ control.due_date|date:"d.m.Y" }} + {% if risk.owner %} + {{ risk.owner }} {% else %} – {% endif %} - {% if control.responsible %} - {{ control.responsible.get_full_name|default:control.responsible.username }} + {% if risk.category %} + {{ risk.category }} {% else %} – {% endif %} - {% if control.wiki_link %} - 🔗 + {% if risk.asset %} + {{ risk.asset }} + {% else %} + – + {% endif %} + + {% if risk.process %} + {{ risk.process }} {% else %} – {% endif %} @@ -82,7 +86,7 @@
{% else %} -

Keine Maßnahmen erfasst.

+

Keine Verknüpften Risiken.

{% endif %}
diff --git a/templates/risks/item_incident.html b/templates/risks/item_incident.html index 48c5400..e9267ca 100644 --- a/templates/risks/item_incident.html +++ b/templates/risks/item_incident.html @@ -4,5 +4,126 @@
  • {{ incident.title }}
  • {% endblock %} {% block content %} +
    +
    +
    +

    Vorfall: {{ incident.title }}

    +

    {{ incident.description }}

    +
    +
    + +
    +
    +

    Überblick

    +
    + +
    +
    +
    +

    Gemeldet von: {{ incident.reported_by|default:"-" }}

    +

    Gemeldet am: {{ incident.date_reported|date:'d.m.Y' }}

    +

    Status: {{ incident.status }}

    +
    +
    +

    Erstellt am: {{ incident.created_at|date:'d.m.Y H:i' }}

    +

    Aktualisiert am: {{ incident.updated_at|date:'d.m.Y H:i' }}

    +
    +
    +
    +
    + +
    +
    +

    Zugehörige Risiken

    +
    +
    + {% if incident.related_risks %} + + + + + + + + + + + + {% for risk in incident.related_risks.all %} + + + + + + + + {% endfor %} + +
    TitelRisikoeignerKategorieAssetProzess
    {{ risk.title }} + {% if risk.owner %} + {{ risk.owner }} + {% else %} + – + {% endif %} + + {% if risk.category %} + {{ risk.category }} + {% else %} + – + {% endif %} + + {% if risk.asset %} + {{ risk.asset }} + {% else %} + – + {% endif %} + + {% if risk.process %} + {{ risk.process }} + {% else %} + – + {% endif %} +
    + {% else %} +

    Keine Verknüpften Risiken.

    + {% endif %} +
    +
    + + + +
    +
    +

    Historie

    +
    +
    + {% if logs %} + + + + + + + + + + {% for log in logs %} + + + + + + {% endfor %} + +
    ZeitpunktBenutzerAktion
    {{ log.action_time|date:"d.m.Y H:i" }}{{ log.user.get_full_name|default:log.user.username }}{{ log.get_change_message }}
    + {% else %} +

    Keine Historie vorhanden.

    + {% endif %} +
    +
    + +

    + +
    {% endblock %} \ No newline at end of file diff --git a/templates/risks/item_risk.html b/templates/risks/item_risk.html index 7d28e00..080821c 100644 --- a/templates/risks/item_risk.html +++ b/templates/risks/item_risk.html @@ -40,7 +40,7 @@

    Kategorie: {{ risk.category|default:"-" }}

    Risikoeigner: {{ risk.owner|default:"-" }}

    Erstellt am: {{ risk.created_at|date:'d.m.Y H:i' }}

    -

    Aktualisiert am: {{ risk.updatet_at|date:'d.m.Y H:i' }}

    +

    Aktualisiert am: {{ risk.updated_at|date:'d.m.Y H:i' }}

    diff --git a/templates/risks/list_incidents.html b/templates/risks/list_incidents.html index 23424ef..34f27b9 100644 --- a/templates/risks/list_incidents.html +++ b/templates/risks/list_incidents.html @@ -8,74 +8,86 @@

    Auswahl

    -
    +
    +
    - -
    -
    - -
    -
    - + +
    +
    + +
    +
    + +
    +
    +
    +
    + + +
    +
    + +
    +
    + +
    +
    +
    +
    + + +
    +
    + +
    +
    + +
    +
    +
    +
    + + +
    +
    + +
    +
    + +
    - - -
    -
    - -
    -
    - -
    -
    -
    -
    - - -
    -
    - -
    -
    - -
    -
    -
    -
    - - -
    -
    - -
    -
    - -
    -
    -
    -
    -
    +

    Vorfälle

    @@ -91,19 +103,25 @@ - - Switch entwendet + {% for i in incidents %} + + {{ i.title }} + {% if i.related_risks.exists %}
      -
    • - Hardware Diebstahl -
    • + {% for r in i.related_risks.all %} +
    • {{ r.title }}
    • + {% endfor %} + {% else %} + Noch kein Risiko zugeordnet + {% endif %}
    - Closed - 08.09.2025 - Kevin Heyer + {{ i.get_status_display }} + {{ i.date_reported|date:"d.m.Y" }} + {{ i.reported_by }} + {% endfor %}