"""Reusable serializers and views for DRF JSON:API applications.""" from rest_framework import viewsets from rest_framework.generics import GenericAPIView from rest_framework_json_api import serializers from rest_framework_json_api.utils import get_included_resources from rest_framework_json_api.views import AutoPrefetchMixin, RelatedMixin from .exceptions import HardenedExceptionHandlingMixin basic_filter = ("exact", "isnull") text_filter = ( "exact", "contains", "iexact", "icontains", "startswith", "istartswith", "endswith", "iendswith", ) date_filter = ("exact", "gte", "lte") int_filter = ("exact", "gte", "lte") class PreloadIncludesMixin: """Apply configured select/prefetch rules for requested JSON:API includes.""" def get_select_related(self, include): return getattr(self, "select_for_includes", {}).get(include) def get_prefetch_related(self, include): return getattr(self, "prefetch_for_includes", {}).get(include) def get_queryset(self, *args, **kwargs): queryset = super().get_queryset(*args, **kwargs) includes = [*get_included_resources(self.request), "__all__"] for include in includes: select_related = self.get_select_related(include) if select_related is not None: queryset = queryset.select_related(*select_related) prefetch_related = self.get_prefetch_related(include) if prefetch_related is None: continue if callable(prefetch_related): prefetch_related = prefetch_related(self) queryset = queryset.prefetch_related(*prefetch_related) return queryset class ReasonableModelViewSet( AutoPrefetchMixin, PreloadIncludesMixin, RelatedMixin, viewsets.ModelViewSet, ): """JSON:API model viewset limited to the standard resource methods.""" http_method_names = ["get", "post", "patch", "delete", "head", "options"] class HardenedModelViewSet(HardenedExceptionHandlingMixin, ReasonableModelViewSet): """ReasonableModelViewSet with sanitized handling for unexpected errors.""" class HardenedGenericAPIView(HardenedExceptionHandlingMixin, GenericAPIView): """GenericAPIView with sanitized handling for unexpected errors.""" class ReasonableModelSerializer(serializers.ModelSerializer): """Include declared relationship fields requested by JSON:API clients.""" def get_field_names(self, declared_fields, info): fields = list(super().get_field_names(declared_fields, info)) request = self.context.get("request") included_serializers = getattr(self, "included_serializers", {}) if request is None or not included_serializers: return fields included_relationships = [] if request.method == "GET": included_relationships = request.query_params.get("include", "").split(",") elif request.method in {"POST", "PATCH"}: included_relationships = request.data.keys() fields.extend( relationship for relationship in included_relationships if relationship in included_serializers and relationship not in fields ) return fields