Change Likelihood from Map_Choices to DB
This commit is contained in:
parent
65dc2231bb
commit
a5a31f4dcf
10 changed files with 127 additions and 107 deletions
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
|
@ -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")
|
|
@ -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"]
|
||||
__all__ = ["AuditLog", "Control", "Incident", "LikelihoodChoice", "Notification", "NotificationKind", "NotificationPreference", "NotificationRule", "ResidualRisk", "Risk", "User"]
|
13
risks/models/likelihood_choice.py
Normal file
13
risks/models/likelihood_choice.py
Normal file
|
@ -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})"
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,16 +467,17 @@ 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,
|
||||
|
|
|
@ -98,8 +98,8 @@
|
|||
<div class="column is-half">
|
||||
<button class="risk-chip {{ risk.likelihood|likelihood_class }}" type="button">
|
||||
<span class="chip-head">{% trans "Likelihood" %}</span>
|
||||
<span class="chip-id">{{ risk.likelihood }}</span>
|
||||
<span class="chip-label">{{ risk.get_likelihood_display }}</span>
|
||||
<span class="chip-id">{{ risk.likelihood.value }} - {{ risk.likelihood.name }}</span>
|
||||
<span class="chip-label">{{ risk.likelihood.description }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -144,8 +144,8 @@
|
|||
<div class="column is-half">
|
||||
<button class="risk-chip {{ risk.residual_risk.likelihood|likelihood_class }}" type="button">
|
||||
<span class="chip-head">{% trans "Likelihood" %}</span>
|
||||
<span class="chip-id">{{ risk.residual_risk.likelihood }}</span>
|
||||
<span class="chip-label">{{ risk.residual_risk.get_likelihood_display }}</span>
|
||||
<span class="chip-id">{{ risk.residual_risk.likelihood.value }} - {{ risk.residual_risk.likelihood.name }}</span>
|
||||
<span class="chip-label">{{ risk.residual_risk.likelihood.description }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -8,10 +8,7 @@
|
|||
<a class="is-active" data-tab="matrix">{% trans "Risk Matrix" %}</a>
|
||||
<a data-tab="details">{% trans "Detail View" %}</a>
|
||||
</div>
|
||||
|
||||
<section class="section">
|
||||
|
||||
<!-- Main Container -->
|
||||
<div class="box">
|
||||
<!-- Panel: Matrix View -->
|
||||
<div class="tab-panel" data-tab="matrix">
|
||||
|
@ -19,8 +16,8 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th class="has-text-left">{% trans "Impact" %} * {% trans "Likelihood" %}</th>
|
||||
{% for l_val, l_label in likelihoods %}
|
||||
<th class="{{ l_val|likelihood_class|to_bg }}">{{ l_label }}</th>
|
||||
{% for likelihood in likelihoods %}
|
||||
<th class="{{ likelihood.value|likelihood_class|to_bg }}">{{ likelihood.value }} ({{ likelihood.name }}) <br> {{ likelihood.description }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -28,8 +25,8 @@
|
|||
{% for i_val, i_label in impacts reversed %}
|
||||
<tr>
|
||||
<th class="has-text-left {{ i_val|impact_class|to_bg }}">{{ i_label }}</th>
|
||||
{% 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 %}
|
||||
<td class="risk-matrix-cell {{ s|score_bg_class }}">
|
||||
<div class="is-flex is-justify-content-center is-align-items-center">
|
||||
{% if s <= 4 %}
|
||||
|
@ -49,12 +46,9 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div><!-- Panel: Matrix View End -->
|
||||
|
||||
</div>
|
||||
<!-- Panel: Details View -->
|
||||
<div class="tab-panel is-hidden" data-tab="details">
|
||||
|
||||
<!-- Mode Toggle (Gross / Net) -->
|
||||
<div class="level mb-3">
|
||||
<div class="level-left">
|
||||
<div class="level-item"><strong>{% trans "Show" %}:</strong></div>
|
||||
|
@ -69,16 +63,15 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- Mode Toggle End -->
|
||||
|
||||
</div>
|
||||
<!-- Gross Risk List -->
|
||||
<div class="details-table" data-mode="gross">
|
||||
<table class="table is-bordered is-fullwidth has-text-centered risk-matrix-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-text-left">{% trans "Impact" %} / {% trans "Likelihood" %}</th>
|
||||
{% for l_val, l_label in likelihoods %}
|
||||
<th class="{{ l_val|likelihood_class|to_bg }}">{{ l_label }}</th>
|
||||
{% for likelihood in likelihoods %}
|
||||
<th class="{{ likelihood.value|likelihood_class|to_bg }}">{{ likelihood.value }} ({{ likelihood.name }})</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -86,41 +79,40 @@
|
|||
{% for i_val, i_label in impacts reversed %}
|
||||
<tr>
|
||||
<th class="has-text-left {{ i_val|impact_class|to_bg }}">{{ i_label }}</th>
|
||||
{% 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 %}
|
||||
<td class="risk-matrix-cell {{ s|score_bg_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 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 %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div><!-- Gross Risk List End -->
|
||||
|
||||
</div>
|
||||
<!-- Net Risk List -->
|
||||
<div class="details-table is-hidden" data-mode="net">
|
||||
<table class="table is-bordered is-fullwidth has-text-centered risk-matrix-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="has-text-left">{% trans "Impact" %} / {% trans "Likelihood" %}</th>
|
||||
{% for l_val, l_label in likelihoods %}
|
||||
<th class="{{ l_val|likelihood_class|to_bg }}">{{ l_label }}</th>
|
||||
{% for likelihood in likelihoods %}
|
||||
<th class="{{ likelihood.value|likelihood_class|to_bg }}">{{ likelihood.value }} ({{ likelihood.name }})</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -128,11 +120,11 @@
|
|||
{% for i_val, i_label in impacts reversed %}
|
||||
<tr>
|
||||
<th class="has-text-left {{ i_val|impact_class|to_bg }}">{{ i_label }}</th>
|
||||
{% 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 %}
|
||||
<td class="risk-matrix-cell {{ s|score_bg_class }}">
|
||||
{% 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 %}
|
||||
|
@ -153,49 +145,31 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div><!-- Net Risk List End -->
|
||||
|
||||
</div><!-- Panel: Details View End -->
|
||||
|
||||
</div><!-- Main Container End -->
|
||||
</section><!-- Section End -->
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Handle ERP-style tabs
|
||||
const tabs = document.querySelectorAll('.erp-tabs a[data-tab]');
|
||||
const panels = document.querySelectorAll('.tab-panel');
|
||||
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
|
||||
// Deactivate all
|
||||
tabs.forEach(x => x.classList.remove('is-active'));
|
||||
panels.forEach(p => p.classList.add('is-hidden'));
|
||||
|
||||
// Activate clicked
|
||||
tab.classList.add('is-active');
|
||||
const target = tab.getAttribute('data-tab');
|
||||
const activePanel = document.querySelector(`.tab-panel[data-tab="${target}"]`);
|
||||
if (activePanel) activePanel.classList.remove('is-hidden');
|
||||
});
|
||||
});
|
||||
|
||||
// Handle Gross/Net toggle buttons in "Details" tab
|
||||
const toggles = document.querySelectorAll('.details-toggle');
|
||||
const tables = document.querySelectorAll('.details-table');
|
||||
|
||||
toggles.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const mode = btn.getAttribute('data-mode'); // "gross" or "net"
|
||||
|
||||
// Toggle active state on buttons
|
||||
const mode = btn.getAttribute('data-mode');
|
||||
toggles.forEach(b => b.classList.toggle('is-active', b === btn));
|
||||
|
||||
// Show the right table
|
||||
tables.forEach(t => {
|
||||
t.classList.toggle('is-hidden', t.getAttribute('data-mode') !== mode);
|
||||
});
|
||||
|
@ -203,6 +177,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
Loading…
Add table
Reference in a new issue