diff options
| author | James Rutherford | 2015-03-11 10:38:03 +0000 |
|---|---|---|
| committer | James Rutherford | 2015-03-11 10:38:03 +0000 |
| commit | 4a2d27975ab5249269aebafd803be87a2107092b (patch) | |
| tree | 55b524c93b02eef404304f734be98871bbb1324f /rest_framework/generics.py | |
| parent | 856dc855c952746f566a6a8de263afe951362dfb (diff) | |
| parent | dc56e5a0f41fdd6350e91a5749023d086bd1640f (diff) | |
| download | django-rest-framework-4a2d27975ab5249269aebafd803be87a2107092b.tar.bz2 | |
Merge pull request #1 from tomchristie/master
Merge in from upstream
Diffstat (limited to 'rest_framework/generics.py')
| -rw-r--r-- | rest_framework/generics.py | 410 |
1 files changed, 102 insertions, 308 deletions
diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 99e9782e..61dcb84a 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -2,25 +2,20 @@ Generic views that provide commonly needed behaviour. """ from __future__ import unicode_literals - -from django.core.exceptions import ImproperlyConfigured, PermissionDenied -from django.core.paginator import Paginator, InvalidPage +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 django.utils.translation import ugettext as _ -from rest_framework import views, mixins, exceptions -from rest_framework.request import clone_request +from rest_framework import views, mixins from rest_framework.settings import api_settings -import warnings -def get_object_or_404(queryset, **filter_kwargs): +def get_object_or_404(queryset, *filter_args, **filter_kwargs): """ - Same as Django's standard shortcut, but make sure to raise 404 + 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_kwargs) + return _get_object_or_404(queryset, *filter_args, **filter_kwargs) except (TypeError, ValueError): raise Http404 @@ -29,205 +24,25 @@ class GenericAPIView(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 - # This shortcut may be used instead of setting either or both - # of the `queryset`/`serializer_class` attributes, although using - # the explicit style is generally preferred. - model = None - - # If you want to use object lookups other than pk, set this attribute. + # If you want to use object lookups other than pk, set 'lookup_field'. # For more complex lookup requirements override `get_object()`. lookup_field = 'pk' - - # Pagination settings - paginate_by = api_settings.PAGINATE_BY - paginate_by_param = api_settings.PAGINATE_BY_PARAM - pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS - page_kwarg = 'page' + lookup_url_kwarg = None # The filter backend classes to use for queryset filtering filter_backends = api_settings.DEFAULT_FILTER_BACKENDS - # The following attributes may be subject to change, - # and should be considered private API. - model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS - paginator_class = Paginator - - ###################################### - # These are pending deprecation... - - pk_url_kwarg = 'pk' - slug_url_kwarg = 'slug' - slug_field = 'slug' - allow_empty = True - filter_backend = api_settings.FILTER_BACKEND - - def get_serializer_context(self): - """ - Extra context provided to the serializer class. - """ - return { - 'request': self.request, - 'format': self.format_kwarg, - 'view': self - } - - def get_serializer(self, instance=None, data=None, - files=None, many=False, partial=False): - """ - Return the serializer instance that should be used for validating and - deserializing input, and for serializing output. - """ - serializer_class = self.get_serializer_class() - context = self.get_serializer_context() - return serializer_class(instance, data=data, files=files, - many=many, partial=partial, context=context) - - def get_pagination_serializer(self, page): - """ - Return a serializer instance to use with paginated data. - """ - class SerializerClass(self.pagination_serializer_class): - class Meta: - object_serializer_class = self.get_serializer_class() - - pagination_serializer_class = SerializerClass - context = self.get_serializer_context() - return pagination_serializer_class(instance=page, context=context) - - def paginate_queryset(self, queryset, page_size=None): - """ - Paginate a queryset if required, either returning a page object, - or `None` if pagination is not configured for this view. - """ - deprecated_style = False - if page_size is not None: - warnings.warn('The `page_size` parameter to `paginate_queryset()` ' - 'is due to be deprecated. ' - 'Note that the return style of this method is also ' - 'changed, and will simply return a page object ' - 'when called without a `page_size` argument.', - PendingDeprecationWarning, stacklevel=2) - deprecated_style = True - else: - # Determine the required page size. - # If pagination is not configured, simply return None. - page_size = self.get_paginate_by() - if not page_size: - return None - - if not self.allow_empty: - warnings.warn( - 'The `allow_empty` parameter is due to be deprecated. ' - 'To use `allow_empty=False` style behavior, You should override ' - '`get_queryset()` and explicitly raise a 404 on empty querysets.', - PendingDeprecationWarning, stacklevel=2 - ) - - paginator = self.paginator_class(queryset, page_size, - allow_empty_first_page=self.allow_empty) - page_kwarg = self.kwargs.get(self.page_kwarg) - page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg) - page = page_kwarg or page_query_param or 1 - try: - page_number = int(page) - except ValueError: - if page == 'last': - page_number = paginator.num_pages - else: - raise Http404(_("Page is not 'last', nor can it be converted to an int.")) - try: - page = paginator.page(page_number) - except InvalidPage as e: - raise Http404(_('Invalid page (%(page_number)s): %(message)s') % { - 'page_number': page_number, - 'message': str(e) - }) - - if deprecated_style: - return (paginator, page, page.object_list, page.has_other_pages()) - return page - - 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. - """ - filter_backends = self.filter_backends or [] - if not filter_backends and self.filter_backend: - warnings.warn( - 'The `filter_backend` attribute and `FILTER_BACKEND` setting ' - 'are due to be deprecated in favor of a `filter_backends` ' - 'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take ' - 'a *list* of filter backend classes.', - PendingDeprecationWarning, stacklevel=2 - ) - filter_backends = [self.filter_backend] - - for backend in filter_backends: - queryset = backend().filter_queryset(self.request, queryset, self) - return queryset - - ######################## - ### The following methods provide default implementations - ### that you may want to override for more complex cases. - - def get_paginate_by(self, queryset=None): - """ - Return the size of pages to use with pagination. - - If `PAGINATE_BY_PARAM` is set it will attempt to get the page size - from a named query parameter in the url, eg. ?page_size=100 - - Otherwise defaults to using `self.paginate_by`. - """ - if queryset is not None: - warnings.warn('The `queryset` parameter to `get_paginate_by()` ' - 'is due to be deprecated.', - PendingDeprecationWarning, stacklevel=2) - - if self.paginate_by_param: - query_params = self.request.QUERY_PARAMS - try: - return int(query_params[self.paginate_by_param]) - except (KeyError, ValueError): - pass - - return self.paginate_by - - 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) - """ - serializer_class = self.serializer_class - if serializer_class is not None: - return serializer_class - - assert self.model is not None, \ - "'%s' should either include a 'serializer_class' attribute, " \ - "or use the 'model' attribute as a shortcut for " \ - "automatically generating a serializer class." \ - % self.__class__.__name__ - - class DefaultSerializer(self.model_serializer_class): - class Meta: - model = self.model - return DefaultSerializer + # The style to use for queryset pagination. + pagination_class = api_settings.DEFAULT_PAGINATION_CLASS def get_queryset(self): """ @@ -235,21 +50,28 @@ class GenericAPIView(views.APIView): 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) """ - if self.queryset is not None: - return self.queryset._clone() - - if self.model is not None: - return self.model._default_manager.all() + assert self.queryset is not None, ( + "'%s' should either include a `queryset` attribute, " + "or override the `get_queryset()` method." + % self.__class__.__name__ + ) - raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" - % 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, queryset=None): + def get_object(self): """ Returns the object the view is displaying. @@ -257,41 +79,19 @@ class GenericAPIView(views.APIView): queryset lookups. Eg if objects are referenced using multiple keyword arguments in the url conf. """ - # Determine the base queryset to use. - if queryset is None: - queryset = self.filter_queryset(self.get_queryset()) - else: - pass # Deprecation warning + queryset = self.filter_queryset(self.get_queryset()) # Perform the lookup filtering. - pk = self.kwargs.get(self.pk_url_kwarg, None) - slug = self.kwargs.get(self.slug_url_kwarg, None) - lookup = self.kwargs.get(self.lookup_field, None) - - if lookup is not None: - filter_kwargs = {self.lookup_field: lookup} - elif pk is not None and self.lookup_field == 'pk': - warnings.warn( - 'The `pk_url_kwarg` attribute is due to be deprecated. ' - 'Use the `lookup_field` attribute instead', - PendingDeprecationWarning - ) - filter_kwargs = {'pk': pk} - elif slug is not None and self.lookup_field == 'pk': - warnings.warn( - 'The `slug_url_kwarg` attribute is due to be deprecated. ' - 'Use the `lookup_field` attribute instead', - PendingDeprecationWarning - ) - filter_kwargs = {self.slug_field: slug} - else: - raise ImproperlyConfigured( - '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__, self.lookup_field) - ) + 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 @@ -299,68 +99,86 @@ class GenericAPIView(views.APIView): return obj - ######################## - ### The following are placeholder methods, - ### and are intended to be overridden. - ### - ### The are not called by GenericAPIView directly, - ### but are used by the mixin methods. + 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['context'] = self.get_serializer_context() + return serializer_class(*args, **kwargs) - def pre_save(self, obj): + def get_serializer_class(self): """ - Placeholder method for calling before saving an object. + 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. - May be used to set attributes on the object that are implicit - in either the request, or the url. + (Eg. admins get full serialization, others get basic serialization) """ - pass + 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 post_save(self, obj, created=False): + def get_serializer_context(self): """ - Placeholder method for calling after saving an object. + Extra context provided to the serializer class. + """ + return { + 'request': self.request, + 'format': self.format_kwarg, + 'view': self + } + + def filter_queryset(self, queryset): """ - pass + Given a queryset, filter it with whichever filter backend is in use. - def metadata(self, request): + 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. """ - Return a dictionary of metadata about the view. - Used to return responses for OPTIONS requests. + for backend in list(self.filter_backends): + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset - We override the default behavior, and add some extra information - about the required request body for POST and PUT operations. + @property + def paginator(self): """ - ret = super(GenericAPIView, self).metadata(request) - - actions = {} - for method in ('PUT', 'POST'): - if method not in self.allowed_methods: - continue - - cloned_request = clone_request(request, method) - try: - # Test global permissions - self.check_permissions(cloned_request) - # Test object permissions - if method == 'PUT': - self.get_object() - except (exceptions.APIException, PermissionDenied, Http404): - pass + The paginator instance associated with the view, or `None`. + """ + if not hasattr(self, '_paginator'): + if self.pagination_class is None: + self._paginator = None else: - # If user has appropriate permissions for the view, include - # appropriate metadata about the fields that should be supplied. - serializer = self.get_serializer() - actions[method] = serializer.metadata() + self._paginator = self.pagination_class() + return self._paginator - if actions: - ret['actions'] = actions + 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) - return ret + 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) -########################################################## -### Concrete view classes that provide method handlers ### -### by composing the mixin classes with the base view. ### -########################################################## +# Concrete view classes that provide method handlers +# by composing the mixin classes with the base view. class CreateAPIView(mixins.CreateModelMixin, GenericAPIView): @@ -473,27 +291,3 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) - - -########################## -### Deprecated classes ### -########################## - -class MultipleObjectAPIView(GenericAPIView): - def __init__(self, *args, **kwargs): - warnings.warn( - 'Subclassing `MultipleObjectAPIView` is due to be deprecated. ' - 'You should simply subclass `GenericAPIView` instead.', - PendingDeprecationWarning, stacklevel=2 - ) - super(MultipleObjectAPIView, self).__init__(*args, **kwargs) - - -class SingleObjectAPIView(GenericAPIView): - def __init__(self, *args, **kwargs): - warnings.warn( - 'Subclassing `SingleObjectAPIView` is due to be deprecated. ' - 'You should simply subclass `GenericAPIView` instead.', - PendingDeprecationWarning, stacklevel=2 - ) - super(SingleObjectAPIView, self).__init__(*args, **kwargs) |
