Change Impact_Choices to DB

This commit is contained in:
Kevin Heyer 2025-09-22 10:14:21 +02:00
parent a5a31f4dcf
commit a314816150
10 changed files with 138 additions and 68 deletions

Binary file not shown.

View file

@ -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")

View file

@ -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"]
__all__ = ["AuditLog", "Control", "ImpactChoice", "Incident", "LikelihoodChoice", "Notification", "NotificationKind", "NotificationPreference", "NotificationRule", "ResidualRisk", "Risk", "User"]

View file

@ -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})"

View file

@ -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)

View file

@ -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,0005,000 € local impact)")),
(3, _("High (5,00015,000 € team-level impact)")),
(4, _("Severe (50,000100,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)

View file

@ -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")

View file

@ -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,

View file

@ -107,8 +107,8 @@
<div class="column is-half">
<button class="risk-chip {{ risk.impact|impact_class }}" type="button">
<span class="chip-head">{% trans "Impact" %}</span>
<span class="chip-id">{{ risk.impact }}</span>
<span class="chip-label">{{ risk.get_impact_display }}</span>
<span class="chip-id">{{ risk.impact.value }} - {{ risk.impact.name }}</span>
<span class="chip-label">{{ risk.impact.description }}</span>
</button>
</div>
@ -153,8 +153,8 @@
<div class="column is-half">
<button class="risk-chip {{ risk.residual_risk.impact|impact_class }}" type="button">
<span class="chip-head">{% trans "Impact" %}</span>
<span class="chip-id">{{ risk.residual_risk.impact }}</span>
<span class="chip-label">{{ risk.residual_risk.get_impact_display }}</span>
<span class="chip-id">{{ risk.residual_risk.impact.value }} - {{ risk.residual_risk.impact.name }}</span>
<span class="chip-label">{{ risk.residual_risk.impact.description }}</span>
</button>
</div>

View file

@ -17,16 +17,18 @@
<tr>
<th class="has-text-left">{% trans "Impact" %} * {% trans "Likelihood" %}</th>
{% for likelihood in likelihoods %}
<th class="{{ likelihood.value|likelihood_class|to_bg }}">{{ likelihood.value }} ({{ likelihood.name }}) <br> {{ likelihood.description }}</th>
<th class="{{ likelihood.value|likelihood_class|to_bg }}">
{{ likelihood.value }} ({{ likelihood.name }}) <br> {{ likelihood.description }}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for i_val, i_label in impacts reversed %}
{% for impact in impacts reversed %}
<tr>
<th class="has-text-left {{ i_val|impact_class|to_bg }}">{{ i_label }}</th>
<th class="has-text-left {{ impact.value|impact_class|to_bg }}">{{ impact.value }} - {{ impact.name }}<br>{{ impact.description }}</th>
{% for likelihood in likelihoods %}
{% with s=i_val|mul:likelihood.value %}
{% with s=impact.value|mul:likelihood.value %}
<td class="risk-matrix-cell {{ s|score_bg_class }}">
<div class="is-flex is-justify-content-center is-align-items-center">
{% if s <= 4 %}
@ -71,18 +73,18 @@
<tr>
<th class="has-text-left">{% trans "Impact" %} / {% trans "Likelihood" %}</th>
{% for likelihood in likelihoods %}
<th class="{{ likelihood.value|likelihood_class|to_bg }}">{{ likelihood.value }} ({{ likelihood.name }})</th>
<th class="{{ likelihood.value|likelihood_class|to_bg }}">{{ likelihood.value }} ({{ likelihood.name }}) <br> {{ likelihood.description }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for i_val, i_label in impacts reversed %}
{% for impact in impacts reversed %}
<tr>
<th class="has-text-left {{ i_val|impact_class|to_bg }}">{{ i_label }}</th>
<th class="has-text-left {{ impact.value|impact_class|to_bg }}">{{ impact.value }} - {{ impact.name }}<br>{{ impact.description }}</th>
{% 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 %}
<td class="risk-matrix-cell {{ likelihood|likelihood_class }}">
{% if cell %}
<ul class="risk-cell-list">
@ -112,33 +114,33 @@
<tr>
<th class="has-text-left">{% trans "Impact" %} / {% trans "Likelihood" %}</th>
{% for likelihood in likelihoods %}
<th class="{{ likelihood.value|likelihood_class|to_bg }}">{{ likelihood.value }} ({{ likelihood.name }})</th>
<th class="{{ likelihood.value|likelihood_class|to_bg }}">{{ likelihood.value }} ({{ likelihood.name }}) <br> {{ likelihood.description }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for i_val, i_label in impacts reversed %}
{% for impact in impacts reversed %}
<tr>
<th class="has-text-left {{ i_val|impact_class|to_bg }}">{{ i_label }}</th>
<th class="has-text-left {{ impact.value|impact_class|to_bg }}">{{ impact.value }} - {{ impact.name }}<br>{{ impact.description }}</th>
{% 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 %}
<td class="risk-matrix-cell {{ likelihood|likelihood_class }}">
{% if cell %}
<ul class="risk-cell-list">
{% for risk in cell %}
<li style="list-style:none;">
<a href="{% url 'risks:show_risk' risk.id %}" class="tag">{{ risk.title }}</a>
</li>
{% endfor %}
</ul>
{% else %}
<span class="has-text-grey"></span>
{% endif %}
</td>
{% endwith %}
{% endwith %}
{% with row=net_matrix|dict_get:impact.value %}
{% with cell=row|dict_get:likelihood.value %}
{% with s=impact.value|mul:likelihood.value %}
<td class="risk-matrix-cell {{ likelihood|likelihood_class }}">
{% if cell %}
<ul class="risk-cell-list">
{% for risk in cell %}
<li style="list-style:none;">
<a href="{% url 'risks:show_risk' risk.id %}" class="tag">{{ risk.title }}</a>
</li>
{% endfor %}
</ul>
{% else %}
<span class="has-text-grey"></span>
{% endif %}
</td>
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
</tr>