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 @@
{% trans "Likelihood" %}
- {{ risk.likelihood }}
- {{ risk.get_likelihood_display }}
+ {{ risk.likelihood.value }} - {{ risk.likelihood.name }}
+ {{ risk.likelihood.description }}
@@ -144,8 +144,8 @@
{% trans "Likelihood" %}
- {{ risk.residual_risk.likelihood }}
- {{ risk.residual_risk.get_likelihood_display }}
+ {{ risk.residual_risk.likelihood.value }} - {{ risk.residual_risk.likelihood.name }}
+ {{ risk.residual_risk.likelihood.description }}
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 %}
-
-
+
-
-
{% trans "Show" %}:
@@ -69,16 +63,15 @@
-
-
+
{% trans "Impact" %} / {% trans "Likelihood" %}
- {% for l_val, l_label in likelihoods %}
- {{ l_label }}
+ {% for likelihood in likelihoods %}
+ {{ likelihood.value }} ({{ likelihood.name }})
{% endfor %}
@@ -86,41 +79,40 @@
{% for i_val, i_label in impacts reversed %}
{{ i_label }}
- {% 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 %}
-
- {% if cell %}
-
- {% else %}
- –
- {% endif %}
-
- {% endwith %}
- {% endwith %}
+ {% with cell=row|dict_get:likelihood.value %}
+ {% with s=i_val|mul:likelihood.value %}
+
+ {% if cell %}
+
+ {% else %}
+ –
+ {% endif %}
+
+ {% endwith %}
+ {% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
-
-
+
{% trans "Impact" %} / {% trans "Likelihood" %}
- {% for l_val, l_label in likelihoods %}
- {{ l_label }}
+ {% for likelihood in likelihoods %}
+ {{ likelihood.value }} ({{ likelihood.name }})
{% endfor %}
@@ -128,11 +120,11 @@
{% for i_val, i_label in impacts reversed %}
{{ i_label }}
- {% for l_val, l_label in likelihoods %}
+ {% for likelihood in likelihoods %}
{% with row=net_matrix|dict_get:i_val %}
- {% with cell=row|dict_get:l_val %}
- {% with s=i_val|mul:l_val %}
-
+ {% with cell=row|dict_get:likelihood.value %}
+ {% with s=i_val|mul:likelihood.value %}
+
{% if cell %}
{% for risk in cell %}
@@ -153,49 +145,31 @@
{% endfor %}
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
{% endblock %}