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:
parent
43e86d0357
commit
686030e4cb
25 changed files with 540 additions and 171 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -50,6 +50,7 @@ MIDDLEWARE = [
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
"django.middleware.csrf.CsrfViewMiddleware",
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"risks.middleware.AuditUserMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
]
|
]
|
||||||
|
|
|
@ -18,8 +18,7 @@ urlpatterns = [
|
||||||
path("api/ping/", ping), # Public healthcheck endpoint
|
path("api/ping/", ping), # Public healthcheck endpoint
|
||||||
path("api/secure-ping/", secure_ping), # Protected API endpoint
|
path("api/secure-ping/", secure_ping), # Protected API endpoint
|
||||||
path("api/", include(router.urls)),
|
path("api/", include(router.urls)),
|
||||||
path("", include("risks.urls")),
|
path("", include("risks.urls", namespace="risks")),
|
||||||
path("risks/", include("risks.urls")),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add OIDC routes only if Single Sign-On is enabled
|
# Add OIDC routes only if Single Sign-On is enabled
|
||||||
|
|
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
|
@ -19,13 +19,6 @@ class UserAdmin(BaseUserAdmin):
|
||||||
return obj.controls_responsible.count()
|
return obj.controls_responsible.count()
|
||||||
responsible_controls_count.short_description = "Controls Responsible"
|
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):
|
class ResidualRiskInline(admin.StackedInline):
|
||||||
"""
|
"""
|
||||||
Inline editor for ResidualRisk, linked one-to-one with Risk
|
Inline editor for ResidualRisk, linked one-to-one with Risk
|
||||||
|
@ -36,6 +29,11 @@ class ResidualRiskInline(admin.StackedInline):
|
||||||
readonly_fields = ("score", "level", "review_required")
|
readonly_fields = ("score", "level", "review_required")
|
||||||
fields = ("likelihood", "impact", "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)
|
@admin.register(Risk)
|
||||||
class RiskAdmin(admin.ModelAdmin):
|
class RiskAdmin(admin.ModelAdmin):
|
||||||
|
@ -50,7 +48,7 @@ class RiskAdmin(admin.ModelAdmin):
|
||||||
)
|
)
|
||||||
list_filter = ("level", "likelihood", "impact", "owner")
|
list_filter = ("level", "likelihood", "impact", "owner")
|
||||||
search_fields = ("title", "asset", "process", "category")
|
search_fields = ("title", "asset", "process", "category")
|
||||||
inlines = [ControlInline, ResidualRiskInline]
|
inlines = [ResidualRiskInline, ControlRisksInline] # Controls hier verknüpfen
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
obj._changed_by = request.user
|
obj._changed_by = request.user
|
||||||
|
@ -82,10 +80,10 @@ class ResidualRiskAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(Control)
|
@admin.register(Control)
|
||||||
class ControlAdmin(admin.ModelAdmin):
|
class ControlAdmin(admin.ModelAdmin):
|
||||||
list_display = ("title", "status", "due_date", "responsible", "risk")
|
list_display = ("title", "status", "due_date", "responsible")
|
||||||
list_filter = ("status", "due_date")
|
list_filter = ("status", "due_date")
|
||||||
|
autocomplete_fields = ("risks", "responsible",)
|
||||||
search_fields = ("title", "description")
|
search_fields = ("title", "description")
|
||||||
autocomplete_fields = ("responsible", "risk")
|
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
obj._changed_by = request.user
|
obj._changed_by = request.user
|
||||||
|
|
8
risks/audit_context.py
Normal file
8
risks/audit_context.py
Normal 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
9
risks/middleware.py
Normal 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)
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
21
risks/migrations/0014_remove_control_risk_control_risks.py
Normal file
21
risks/migrations/0014_remove_control_risk_control_risks.py
Normal 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"),
|
||||||
|
),
|
||||||
|
]
|
20
risks/migrations/0015_alter_auditlog_changes.py
Normal file
20
risks/migrations/0015_alter_auditlog_changes.py
Normal 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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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",
|
||||||
|
),
|
||||||
|
]
|
24
risks/migrations/0017_alter_incident_status.py
Normal file
24
risks/migrations/0017_alter_incident_status.py
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,7 +1,16 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from multiselectfield import MultiSelectField
|
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):
|
class User(AbstractUser):
|
||||||
"""
|
"""
|
||||||
|
@ -52,7 +61,7 @@ class Risk(models.Model):
|
||||||
process = 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)
|
category = models.CharField(max_length=255, blank=True, null=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=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 Protection Goals
|
||||||
cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True)
|
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)
|
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):
|
def save(self, *args, **kwargs):
|
||||||
# Load previous state (if it exists)
|
# Load previous state (if it exists)
|
||||||
if self.pk:
|
if self.pk:
|
||||||
old = ResidualRisk.objects.get(pk=self.pk)
|
old = ResidualRisk.objects.get(pk=self.pk)
|
||||||
if old.likelihood != self.likelihood or old.impact != self.impact:
|
if old.likelihood != self.likelihood or old.impact != self.impact:
|
||||||
self.review_required = False
|
self.review_required = True
|
||||||
|
|
||||||
self.score = self.likelihood * self.impact
|
self.score = self.likelihood * self.impact
|
||||||
|
|
||||||
|
@ -174,9 +186,11 @@ class Control(models.Model):
|
||||||
)
|
)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
wiki_link = models.URLField(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
|
# Relation to risk
|
||||||
risk = models.ForeignKey(Risk, on_delete=models.CASCADE, related_name="controls")
|
risks = models.ManyToManyField("Risk", related_name="controls")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.title} ({self.get_status_display()})"
|
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)
|
action = models.CharField(max_length=10, choices=ACTION_CHOICES)
|
||||||
model = models.CharField(max_length=100)
|
model = models.CharField(max_length=100)
|
||||||
object_id = models.CharField(max_length=50)
|
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)
|
timestamp = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -214,7 +228,7 @@ class Incident(models.Model):
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
("open", "Opened"),
|
("open", "Opened"),
|
||||||
("in_progress", "In Progress"),
|
("in_progress", "In Progress"),
|
||||||
("close", "Closed"),
|
("closed", "Closed"),
|
||||||
]
|
]
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
description = models.TextField(blank=True, null=True)
|
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")
|
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)
|
status = models.CharField(max_length=12, choices=STATUS_CHOICES)
|
||||||
related_risks = models.ManyToManyField("Risk", blank=True, related_name="incidents")
|
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):
|
class Notification(models.Model):
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="notifications")
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name="notifications")
|
||||||
|
|
|
@ -18,17 +18,21 @@ class ResidualRiskSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class ControlSerializer(serializers.ModelSerializer):
|
class ControlSerializer(serializers.ModelSerializer):
|
||||||
|
risks = serializers.PrimaryKeyRelatedField(many=True, queryset=Risk.objects.all())
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Control
|
model = Control
|
||||||
fields = [
|
fields = [
|
||||||
"id",
|
"id",
|
||||||
"title",
|
"title",
|
||||||
"status",
|
"status",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
"due_date",
|
"due_date",
|
||||||
"responsible",
|
"responsible",
|
||||||
"description",
|
"description",
|
||||||
"wiki_link",
|
"wiki_link",
|
||||||
"risk",
|
"risks",
|
||||||
]
|
]
|
||||||
|
|
||||||
class RiskSerializer(serializers.ModelSerializer):
|
class RiskSerializer(serializers.ModelSerializer):
|
||||||
|
@ -44,16 +48,14 @@ class RiskSerializer(serializers.ModelSerializer):
|
||||||
"process",
|
"process",
|
||||||
"category",
|
"category",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updatet_at",
|
"updated_at",
|
||||||
"likelihood",
|
"likelihood",
|
||||||
"impact",
|
"impact",
|
||||||
"score",
|
"score",
|
||||||
"level",
|
"level",
|
||||||
"owner",
|
"owner",
|
||||||
"follow_up",
|
"follow_up",
|
||||||
"confidentiality",
|
"cia",
|
||||||
"integrity",
|
|
||||||
"availability",
|
|
||||||
"controls",
|
"controls",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -93,15 +95,30 @@ class RiskSummarySerializer(serializers.ModelSerializer):
|
||||||
fields = ["id", "title", "score", "level"]
|
fields = ["id", "title", "score", "level"]
|
||||||
|
|
||||||
class IncidentSerializer(serializers.ModelSerializer):
|
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:
|
class Meta:
|
||||||
model = Incident
|
model = Incident
|
||||||
fields = [
|
fields = [
|
||||||
"id",
|
"id", "title", "description", "date_reported",
|
||||||
"title",
|
"created_at", "updated_at", "status", "related_risks",
|
||||||
"description",
|
|
||||||
"date_reported",
|
|
||||||
"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
|
||||||
|
|
|
@ -2,6 +2,7 @@ from datetime import date, datetime
|
||||||
from django.db.models import Model
|
from django.db.models import Model
|
||||||
from django.db.models.signals import post_save, post_delete, m2m_changed
|
from django.db.models.signals import post_save, post_delete, m2m_changed
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
from .audit_context import get_current_user
|
||||||
from .models import Control, Risk, ResidualRisk, AuditLog, Incident
|
from .models import Control, Risk, ResidualRisk, AuditLog, Incident
|
||||||
from .utils import model_diff
|
from .utils import model_diff
|
||||||
|
|
||||||
|
@ -21,8 +22,6 @@ def serialize_value(value):
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@receiver(post_save, sender=Risk)
|
@receiver(post_save, sender=Risk)
|
||||||
def log_risk_save(sender, instance, created, **kwargs):
|
def log_risk_save(sender, instance, created, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
AuditLog.objects.create(
|
AuditLog.objects.create(
|
||||||
user=getattr(instance, "_changed_by", None),
|
user=getattr(instance, "_changed_by", None),
|
||||||
|
@ -39,7 +38,6 @@ def log_risk_save(sender, instance, created, **kwargs):
|
||||||
else:
|
else:
|
||||||
old = Risk.objects.get(pk=instance.pk)
|
old = Risk.objects.get(pk=instance.pk)
|
||||||
changes = model_diff(old, instance)
|
changes = model_diff(old, instance)
|
||||||
|
|
||||||
if changes:
|
if changes:
|
||||||
clean_changes = {
|
clean_changes = {
|
||||||
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
|
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",
|
action="update",
|
||||||
model="Risk",
|
model="Risk",
|
||||||
object_id=instance.pk,
|
object_id=instance.pk,
|
||||||
changes=changes,
|
changes=clean_changes,
|
||||||
)
|
)
|
||||||
|
|
||||||
@receiver(post_delete, sender=Risk)
|
@receiver(post_delete, sender=Risk)
|
||||||
|
@ -58,8 +56,9 @@ def log_risk_delete(sender, instance, **kwargs):
|
||||||
"""
|
"""
|
||||||
Signal that runs after a Risk is deleted.
|
Signal that runs after a Risk is deleted.
|
||||||
"""
|
"""
|
||||||
|
user = getattr(instance, "_changed_by", None) or get_current_user()
|
||||||
AuditLog.objects.create(
|
AuditLog.objects.create(
|
||||||
user=getattr(instance, "_changed_by", None),
|
user=user,
|
||||||
action="delete",
|
action="delete",
|
||||||
model="Risk",
|
model="Risk",
|
||||||
object_id=instance.pk,
|
object_id=instance.pk,
|
||||||
|
@ -88,7 +87,6 @@ def log_control_save(sender, instance, created, **kwargs):
|
||||||
else:
|
else:
|
||||||
old = Control.objects.get(pk=instance.pk)
|
old = Control.objects.get(pk=instance.pk)
|
||||||
changes = model_diff(old, instance)
|
changes = model_diff(old, instance)
|
||||||
|
|
||||||
if changes:
|
if changes:
|
||||||
clean_changes = {
|
clean_changes = {
|
||||||
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
|
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",
|
action="update",
|
||||||
model="Control",
|
model="Control",
|
||||||
object_id=instance.pk,
|
object_id=instance.pk,
|
||||||
changes=changes,
|
changes=clean_changes,
|
||||||
)
|
)
|
||||||
|
|
||||||
@receiver(post_delete, sender=Control)
|
@receiver(post_delete, sender=Control)
|
||||||
def log_control_delete(sender, instance, **kwargs):
|
def log_control_delete(sender, instance, **kwargs):
|
||||||
|
user = getattr(instance, "_changed_by", None) or get_current_user()
|
||||||
AuditLog.objects.create(
|
AuditLog.objects.create(
|
||||||
user=getattr(instance, "_changed_by", None),
|
user=user,
|
||||||
action="delete",
|
action="delete",
|
||||||
model="Control",
|
model="Control",
|
||||||
object_id=instance.pk,
|
object_id=instance.pk,
|
||||||
changes=None,
|
changes=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@receiver(post_save, sender=Control)
|
@receiver(m2m_changed, sender=Control.risks.through)
|
||||||
def update_residual_risk_on_control_change(sender, instance, **kwargs):
|
def control_risks_changed(sender, instance, action, reverse, pk_set, **kwargs):
|
||||||
"""
|
if action in {"post_add", "post_remove", "post_clear"}:
|
||||||
Whenever a control is saved, check if the related risk has a residual risk.
|
if action == "post_clear":
|
||||||
If a control is completed or verified, flag the residual risk for review.
|
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
|
from .models import ResidualRisk
|
||||||
|
for risk in affected_risks:
|
||||||
# Ensure residual risk exists
|
residual, _ = ResidualRisk.objects.get_or_create(risk=risk)
|
||||||
residual, created = ResidualRisk.objects.get_or_create(risk=risk)
|
residual.review_required = True
|
||||||
|
residual.save()
|
||||||
# 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
|
# Residual risks
|
||||||
|
@ -151,7 +153,6 @@ def log_residual_save(sender, instance, created, **kwargs):
|
||||||
else:
|
else:
|
||||||
old = ResidualRisk.objects.get(pk=instance.pk)
|
old = ResidualRisk.objects.get(pk=instance.pk)
|
||||||
changes = model_diff(old, instance)
|
changes = model_diff(old, instance)
|
||||||
|
|
||||||
if changes:
|
if changes:
|
||||||
clean_changes = {
|
clean_changes = {
|
||||||
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
|
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)
|
@receiver(post_delete, sender=ResidualRisk)
|
||||||
def log_residual_delete(sender, instance, **kwargs):
|
def log_residual_delete(sender, instance, **kwargs):
|
||||||
|
user = getattr(instance, "_changed_by", None) or get_current_user()
|
||||||
AuditLog.objects.create(
|
AuditLog.objects.create(
|
||||||
user=getattr(instance, "_changed_by", None),
|
user=user,
|
||||||
action="delete",
|
action="delete",
|
||||||
model="ResidualRisk",
|
model="ResidualRisk",
|
||||||
object_id=instance.pk,
|
object_id=instance.pk,
|
||||||
|
@ -187,12 +189,16 @@ def log_incident_save(sender, instance, created, **kwargs):
|
||||||
action="create",
|
action="create",
|
||||||
model="Incident",
|
model="Incident",
|
||||||
object_id=instance.pk,
|
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:
|
else:
|
||||||
old = Incident.objects.get(pk=instance.pk)
|
old = Incident.objects.get(pk=instance.pk)
|
||||||
changes = model_diff(old, instance)
|
changes = model_diff(old, instance)
|
||||||
|
|
||||||
if changes:
|
if changes:
|
||||||
clean_changes = {
|
clean_changes = {
|
||||||
field: {"old": serialize_value(vals["old"]), "new": serialize_value(vals["new"])}
|
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",
|
action="update",
|
||||||
model="Incident",
|
model="Incident",
|
||||||
object_id=instance.pk,
|
object_id=instance.pk,
|
||||||
changes=changes,
|
changes=clean_changes,
|
||||||
)
|
)
|
||||||
|
|
||||||
@receiver(m2m_changed, sender=Incident.related_risks.through)
|
@receiver(m2m_changed, sender=Incident.related_risks.through)
|
||||||
def log_incident_risks_change(sender, instance, action, reverse, model, pk_set, **kwargs):
|
def log_incident_risks_change(sender, instance, action, reverse, model, pk_set, **kwargs):
|
||||||
if action in ["post_add", "post_remove", "post_clear"]:
|
if action in ["post_add", "post_remove", "post_clear"]:
|
||||||
|
user = getattr(instance, "_changed_by", None) or get_current_user()
|
||||||
AuditLog.objects.create(
|
AuditLog.objects.create(
|
||||||
user=getattr(instance, "_changed_by", None),
|
user=user,
|
||||||
action="update",
|
action="update",
|
||||||
model="Incident",
|
model="Incident",
|
||||||
object_id=instance.pk,
|
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)
|
@receiver(post_delete, sender=Incident)
|
||||||
def log_incident_delete(sender, instance, **kwargs):
|
def log_incident_delete(sender, instance, **kwargs):
|
||||||
|
user = getattr(instance, "_changed_by", None) or get_current_user()
|
||||||
AuditLog.objects.create(
|
AuditLog.objects.create(
|
||||||
user=getattr(instance, "_changed_by", None),
|
user=user,
|
||||||
action="delete",
|
action="delete",
|
||||||
model="Incident",
|
model="Incident",
|
||||||
object_id=instance.pk,
|
object_id=instance.pk,
|
||||||
|
|
|
@ -24,12 +24,10 @@ class RiskViewSet(viewsets.ModelViewSet):
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
instance._changed_by = self.request.user
|
instance._changed_by = self.request.user
|
||||||
instance.save()
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
instance._changed_by = self.request.user
|
instance._changed_by = self.request.user
|
||||||
instance.save()
|
|
||||||
|
|
||||||
class ControlViewSet(viewsets.ModelViewSet):
|
class ControlViewSet(viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
|
@ -43,12 +41,10 @@ class ControlViewSet(viewsets.ModelViewSet):
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
instance._changed_by = self.request.user
|
instance._changed_by = self.request.user
|
||||||
instance.save()
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
instance._changed_by = self.request.user
|
instance._changed_by = self.request.user
|
||||||
instance.save()
|
|
||||||
|
|
||||||
class ResidualRiskViewSet(viewsets.ModelViewSet):
|
class ResidualRiskViewSet(viewsets.ModelViewSet):
|
||||||
queryset = ResidualRisk.objects.all()
|
queryset = ResidualRisk.objects.all()
|
||||||
|
@ -66,12 +62,10 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
instance._changed_by = self.request.user
|
instance._changed_by = self.request.user
|
||||||
instance.save()
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
instance._changed_by = self.request.user
|
instance._changed_by = self.request.user
|
||||||
instance.save()
|
|
||||||
|
|
||||||
class AuditViewSet(viewsets.ReadOnlyModelViewSet):
|
class AuditViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
|
@ -92,12 +86,10 @@ class IncidentViewSet(viewsets.ModelViewSet):
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
instance = serializer.save(reported_by=self.request.user)
|
instance = serializer.save(reported_by=self.request.user)
|
||||||
instance._changed_by = self.request.user
|
instance._changed_by = self.request.user
|
||||||
instance.save()
|
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
instance = serializer.save()
|
instance = serializer.save()
|
||||||
instance._changed_by = self.request.user
|
instance._changed_by = self.request.user
|
||||||
instance.save()
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Web
|
# Web
|
||||||
|
@ -146,9 +138,8 @@ def show_risk(request, id):
|
||||||
return render(request, "risks/item_risk.html", {"risk": risk, "logs": logs})
|
return render(request, "risks/item_risk.html", {"risk": risk, "logs": logs})
|
||||||
|
|
||||||
def list_controls(request):
|
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")
|
control_id = request.GET.get("control")
|
||||||
risk_id = request.GET.get("risk")
|
risk_id = request.GET.get("risk")
|
||||||
status = request.GET.get("status")
|
status = request.GET.get("status")
|
||||||
|
@ -157,13 +148,13 @@ def list_controls(request):
|
||||||
if control_id:
|
if control_id:
|
||||||
qs = qs.filter(id=control_id)
|
qs = qs.filter(id=control_id)
|
||||||
if risk_id:
|
if risk_id:
|
||||||
qs = qs.filter(risk_id=risk_id)
|
qs = qs.filter(risks__id=risk_id) # FIX
|
||||||
if status:
|
if status:
|
||||||
qs = qs.filter(status=status)
|
qs = qs.filter(status=status)
|
||||||
if responsible_id:
|
if responsible_id:
|
||||||
qs = qs.filter(responsible_id=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")
|
risks = Risk.objects.all().order_by("title")
|
||||||
users = User.objects.filter(responsible_controls__isnull=False).distinct().order_by("username")
|
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})
|
return render(request, "risks/item_control.html", {"control": control, "logs": logs})
|
||||||
|
|
||||||
|
|
||||||
def list_incidents(request):
|
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):
|
def show_incident(request, id):
|
||||||
incident = Incident.objects.get(pk=id)
|
incident = get_object_or_404(Incident, pk=id)
|
||||||
return render(request, "risks/item_incident.html", {"incident": incident })
|
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})
|
|
@ -20,59 +20,63 @@
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="columns is-multiline">
|
<div class="columns is-multiline">
|
||||||
<div class="column is-half">
|
<div class="column is-half">
|
||||||
<p>
|
<p><strong>Verantwortliche/r:</strong> {{ control.responsible|default:"-" }}</p>
|
||||||
<strong>Verknüpfte Risiken:</strong>
|
|
||||||
</p>
|
|
||||||
<p><strong><a>Zum Wiki Eintrag</a></strong></p>
|
<p><strong><a>Zum Wiki Eintrag</a></strong></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="column is-half">
|
<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>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>
|
</div>
|
||||||
</div> <!-- Ende Inhalt Überblick -->
|
</div> <!-- Ende Inhalt Überblick -->
|
||||||
</div> <!-- Ende Überblick -->
|
</div> <!-- Ende Überblick -->
|
||||||
|
|
||||||
<!-- Maßnahmen -->
|
<!-- Risiken -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
<p class="card-header-title">Maßnahmen</p>
|
<p class="card-header-title">Verknüpfte Risiken</p>
|
||||||
</header>
|
</header>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
{% if control.controls.all %}
|
{% if control.risks %}
|
||||||
<table class="table is-striped is-hoverable is-fullwidth">
|
<table class="table is-striped is-hoverable is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Titel</th>
|
<th>Titel</th>
|
||||||
<th>Status</th>
|
<th>Risikoeigner</th>
|
||||||
<th>Frist</th>
|
<th>Kategorie</th>
|
||||||
<th>Verantwortlicher</th>
|
<th>Asset</th>
|
||||||
<th>Link</th>
|
<th>Prozess</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for control in control.controls.all %}
|
{% for risk in control.risks.all %}
|
||||||
<tr onclick="window.location.href='/risks/controls/{{ control.id }}';" style="cursor:pointer;">
|
<tr onclick="window.location.href='/risks/risks/{{ risk.id }}';" style="cursor:pointer;">
|
||||||
<td>{{ control.title }}</td>
|
<td>{{ risk.title }}</td>
|
||||||
<td>{{ control.get_status_display }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
{% if control.due_date %}
|
{% if risk.owner %}
|
||||||
{{ control.due_date|date:"d.m.Y" }}
|
{{ risk.owner }}
|
||||||
{% else %}
|
{% else %}
|
||||||
–
|
–
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if control.responsible %}
|
{% if risk.category %}
|
||||||
{{ control.responsible.get_full_name|default:control.responsible.username }}
|
{{ risk.category }}
|
||||||
{% else %}
|
{% else %}
|
||||||
–
|
–
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if control.wiki_link %}
|
{% if risk.asset %}
|
||||||
<a href="{{ control.wiki_link }}" target="_blank">🔗</a>
|
{{ risk.asset }}
|
||||||
|
{% else %}
|
||||||
|
–
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if risk.process %}
|
||||||
|
{{ risk.process }}
|
||||||
{% else %}
|
{% else %}
|
||||||
–
|
–
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -82,7 +86,7 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="has-text-grey">Keine Maßnahmen erfasst.</p>
|
<p class="has-text-grey">Keine Verknüpften Risiken.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,5 +4,126 @@
|
||||||
<li><a href="{% url 'risks:show_incident' incident.id %}">{{ incident.title }}</a></li>
|
<li><a href="{% url 'risks:show_incident' incident.id %}">{{ incident.title }}</a></li>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% 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 %}
|
{% endblock %}
|
|
@ -40,7 +40,7 @@
|
||||||
<p><strong>Kategorie:</strong> {{ risk.category|default:"-" }}</p>
|
<p><strong>Kategorie:</strong> {{ risk.category|default:"-" }}</p>
|
||||||
<p><strong>Risikoeigner:</strong> {{ risk.owner|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>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>
|
||||||
</div>
|
</div>
|
||||||
<!-- Risikobewertung -->
|
<!-- Risikobewertung -->
|
||||||
|
|
|
@ -8,74 +8,86 @@
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h2 class="title is-5">Auswahl</h2>
|
<h2 class="title is-5">Auswahl</h2>
|
||||||
|
|
||||||
<div class="columns is-multiline">
|
<form method="get">
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
|
||||||
<!-- Vorfälle -->
|
<!-- Vorfälle -->
|
||||||
<div class="column is-3">
|
<div class="column is-3">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">Vorfall</label>
|
<label class="label">Vorfall</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
<select>
|
<select>
|
||||||
<option>Alle</option>
|
<option>Alle</option>
|
||||||
<option>Vorfall A</option>
|
{% for i in incidents %}
|
||||||
<option>Vorfall B</option>
|
<option value="{{ i.id }}" {% if request.GET.risk == i.id|stringformat:"s" %}selected{% endif %}>
|
||||||
</select>
|
{{ 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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
<!-- 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>
|
<h2 class="title is-5">Vorfälle</h2>
|
||||||
|
|
||||||
|
@ -91,19 +103,25 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr onclick="window.location.href='/risks/incidents/1';" style="cursor:pointer;">
|
{% for i in incidents %}
|
||||||
<td>Switch entwendet</td>
|
<tr onclick="window.location.href='{% url 'risks:show_incident' i.id %}'" style="cursor:pointer;">
|
||||||
|
<td>{{ i.title }}</td>
|
||||||
<td>
|
<td>
|
||||||
|
{% if i.related_risks.exists %}
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
{% for r in i.related_risks.all %}
|
||||||
<a href="/risks/risks/1" onclick="event.stopPropagation();">Hardware Diebstahl</a>
|
<li>{{ r.title }}</li>
|
||||||
</li>
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
Noch kein Risiko zugeordnet
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
<td>Closed</td>
|
<td>{{ i.get_status_display }}</td>
|
||||||
<td>08.09.2025</td>
|
<td>{{ i.date_reported|date:"d.m.Y" }}</td>
|
||||||
<td>Kevin Heyer</td>
|
<td>{{ i.reported_by }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Reference in a new issue