diff --git a/db.sqlite3 b/db.sqlite3 index 55eb1e3..99ed676 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ 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 @@