Paginated audit log API for staff and the compliance export

Python · Django · beginner · modification

Adds the audit log API the staff audit page has been waiting for, with the paging the compliance export needs. Plain Django view on session auth: anonymous 401, non-staff 403. `page` and `page_size` are validated up front (400 with a generic message), the ordering has an `id` tie-breaker so entries that share a timestamp keep a stable order across pages, `select_related` keeps the actor lookup out of the loop, and a page past the end skips the row query. Checked on staging (30 entries): one page with all 30 and `has_more` false; bad parameters return 400.

Django 5.1 on PostgreSQL 16, `USE_TZ = True`, session auth. `audit/models.py` (existing, with its migration) defines `AuditEntry`: `actor` (foreign key to `User`, `null=True`, `on_delete=SET_NULL`), `action` (`CharField`) and `created_at` (`DateTimeField`), with an index on (`created_at`, `id`). Entries are inserted by the existing `audit.record()` helper and are never deleted; only `actor` is cleared when its user is deleted. The `audit` app is already in `INSTALLED_APPS`; the project URLconf is `config/urls.py`. Production holds about 40,000 entries; the staging seed has 30.

Requirements

Files touched

--- audit/views.py
+from django.http import JsonResponse
+from django.views.decorators.http import require_GET
+
+from .models import AuditEntry
+
+DEFAULT_PAGE_SIZE = 50
+MAX_PAGE_SIZE = 200
+
+
+def _bad_pagination():
+    return JsonResponse({"error": "invalid pagination"}, status=400)
+
+
+@require_GET
+def audit_log(request):
+    if not request.user.is_authenticated:
+        return JsonResponse({"error": "authentication required"}, status=401)
+    if not request.user.is_staff:
+        return JsonResponse({"error": "forbidden"}, status=403)
+
+    try:
+        page = int(request.GET.get("page", 1))
+        page_size = int(request.GET.get("page_size", DEFAULT_PAGE_SIZE))
+    except ValueError:
+        return _bad_pagination()
+    if page < 1 or not 1 <= page_size <= MAX_PAGE_SIZE:
+        return _bad_pagination()
+
+    entries = AuditEntry.objects.select_related("actor").order_by("-created_at", "-id")

Review this PR

Python practice