From a5a31f4dcf9d2f5ac9da0b751b5e08441580fb16 Mon Sep 17 00:00:00 2001 From: Kevin Heyer Date: Mon, 22 Sep 2025 09:44:51 +0200 Subject: [PATCH] Change Likelihood from Map_Choices to DB --- db.sqlite3 | Bin 278528 -> 262144 bytes risks/admin.py | 9 +++ risks/models/__init__.py | 3 +- risks/models/likelihood_choice.py | 13 ++++ risks/models/residual_risk.py | 11 +++- risks/models/risk.py | 16 ++--- risks/templatetags/risk_extras.py | 55 ++++++++++------ risks/views.py | 15 +++-- templates/risks/item_risk.html | 8 +-- templates/risks/risk_matrix.html | 104 +++++++++++------------------- 10 files changed, 127 insertions(+), 107 deletions(-) create mode 100644 risks/models/likelihood_choice.py diff --git a/db.sqlite3 b/db.sqlite3 index 16fc25284c30781f6b420dc8fc50c8823af5acd1..55eb1e34204a9d903c208ad23d63a0e92dc3fb46 100644 GIT binary patch delta 9209 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 literal 278528 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 diff --git a/risks/admin.py b/risks/admin.py index 3351370..c0c4fe7 100644 --- a/risks/admin.py +++ b/risks/admin.py @@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _ from .models import ( Control, Incident, + LikelihoodChoice, Notification, NotificationPreference, NotificationRule, @@ -230,3 +231,11 @@ class UserAdmin(BaseUserAdmin): def responsible_controls_count(self, obj): return obj.controls_responsible.count() responsible_controls_count.short_description = _("Controls Responsible") + + +# --------------------------------------------------------------------------- +# LikelihoodChoice +# --------------------------------------------------------------------------- +@admin.register(LikelihoodChoice) +class LikelihoodChoiceAdmin(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 5c124ff..fd79736 100644 --- a/risks/models/__init__.py +++ b/risks/models/__init__.py @@ -1,6 +1,7 @@ from .auditlog import AuditLog from .control import Control from .incident import Incident +from .likelihood_choice import LikelihoodChoice from .notification import Notification from .notification_kind import NotificationKind from .notification_preference import NotificationPreference @@ -10,4 +11,4 @@ from .risk import Risk from .user import User -__all__ = ["AuditLog", "Control", "Incident", "Notification", "NotificationKind", "NotificationPreference", "NotificationRule", "ResidualRisk", "Risk", "User"] \ No newline at end of file +__all__ = ["AuditLog", "Control", "Incident", "LikelihoodChoice", "Notification", "NotificationKind", "NotificationPreference", "NotificationRule", "ResidualRisk", "Risk", "User"] \ No newline at end of file diff --git a/risks/models/likelihood_choice.py b/risks/models/likelihood_choice.py new file mode 100644 index 0000000..fa624ff --- /dev/null +++ b/risks/models/likelihood_choice.py @@ -0,0 +1,13 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +class LikelihoodChoice(models.Model): + """ + Likelihood choices for Risks and Controls. + """ + name = models.CharField(_("Likelihood 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 15c2140..fe7eaeb 100644 --- a/risks/models/residual_risk.py +++ b/risks/models/residual_risk.py @@ -1,7 +1,9 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from .likelihood_choice import LikelihoodChoice from .risk import Risk + # --------------------------------------------------------------------------- # Residual Risk # --------------------------------------------------------------------------- @@ -13,7 +15,12 @@ class ResidualRisk(models.Model): verbose_name_plural = _("Residual Risks") risk = models.OneToOneField("Risk", on_delete=models.CASCADE, related_name="residual_risk") - likelihood = models.IntegerField(choices=Risk.LIKELIHOOD_CHOICES, default=1) + likelihood = models.ForeignKey( + LikelihoodChoice, + on_delete=models.PROTECT, + verbose_name=_("Likelihood"), + related_name="residual_risks", + ) impact = models.IntegerField(choices=Risk.IMPACT_CHOICES, default=1) score = models.IntegerField(editable=False) level = models.CharField(max_length=50, editable=False) @@ -29,7 +36,7 @@ class ResidualRisk(models.Model): self.status = "review_required" # Calculate residual risk score and level - self.score = self.likelihood * self.impact + self.score = self.likelihood.value * self.impact if self.score <= 4: self.level = "Low" elif self.score <= 8: diff --git a/risks/models/risk.py b/risks/models/risk.py index dfc0ccd..c8b9406 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 .likelihood_choice import LikelihoodChoice # --------------------------------------------------------------------------- # Risk @@ -18,12 +19,6 @@ class Risk(models.Model): ("closed", _("Closed")), ("review_required", _("Review required")), ] - LIKELIHOOD_CHOICES = [ - (1, _("Very low – occurs less than once every 5 years")), - (2, _("Low – once every 1–5 years")), - (3, _("Likely – once per year or more")), - (4, _("Very likely – multiple times per year/monthly")), - ] IMPACT_CHOICES = [ (1, _("Very Low (< 1,000 € – minor operational impact)")), (2, _("Low (1,000–5,000 € – local impact)")), @@ -58,7 +53,12 @@ class Risk(models.Model): cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True) # Risk evaluation before controls - likelihood = models.IntegerField(choices=LIKELIHOOD_CHOICES, default=1) + likelihood = models.ForeignKey( + LikelihoodChoice, + on_delete=models.PROTECT, + verbose_name=_("Likelihood"), + related_name="risks", + ) impact = models.IntegerField(choices=IMPACT_CHOICES, default=1) # Calculated fields @@ -85,7 +85,7 @@ class Risk(models.Model): self.status = "review_required" # Calculate risk score and level - self.score = self.likelihood * self.impact + self.score = self.likelihood.value * self.impact if self.score <= 4: self.level = "Low" elif self.score <= 8: diff --git a/risks/templatetags/risk_extras.py b/risks/templatetags/risk_extras.py index 872ce70..3560576 100644 --- a/risks/templatetags/risk_extras.py +++ b/risks/templatetags/risk_extras.py @@ -1,16 +1,50 @@ from django import template from django.utils.html import format_html -from ..models import Control, Incident, Risk +from ..models import Control, 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) -_LIKELIHOOD_LABELS = dict(Risk.LIKELIHOOD_CHOICES) _IMPACT_LABELS = dict(Risk.IMPACT_CHOICES) LEVEL_ID_MAP = {"Low": 1, "Medium": 2, "High": 3, "Critical": 4} +# Likelihood +def get_likelihood_label(likelihood_obj): + if not likelihood_obj: + return "" + return f"{likelihood_obj.value} ({likelihood_obj.name})" + +@register.filter +def likelihood_id_label(likelihood): + if isinstance(likelihood, LikelihoodChoice): + return f"{likelihood.value} ({likelihood.name})" + return likelihood + +def get_likelihood_value(likelihood): + if isinstance(likelihood, LikelihoodChoice): + return likelihood.value + return likelihood + +@register.filter +def likelihood_value(likelihood_obj): + return get_likelihood_value(likelihood_obj) + +def get_likelihood_class(likelihood): + value = get_likelihood_value(likelihood) + LIKELIHOOD_MAP = { + 1: "is-control-verylow", + 2: "is-control-low", + 3: "is-control-mid", + 4: "is-control-high", + } + return LIKELIHOOD_MAP.get(value, "is-light") + +@register.filter +def likelihood_class(likelihood): + return get_likelihood_class(likelihood) + @register.simple_tag def sort_url(request, field, current_sort, current_dir): query = request.GET.copy() @@ -56,16 +90,6 @@ def _short(label: str) -> str: return label.split(sep, 1)[0].strip() return label.strip() -@register.filter -def likelihood_id_label(val): - try: - i = int(val) - except (TypeError, ValueError): - return "" - label = _LIKELIHOOD_LABELS.get(i, "") - short = _short(str(label)) if label else "" - return format_html("{} ({})", i, short) if label else format_html("{}", i) - @register.filter def impact_id_label(val): try: @@ -115,13 +139,6 @@ LEVEL_MAP = { "Critical": "is-control-veryhigh", } -@register.filter -def likelihood_class(val): - try: - return LIKELIHOOD_MAP.get(int(val), "is-light") - except (TypeError, ValueError): - return "is-light" - @register.filter def impact_class(val): try: diff --git a/risks/views.py b/risks/views.py index 2ffd5ab..ed48310 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, Notification +from .models import AuditLog, Risk, Control, ResidualRisk, AuditLog, Incident, LikelihoodChoice, Notification from .serializers import ( ControlSerializer, RiskSerializer, ResidualRiskSerializer, UserSerializer, AuditSerializer, IncidentSerializer, @@ -467,20 +467,21 @@ 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]) - likelihoods = sorted(Risk.LIKELIHOOD_CHOICES, key=lambda x: x[0]) + likelihoods = LikelihoodChoice.objects.all().order_by('value') - gross_matrix = {i: {l: [] for l, _ in likelihoods} for i, _ in impacts} - net_matrix = {i: {l: [] for l, _ in likelihoods} for i, _ in impacts} + # 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} for r in risks: - gross_matrix[r.impact][r.likelihood].append(r) + gross_matrix[r.impact][r.likelihood.value].append(r) rr = getattr(r, "residual_risk", None) if rr: - net_matrix[rr.impact][rr.likelihood].append(r) + net_matrix[rr.impact][rr.likelihood.value].append(r) return render(request, "risks/risk_matrix.html", { "impacts": impacts, "likelihoods": likelihoods, "gross_matrix": gross_matrix, "net_matrix": net_matrix, - }) + }) \ No newline at end of file diff --git a/templates/risks/item_risk.html b/templates/risks/item_risk.html index 88785d9..062553a 100644 --- a/templates/risks/item_risk.html +++ b/templates/risks/item_risk.html @@ -98,8 +98,8 @@
@@ -144,8 +144,8 @@
diff --git a/templates/risks/risk_matrix.html b/templates/risks/risk_matrix.html index ac2ef6c..ba23e7b 100644 --- a/templates/risks/risk_matrix.html +++ b/templates/risks/risk_matrix.html @@ -8,10 +8,7 @@ {% trans "Risk Matrix" %} {% trans "Detail View" %} -
- -
@@ -19,8 +16,8 @@ {% trans "Impact" %} * {% trans "Likelihood" %} - {% for l_val, l_label in likelihoods %} - {{ l_label }} + {% for likelihood in likelihoods %} + {{ likelihood.value }} ({{ likelihood.name }})
{{ likelihood.description }} {% endfor %} @@ -28,8 +25,8 @@ {% for i_val, i_label in impacts reversed %} {{ i_label }} - {% for l_val, l_label in likelihoods %} - {% with s=i_val|mul:l_val %} + {% for likelihood in likelihoods %} + {% with s=i_val|mul:likelihood.value %}
{% if s <= 4 %} @@ -49,12 +46,9 @@ {% endfor %} -
- +
-
- +
- {% for l_val, l_label in likelihoods %} - + {% for likelihood in likelihoods %} + {% endfor %} @@ -86,41 +79,40 @@ {% for i_val, i_label in impacts reversed %} - {% for l_val, l_label in likelihoods %} + {% for likelihood in likelihoods %} {% with row=gross_matrix|dict_get:i_val %} - {% with cell=row|dict_get:l_val %} - {% with s=i_val|mul:l_val %} - - {% endwith %} - {% endwith %} + {% with cell=row|dict_get:likelihood.value %} + {% with s=i_val|mul:likelihood.value %} + + {% endwith %} + {% endwith %} {% endwith %} {% endfor %} {% endfor %}
{% trans "Impact" %} / {% trans "Likelihood" %}{{ l_label }}{{ likelihood.value }} ({{ likelihood.name }})
{{ i_label }} - {% if cell %} - - {% else %} - - {% endif %} - + {% if cell %} + + {% else %} + + {% endif %} +
-
- + - - - - -
- - - + + + + - - {% endblock %}