mirror of
https://github.com/jazzband/django-auditlog.git
synced 2026-03-16 22:20:26 +00:00
* Add CustomLogEntry model support and update tests: - Added support for CustomLogEntry data model to extend django-auditlog capabilities - Updated existing test cases to align with new model structure and data handling logic - Added new test cases to validate CustomLogEntry behavior, model registration, and signal handling - Ensured backward compatibility with existing LogEntry model where applicable * Update auditlog/__init__.py Co-authored-by: Youngkwang Yang <me@youngkwang.dev> * run only one custom model test matrix (#761) --------- Co-authored-by: Youngkwang Yang <me@youngkwang.dev>
179 lines
6.1 KiB
Python
179 lines
6.1 KiB
Python
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('<a href="{}">{}</a>', 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(
|
|
'<a href="{}">{} - {}</a>', 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(
|
|
'<a href="{}" title="{}">{}</a>', 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(
|
|
"<path:object_id>/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('<a href="{}">{}</a>', url, _("View"))
|