diff --git a/db.sqlite3 b/db.sqlite3 index 16fc252..55eb1e3 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ 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 %}