This commit is contained in:
Tom Christie 2014-08-29 15:58:52 +00:00
commit 84fe1feceb
22 changed files with 925 additions and 2806 deletions

View File

@ -19,8 +19,8 @@ Typically when using the generic views, you'll override the view, and set severa
from django.contrib.auth.models import User from django.contrib.auth.models import User
from myapp.serializers import UserSerializer from myapp.serializers import UserSerializer
from rest_framework import generics from rest_framework import generics
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
class UserList(generics.ListCreateAPIView): class UserList(generics.ListCreateAPIView):
queryset = User.objects.all() queryset = User.objects.all()
@ -212,8 +212,6 @@ Provides a `.list(request, *args, **kwargs)` method, that implements listing a q
If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated. If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated.
If the queryset is empty this returns a `200 OK` response, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`.
## CreateModelMixin ## CreateModelMixin
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance. Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.

View File

@ -10,7 +10,6 @@ from __future__ import unicode_literals
from django.utils import six from django.utils import six
from rest_framework.views import APIView from rest_framework.views import APIView
import types import types
import warnings
def api_view(http_method_names): def api_view(http_method_names):
@ -130,37 +129,3 @@ def list_route(methods=['get'], **kwargs):
func.kwargs = kwargs func.kwargs = kwargs
return func return func
return decorator return decorator
# These are now pending deprecation, in favor of `detail_route` and `list_route`.
def link(**kwargs):
"""
Used to mark a method on a ViewSet that should be routed for detail GET requests.
"""
msg = 'link is pending deprecation. Use detail_route instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
def decorator(func):
func.bind_to_methods = ['get']
func.detail = True
func.kwargs = kwargs
return func
return decorator
def action(methods=['post'], **kwargs):
"""
Used to mark a method on a ViewSet that should be routed for detail POST requests.
"""
msg = 'action is pending deprecation. Use detail_route instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
def decorator(func):
func.bind_to_methods = methods
func.detail = True
func.kwargs = kwargs
return func
return decorator

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ Generic views that provide commonly needed behaviour.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator, InvalidPage from django.core.paginator import Paginator, InvalidPage
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404 as _get_object_or_404 from django.shortcuts import get_object_or_404 as _get_object_or_404
@ -11,7 +11,6 @@ from django.utils.translation import ugettext as _
from rest_framework import views, mixins, exceptions from rest_framework import views, mixins, exceptions
from rest_framework.request import clone_request from rest_framework.request import clone_request
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
import warnings
def strict_positive_int(integer_string, cutoff=None): def strict_positive_int(integer_string, cutoff=None):
@ -51,11 +50,6 @@ class GenericAPIView(views.APIView):
queryset = None queryset = None
serializer_class = 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 this attribute.
# For more complex lookup requirements override `get_object()`. # For more complex lookup requirements override `get_object()`.
lookup_field = 'pk' lookup_field = 'pk'
@ -71,20 +65,10 @@ class GenericAPIView(views.APIView):
# The filter backend classes to use for queryset filtering # The filter backend classes to use for queryset filtering
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
# The following attributes may be subject to change, # The following attribute may be subject to change,
# and should be considered private API. # and should be considered private API.
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
paginator_class = Paginator 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): def get_serializer_context(self):
""" """
Extra context provided to the serializer class. Extra context provided to the serializer class.
@ -95,18 +79,16 @@ class GenericAPIView(views.APIView):
'view': self 'view': self
} }
def get_serializer(self, instance=None, data=None, files=None, many=False, def get_serializer(self, instance=None, data=None, many=False, partial=False):
partial=False, allow_add_remove=False):
""" """
Return the serializer instance that should be used for validating and Return the serializer instance that should be used for validating and
deserializing input, and for serializing output. deserializing input, and for serializing output.
""" """
serializer_class = self.get_serializer_class() serializer_class = self.get_serializer_class()
context = self.get_serializer_context() context = self.get_serializer_context()
return serializer_class(instance, data=data, files=files, return serializer_class(
many=many, partial=partial, instance, data=data, many=many, partial=partial, context=context
allow_add_remove=allow_add_remove, )
context=context)
def get_pagination_serializer(self, page): def get_pagination_serializer(self, page):
""" """
@ -120,37 +102,16 @@ class GenericAPIView(views.APIView):
context = self.get_serializer_context() context = self.get_serializer_context()
return pagination_serializer_class(instance=page, context=context) return pagination_serializer_class(instance=page, context=context)
def paginate_queryset(self, queryset, page_size=None): def paginate_queryset(self, queryset):
""" """
Paginate a queryset if required, either returning a page object, Paginate a queryset if required, either returning a page object,
or `None` if pagination is not configured for this view. or `None` if pagination is not configured for this view.
""" """
deprecated_style = False page_size = self.get_paginate_by()
if page_size is not None: if not page_size:
warnings.warn('The `page_size` parameter to `paginate_queryset()` ' return None
'is 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.',
DeprecationWarning, 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: paginator = self.paginator_class(queryset, page_size)
warnings.warn(
'The `allow_empty` parameter is deprecated. '
'To use `allow_empty=False` style behavior, You should override '
'`get_queryset()` and explicitly raise a 404 on empty querysets.',
DeprecationWarning, 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_kwarg = self.kwargs.get(self.page_kwarg)
page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg) page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg)
page = page_kwarg or page_query_param or 1 page = page_kwarg or page_query_param or 1
@ -170,8 +131,6 @@ class GenericAPIView(views.APIView):
'message': str(exc) 'message': str(exc)
}) })
if deprecated_style:
return (paginator, page, page.object_list, page.has_other_pages())
return page return page
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
@ -191,29 +150,12 @@ class GenericAPIView(views.APIView):
""" """
Returns the list of filter backends that this view requires. Returns the list of filter backends that this view requires.
""" """
if self.filter_backends is None: return list(self.filter_backends)
filter_backends = []
else:
# Note that we are returning a *copy* of the class attribute,
# so that it is safe for the view to mutate it if needed.
filter_backends = list(self.filter_backends)
if not filter_backends and self.filter_backend:
warnings.warn(
'The `filter_backend` attribute and `FILTER_BACKEND` setting '
'are deprecated in favor of a `filter_backends` '
'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take '
'a *list* of filter backend classes.',
DeprecationWarning, stacklevel=2
)
filter_backends = [self.filter_backend]
return filter_backends
# The following methods provide default implementations # The following methods provide default implementations
# that you may want to override for more complex cases. # that you may want to override for more complex cases.
def get_paginate_by(self, queryset=None): def get_paginate_by(self):
""" """
Return the size of pages to use with pagination. Return the size of pages to use with pagination.
@ -222,11 +164,6 @@ class GenericAPIView(views.APIView):
Otherwise defaults to using `self.paginate_by`. Otherwise defaults to using `self.paginate_by`.
""" """
if queryset is not None:
warnings.warn('The `queryset` parameter to `get_paginate_by()` '
'is deprecated.',
DeprecationWarning, stacklevel=2)
if self.paginate_by_param: if self.paginate_by_param:
try: try:
return strict_positive_int( return strict_positive_int(
@ -248,26 +185,13 @@ class GenericAPIView(views.APIView):
(Eg. admins get full serialization, others get basic serialization) (Eg. admins get full serialization, others get basic serialization)
""" """
serializer_class = self.serializer_class assert self.serializer_class is not None, (
if serializer_class is not None: "'%s' should either include a `serializer_class` attribute, "
return serializer_class "or override the `get_serializer_class()` method."
% self.__class__.__name__
warnings.warn(
'The `.model` attribute on view classes is now deprecated in favor '
'of the more explicit `serializer_class` and `queryset` attributes.',
DeprecationWarning, stacklevel=2
) )
assert self.model is not None, \ return self.serializer_class
"'%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
def get_queryset(self): def get_queryset(self):
""" """
@ -284,21 +208,15 @@ class GenericAPIView(views.APIView):
(Eg. return a list of items that is specific to the user) (Eg. return a list of items that is specific to the user)
""" """
if self.queryset is not None: assert self.queryset is not None, (
return self.queryset._clone() "'%s' should either include a `queryset` attribute, "
"or override the `get_queryset()` method."
% self.__class__.__name__
)
if self.model is not None: return self.queryset._clone()
warnings.warn(
'The `.model` attribute on view classes is now deprecated in favor '
'of the more explicit `serializer_class` and `queryset` attributes.',
DeprecationWarning, stacklevel=2
)
return self.model._default_manager.all()
error_format = "'%s' must define 'queryset' or 'model'" def get_object(self):
raise ImproperlyConfigured(error_format % self.__class__.__name__)
def get_object(self, queryset=None):
""" """
Returns the object the view is displaying. Returns the object the view is displaying.
@ -306,43 +224,19 @@ class GenericAPIView(views.APIView):
queryset lookups. Eg if objects are referenced using multiple queryset lookups. Eg if objects are referenced using multiple
keyword arguments in the url conf. keyword arguments in the url conf.
""" """
# Determine the base queryset to use. queryset = self.filter_queryset(self.get_queryset())
if queryset is None:
queryset = self.filter_queryset(self.get_queryset())
else:
pass # Deprecation warning
# Perform the lookup filtering. # Perform the lookup filtering.
# Note that `pk` and `slug` are deprecated styles of lookup filtering.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup = self.kwargs.get(lookup_url_kwarg, None)
pk = self.kwargs.get(self.pk_url_kwarg, None)
slug = self.kwargs.get(self.slug_url_kwarg, None)
if lookup is not None: assert lookup_url_kwarg in self.kwargs, (
filter_kwargs = {self.lookup_field: lookup} 'Expected view %s to be called with a URL keyword argument '
elif pk is not None and self.lookup_field == 'pk': 'named "%s". Fix your URL conf, or set the `.lookup_field` '
warnings.warn( 'attribute on the view correctly.' %
'The `pk_url_kwarg` attribute is deprecated. ' (self.__class__.__name__, lookup_url_kwarg)
'Use the `lookup_field` attribute instead', )
DeprecationWarning
)
filter_kwargs = {'pk': pk}
elif slug is not None and self.lookup_field == 'pk':
warnings.warn(
'The `slug_url_kwarg` attribute is deprecated. '
'Use the `lookup_field` attribute instead',
DeprecationWarning
)
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)
)
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs) obj = get_object_or_404(queryset, **filter_kwargs)
# May raise a permission denied # May raise a permission denied
@ -540,25 +434,3 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs) return self.destroy(request, *args, **kwargs)
# Deprecated classes
class MultipleObjectAPIView(GenericAPIView):
def __init__(self, *args, **kwargs):
warnings.warn(
'Subclassing `MultipleObjectAPIView` is deprecated. '
'You should simply subclass `GenericAPIView` instead.',
DeprecationWarning, stacklevel=2
)
super(MultipleObjectAPIView, self).__init__(*args, **kwargs)
class SingleObjectAPIView(GenericAPIView):
def __init__(self, *args, **kwargs):
warnings.warn(
'Subclassing `SingleObjectAPIView` is deprecated. '
'You should simply subclass `GenericAPIView` instead.',
DeprecationWarning, stacklevel=2
)
super(SingleObjectAPIView, self).__init__(*args, **kwargs)

View File

@ -12,10 +12,9 @@ from rest_framework import status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.request import clone_request from rest_framework.request import clone_request
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
import warnings
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None): def _get_validation_exclusions(obj, lookup_field=None):
""" """
Given a model instance, and an optional pk and slug field, Given a model instance, and an optional pk and slug field,
return the full list of all other field names on that model. return the full list of all other field names on that model.
@ -23,23 +22,13 @@ def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None)
For use when performing full_clean on a model instance, For use when performing full_clean on a model instance,
so we only clean the required fields. so we only clean the required fields.
""" """
include = [] if lookup_field == 'pk':
if pk:
# Deprecated
pk_field = obj._meta.pk pk_field = obj._meta.pk
while pk_field.rel: while pk_field.rel:
pk_field = pk_field.rel.to._meta.pk pk_field = pk_field.rel.to._meta.pk
include.append(pk_field.name) lookup_field = pk_field.name
if slug_field: return [field.name for field in obj._meta.fields if field.name != lookup_field]
# Deprecated
include.append(slug_field)
if lookup_field and lookup_field != 'pk':
include.append(lookup_field)
return [field.name for field in obj._meta.fields if field.name not in include]
class CreateModelMixin(object): class CreateModelMixin(object):
@ -47,12 +36,10 @@ class CreateModelMixin(object):
Create a model instance. Create a model instance.
""" """
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA, files=request.FILES) serializer = self.get_serializer(data=request.DATA)
if serializer.is_valid(): if serializer.is_valid():
self.pre_save(serializer.object) self.object = serializer.save()
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data) headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers) headers=headers)
@ -70,24 +57,9 @@ class ListModelMixin(object):
""" """
List a queryset. List a queryset.
""" """
empty_error = "Empty list and '%(class_name)s.allow_empty' is False."
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
self.object_list = self.filter_queryset(self.get_queryset()) self.object_list = self.filter_queryset(self.get_queryset())
# Default is to allow empty querysets. This can be altered by setting
# `.allow_empty = False`, to raise 404 errors on empty querysets.
if not self.allow_empty and not self.object_list:
warnings.warn(
'The `allow_empty` parameter is deprecated. '
'To use `allow_empty=False` style behavior, You should override '
'`get_queryset()` and explicitly raise a 404 on empty querysets.',
DeprecationWarning
)
class_name = self.__class__.__name__
error_msg = self.empty_error % {'class_name': class_name}
raise Http404(error_msg)
# Switch between paginated or standard style responses # Switch between paginated or standard style responses
page = self.paginate_queryset(self.object_list) page = self.paginate_queryset(self.object_list)
if page is not None: if page is not None:
@ -116,26 +88,20 @@ class UpdateModelMixin(object):
partial = kwargs.pop('partial', False) partial = kwargs.pop('partial', False)
self.object = self.get_object_or_none() self.object = self.get_object_or_none()
serializer = self.get_serializer(self.object, data=request.DATA, serializer = self.get_serializer(self.object, data=request.DATA, partial=partial)
files=request.FILES, partial=partial)
if not serializer.is_valid(): if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try: lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
self.pre_save(serializer.object) lookup_value = self.kwargs[lookup_url_kwarg]
except ValidationError as err: extras = {self.lookup_field: lookup_value}
# full_clean on model instance may be called in pre_save,
# so we have to handle eventual errors.
return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST)
if self.object is None: if self.object is None:
self.object = serializer.save(force_insert=True) self.object = serializer.save(extras=extras)
self.post_save(self.object, created=True)
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
self.object = serializer.save(force_update=True) self.object = serializer.save(extras=extras)
self.post_save(self.object, created=False)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs):
@ -161,26 +127,15 @@ class UpdateModelMixin(object):
""" """
Set any attributes on the object that are implicit in the request. Set any attributes on the object that are implicit in the request.
""" """
# pk and/or slug attributes are implicit in the URL.
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup = self.kwargs.get(lookup_url_kwarg, None) lookup_value = self.kwargs[lookup_url_kwarg]
pk = self.kwargs.get(self.pk_url_kwarg, None)
slug = self.kwargs.get(self.slug_url_kwarg, None)
slug_field = slug and self.slug_field or None
if lookup: setattr(obj, self.lookup_field, lookup_value)
setattr(obj, self.lookup_field, lookup)
if pk:
setattr(obj, 'pk', pk)
if slug:
setattr(obj, slug_field, slug)
# Ensure we clean the attributes so that we don't eg return integer # Ensure we clean the attributes so that we don't eg return integer
# pk using a string representation, as provided by the url conf kwarg. # pk using a string representation, as provided by the url conf kwarg.
if hasattr(obj, 'full_clean'): if hasattr(obj, 'full_clean'):
exclude = _get_validation_exclusions(obj, pk, slug_field, self.lookup_field) exclude = _get_validation_exclusions(obj, self.lookup_field)
obj.full_clean(exclude) obj.full_clean(exclude)

View File

@ -48,17 +48,17 @@ class DefaultObjectSerializer(serializers.Field):
super(DefaultObjectSerializer, self).__init__(source=source) super(DefaultObjectSerializer, self).__init__(source=source)
class PaginationSerializerOptions(serializers.SerializerOptions): # class PaginationSerializerOptions(serializers.SerializerOptions):
""" # """
An object that stores the options that may be provided to a # An object that stores the options that may be provided to a
pagination serializer by using the inner `Meta` class. # pagination serializer by using the inner `Meta` class.
Accessible on the instance as `serializer.opts`. # Accessible on the instance as `serializer.opts`.
""" # """
def __init__(self, meta): # def __init__(self, meta):
super(PaginationSerializerOptions, self).__init__(meta) # super(PaginationSerializerOptions, self).__init__(meta)
self.object_serializer_class = getattr(meta, 'object_serializer_class', # self.object_serializer_class = getattr(meta, 'object_serializer_class',
DefaultObjectSerializer) # DefaultObjectSerializer)
class BasePaginationSerializer(serializers.Serializer): class BasePaginationSerializer(serializers.Serializer):
@ -66,7 +66,7 @@ class BasePaginationSerializer(serializers.Serializer):
A base class for pagination serializers to inherit from, A base class for pagination serializers to inherit from,
to make implementing custom serializers more easy. to make implementing custom serializers more easy.
""" """
_options_class = PaginationSerializerOptions # _options_class = PaginationSerializerOptions
results_field = 'results' results_field = 'results'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -1,595 +0,0 @@
"""
Serializer fields that deal with relationships.
These fields allow you to specify the style that should be used to represent
model relationships, including hyperlinks, primary keys, or slugs.
"""
from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
from django import forms
from django.db.models.fields import BLANK_CHOICE_DASH
from django.forms import widgets
from django.forms.models import ModelChoiceIterator
from django.utils.translation import ugettext_lazy as _
from rest_framework.fields import Field, WritableField, get_component, is_simple_callable
from rest_framework.reverse import reverse
from rest_framework.compat import urlparse
from rest_framework.compat import smart_text
import warnings
# Relational fields
# Not actually Writable, but subclasses may need to be.
class RelatedField(WritableField):
"""
Base class for related model fields.
This represents a relationship using the unicode representation of the target.
"""
widget = widgets.Select
many_widget = widgets.SelectMultiple
form_field_class = forms.ChoiceField
many_form_field_class = forms.MultipleChoiceField
null_values = (None, '', 'None')
cache_choices = False
empty_label = None
read_only = True
many = False
def __init__(self, *args, **kwargs):
queryset = kwargs.pop('queryset', None)
self.many = kwargs.pop('many', self.many)
if self.many:
self.widget = self.many_widget
self.form_field_class = self.many_form_field_class
kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
super(RelatedField, self).__init__(*args, **kwargs)
if not self.required:
# Accessed in ModelChoiceIterator django/forms/models.py:1034
# If set adds empty choice.
self.empty_label = BLANK_CHOICE_DASH[0][1]
self.queryset = queryset
def initialize(self, parent, field_name):
super(RelatedField, self).initialize(parent, field_name)
if self.queryset is None and not self.read_only:
manager = getattr(self.parent.opts.model, self.source or field_name)
if hasattr(manager, 'related'): # Forward
self.queryset = manager.related.model._default_manager.all()
else: # Reverse
self.queryset = manager.field.rel.to._default_manager.all()
# We need this stuff to make form choices work...
def prepare_value(self, obj):
return self.to_native(obj)
def label_from_instance(self, obj):
"""
Return a readable representation for use with eg. select widgets.
"""
desc = smart_text(obj)
ident = smart_text(self.to_native(obj))
if desc == ident:
return desc
return "%s - %s" % (desc, ident)
def _get_queryset(self):
return self._queryset
def _set_queryset(self, queryset):
self._queryset = queryset
self.widget.choices = self.choices
queryset = property(_get_queryset, _set_queryset)
def _get_choices(self):
# If self._choices is set, then somebody must have manually set
# the property self.choices. In this case, just return self._choices.
if hasattr(self, '_choices'):
return self._choices
# Otherwise, execute the QuerySet in self.queryset to determine the
# choices dynamically. Return a fresh ModelChoiceIterator that has not been
# consumed. Note that we're instantiating a new ModelChoiceIterator *each*
# time _get_choices() is called (and, thus, each time self.choices is
# accessed) so that we can ensure the QuerySet has not been consumed. This
# construct might look complicated but it allows for lazy evaluation of
# the queryset.
return ModelChoiceIterator(self)
def _set_choices(self, value):
# Setting choices also sets the choices on the widget.
# choices can be any iterable, but we call list() on it because
# it will be consumed more than once.
self._choices = self.widget.choices = list(value)
choices = property(_get_choices, _set_choices)
# Default value handling
def get_default_value(self):
default = super(RelatedField, self).get_default_value()
if self.many and default is None:
return []
return default
# Regular serializer stuff...
def field_to_native(self, obj, field_name):
try:
if self.source == '*':
return self.to_native(obj)
source = self.source or field_name
value = obj
for component in source.split('.'):
if value is None:
break
value = get_component(value, component)
except ObjectDoesNotExist:
return None
if value is None:
return None
if self.many:
if is_simple_callable(getattr(value, 'all', None)):
return [self.to_native(item) for item in value.all()]
else:
# Also support non-queryset iterables.
# This allows us to also support plain lists of related items.
return [self.to_native(item) for item in value]
return self.to_native(value)
def field_from_native(self, data, files, field_name, into):
if self.read_only:
return
try:
if self.many:
try:
# Form data
value = data.getlist(field_name)
if value == [''] or value == []:
raise KeyError
except AttributeError:
# Non-form data
value = data[field_name]
else:
value = data[field_name]
except KeyError:
if self.partial:
return
value = self.get_default_value()
if value in self.null_values:
if self.required:
raise ValidationError(self.error_messages['required'])
into[(self.source or field_name)] = None
elif self.many:
into[(self.source or field_name)] = [self.from_native(item) for item in value]
else:
into[(self.source or field_name)] = self.from_native(value)
# PrimaryKey relationships
class PrimaryKeyRelatedField(RelatedField):
"""
Represents a relationship as a pk value.
"""
read_only = False
default_error_messages = {
'does_not_exist': _("Invalid pk '%s' - object does not exist."),
'incorrect_type': _('Incorrect type. Expected pk value, received %s.'),
}
# TODO: Remove these field hacks...
def prepare_value(self, obj):
return self.to_native(obj.pk)
def label_from_instance(self, obj):
"""
Return a readable representation for use with eg. select widgets.
"""
desc = smart_text(obj)
ident = smart_text(self.to_native(obj.pk))
if desc == ident:
return desc
return "%s - %s" % (desc, ident)
# TODO: Possibly change this to just take `obj`, through prob less performant
def to_native(self, pk):
return pk
def from_native(self, data):
if self.queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
try:
return self.queryset.get(pk=data)
except ObjectDoesNotExist:
msg = self.error_messages['does_not_exist'] % smart_text(data)
raise ValidationError(msg)
except (TypeError, ValueError):
received = type(data).__name__
msg = self.error_messages['incorrect_type'] % received
raise ValidationError(msg)
def field_to_native(self, obj, field_name):
if self.many:
# To-many relationship
queryset = None
if not self.source:
# Prefer obj.serializable_value for performance reasons
try:
queryset = obj.serializable_value(field_name)
except AttributeError:
pass
if queryset is None:
# RelatedManager (reverse relationship)
source = self.source or field_name
queryset = obj
for component in source.split('.'):
if queryset is None:
return []
queryset = get_component(queryset, component)
# Forward relationship
if is_simple_callable(getattr(queryset, 'all', None)):
return [self.to_native(item.pk) for item in queryset.all()]
else:
# Also support non-queryset iterables.
# This allows us to also support plain lists of related items.
return [self.to_native(item.pk) for item in queryset]
# To-one relationship
try:
# Prefer obj.serializable_value for performance reasons
pk = obj.serializable_value(self.source or field_name)
except AttributeError:
# RelatedObject (reverse relationship)
try:
pk = getattr(obj, self.source or field_name).pk
except (ObjectDoesNotExist, AttributeError):
return None
# Forward relationship
return self.to_native(pk)
# Slug relationships
class SlugRelatedField(RelatedField):
"""
Represents a relationship using a unique field on the target.
"""
read_only = False
default_error_messages = {
'does_not_exist': _("Object with %s=%s does not exist."),
'invalid': _('Invalid value.'),
}
def __init__(self, *args, **kwargs):
self.slug_field = kwargs.pop('slug_field', None)
assert self.slug_field, 'slug_field is required'
super(SlugRelatedField, self).__init__(*args, **kwargs)
def to_native(self, obj):
return getattr(obj, self.slug_field)
def from_native(self, data):
if self.queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
try:
return self.queryset.get(**{self.slug_field: data})
except ObjectDoesNotExist:
raise ValidationError(self.error_messages['does_not_exist'] %
(self.slug_field, smart_text(data)))
except (TypeError, ValueError):
msg = self.error_messages['invalid']
raise ValidationError(msg)
# Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField):
"""
Represents a relationship using hyperlinking.
"""
read_only = False
lookup_field = 'pk'
default_error_messages = {
'no_match': _('Invalid hyperlink - No URL match'),
'incorrect_match': _('Invalid hyperlink - Incorrect URL match'),
'configuration_error': _('Invalid hyperlink due to configuration error'),
'does_not_exist': _("Invalid hyperlink - object does not exist."),
'incorrect_type': _('Incorrect type. Expected url string, received %s.'),
}
# These are all deprecated
pk_url_kwarg = 'pk'
slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
def __init__(self, *args, **kwargs):
try:
self.view_name = kwargs.pop('view_name')
except KeyError:
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
self.format = kwargs.pop('format', None)
# These are deprecated
if 'pk_url_kwarg' in kwargs:
msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
if 'slug_url_kwarg' in kwargs:
msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
if 'slug_field' in kwargs:
msg = 'slug_field is deprecated. Use lookup_field instead.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
self.slug_field = kwargs.pop('slug_field', self.slug_field)
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
def get_url(self, obj, view_name, request, format):
"""
Given an object, return the URL that hyperlinks to the object.
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
attributes are not configured to correctly match the URL conf.
"""
lookup_field = getattr(obj, self.lookup_field)
kwargs = {self.lookup_field: lookup_field}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except NoReverseMatch:
pass
if self.pk_url_kwarg != 'pk':
# Only try pk if it has been explicitly set.
# Otherwise, the default `lookup_field = 'pk'` has us covered.
pk = obj.pk
kwargs = {self.pk_url_kwarg: pk}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except NoReverseMatch:
pass
slug = getattr(obj, self.slug_field, None)
if slug is not None:
# Only try slug if it corresponds to an attribute on the object.
kwargs = {self.slug_url_kwarg: slug}
try:
ret = reverse(view_name, kwargs=kwargs, request=request, format=format)
if self.slug_field == 'slug' and self.slug_url_kwarg == 'slug':
# If the lookup succeeds using the default slug params,
# then `slug_field` is being used implicitly, and we
# we need to warn about the pending deprecation.
msg = 'Implicit slug field hyperlinked fields are deprecated.' \
'You should set `lookup_field=slug` on the HyperlinkedRelatedField.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
return ret
except NoReverseMatch:
pass
raise NoReverseMatch()
def get_object(self, queryset, view_name, view_args, view_kwargs):
"""
Return the object corresponding to a matched URL.
Takes the matched URL conf arguments, and the queryset, and should
return an object instance, or raise an `ObjectDoesNotExist` exception.
"""
lookup = view_kwargs.get(self.lookup_field, None)
pk = view_kwargs.get(self.pk_url_kwarg, None)
slug = view_kwargs.get(self.slug_url_kwarg, None)
if lookup is not None:
filter_kwargs = {self.lookup_field: lookup}
elif pk is not None:
filter_kwargs = {'pk': pk}
elif slug is not None:
filter_kwargs = {self.slug_field: slug}
else:
raise ObjectDoesNotExist()
return queryset.get(**filter_kwargs)
def to_native(self, obj):
view_name = self.view_name
request = self.context.get('request', None)
format = self.format or self.context.get('format', None)
assert request is not None, (
"`HyperlinkedRelatedField` requires the request in the serializer "
"context. Add `context={'request': request}` when instantiating "
"the serializer."
)
# If the object has not yet been saved then we cannot hyperlink to it.
if getattr(obj, 'pk', None) is None:
return
# Return the hyperlink, or error if incorrectly configured.
try:
return self.get_url(obj, view_name, request, format)
except NoReverseMatch:
msg = (
'Could not resolve URL for hyperlinked relationship using '
'view name "%s". You may have failed to include the related '
'model in your API, or incorrectly configured the '
'`lookup_field` attribute on this field.'
)
raise Exception(msg % view_name)
def from_native(self, value):
# Convert URL -> model instance pk
# TODO: Use values_list
queryset = self.queryset
if queryset is None:
raise Exception('Writable related fields must include a `queryset` argument')
try:
http_prefix = value.startswith(('http:', 'https:'))
except AttributeError:
msg = self.error_messages['incorrect_type']
raise ValidationError(msg % type(value).__name__)
if http_prefix:
# If needed convert absolute URLs to relative path
value = urlparse.urlparse(value).path
prefix = get_script_prefix()
if value.startswith(prefix):
value = '/' + value[len(prefix):]
try:
match = resolve(value)
except Exception:
raise ValidationError(self.error_messages['no_match'])
if match.view_name != self.view_name:
raise ValidationError(self.error_messages['incorrect_match'])
try:
return self.get_object(queryset, match.view_name,
match.args, match.kwargs)
except (ObjectDoesNotExist, TypeError, ValueError):
raise ValidationError(self.error_messages['does_not_exist'])
class HyperlinkedIdentityField(Field):
"""
Represents the instance, or a property on the instance, using hyperlinking.
"""
lookup_field = 'pk'
read_only = True
# These are all deprecated
pk_url_kwarg = 'pk'
slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
def __init__(self, *args, **kwargs):
try:
self.view_name = kwargs.pop('view_name')
except KeyError:
msg = "HyperlinkedIdentityField requires 'view_name' argument"
raise ValueError(msg)
self.format = kwargs.pop('format', None)
lookup_field = kwargs.pop('lookup_field', None)
self.lookup_field = lookup_field or self.lookup_field
# These are deprecated
if 'pk_url_kwarg' in kwargs:
msg = 'pk_url_kwarg is deprecated. Use lookup_field instead.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
if 'slug_url_kwarg' in kwargs:
msg = 'slug_url_kwarg is deprecated. Use lookup_field instead.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
if 'slug_field' in kwargs:
msg = 'slug_field is deprecated. Use lookup_field instead.'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
self.slug_field = kwargs.pop('slug_field', self.slug_field)
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
def field_to_native(self, obj, field_name):
request = self.context.get('request', None)
format = self.context.get('format', None)
view_name = self.view_name
assert request is not None, (
"`HyperlinkedIdentityField` requires the request in the serializer"
" context. Add `context={'request': request}` when instantiating "
"the serializer."
)
# By default use whatever format is given for the current context
# unless the target is a different type to the source.
#
# Eg. Consider a HyperlinkedIdentityField pointing from a json
# representation to an html property of that representation...
#
# '/snippets/1/' should link to '/snippets/1/highlight/'
# ...but...
# '/snippets/1/.json' should link to '/snippets/1/highlight/.html'
if format and self.format and self.format != format:
format = self.format
# Return the hyperlink, or error if incorrectly configured.
try:
return self.get_url(obj, view_name, request, format)
except NoReverseMatch:
msg = (
'Could not resolve URL for hyperlinked relationship using '
'view name "%s". You may have failed to include the related '
'model in your API, or incorrectly configured the '
'`lookup_field` attribute on this field.'
)
raise Exception(msg % view_name)
def get_url(self, obj, view_name, request, format):
"""
Given an object, return the URL that hyperlinks to the object.
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
attributes are not configured to correctly match the URL conf.
"""
lookup_field = getattr(obj, self.lookup_field, None)
kwargs = {self.lookup_field: lookup_field}
# Handle unsaved object case
if lookup_field is None:
return None
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except NoReverseMatch:
pass
if self.pk_url_kwarg != 'pk':
# Only try pk lookup if it has been explicitly set.
# Otherwise, the default `lookup_field = 'pk'` has us covered.
kwargs = {self.pk_url_kwarg: obj.pk}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except NoReverseMatch:
pass
slug = getattr(obj, self.slug_field, None)
if slug:
# Only use slug lookup if a slug field exists on the model
kwargs = {self.slug_url_kwarg: slug}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except NoReverseMatch:
pass
raise NoReverseMatch()

View File

@ -458,7 +458,7 @@ class BrowsableAPIRenderer(BaseRenderer):
): ):
return return
serializer = view.get_serializer(instance=obj, data=data, files=files) serializer = view.get_serializer(instance=obj, data=data)
serializer.is_valid() serializer.is_valid()
data = serializer.data data = serializer.data
@ -579,10 +579,10 @@ class BrowsableAPIRenderer(BaseRenderer):
'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes], 'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
'response_headers': response_headers, 'response_headers': response_headers,
'put_form': self.get_rendered_html_form(view, 'PUT', request), #'put_form': self.get_rendered_html_form(view, 'PUT', request),
'post_form': self.get_rendered_html_form(view, 'POST', request), #'post_form': self.get_rendered_html_form(view, 'POST', request),
'delete_form': self.get_rendered_html_form(view, 'DELETE', request), #'delete_form': self.get_rendered_html_form(view, 'DELETE', request),
'options_form': self.get_rendered_html_form(view, 'OPTIONS', request), #'options_form': self.get_rendered_html_form(view, 'OPTIONS', request),
'raw_data_put_form': raw_data_put_form, 'raw_data_put_form': raw_data_put_form,
'raw_data_post_form': raw_data_post_form, 'raw_data_post_form': raw_data_post_form,

File diff suppressed because it is too large Load Diff

View File

@ -111,9 +111,6 @@ DEFAULTS = {
), ),
'TIME_FORMAT': None, 'TIME_FORMAT': None,
# Pending deprecation
'FILTER_BACKEND': None,
} }
@ -129,7 +126,6 @@ IMPORT_STRINGS = (
'DEFAULT_PAGINATION_SERIALIZER_CLASS', 'DEFAULT_PAGINATION_SERIALIZER_CLASS',
'DEFAULT_FILTER_BACKENDS', 'DEFAULT_FILTER_BACKENDS',
'EXCEPTION_HANDLER', 'EXCEPTION_HANDLER',
'FILTER_BACKEND',
'TEST_REQUEST_RENDERER_CLASSES', 'TEST_REQUEST_RENDERER_CLASSES',
'UNAUTHENTICATED_USER', 'UNAUTHENTICATED_USER',
'UNAUTHENTICATED_TOKEN', 'UNAUTHENTICATED_TOKEN',
@ -196,15 +192,9 @@ class APISettings(object):
if val and attr in self.import_strings: if val and attr in self.import_strings:
val = perform_import(val, attr) val = perform_import(val, attr)
self.validate_setting(attr, val)
# Cache the result # Cache the result
setattr(self, attr, val) setattr(self, attr, val)
return val return val
def validate_setting(self, attr, val):
if attr == 'FILTER_BACKEND' and val is not None:
# Make sure we can initialize the class
val()
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)

View File

@ -7,7 +7,7 @@ from django.db.models.query import QuerySet
from django.utils.datastructures import SortedDict from django.utils.datastructures import SortedDict
from django.utils.functional import Promise from django.utils.functional import Promise
from rest_framework.compat import force_text from rest_framework.compat import force_text
from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata # from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
import datetime import datetime
import decimal import decimal
import types import types
@ -106,14 +106,14 @@ else:
SortedDict, SortedDict,
yaml.representer.SafeRepresenter.represent_dict yaml.representer.SafeRepresenter.represent_dict
) )
SafeDumper.add_representer( # SafeDumper.add_representer(
DictWithMetadata, # DictWithMetadata,
yaml.representer.SafeRepresenter.represent_dict # yaml.representer.SafeRepresenter.represent_dict
) # )
SafeDumper.add_representer( # SafeDumper.add_representer(
SortedDictWithMetadata, # SortedDictWithMetadata,
yaml.representer.SafeRepresenter.represent_dict # yaml.representer.SafeRepresenter.represent_dict
) # )
SafeDumper.add_representer( SafeDumper.add_representer(
types.GeneratorType, types.GeneratorType,
yaml.representer.SafeRepresenter.represent_list yaml.representer.SafeRepresenter.represent_list

View File

@ -0,0 +1,86 @@
"""
Helpers for dealing with HTML input.
"""
def is_html_input(dictionary):
# MultiDict type datastructures are used to represent HTML form input,
# which may have more than one value for each key.
return hasattr(dictionary, 'getlist')
def parse_html_list(dictionary, prefix=''):
"""
Used to suport list values in HTML forms.
Supports lists of primitives and/or dictionaries.
* List of primitives.
{
'[0]': 'abc',
'[1]': 'def',
'[2]': 'hij'
}
-->
[
'abc',
'def',
'hij'
]
* List of dictionaries.
{
'[0]foo': 'abc',
'[0]bar': 'def',
'[1]foo': 'hij',
'[2]bar': 'klm',
}
-->
[
{'foo': 'abc', 'bar': 'def'},
{'foo': 'hij', 'bar': 'klm'}
]
"""
Dict = type(dictionary)
ret = {}
regex = re.compile(r'^%s\[([0-9]+)\](.*)$' % re.escape(prefix))
for field, value in dictionary.items():
match = regex.match(field)
if not match:
continue
index, key = match.groups()
index = int(index)
if not key:
ret[index] = value
elif isinstance(ret.get(index), dict):
ret[index][key] = value
else:
ret[index] = Dict({key: value})
return [ret[item] for item in sorted(ret.keys())]
def parse_html_dict(dictionary, prefix):
"""
Used to support dictionary values in HTML forms.
{
'profile.username': 'example',
'profile.email': 'example@example.com',
}
-->
{
'profile': {
'username': 'example,
'email': 'example@example.com'
}
}
"""
ret = {}
regex = re.compile(r'^%s\.(.+)$' % re.escape(prefix))
for field, value in dictionary.items():
match = regex.match(field)
if not match:
continue
key = match.groups()[0]
ret[key] = value
return ret

View File

@ -1,7 +0,0 @@
from rest_framework import serializers
from tests.models import NullableForeignKeySource
class NullableFKSourceSerializer(serializers.ModelSerializer):
class Meta:
model = NullableForeignKeySource

View File

@ -16,9 +16,14 @@ factory = APIRequestFactory()
if django_filters: if django_filters:
class FilterableItemSerializer(serializers.ModelSerializer):
class Meta:
model = FilterableItem
# Basic filter on a list view. # Basic filter on a list view.
class FilterFieldsRootView(generics.ListCreateAPIView): class FilterFieldsRootView(generics.ListCreateAPIView):
model = FilterableItem queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_fields = ['decimal', 'date'] filter_fields = ['decimal', 'date']
filter_backends = (filters.DjangoFilterBackend,) filter_backends = (filters.DjangoFilterBackend,)
@ -33,7 +38,8 @@ if django_filters:
fields = ['text', 'decimal', 'date'] fields = ['text', 'decimal', 'date']
class FilterClassRootView(generics.ListCreateAPIView): class FilterClassRootView(generics.ListCreateAPIView):
model = FilterableItem queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_class = SeveralFieldsFilter filter_class = SeveralFieldsFilter
filter_backends = (filters.DjangoFilterBackend,) filter_backends = (filters.DjangoFilterBackend,)
@ -46,12 +52,14 @@ if django_filters:
fields = ['text'] fields = ['text']
class IncorrectlyConfiguredRootView(generics.ListCreateAPIView): class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
model = FilterableItem queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_class = MisconfiguredFilter filter_class = MisconfiguredFilter
filter_backends = (filters.DjangoFilterBackend,) filter_backends = (filters.DjangoFilterBackend,)
class FilterClassDetailView(generics.RetrieveAPIView): class FilterClassDetailView(generics.RetrieveAPIView):
model = FilterableItem queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_class = SeveralFieldsFilter filter_class = SeveralFieldsFilter
filter_backends = (filters.DjangoFilterBackend,) filter_backends = (filters.DjangoFilterBackend,)
@ -63,15 +71,12 @@ if django_filters:
model = BaseFilterableItem model = BaseFilterableItem
class BaseFilterableItemFilterRootView(generics.ListCreateAPIView): class BaseFilterableItemFilterRootView(generics.ListCreateAPIView):
model = FilterableItem queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
filter_class = BaseFilterableItemFilter filter_class = BaseFilterableItemFilter
filter_backends = (filters.DjangoFilterBackend,) filter_backends = (filters.DjangoFilterBackend,)
# Regression test for #814 # Regression test for #814
class FilterableItemSerializer(serializers.ModelSerializer):
class Meta:
model = FilterableItem
class FilterFieldsQuerysetView(generics.ListCreateAPIView): class FilterFieldsQuerysetView(generics.ListCreateAPIView):
queryset = FilterableItem.objects.all() queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer serializer_class = FilterableItemSerializer
@ -323,6 +328,11 @@ class SearchFilterModel(models.Model):
text = models.CharField(max_length=100) text = models.CharField(max_length=100)
class SearchFilterSerializer(serializers.ModelSerializer):
class Meta:
model = SearchFilterModel
class SearchFilterTests(TestCase): class SearchFilterTests(TestCase):
def setUp(self): def setUp(self):
# Sequence of title/text is: # Sequence of title/text is:
@ -342,7 +352,8 @@ class SearchFilterTests(TestCase):
def test_search(self): def test_search(self):
class SearchListView(generics.ListAPIView): class SearchListView(generics.ListAPIView):
model = SearchFilterModel queryset = SearchFilterModel.objects.all()
serializer_class = SearchFilterSerializer
filter_backends = (filters.SearchFilter,) filter_backends = (filters.SearchFilter,)
search_fields = ('title', 'text') search_fields = ('title', 'text')
@ -359,7 +370,8 @@ class SearchFilterTests(TestCase):
def test_exact_search(self): def test_exact_search(self):
class SearchListView(generics.ListAPIView): class SearchListView(generics.ListAPIView):
model = SearchFilterModel queryset = SearchFilterModel.objects.all()
serializer_class = SearchFilterSerializer
filter_backends = (filters.SearchFilter,) filter_backends = (filters.SearchFilter,)
search_fields = ('=title', 'text') search_fields = ('=title', 'text')
@ -375,7 +387,8 @@ class SearchFilterTests(TestCase):
def test_startswith_search(self): def test_startswith_search(self):
class SearchListView(generics.ListAPIView): class SearchListView(generics.ListAPIView):
model = SearchFilterModel queryset = SearchFilterModel.objects.all()
serializer_class = SearchFilterSerializer
filter_backends = (filters.SearchFilter,) filter_backends = (filters.SearchFilter,)
search_fields = ('title', '^text') search_fields = ('title', '^text')
@ -392,7 +405,8 @@ class SearchFilterTests(TestCase):
def test_search_with_nonstandard_search_param(self): def test_search_with_nonstandard_search_param(self):
with temporary_setting('SEARCH_PARAM', 'query', module=filters): with temporary_setting('SEARCH_PARAM', 'query', module=filters):
class SearchListView(generics.ListAPIView): class SearchListView(generics.ListAPIView):
model = SearchFilterModel queryset = SearchFilterModel.objects.all()
serializer_class = SearchFilterSerializer
filter_backends = (filters.SearchFilter,) filter_backends = (filters.SearchFilter,)
search_fields = ('title', 'text') search_fields = ('title', 'text')
@ -418,6 +432,11 @@ class OrderingFilterRelatedModel(models.Model):
related_name="relateds") related_name="relateds")
class OrderingFilterSerializer(serializers.ModelSerializer):
class Meta:
model = OrdringFilterModel
class OrderingFilterTests(TestCase): class OrderingFilterTests(TestCase):
def setUp(self): def setUp(self):
# Sequence of title/text is: # Sequence of title/text is:
@ -440,7 +459,8 @@ class OrderingFilterTests(TestCase):
def test_ordering(self): def test_ordering(self):
class OrderingListView(generics.ListAPIView): class OrderingListView(generics.ListAPIView):
model = OrdringFilterModel queryset = OrdringFilterModel.objects.all()
serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('title',) ordering = ('title',)
ordering_fields = ('text',) ordering_fields = ('text',)
@ -459,7 +479,8 @@ class OrderingFilterTests(TestCase):
def test_reverse_ordering(self): def test_reverse_ordering(self):
class OrderingListView(generics.ListAPIView): class OrderingListView(generics.ListAPIView):
model = OrdringFilterModel queryset = OrdringFilterModel.objects.all()
serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('title',) ordering = ('title',)
ordering_fields = ('text',) ordering_fields = ('text',)
@ -478,7 +499,8 @@ class OrderingFilterTests(TestCase):
def test_incorrectfield_ordering(self): def test_incorrectfield_ordering(self):
class OrderingListView(generics.ListAPIView): class OrderingListView(generics.ListAPIView):
model = OrdringFilterModel queryset = OrdringFilterModel.objects.all()
serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('title',) ordering = ('title',)
ordering_fields = ('text',) ordering_fields = ('text',)
@ -497,7 +519,8 @@ class OrderingFilterTests(TestCase):
def test_default_ordering(self): def test_default_ordering(self):
class OrderingListView(generics.ListAPIView): class OrderingListView(generics.ListAPIView):
model = OrdringFilterModel queryset = OrdringFilterModel.objects.all()
serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('title',) ordering = ('title',)
oredering_fields = ('text',) oredering_fields = ('text',)
@ -516,7 +539,8 @@ class OrderingFilterTests(TestCase):
def test_default_ordering_using_string(self): def test_default_ordering_using_string(self):
class OrderingListView(generics.ListAPIView): class OrderingListView(generics.ListAPIView):
model = OrdringFilterModel queryset = OrdringFilterModel.objects.all()
serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = 'title' ordering = 'title'
ordering_fields = ('text',) ordering_fields = ('text',)
@ -545,7 +569,7 @@ class OrderingFilterTests(TestCase):
new_related.save() new_related.save()
class OrderingListView(generics.ListAPIView): class OrderingListView(generics.ListAPIView):
model = OrdringFilterModel serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = 'title' ordering = 'title'
ordering_fields = '__all__' ordering_fields = '__all__'
@ -567,7 +591,8 @@ class OrderingFilterTests(TestCase):
def test_ordering_with_nonstandard_ordering_param(self): def test_ordering_with_nonstandard_ordering_param(self):
with temporary_setting('ORDERING_PARAM', 'order', filters): with temporary_setting('ORDERING_PARAM', 'order', filters):
class OrderingListView(generics.ListAPIView): class OrderingListView(generics.ListAPIView):
model = OrdringFilterModel queryset = OrdringFilterModel.objects.all()
serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,) filter_backends = (filters.OrderingFilter,)
ordering = ('title',) ordering = ('title',)
ordering_fields = ('text',) ordering_fields = ('text',)

View File

@ -11,18 +11,30 @@ from tests.models import ForeignKeySource, ForeignKeyTarget
factory = APIRequestFactory() factory = APIRequestFactory()
class BasicSerializer(serializers.ModelSerializer):
class Meta:
model = BasicModel
class ForeignKeySerializer(serializers.ModelSerializer):
class Meta:
model = ForeignKeySource
class RootView(generics.ListCreateAPIView): class RootView(generics.ListCreateAPIView):
""" """
Example description for OPTIONS. Example description for OPTIONS.
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
class InstanceView(generics.RetrieveUpdateDestroyAPIView): class InstanceView(generics.RetrieveUpdateDestroyAPIView):
""" """
Example description for OPTIONS. Example description for OPTIONS.
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
def get_queryset(self): def get_queryset(self):
queryset = super(InstanceView, self).get_queryset() queryset = super(InstanceView, self).get_queryset()
@ -33,7 +45,8 @@ class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
""" """
FK: example description for OPTIONS. FK: example description for OPTIONS.
""" """
model = ForeignKeySource queryset = ForeignKeySource.objects.all()
serializer_class = ForeignKeySerializer
class SlugSerializer(serializers.ModelSerializer): class SlugSerializer(serializers.ModelSerializer):
@ -48,7 +61,7 @@ class SlugBasedInstanceView(InstanceView):
""" """
A model with a slug-field. A model with a slug-field.
""" """
model = SlugBasedModel queryset = SlugBasedModel.objects.all()
serializer_class = SlugSerializer serializer_class = SlugSerializer
lookup_field = 'slug' lookup_field = 'slug'
@ -503,7 +516,7 @@ class TestOverriddenGetObject(TestCase):
""" """
Example detail view for override of get_object(). Example detail view for override of get_object().
""" """
model = BasicModel serializer_class = BasicSerializer
def get_object(self): def get_object(self):
pk = int(self.kwargs['pk']) pk = int(self.kwargs['pk'])
@ -573,7 +586,7 @@ class ClassASerializer(serializers.ModelSerializer):
class ExampleView(generics.ListCreateAPIView): class ExampleView(generics.ListCreateAPIView):
serializer_class = ClassASerializer serializer_class = ClassASerializer
model = ClassA queryset = ClassA.objects.all()
class TestM2MBrowseableAPI(TestCase): class TestM2MBrowseableAPI(TestCase):
@ -603,7 +616,7 @@ class TwoFieldModel(models.Model):
class DynamicSerializerView(generics.ListCreateAPIView): class DynamicSerializerView(generics.ListCreateAPIView):
model = TwoFieldModel queryset = TwoFieldModel.objects.all()
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer) renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
def get_serializer_class(self): def get_serializer_class(self):
@ -612,8 +625,11 @@ class DynamicSerializerView(generics.ListCreateAPIView):
class Meta: class Meta:
model = TwoFieldModel model = TwoFieldModel
fields = ('field_b',) fields = ('field_b',)
return DynamicSerializer else:
return super(DynamicSerializerView, self).get_serializer_class() class DynamicSerializer(serializers.ModelSerializer):
class Meta:
model = TwoFieldModel
return DynamicSerializer
class TestFilterBackendAppliedToViews(TestCase): class TestFilterBackendAppliedToViews(TestCase):

View File

@ -39,59 +39,85 @@ class AlbumSerializer(serializers.ModelSerializer):
fields = ('title', 'url') fields = ('title', 'url')
class BasicSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = BasicModel
class AnchorSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Anchor
class ManyToManySerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ManyToManyModel
class BlogPostSerializer(serializers.ModelSerializer):
class Meta:
model = BlogPost
class OptionalRelationSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = OptionalRelationModel
class BasicList(generics.ListCreateAPIView): class BasicList(generics.ListCreateAPIView):
model = BasicModel queryset = BasicModel.objects.all()
model_serializer_class = serializers.HyperlinkedModelSerializer serializer_class = BasicSerializer
class BasicDetail(generics.RetrieveUpdateDestroyAPIView): class BasicDetail(generics.RetrieveUpdateDestroyAPIView):
model = BasicModel queryset = BasicModel.objects.all()
model_serializer_class = serializers.HyperlinkedModelSerializer serializer_class = BasicSerializer
class AnchorDetail(generics.RetrieveAPIView): class AnchorDetail(generics.RetrieveAPIView):
model = Anchor queryset = Anchor.objects.all()
model_serializer_class = serializers.HyperlinkedModelSerializer serializer_class = AnchorSerializer
class ManyToManyList(generics.ListAPIView): class ManyToManyList(generics.ListAPIView):
model = ManyToManyModel queryset = ManyToManyModel.objects.all()
model_serializer_class = serializers.HyperlinkedModelSerializer serializer_class = ManyToManySerializer
class ManyToManyDetail(generics.RetrieveAPIView): class ManyToManyDetail(generics.RetrieveAPIView):
model = ManyToManyModel queryset = ManyToManyModel.objects.all()
model_serializer_class = serializers.HyperlinkedModelSerializer serializer_class = ManyToManySerializer
class BlogPostCommentListCreate(generics.ListCreateAPIView): class BlogPostCommentListCreate(generics.ListCreateAPIView):
model = BlogPostComment queryset = BlogPostComment.objects.all()
serializer_class = BlogPostCommentSerializer serializer_class = BlogPostCommentSerializer
class BlogPostCommentDetail(generics.RetrieveAPIView): class BlogPostCommentDetail(generics.RetrieveAPIView):
model = BlogPostComment queryset = BlogPostComment.objects.all()
serializer_class = BlogPostCommentSerializer serializer_class = BlogPostCommentSerializer
class BlogPostDetail(generics.RetrieveAPIView): class BlogPostDetail(generics.RetrieveAPIView):
model = BlogPost queryset = BlogPost.objects.all()
serializer_class = BlogPostSerializer
class PhotoListCreate(generics.ListCreateAPIView): class PhotoListCreate(generics.ListCreateAPIView):
model = Photo queryset = Photo.objects.all()
model_serializer_class = PhotoSerializer serializer_class = PhotoSerializer
class AlbumDetail(generics.RetrieveAPIView): class AlbumDetail(generics.RetrieveAPIView):
model = Album queryset = Album.objects.all()
serializer_class = AlbumSerializer serializer_class = AlbumSerializer
lookup_field = 'title' lookup_field = 'title'
class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView): class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
model = OptionalRelationModel queryset = OptionalRelationModel.objects.all()
model_serializer_class = serializers.HyperlinkedModelSerializer serializer_class = OptionalRelationSerializer
urlpatterns = patterns( urlpatterns = patterns(

View File

@ -1,10 +1,19 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from rest_framework import serializers, generics
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from tests.models import NullableForeignKeySource from tests.models import NullableForeignKeySource
from tests.serializers import NullableFKSourceSerializer
from tests.views import NullableFKSourceDetail
class NullableFKSourceSerializer(serializers.ModelSerializer):
class Meta:
model = NullableForeignKeySource
class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = NullableForeignKeySource.objects.all()
serializer_class = NullableFKSourceSerializer
urlpatterns = patterns( urlpatterns = patterns(

View File

@ -4,7 +4,7 @@ from decimal import Decimal
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from rest_framework import generics, status, pagination, filters, serializers from rest_framework import generics, serializers, status, pagination, filters
from rest_framework.compat import django_filters from rest_framework.compat import django_filters
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from .models import BasicModel, FilterableItem from .models import BasicModel, FilterableItem
@ -22,11 +22,22 @@ def split_arguments_from_url(url):
return path, args return path, args
class BasicSerializer(serializers.ModelSerializer):
class Meta:
model = BasicModel
class FilterableItemSerializer(serializers.ModelSerializer):
class Meta:
model = FilterableItem
class RootView(generics.ListCreateAPIView): class RootView(generics.ListCreateAPIView):
""" """
Example description for OPTIONS. Example description for OPTIONS.
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
paginate_by = 10 paginate_by = 10
@ -34,14 +45,16 @@ class DefaultPageSizeKwargView(generics.ListAPIView):
""" """
View for testing default paginate_by_param usage View for testing default paginate_by_param usage
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
class PaginateByParamView(generics.ListAPIView): class PaginateByParamView(generics.ListAPIView):
""" """
View for testing custom paginate_by_param usage View for testing custom paginate_by_param usage
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
paginate_by_param = 'page_size' paginate_by_param = 'page_size'
@ -49,7 +62,8 @@ class MaxPaginateByView(generics.ListAPIView):
""" """
View for testing custom max_paginate_by usage View for testing custom max_paginate_by usage
""" """
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
paginate_by = 3 paginate_by = 3
max_paginate_by = 5 max_paginate_by = 5
paginate_by_param = 'page_size' paginate_by_param = 'page_size'
@ -140,7 +154,8 @@ class IntegrationTestPaginationAndFiltering(TestCase):
fields = ['text', 'decimal', 'date'] fields = ['text', 'decimal', 'date']
class FilterFieldsRootView(generics.ListCreateAPIView): class FilterFieldsRootView(generics.ListCreateAPIView):
model = FilterableItem queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
paginate_by = 10 paginate_by = 10
filter_class = DecimalFilter filter_class = DecimalFilter
filter_backends = (filters.DjangoFilterBackend,) filter_backends = (filters.DjangoFilterBackend,)
@ -188,7 +203,8 @@ class IntegrationTestPaginationAndFiltering(TestCase):
return queryset.filter(decimal__lt=Decimal(request.GET['decimal'])) return queryset.filter(decimal__lt=Decimal(request.GET['decimal']))
class BasicFilterFieldsRootView(generics.ListCreateAPIView): class BasicFilterFieldsRootView(generics.ListCreateAPIView):
model = FilterableItem queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
paginate_by = 10 paginate_by = 10
filter_backends = (DecimalFilterBackend,) filter_backends = (DecimalFilterBackend,)
@ -387,7 +403,7 @@ class TestContextPassedToCustomField(TestCase):
def test_with_pagination(self): def test_with_pagination(self):
class ListView(generics.ListCreateAPIView): class ListView(generics.ListCreateAPIView):
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicModelSerializer serializer_class = BasicModelSerializer
paginate_by = 1 paginate_by = 1

View File

@ -3,7 +3,7 @@ from django.contrib.auth.models import User, Permission, Group
from django.db import models from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import unittest from django.utils import unittest
from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING from rest_framework import generics, serializers, status, permissions, authentication, HTTP_HEADER_ENCODING
from rest_framework.compat import guardian, get_model_name from rest_framework.compat import guardian, get_model_name
from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.filters import DjangoObjectPermissionsFilter
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
@ -13,14 +13,21 @@ import base64
factory = APIRequestFactory() factory = APIRequestFactory()
class BasicSerializer(serializers.ModelSerializer):
class Meta:
model = BasicModel
class RootView(generics.ListCreateAPIView): class RootView(generics.ListCreateAPIView):
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [permissions.DjangoModelPermissions] permission_classes = [permissions.DjangoModelPermissions]
class InstanceView(generics.RetrieveUpdateDestroyAPIView): class InstanceView(generics.RetrieveUpdateDestroyAPIView):
model = BasicModel queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [permissions.DjangoModelPermissions] permission_classes = [permissions.DjangoModelPermissions]
@ -167,6 +174,11 @@ class BasicPermModel(models.Model):
) )
class BasicPermSerializer(serializers.ModelSerializer):
class Meta:
model = BasicPermModel
# Custom object-level permission, that includes 'view' permissions # Custom object-level permission, that includes 'view' permissions
class ViewObjectPermissions(permissions.DjangoObjectPermissions): class ViewObjectPermissions(permissions.DjangoObjectPermissions):
perms_map = { perms_map = {
@ -181,7 +193,8 @@ class ViewObjectPermissions(permissions.DjangoObjectPermissions):
class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView): class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
model = BasicPermModel queryset = BasicPermModel.objects.all()
serializer_class = BasicPermSerializer
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [ViewObjectPermissions] permission_classes = [ViewObjectPermissions]
@ -189,7 +202,8 @@ object_permissions_view = ObjectPermissionInstanceView.as_view()
class ObjectPermissionListView(generics.ListAPIView): class ObjectPermissionListView(generics.ListAPIView):
model = BasicPermModel queryset = BasicPermModel.objects.all()
serializer_class = BasicPermSerializer
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [ViewObjectPermissions] permission_classes = [ViewObjectPermissions]

View File

@ -86,14 +86,15 @@ class HTMLView1(APIView):
class HTMLNewModelViewSet(viewsets.ModelViewSet): class HTMLNewModelViewSet(viewsets.ModelViewSet):
model = BasicModel serializer_class = BasicModelSerializer
queryset = BasicModel.objects.all()
class HTMLNewModelView(generics.ListCreateAPIView): class HTMLNewModelView(generics.ListCreateAPIView):
renderer_classes = (BrowsableAPIRenderer,) renderer_classes = (BrowsableAPIRenderer,)
permission_classes = [] permission_classes = []
serializer_class = BasicModelSerializer serializer_class = BasicModelSerializer
model = BasicModel queryset = BasicModel.objects.all()
new_model_viewset_router = routers.DefaultRouter() new_model_viewset_router = routers.DefaultRouter()

View File

@ -22,7 +22,7 @@ class ValidationModelSerializer(serializers.ModelSerializer):
class UpdateValidationModel(generics.RetrieveUpdateDestroyAPIView): class UpdateValidationModel(generics.RetrieveUpdateDestroyAPIView):
model = ValidationModel queryset = ValidationModel.objects.all()
serializer_class = ValidationModelSerializer serializer_class = ValidationModelSerializer
@ -117,7 +117,7 @@ class ValidationMaxValueValidatorModelSerializer(serializers.ModelSerializer):
class UpdateMaxValueValidationModel(generics.RetrieveUpdateDestroyAPIView): class UpdateMaxValueValidationModel(generics.RetrieveUpdateDestroyAPIView):
model = ValidationMaxValueValidatorModel queryset = ValidationMaxValueValidatorModel.objects.all()
serializer_class = ValidationMaxValueValidatorModelSerializer serializer_class = ValidationMaxValueValidatorModelSerializer

View File

@ -1,8 +0,0 @@
from rest_framework import generics
from .models import NullableForeignKeySource
from .serializers import NullableFKSourceSerializer
class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView):
model = NullableForeignKeySource
model_serializer_class = NullableFKSourceSerializer