ISO-27001-Risk-Management/risks/admin.py
2025-09-22 09:44:51 +02:00

241 lines
No EOL
9.3 KiB
Python

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from .models import (
Control,
Incident,
LikelihoodChoice,
Notification,
NotificationPreference,
NotificationRule,
Risk,
ResidualRisk,
User,
)
# ---------------------------------------------------------------------------
# Global Admin Settings
# ---------------------------------------------------------------------------
admin.site.site_header = _("Administration")
admin.site.site_title = _("Admin")
admin.site.index_title = _("Administration")
# ---- Inlines ----
class NotificationPreferenceInline(admin.StackedInline):
"""Preferences inline for notifications on User model"""
model = NotificationPreference
can_delete = False
extra = 0
fieldsets = (
(_("Risks"), {"fields": ("risk_created", "risk_updated", "risk_deleted")}),
(_("Controls"), {"fields": ("control_created", "control_updated", "control_deleted")}),
(_("Residual risks"), {"fields": ("residual_created", "residual_updated", "residual_deleted")}),
(_("Reviews"), {"fields": ("review_required", "review_completed")}),
(_("Incidents"), {"fields": ("incident_created", "incident_updated", "incident_deleted")}),
(_("Users"), {"fields": ("user_created", "user_deleted")}),
)
class ResidualRiskInline(admin.StackedInline):
"""Inline editor for ResidualRisk (one-to-one with Risk)"""
model = ResidualRisk
extra = 0
can_delete = False
readonly_fields = ("score", "level", "review_required")
fields = ("likelihood", "impact", "score", "level", "review_required")
class ControlRisksInline(admin.TabularInline):
"""M2M relation between Risk and Control"""
model = Control.risks.through
fk_name = "risk"
extra = 1
autocomplete_fields = ("control",)
class NotificationInline(admin.TabularInline):
"""Inline display of notifications on User model"""
model = Notification
fields = ("created_at", "message", "read", "sent")
readonly_fields = ("created_at", "message")
extra = 0
ordering = ("-created_at",)
# ---- Shared Mixins ----
class ChangedByMixin:
"""Automatically track user who created/changed/deleted"""
def save_model(self, request, obj, form, change):
obj._changed_by = request.user
super().save_model(request, obj, form, change)
def delete_model(self, request, obj):
obj._changed_by = request.user
super().delete_model(request, obj)
class RedirectOnSaveMixin:
"""Redirect to detail view instead of staying in admin"""
redirect_url_name = None
def response_add(self, request, obj, post_url_continue=None):
return HttpResponseRedirect(reverse(self.redirect_url_name, args=[obj.pk]))
def response_change(self, request, obj):
return HttpResponseRedirect(reverse(self.redirect_url_name, args=[obj.pk]))
# ---------------------------------------------------------------------------
# Risk
# ---------------------------------------------------------------------------
@admin.register(Risk)
class RiskAdmin(ChangedByMixin, RedirectOnSaveMixin, admin.ModelAdmin):
redirect_url_name = "risks:show_risk"
list_display = ("title", "owner_name", "score", "level", "likelihood", "impact", "follow_up", "status")
list_filter = ("status", "level", "likelihood", "impact", "owner")
search_fields = ("title", "asset", "process", "category")
inlines = [ResidualRiskInline, ControlRisksInline]
def owner_name(self, obj):
if not obj.owner:
return "-"
return obj.owner.get_full_name() or obj.owner.username
# ---------------------------------------------------------------------------
# Residual Risk
# ---------------------------------------------------------------------------
@admin.register(ResidualRisk)
class ResidualRiskAdmin(ChangedByMixin, RedirectOnSaveMixin, admin.ModelAdmin):
redirect_url_name = "risks:show_risk"
list_display = ("risk", "score", "level", "likelihood", "impact", "review_required")
list_filter = ("level", "likelihood", "impact", "review_required")
# ---------------------------------------------------------------------------
# Control
# ---------------------------------------------------------------------------
@admin.register(Control)
class ControlAdmin(ChangedByMixin, RedirectOnSaveMixin, admin.ModelAdmin):
redirect_url_name = "risks:show_control"
list_display = ("title", "status", "due_date", "responsible")
list_filter = ("status", "due_date")
autocomplete_fields = ("risks", "responsible")
search_fields = ("title", "description")
# ---------------------------------------------------------------------------
# Incident
# ---------------------------------------------------------------------------
@admin.register(Incident)
class IncidentAdmin(ChangedByMixin, RedirectOnSaveMixin, admin.ModelAdmin):
redirect_url_name = "risks:show_incident"
list_display = ("title", "date_reported", "reported_by", "status")
list_filter = ("status", "date_reported", "reported_by")
search_fields = ("title", "description")
autocomplete_fields = ("related_risks",)
filter_horizontal = ("related_risks",)
def get_changeform_initial_data(self, request):
initial = super().get_changeform_initial_data(request)
risk_id = request.GET.get("related_risks")
if risk_id:
initial["related_risks"] = [risk_id]
return initial
# ---------------------------------------------------------------------------
# Notification
# ---------------------------------------------------------------------------
@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
date_hierarchy = "created_at"
list_display = ("id", "created_at", "user_display", "short_message", "read", "sent")
list_display_links = ("id", "short_message")
list_filter = ("read", "sent", "created_at")
search_fields = ("message", "user__username", "user__first_name", "user__last_name", "user__email")
list_select_related = ("user",)
list_editable = ("read", "sent")
ordering = ("-created_at",)
autocomplete_fields = ("user",)
actions = ["mark_as_read", "mark_as_unread", "mark_as_sent", "mark_as_unsent"]
@admin.display(description=_("User"))
def user_display(self, obj):
return obj.user.get_full_name() if obj.user else ""
@admin.display(description=_("Message"))
def short_message(self, obj):
msg = obj.message or ""
return (msg[:80] + "") if len(msg) > 80 else msg
# Bulk actions
@admin.action(description=_("Mark selected as read"))
def mark_as_read(self, request, queryset):
n = queryset.update(read=True)
self.message_user(request, _("%(n)d notifications marked as read.") % {"n": n})
@admin.action(description=_("Mark selected as unread"))
def mark_as_unread(self, request, queryset):
n = queryset.update(read=False)
self.message_user(request, _("%(n)d notifications marked as unread.") % {"n": n})
@admin.action(description=_("Mark selected as sent"))
def mark_as_sent(self, request, queryset):
n = queryset.update(sent=True)
self.message_user(request, _("%(n)d notifications marked as sent.") % {"n": n})
@admin.action(description=_("Mark selected as unsent"))
def mark_as_unsent(self, request, queryset):
n = queryset.update(sent=False)
self.message_user(request, _("%(n)d notifications marked as unsent.") % {"n": n})
# ---- Notification Rule ----
@admin.register(NotificationRule)
class NotificationRuleAdmin(admin.ModelAdmin):
list_display = ("kind", "enabled_in_app", "enabled_email", "to_owner", "to_staff", "short_extras")
list_editable = ("enabled_in_app", "enabled_email", "to_owner", "to_staff")
list_filter = ("enabled_in_app", "enabled_email", "to_owner", "to_staff")
search_fields = ("kind", "extra_recipients")
ordering = ("kind",)
@admin.display(description=_("Extra recipients"))
def short_extras(self, obj):
txt = (obj.extra_recipients or "").replace("\n", ", ")
return (txt[:50] + "") if len(txt) > 50 else txt
# ---------------------------------------------------------------------------
# User (extension)
# ---------------------------------------------------------------------------
@admin.register(User)
class UserAdmin(BaseUserAdmin):
fieldsets = BaseUserAdmin.fieldsets + (
(_("SSO Information"), {"fields": ("is_sso_user",)}),
)
list_display = (
"username", "email", "is_staff", "is_superuser", "is_sso_user",
"owned_risks_count", "responsible_controls_count"
)
inlines = [NotificationInline, NotificationPreferenceInline]
def owned_risks_count(self, obj):
return obj.risks_owned.count()
owned_risks_count.short_description = _("Risks Owned")
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")