From a3148161500eaea58903986b216a59f1794509cc Mon Sep 17 00:00:00 2001 From: Kevin Heyer Date: Mon, 22 Sep 2025 10:14:21 +0200 Subject: [PATCH] Change Impact_Choices to DB --- db.sqlite3 | Bin 262144 -> 278528 bytes risks/admin.py | 8 ++++ risks/models/__init__.py | 3 +- risks/models/impact_choice.py | 13 ++++++ risks/models/residual_risk.py | 8 +++- risks/models/risk.py | 15 +++---- risks/templatetags/risk_extras.py | 71 ++++++++++++++++++++++-------- risks/views.py | 18 +++++--- templates/risks/item_risk.html | 8 ++-- templates/risks/risk_matrix.html | 62 +++++++++++++------------- 10 files changed, 138 insertions(+), 68 deletions(-) create mode 100644 risks/models/impact_choice.py diff --git a/db.sqlite3 b/db.sqlite3 index 55eb1e34204a9d903c208ad23d63a0e92dc3fb46..99ed67656e865f5dc727d3efcc94ba9ff0df635f 100644 GIT binary patch delta 2997 zcma)8Yj6|S72dmdSF2rF_pTLN3M9bV0c_bw*6L}=HkM+D4F&=>^bsZ%dc~FzGOe0c1p-(XglFY5>F?6Fb$-IHpxr_WTq`s(n;^` z%C?NtxI3e>t9#G+&N<&X_c1mmj*V5m^iX9FMNu2bZ}#aqzRm-B&h44RmR2%mTT6s> znOT)Wcb*gNjBr}$F36%y$nCVUlx<&RC_TJ)*yVD0qp{3LYA8CC8crt<#4_r5G`(+s zEHN@WGe6>XxqS{-(BXDVuI7--9g@9HS@z1Vps$*sHqD`WN)U3ZiWIOZy@CM zI^ABk>}oFjez~1pgB1#)DHi_%-^3I641NGVg+IU>u^nAR_s|r&fo9MT3bK?$IW$8( ztfo!gRqDI^u_7n~EwKgeS&IqUOXINv$@EBOu#Yy2u!h%lRY_o~F_dRV%E+)*%z#YoM05O(KE-+{-yuNuS3q>@Q=eDn+ z(G=nTIb^FA8OpXsBd$Ey!Y{wmDZ?;2mginNY(xq=j$S~=h{=D<53&#dF|5gUO|I8u zTjAB$`XVsQ+|6^>4;$Ii?~vK5dIb_7k+-T^TW9%}^AmBAJVzSZAP5g5vHB_z^4)CqTDBS(^dA zsr_L4iQBZHXn2qQchmXXgi+2=M_It<)TBsPS%hc!BgP+dA9Gs_hfKFj+fCoj%cA#Z zar!dLn5@(e@^5SLiVwcV5LZ-){Ph=~_rfkve7SMKKWY z^!ZC*17;tfcpfO5-US7A%iuv;D1cL6u4*uwH&K<;1ZC2VZ48%AMq%U@`L! zvjRR#P2lgLFHp|>C2`g*L!S7Bs&n!jJGh0j)zp9&f+MkbCN;kd=f7lShk>pt-rWOs zf?{7Bm;k>{s@03Gylq-KEAS>vwc$r(@$;Z&NOXV3nm-sxO5?=4;g!UcY!YD zGY>qWJlhJ}l($>ph2mSSuw5(Zc~asx8lT(F**a^$cujc;<_`~*%3v%W_sdCH3Ae!2 z9^F&**@Z-+`-)Uuu)ITX;)5t2peqPkljvSz(X!bPa=XGRgrh!7xOInDkr5nkkY7ia4v!G z4Td~krzhy~`+^BG!QZRAbq!W;mg-wcRrQiOqx zYS|{B*LXpNTjon#B8+|nvQ%|DQ0w+Ts|Lvt)IheZwGjtksYg2b-cjb7W!R=SOrD7F+e_^1{oB2oLJH3UR7#W>rVe-6wH=kR@V zzVC2mbhKo2)H=Ajq=OLB%J<6JbZA`z?RYQqDwf80&R@-s@(P2WW$Hy+u|iHX@RsUa zTosan=4E1-1pznFXgja>%sPR)te&*-Rm!PT5N92=@_DovC}fBtY}D zt)#2CT4p+@A8C(5!W_4igGJ05(;0uGHw6jll&#E-I4P@Ha!$>+KeO6fAYL*}^xHiz zDr==zU`YQ!e~3)0)v%paW@s(1SSfl7P-dsBht++NpyCe(>Ha$Qas(>q8h0`RPf}lQ zXMgwO!_|S@Q^VQb>|mgCYnLm>z(?KZa&(It**KIHItn@9C2P@ne*{ zK2Iv;ei{~=tdk3rTJ`DzvFIXE-}crhpUAc1uVN7P3k!6Sx3%AxH)1-|1DdT*YJX`D za+UEDj|nr9o9Lkv)NRYqc|mTgk^K)u*-u$6_-Y&iQWbY}lw&7gOpUSS7Pyl+EwGBE zlh90~?4=|`*!m<-e4T_km+3;^Sy!H>e}^aMsC9UnijH{XT2;)@Y+}I`qLr&T?`NJ1 z%0J2mc|tlb)rfnIWur^~NIR?V;eh`Z-RPt&lX^YHShFF?!#4%?>|Mazv?g>=cSIJ( zMHpKfXMs4hb15m0P{#Jep;Or1z{Xl2#^Obx7g``J>Uk33Cf2cSO^}y2+%5aJhKeR{ zX0axyDi*n3$W@ym2~>6Hk>%w3Q(~Y}$2`F$`~!c-%lI??fZyVmd{ob19%r1A{@0M@ z--Y`)Y=+=9{1X@P3jW4{KjL@z6@HHAuEUO@J97q-R;8{D5dQymV}tpNxz7w52Mkkp U^-gV08{pHsO>+k>!GT-<1^AjArT_o{ diff --git a/risks/admin.py b/risks/admin.py index c0c4fe7..f50b438 100644 --- a/risks/admin.py +++ b/risks/admin.py @@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _ from .models import ( Control, + ImpactChoice, Incident, LikelihoodChoice, Notification, @@ -238,4 +239,11 @@ class UserAdmin(BaseUserAdmin): # --------------------------------------------------------------------------- @admin.register(LikelihoodChoice) class LikelihoodChoiceAdmin(admin.ModelAdmin): + list_display = ("value", "name", "description") + +# --------------------------------------------------------------------------- +# ImpactChoice +# --------------------------------------------------------------------------- +@admin.register(ImpactChoice) +class ImpactChoiceAdmin(admin.ModelAdmin): list_display = ("value", "name", "description") \ No newline at end of file diff --git a/risks/models/__init__.py b/risks/models/__init__.py index fd79736..b6ece01 100644 --- a/risks/models/__init__.py +++ b/risks/models/__init__.py @@ -1,5 +1,6 @@ from .auditlog import AuditLog from .control import Control +from .impact_choice import ImpactChoice from .incident import Incident from .likelihood_choice import LikelihoodChoice from .notification import Notification @@ -11,4 +12,4 @@ from .risk import Risk from .user import User -__all__ = ["AuditLog", "Control", "Incident", "LikelihoodChoice", "Notification", "NotificationKind", "NotificationPreference", "NotificationRule", "ResidualRisk", "Risk", "User"] \ No newline at end of file +__all__ = ["AuditLog", "Control", "ImpactChoice", "Incident", "LikelihoodChoice", "Notification", "NotificationKind", "NotificationPreference", "NotificationRule", "ResidualRisk", "Risk", "User"] \ No newline at end of file diff --git a/risks/models/impact_choice.py b/risks/models/impact_choice.py new file mode 100644 index 0000000..933022d --- /dev/null +++ b/risks/models/impact_choice.py @@ -0,0 +1,13 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +class ImpactChoice(models.Model): + """ + Impact choices for Risks and Controls. + """ + name = models.CharField(_("Impact Name"), max_length=50) + description = models.TextField(_("Description"), blank=True, null=True) + value = models.IntegerField(_("Numeric Value"), unique=True, default=1) + + def __str__(self): + return f"{self.value} - {self.name} ({self.description})" \ No newline at end of file diff --git a/risks/models/residual_risk.py b/risks/models/residual_risk.py index fe7eaeb..1fbd264 100644 --- a/risks/models/residual_risk.py +++ b/risks/models/residual_risk.py @@ -1,5 +1,6 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from .impact_choice import ImpactChoice from .likelihood_choice import LikelihoodChoice from .risk import Risk @@ -21,7 +22,12 @@ class ResidualRisk(models.Model): verbose_name=_("Likelihood"), related_name="residual_risks", ) - impact = models.IntegerField(choices=Risk.IMPACT_CHOICES, default=1) + impact = models.ForeignKey( + ImpactChoice, + on_delete=models.PROTECT, + verbose_name=_("Impact"), + related_name="residual_risks", + ) score = models.IntegerField(editable=False) level = models.CharField(max_length=50, editable=False) review_required = models.BooleanField(default=False) diff --git a/risks/models/risk.py b/risks/models/risk.py index c8b9406..347ff5c 100644 --- a/risks/models/risk.py +++ b/risks/models/risk.py @@ -2,6 +2,7 @@ from django.db import models from django.conf import settings from django.utils.translation import gettext_lazy as _ from multiselectfield import MultiSelectField +from .impact_choice import ImpactChoice from .likelihood_choice import LikelihoodChoice # --------------------------------------------------------------------------- @@ -19,13 +20,6 @@ class Risk(models.Model): ("closed", _("Closed")), ("review_required", _("Review required")), ] - IMPACT_CHOICES = [ - (1, _("Very Low (< 1,000 € – minor operational impact)")), - (2, _("Low (1,000–5,000 € – local impact)")), - (3, _("High (5,000–15,000 € – team-level impact)")), - (4, _("Severe (50,000–100,000 € – regional impact)")), - (5, _("Critical (> 100,000 € – existential threat)")), - ] CIA_CHOICES = [ ("1", _("Confidentiality")), ("2", _("Integrity")), @@ -59,7 +53,12 @@ class Risk(models.Model): verbose_name=_("Likelihood"), related_name="risks", ) - impact = models.IntegerField(choices=IMPACT_CHOICES, default=1) + impact = models.ForeignKey( + ImpactChoice, + on_delete=models.PROTECT, + verbose_name=_("impact"), + related_name="risks", + ) # Calculated fields score = models.IntegerField(editable=False) diff --git a/risks/templatetags/risk_extras.py b/risks/templatetags/risk_extras.py index 3560576..a1dc4e0 100644 --- a/risks/templatetags/risk_extras.py +++ b/risks/templatetags/risk_extras.py @@ -1,16 +1,19 @@ from django import template from django.utils.html import format_html -from ..models import Control, Incident, Risk, LikelihoodChoice +from ..models import Control, ImpactChoice, Incident, Risk, LikelihoodChoice register = template.Library() _RISK_STATUS_MAP = dict(Risk.STATUS_CHOICES) _CONTROL_STATUS_MAP = dict(Control.STATUS_CHOICES) _INCIDENT_STATUS_MAP = dict(Incident.STATUS_CHOICES) -_IMPACT_LABELS = dict(Risk.IMPACT_CHOICES) LEVEL_ID_MAP = {"Low": 1, "Medium": 2, "High": 3, "Critical": 4} +# --------------------------------------------------------------------------- # Likelihood +# --------------------------------------------------------------------------- + +# Likelihood Label def get_likelihood_label(likelihood_obj): if not likelihood_obj: return "" @@ -22,6 +25,7 @@ def likelihood_id_label(likelihood): return f"{likelihood.value} ({likelihood.name})" return likelihood +# Likelihood Value def get_likelihood_value(likelihood): if isinstance(likelihood, LikelihoodChoice): return likelihood.value @@ -31,6 +35,7 @@ def get_likelihood_value(likelihood): def likelihood_value(likelihood_obj): return get_likelihood_value(likelihood_obj) +# Likelihood Class def get_likelihood_class(likelihood): value = get_likelihood_value(likelihood) LIKELIHOOD_MAP = { @@ -45,6 +50,52 @@ def get_likelihood_class(likelihood): def likelihood_class(likelihood): return get_likelihood_class(likelihood) +# --------------------------------------------------------------------------- +# Impact +# --------------------------------------------------------------------------- + +# Impact Label +def get_impact_label(impact_obj): + if not impact_obj: + return "" + return f"{impact_obj.value} ({impact_obj.name})" + +@register.filter +def impact_id_label(impact): + if isinstance(impact, ImpactChoice): + return f"{impact.value} ({impact.name})" + return impact + +# Impact Value +def get_impact_value(impact): + if isinstance(impact, ImpactChoice): + return impact.value + return impact + +@register.filter +def impact_value(impact_obj): + return get_impact_value(impact_obj) + +# Impact Class +def get_impact_class(impact): + value = get_impact_value(impact) + IMPACT_MAP = { + 1: "is-control-verylow", + 2: "is-control-low", + 3: "is-control-mid", + 4: "is-control-high", + 5: "is-control-veryhigh", + } + return IMPACT_MAP.get(value, "is-light") + +@register.filter +def impact_class(impact): + return get_impact_class(impact) + +# --------------------------------------------------------------------------- +# Unsorted +# --------------------------------------------------------------------------- + @register.simple_tag def sort_url(request, field, current_sort, current_dir): query = request.GET.copy() @@ -90,15 +141,6 @@ def _short(label: str) -> str: return label.split(sep, 1)[0].strip() return label.strip() -@register.filter -def impact_id_label(val): - try: - i = int(val) - except (TypeError, ValueError): - return "" - label = _IMPACT_LABELS.get(i, "") - short = _short(str(label)) if label else "" - return format_html("{} ({})", i, short) if label else format_html("{}", i) @register.filter def risk_status_label(code): @@ -139,13 +181,6 @@ LEVEL_MAP = { "Critical": "is-control-veryhigh", } -@register.filter -def impact_class(val): - try: - return IMPACT_MAP.get(int(val), "is-light") - except (TypeError, ValueError): - return "is-light" - @register.filter def level_class(level): return LEVEL_MAP.get(str(level), "is-light") diff --git a/risks/views.py b/risks/views.py index ed48310..313bed2 100644 --- a/risks/views.py +++ b/risks/views.py @@ -13,7 +13,7 @@ from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated from .forms import RiskStatusForm, ControlStatusForm, IncidentStatusForm, ResidualReviewForm -from .models import AuditLog, Risk, Control, ResidualRisk, AuditLog, Incident, LikelihoodChoice, Notification +from .models import AuditLog, ImpactChoice, Risk, Control, ResidualRisk, AuditLog, Incident, LikelihoodChoice, Notification from .serializers import ( ControlSerializer, RiskSerializer, ResidualRiskSerializer, UserSerializer, AuditSerializer, IncidentSerializer, @@ -466,18 +466,24 @@ def update_residual_review(request, risk_id): def risk_matrix(request): """Show gross/net risk matrix.""" risks = Risk.objects.select_related("owner", "residual_risk").all() - impacts = sorted(Risk.IMPACT_CHOICES, key=lambda x: x[0]) + impacts = ImpactChoice.objects.all().order_by('value') likelihoods = LikelihoodChoice.objects.all().order_by('value') # Erstelle die Matrizen mit den Werten der LikelihoodChoice-Objekte - gross_matrix = {i: {likelihood.value: [] for likelihood in likelihoods} for i, _ in impacts} - net_matrix = {i: {likelihood.value: [] for likelihood in likelihoods} for i, _ in impacts} + gross_matrix = { + impact.value: {likelihood.value: [] for likelihood in likelihoods} + for impact in impacts + } + net_matrix = { + impact.value: {likelihood.value: [] for likelihood in likelihoods} + for impact in impacts + } for r in risks: - gross_matrix[r.impact][r.likelihood.value].append(r) + gross_matrix[r.impact.value][r.likelihood.value].append(r) rr = getattr(r, "residual_risk", None) if rr: - net_matrix[rr.impact][rr.likelihood.value].append(r) + net_matrix[rr.impact.value][rr.likelihood.value].append(r) return render(request, "risks/risk_matrix.html", { "impacts": impacts, diff --git a/templates/risks/item_risk.html b/templates/risks/item_risk.html index 062553a..f9b4828 100644 --- a/templates/risks/item_risk.html +++ b/templates/risks/item_risk.html @@ -107,8 +107,8 @@
@@ -153,8 +153,8 @@
diff --git a/templates/risks/risk_matrix.html b/templates/risks/risk_matrix.html index ba23e7b..a6110fb 100644 --- a/templates/risks/risk_matrix.html +++ b/templates/risks/risk_matrix.html @@ -17,16 +17,18 @@ {% trans "Impact" %} * {% trans "Likelihood" %} {% for likelihood in likelihoods %} - {{ likelihood.value }} ({{ likelihood.name }})
{{ likelihood.description }} + + {{ likelihood.value }} ({{ likelihood.name }})
{{ likelihood.description }} + {% endfor %} - {% for i_val, i_label in impacts reversed %} + {% for impact in impacts reversed %} - {{ i_label }} + {{ impact.value }} - {{ impact.name }}
{{ impact.description }} {% for likelihood in likelihoods %} - {% with s=i_val|mul:likelihood.value %} + {% with s=impact.value|mul:likelihood.value %}
{% if s <= 4 %} @@ -71,18 +73,18 @@ {% trans "Impact" %} / {% trans "Likelihood" %} {% for likelihood in likelihoods %} - {{ likelihood.value }} ({{ likelihood.name }}) + {{ likelihood.value }} ({{ likelihood.name }})
{{ likelihood.description }} {% endfor %} - {% for i_val, i_label in impacts reversed %} + {% for impact in impacts reversed %} - {{ i_label }} + {{ impact.value }} - {{ impact.name }}
{{ impact.description }} {% for likelihood in likelihoods %} - {% with row=gross_matrix|dict_get:i_val %} + {% with row=gross_matrix|dict_get:impact.value %} {% with cell=row|dict_get:likelihood.value %} - {% with s=i_val|mul:likelihood.value %} + {% with s=impact.value|mul:likelihood.value %} {% if cell %}
    @@ -112,33 +114,33 @@ {% trans "Impact" %} / {% trans "Likelihood" %} {% for likelihood in likelihoods %} - {{ likelihood.value }} ({{ likelihood.name }}) + {{ likelihood.value }} ({{ likelihood.name }})
    {{ likelihood.description }} {% endfor %} - {% for i_val, i_label in impacts reversed %} + {% for impact in impacts reversed %} - {{ i_label }} + {{ impact.value }} - {{ impact.name }}
    {{ impact.description }} {% for likelihood in likelihoods %} - {% with row=net_matrix|dict_get:i_val %} - {% with cell=row|dict_get:likelihood.value %} - {% with s=i_val|mul:likelihood.value %} - - {% if cell %} - - {% else %} - - {% endif %} - - {% endwith %} - {% endwith %} + {% with row=net_matrix|dict_get:impact.value %} + {% with cell=row|dict_get:likelihood.value %} + {% with s=impact.value|mul:likelihood.value %} + + {% if cell %} + + {% else %} + + {% endif %} + + {% endwith %} + {% endwith %} {% endwith %} {% endfor %}