From 10f1ea26878fc94fe2b30bb6b499dd75181e42ce Mon Sep 17 00:00:00 2001 From: Christian Franke Date: Tue, 5 Sep 2023 13:17:38 +0200 Subject: [PATCH] generics/mixins: Move GenericAPIView code to GenericModelMixin There are third party libraries, for example ADRF, which provide their own customized implementation of `APIView`. It is useful to be able to create `GenericAPIView` variants based on these customized `APIView` variants. However, as `GenericAPIView` inherits directly from Django Rest Framework's `APIView`, there is no clean way to achieve this without duplicating the code of `GenericAPIView`. Resolve this by moving the code of `GenericAPIView` to a mixin called `GenericModelMixin` and creating `GenericAPIView` by mixing `GenericModelMixin` with Django Rest Framework's `APIView`. This way, other libraries like ADRF can mix `GenericModelMixin` with their customized `APIView` variant and resuse the code of the `GenericModelMixin` without duplicating it. --- rest_framework/generics.py | 177 +------------------------------------ rest_framework/mixins.py | 177 ++++++++++++++++++++++++++++++++++++- 2 files changed, 180 insertions(+), 174 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 167303321..749bd905b 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -1,185 +1,16 @@ """ Generic views that provide commonly needed behaviour. """ -from django.core.exceptions import ValidationError -from django.db.models.query import QuerySet -from django.http import Http404 -from django.shortcuts import get_object_or_404 as _get_object_or_404 - from rest_framework import mixins, views -from rest_framework.settings import api_settings + +get_object_or_404 = mixins.get_object_or_404 -def get_object_or_404(queryset, *filter_args, **filter_kwargs): - """ - Same as Django's standard shortcut, but make sure to also raise 404 - if the filter_kwargs don't match the required types. - """ - try: - return _get_object_or_404(queryset, *filter_args, **filter_kwargs) - except (TypeError, ValueError, ValidationError): - raise Http404 - - -class GenericAPIView(views.APIView): +class GenericAPIView(mixins.GenericModelMixin, views.APIView): """ Base class for all other generic views. """ - # You'll need to either set these attributes, - # or override `get_queryset()`/`get_serializer_class()`. - # If you are overriding a view method, it is important that you call - # `get_queryset()` instead of accessing the `queryset` property directly, - # as `queryset` will get evaluated only once, and those results are cached - # for all subsequent requests. - queryset = None - serializer_class = None - - # If you want to use object lookups other than pk, set 'lookup_field'. - # For more complex lookup requirements override `get_object()`. - lookup_field = 'pk' - lookup_url_kwarg = None - - # The filter backend classes to use for queryset filtering - filter_backends = api_settings.DEFAULT_FILTER_BACKENDS - - # The style to use for queryset pagination. - pagination_class = api_settings.DEFAULT_PAGINATION_CLASS - - # Allow generic typing checking for generic views. - def __class_getitem__(cls, *args, **kwargs): - return cls - - def get_queryset(self): - """ - Get the list of items for this view. - This must be an iterable, and may be a queryset. - Defaults to using `self.queryset`. - - This method should always be used rather than accessing `self.queryset` - directly, as `self.queryset` gets evaluated only once, and those results - are cached for all subsequent requests. - - You may want to override this if you need to provide different - querysets depending on the incoming request. - - (Eg. return a list of items that is specific to the user) - """ - assert self.queryset is not None, ( - "'%s' should either include a `queryset` attribute, " - "or override the `get_queryset()` method." - % self.__class__.__name__ - ) - - queryset = self.queryset - if isinstance(queryset, QuerySet): - # Ensure queryset is re-evaluated on each request. - queryset = queryset.all() - return queryset - - def get_object(self): - """ - Returns the object the view is displaying. - - You may want to override this if you need to provide non-standard - queryset lookups. Eg if objects are referenced using multiple - keyword arguments in the url conf. - """ - queryset = self.filter_queryset(self.get_queryset()) - - # Perform the lookup filtering. - 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(self, *args, **kwargs): - """ - Return the serializer instance that should be used for validating and - deserializing input, and for serializing output. - """ - serializer_class = self.get_serializer_class() - kwargs.setdefault('context', self.get_serializer_context()) - return serializer_class(*args, **kwargs) - - def get_serializer_class(self): - """ - Return the class to use for the serializer. - Defaults to using `self.serializer_class`. - - You may want to override this if you need to provide different - serializations depending on the incoming request. - - (Eg. admins get full serialization, others get basic serialization) - """ - assert self.serializer_class is not None, ( - "'%s' should either include a `serializer_class` attribute, " - "or override the `get_serializer_class()` method." - % self.__class__.__name__ - ) - - return self.serializer_class - - def get_serializer_context(self): - """ - Extra context provided to the serializer class. - """ - return { - 'request': self.request, - 'format': self.format_kwarg, - 'view': self - } - - def filter_queryset(self, queryset): - """ - Given a queryset, filter it with whichever filter backend is in use. - - You are unlikely to want to override this method, although you may need - to call it either from a list view, or from a custom `get_object` - method if you want to apply the configured filtering backend to the - default queryset. - """ - for backend in list(self.filter_backends): - queryset = backend().filter_queryset(self.request, queryset, self) - return queryset - - @property - def paginator(self): - """ - The paginator instance associated with the view, or `None`. - """ - if not hasattr(self, '_paginator'): - if self.pagination_class is None: - self._paginator = None - else: - self._paginator = self.pagination_class() - return self._paginator - - def paginate_queryset(self, queryset): - """ - Return a single page of results, or `None` if pagination is disabled. - """ - if self.paginator is None: - return None - return self.paginator.paginate_queryset(queryset, self.request, view=self) - - def get_paginated_response(self, data): - """ - Return a paginated style `Response` object for the given output data. - """ - assert self.paginator is not None - return self.paginator.get_paginated_response(data) + pass # Concrete view classes that provide method handlers diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 6ac6366c7..daeee88f3 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -4,13 +4,188 @@ Basic building blocks for generic class based views. We don't bind behaviour to http method handlers yet, which allows mixin classes to be composed in interesting ways. """ -from django.db.models.query import prefetch_related_objects +from django.core.exceptions import ValidationError +from django.db.models.query import QuerySet, prefetch_related_objects +from django.http import Http404 +from django.shortcuts import get_object_or_404 as _get_object_or_404 from rest_framework import status from rest_framework.response import Response from rest_framework.settings import api_settings +def get_object_or_404(queryset, *filter_args, **filter_kwargs): + """ + Same as Django's standard shortcut, but make sure to also raise 404 + if the filter_kwargs don't match the required types. + """ + try: + return _get_object_or_404(queryset, *filter_args, **filter_kwargs) + except (TypeError, ValueError, ValidationError): + raise Http404 + + +class GenericModelMixin: + """ + Base mixin for all other generic mixins. + """ + # You'll need to either set these attributes, + # or override `get_queryset()`/`get_serializer_class()`. + # If you are overriding a view method, it is important that you call + # `get_queryset()` instead of accessing the `queryset` property directly, + # as `queryset` will get evaluated only once, and those results are cached + # for all subsequent requests. + queryset = None + serializer_class = None + + # If you want to use object lookups other than pk, set 'lookup_field'. + # For more complex lookup requirements override `get_object()`. + lookup_field = 'pk' + lookup_url_kwarg = None + + # The filter backend classes to use for queryset filtering + filter_backends = api_settings.DEFAULT_FILTER_BACKENDS + + # The style to use for queryset pagination. + pagination_class = api_settings.DEFAULT_PAGINATION_CLASS + + # Allow generic typing checking for generic views. + def __class_getitem__(cls, *args, **kwargs): + return cls + + def get_queryset(self): + """ + Get the list of items for this view. + This must be an iterable, and may be a queryset. + Defaults to using `self.queryset`. + + This method should always be used rather than accessing `self.queryset` + directly, as `self.queryset` gets evaluated only once, and those results + are cached for all subsequent requests. + + You may want to override this if you need to provide different + querysets depending on the incoming request. + + (Eg. return a list of items that is specific to the user) + """ + assert self.queryset is not None, ( + "'%s' should either include a `queryset` attribute, " + "or override the `get_queryset()` method." + % self.__class__.__name__ + ) + + queryset = self.queryset + if isinstance(queryset, QuerySet): + # Ensure queryset is re-evaluated on each request. + queryset = queryset.all() + return queryset + + def get_object(self): + """ + Returns the object the view is displaying. + + You may want to override this if you need to provide non-standard + queryset lookups. Eg if objects are referenced using multiple + keyword arguments in the url conf. + """ + queryset = self.filter_queryset(self.get_queryset()) + + # Perform the lookup filtering. + 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(self, *args, **kwargs): + """ + Return the serializer instance that should be used for validating and + deserializing input, and for serializing output. + """ + serializer_class = self.get_serializer_class() + kwargs.setdefault('context', self.get_serializer_context()) + return serializer_class(*args, **kwargs) + + def get_serializer_class(self): + """ + Return the class to use for the serializer. + Defaults to using `self.serializer_class`. + + You may want to override this if you need to provide different + serializations depending on the incoming request. + + (Eg. admins get full serialization, others get basic serialization) + """ + assert self.serializer_class is not None, ( + "'%s' should either include a `serializer_class` attribute, " + "or override the `get_serializer_class()` method." + % self.__class__.__name__ + ) + + return self.serializer_class + + def get_serializer_context(self): + """ + Extra context provided to the serializer class. + """ + return { + 'request': self.request, + 'format': self.format_kwarg, + 'view': self + } + + def filter_queryset(self, queryset): + """ + Given a queryset, filter it with whichever filter backend is in use. + + You are unlikely to want to override this method, although you may need + to call it either from a list view, or from a custom `get_object` + method if you want to apply the configured filtering backend to the + default queryset. + """ + for backend in list(self.filter_backends): + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset + + @property + def paginator(self): + """ + The paginator instance associated with the view, or `None`. + """ + if not hasattr(self, '_paginator'): + if self.pagination_class is None: + self._paginator = None + else: + self._paginator = self.pagination_class() + return self._paginator + + def paginate_queryset(self, queryset): + """ + Return a single page of results, or `None` if pagination is disabled. + """ + if self.paginator is None: + return None + return self.paginator.paginate_queryset(queryset, self.request, view=self) + + def get_paginated_response(self, data): + """ + Return a paginated style `Response` object for the given output data. + """ + assert self.paginator is not None + return self.paginator.get_paginated_response(data) + + class CreateModelMixin: """ Create a model instance.