Files
bugsink/events/api_views.py
Klaas van Schelven a4e84fa0a3 Add Swagger using drf-spectacular
See #146

DRF 3.16 and Django 5.2 are not in drf-spectacular's published
list of supported but here's some sources that give reason to believe
they are supported _in practice_:

* https://github.com/tfranzel/drf-spectacular/issues/1417
* https://github.com/tfranzel/drf-spectacular/issues/1414
2025-09-12 11:46:44 +02:00

89 lines
3.4 KiB
Python

from django.shortcuts import get_object_or_404
from rest_framework import viewsets
from rest_framework.exceptions import ValidationError
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes
from bugsink.utils import assert_
from bugsink.api_pagination import AscDescCursorPagination
from bugsink.api_mixins import AtomicRequestMixin
from .models import Event
from .serializers import EventListSerializer, EventDetailSerializer
class EventPagination(AscDescCursorPagination):
# Cursor pagination requires an indexed, mostly-stable ordering field. We use `digest_order`: we require
# ?issue=<uuid> and have a composite (issue_id, digest_order) index, so ORDER BY digest_order after filtering by
# issue is fast and cursor-stable. (also note that digest_order comes in in-order).
base_ordering = ("digest_order",)
page_size = 250
default_direction = "desc" # newest first by default, aligned with UI
class EventViewSet(AtomicRequestMixin, viewsets.ReadOnlyModelViewSet):
"""
LIST requires: ?issue=<uuid>
Optional: ?order=asc|desc (default: desc)
LIST omits `data`, ordered by digest_order
RETRIEVE includes `data` (pure PK lookup; no filters/order applied)
"""
queryset = Event.objects.all() # router requirement for basename inference
serializer_class = EventListSerializer
pagination_class = EventPagination
def filter_queryset(self, queryset):
query_params = self.request.query_params
if "issue" not in query_params:
raise ValidationError({"issue": ["This field is required."]})
return queryset.filter(issue=query_params["issue"])
@extend_schema(
parameters=[
OpenApiParameter(
name="issue",
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
required=True,
description="Filter events by issue UUID (required).",
),
OpenApiParameter(
name="order",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
required=False,
enum=["asc", "desc"],
description="Sort order of digest_order (default: desc).",
),
]
)
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
def get_object(self):
"""
DRF's get_object(), but we intentionally bypass filter_queryset for detail routes to keep PK lookups
db-index-friendly (no WHERE filters other than the PK which is already indexed).
"""
queryset = self.get_queryset() # no filter_queryset() here
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
assert_(lookup_url_kwarg in self.kwargs, (
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwarg)
))
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)
# May raise a permission denied
self.check_object_permissions(self.request, obj)
return obj
def get_serializer_class(self):
return EventDetailSerializer if self.action == "retrieve" else EventListSerializer