from urllib.parse import unquote from django import urls as urlresolvers from django.conf import settings from django.contrib import admin from django.contrib.admin.views.main import PAGE_VAR from django.core.exceptions import PermissionDenied from django.http import HttpRequest from django.template.response import TemplateResponse from django.urls import path, reverse from django.urls.exceptions import NoReverseMatch from django.utils.html import format_html from django.utils.text import capfirst from django.utils.timezone import is_aware, localtime from django.utils.translation import gettext_lazy as _ from auditlog import get_logentry_model from auditlog.render import get_field_verbose_name, render_logentry_changes_html from auditlog.signals import accessed LogEntry = get_logentry_model() MAX = 75 class LogEntryAdminMixin: request: HttpRequest CID_TITLE = _("Click to filter by records with this correlation id") @admin.display(description=_("Created")) def created(self, obj): if is_aware(obj.timestamp): return localtime(obj.timestamp) return obj.timestamp @admin.display(description=_("User")) def user_url(self, obj): if obj.actor: app_label, model = settings.AUTH_USER_MODEL.split(".") viewname = f"admin:{app_label}_{model.lower()}_change" try: link = urlresolvers.reverse(viewname, args=[obj.actor.pk]) except NoReverseMatch: return "%s" % (obj.actor) return format_html('{}', link, obj.actor) return "system" @admin.display(description=_("Resource")) def resource_url(self, obj): app_label, model = obj.content_type.app_label, obj.content_type.model viewname = f"admin:{app_label}_{model}_change" try: args = [obj.object_pk] if obj.object_id is None else [obj.object_id] link = urlresolvers.reverse(viewname, args=args) except NoReverseMatch: return obj.object_repr else: return format_html( '{} - {}', link, obj.content_type, obj.object_repr ) @admin.display(description=_("Changes")) def msg_short(self, obj): if obj.action in [LogEntry.Action.DELETE, LogEntry.Action.ACCESS]: return "" # delete changes = obj.changes_dict s = "" if len(changes) == 1 else "s" fields = ", ".join(changes.keys()) if len(fields) > MAX: i = fields.rfind(" ", 0, MAX) fields = fields[:i] + " .." return "%d change%s: %s" % (len(changes), s, fields) @admin.display(description=_("Changes")) def msg(self, obj): return render_logentry_changes_html(obj) @admin.display(description="Correlation ID") def cid_url(self, obj): cid = obj.cid if cid: url = self._add_query_parameter("cid", cid) return format_html( '{}', url, self.CID_TITLE, cid ) def _add_query_parameter(self, key: str, value: str): full_path = self.request.get_full_path() delimiter = "&" if "?" in full_path else "?" return f"{full_path}{delimiter}{key}={value}" def field_verbose_name(self, obj, field_name: str): """ Use `auditlog.render.get_field_verbose_name` instead. This method is kept for backward compatibility. """ return get_field_verbose_name(obj, field_name) class LogAccessMixin: def render_to_response(self, context, **response_kwargs): obj = self.get_object() accessed.send(obj.__class__, instance=obj) return super().render_to_response(context, **response_kwargs) class AuditlogHistoryAdminMixin: """ Add an audit log history view to a model admin. """ auditlog_history_template = "auditlog/object_history.html" show_auditlog_history_link = False auditlog_history_per_page = 10 def get_list_display(self, request): list_display = list(super().get_list_display(request)) if self.show_auditlog_history_link and "auditlog_link" not in list_display: list_display.append("auditlog_link") return list_display def get_urls(self): opts = self.model._meta info = opts.app_label, opts.model_name my_urls = [ path( "/auditlog/", self.admin_site.admin_view(self.auditlog_history_view), name="%s_%s_auditlog" % info, ) ] return my_urls + super().get_urls() def auditlog_history_view(self, request, object_id, extra_context=None): obj = self.get_object(request, unquote(object_id)) if not self.has_view_permission(request, obj): raise PermissionDenied log_entries = ( LogEntry.objects.get_for_object(obj) .select_related("actor") .order_by("-timestamp") ) paginator = self.get_paginator( request, log_entries, self.auditlog_history_per_page ) page_number = request.GET.get(PAGE_VAR, 1) page_obj = paginator.get_page(page_number) page_range = paginator.get_elided_page_range(page_obj.number) context = { **self.admin_site.each_context(request), "title": _("Audit log: %s") % obj, "module_name": str(capfirst(self.model._meta.verbose_name_plural)), "page_range": page_range, "page_var": PAGE_VAR, "pagination_required": paginator.count > self.auditlog_history_per_page, "object": obj, "opts": self.model._meta, "log_entries": page_obj, **(extra_context or {}), } return TemplateResponse(request, self.auditlog_history_template, context) @admin.display(description=_("Audit log")) def auditlog_link(self, obj): opts = self.model._meta url = reverse( f"admin:{opts.app_label}_{opts.model_name}_auditlog", args=[obj.pk], ) return format_html('{}', url, _("View"))