diff --git a/auditlog/admin.py b/auditlog/admin.py index d5ad9cd..40b25bf 100644 --- a/auditlog/admin.py +++ b/auditlog/admin.py @@ -1,5 +1,6 @@ from functools import cached_property +import re from django.apps import apps from django.contrib import admin from django.contrib.auth import get_user_model @@ -11,6 +12,10 @@ from auditlog.filters import CIDFilter, ResourceTypeFilter from auditlog.mixins import LogEntryAdminMixin from auditlog.models import LogEntry +STRUCTURED_SEARCH_PATTERN = re.compile( + r"^(?P[A-Za-z_][A-Za-z0-9_]*):(?P[1-9][0-9]*)$" +) + @admin.register(LogEntry) class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): @@ -61,54 +66,63 @@ class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): self.request = request queryset = super().get_queryset(request=request) - # Check for the `ss` parameter for structured search - structured_search = request.GET.get("ss") - if structured_search: - # Parse structured search term as 'ModelName:id' - try: - model_name, object_id = structured_search.split(":") - object_id = int(object_id) - except (ValueError, TypeError): - # If the format is incorrect, return an empty queryset and show a message - if not getattr(request, "_message_shown", False): - self.message_user( - request, - "Structured search format must be 'ModelName:id'.", - level="warning", - ) - request._message_shown = True - return queryset.none() + # Custom tampering with Django Admin search. + # We support a structured search here to allow + # searching for log entries related to a specific object. + search = request.GET.get("q") + if search: + match = STRUCTURED_SEARCH_PATTERN.match(search) + if match: + try: + model_name = match.group("model_name") + object_id = int(match.group("id")) + except (ValueError, TypeError): + # If the format is incorrect, return an empty queryset and show a message + if not getattr(request, "_message_shown", False): + self.message_user( + request, + "Structured search format must be 'ModelName:id'.", + level="warning", + ) + request._message_shown = True + return queryset.none() - # Attempt to retrieve the specified model - try: - model = apps.get_model(app_label="api", model_name=model_name) - if not model: - raise LookupError - except LookupError: - if not getattr(request, "_message_shown", False): - self.message_user( - request, - f"Model '{model_name}' does not exist.", - level="warning", + # Attempt to retrieve the specified model + try: + model = apps.get_model( + app_label="api", model_name=model_name ) - request._message_shown = True - return queryset.none() + if not model: + raise LookupError + except LookupError: + if not getattr(request, "_message_shown", False): + self.message_user( + request, + f"Model '{model_name}' does not exist.", + level="warning", + ) + request._message_shown = True + return queryset.none() - # Attempt to retrieve the object and filter log entries - try: - model.objects.only("id").get(pk=object_id) # Lookup. - content_type = ContentType.objects.get_for_model(model) - queryset = queryset.filter( - content_type=content_type, object_id=object_id - ) - except ObjectDoesNotExist: - if not getattr(request, "_message_shown", False): - self.message_user( - request, - f"{model_name} instance with ID {object_id} does not exist.", - level="warning", + # Attempt to retrieve the object and filter log entries + try: + model.objects.only("id").get(pk=object_id) # Lookup. + content_type = ContentType.objects.get_for_model(model) + queryset = queryset.filter( + content_type=content_type, object_id=object_id ) - request._message_shown = True - return queryset.none() + # We need to remove `q` from the request to prevent Django + # from trying to search again with structured search query. + request.GET = request.GET.copy() + request.GET.pop("q", None) + except ObjectDoesNotExist: + if not getattr(request, "_message_shown", False): + self.message_user( + request, + f"{model_name} instance with ID {object_id} does not exist.", + level="warning", + ) + request._message_shown = True + return queryset.none() return queryset # Return filtered or default queryset based on the presence of `ss`