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