From f27a28682bdb1b4eea0ec9afca2eb2835c735f55 Mon Sep 17 00:00:00 2001
From: Greg Doermann
Date: Wed, 20 Aug 2014 11:00:37 -0600
Subject: Frameworks throws AssertionError saying you cannot set required=True
and read_only=True on editable=False model fields. We should not make the
field required if editable=False.
---
rest_framework/serializers.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index be8ad3f2..27af7ef3 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -831,7 +831,7 @@ class ModelSerializer(Serializer):
}
if model_field:
- kwargs['required'] = not(model_field.null or model_field.blank)
+ kwargs['required'] = not(model_field.null or model_field.blank) and model_field.editable
if model_field.help_text is not None:
kwargs['help_text'] = model_field.help_text
if model_field.verbose_name is not None:
@@ -854,7 +854,7 @@ class ModelSerializer(Serializer):
"""
kwargs = {}
- if model_field.null or model_field.blank:
+ if model_field.null or model_field.blank and model_field.editable:
kwargs['required'] = False
if isinstance(model_field, models.AutoField) or not model_field.editable:
@@ -1110,7 +1110,7 @@ class HyperlinkedModelSerializer(ModelSerializer):
}
if model_field:
- kwargs['required'] = not(model_field.null or model_field.blank)
+ kwargs['required'] = not(model_field.null or model_field.blank) and model_field.editable
if model_field.help_text is not None:
kwargs['help_text'] = model_field.help_text
if model_field.verbose_name is not None:
--
cgit v1.2.3
From d8eb9e6d45c227582559ec4318b1f92562c718da Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Aug 2014 10:48:16 +0100
Subject: Docs whitespace fix.
---
docs/api-guide/generic-views.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index b1c4e65a..d30b7f9b 100755
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -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 myapp.serializers import UserSerializer
- from rest_framework import generics
- from rest_framework.permissions import IsAdminUser
+ from rest_framework import generics
+ from rest_framework.permissions import IsAdminUser
class UserList(generics.ListCreateAPIView):
queryset = User.objects.all()
--
cgit v1.2.3
From f62c874ea9621ae67fb56e7e453dca8fd5039051 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Aug 2014 10:48:40 +0100
Subject: Remove `filter_backend`.
Closes #1775.
---
rest_framework/generics.py | 20 +-------------------
rest_framework/settings.py | 10 ----------
2 files changed, 1 insertion(+), 29 deletions(-)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index a6f68657..8bacf470 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -83,7 +83,6 @@ class GenericAPIView(views.APIView):
slug_url_kwarg = 'slug'
slug_field = 'slug'
allow_empty = True
- filter_backend = api_settings.FILTER_BACKEND
def get_serializer_context(self):
"""
@@ -191,24 +190,7 @@ class GenericAPIView(views.APIView):
"""
Returns the list of filter backends that this view requires.
"""
- if self.filter_backends is None:
- 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
+ return list(self.filter_backends)
# The following methods provide default implementations
# that you may want to override for more complex cases.
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 644751f8..bbe7a56a 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -111,9 +111,6 @@ DEFAULTS = {
),
'TIME_FORMAT': None,
- # Pending deprecation
- 'FILTER_BACKEND': None,
-
}
@@ -129,7 +126,6 @@ IMPORT_STRINGS = (
'DEFAULT_PAGINATION_SERIALIZER_CLASS',
'DEFAULT_FILTER_BACKENDS',
'EXCEPTION_HANDLER',
- 'FILTER_BACKEND',
'TEST_REQUEST_RENDERER_CLASSES',
'UNAUTHENTICATED_USER',
'UNAUTHENTICATED_TOKEN',
@@ -196,15 +192,9 @@ class APISettings(object):
if val and attr in self.import_strings:
val = perform_import(val, attr)
- self.validate_setting(attr, val)
-
# Cache the result
setattr(self, attr, 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)
--
cgit v1.2.3
From 0f8fdf4e72b67ff46474c13c8b532bf319a58099 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Aug 2014 10:57:24 +0100
Subject: Remove `allow_empty`.
Closes #1774.
---
docs/api-guide/generic-views.md | 2 --
rest_framework/generics.py | 12 +-----------
2 files changed, 1 insertion(+), 13 deletions(-)
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index d30b7f9b..49be0cae 100755
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -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 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
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 8bacf470..cb8061b7 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -82,7 +82,6 @@ class GenericAPIView(views.APIView):
pk_url_kwarg = 'pk'
slug_url_kwarg = 'slug'
slug_field = 'slug'
- allow_empty = True
def get_serializer_context(self):
"""
@@ -140,16 +139,7 @@ class GenericAPIView(views.APIView):
if not page_size:
return None
- if not self.allow_empty:
- 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)
+ paginator = self.paginator_class(queryset, page_size)
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
--
cgit v1.2.3
From b3bbf416707cf8c71861b0fd6e966a557acef412 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Aug 2014 11:09:35 +0100
Subject: Remove `allow_empty`
---
rest_framework/mixins.py | 15 ---------------
1 file changed, 15 deletions(-)
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 2cc87eef..dc4c9f35 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -70,24 +70,9 @@ class ListModelMixin(object):
"""
List a queryset.
"""
- empty_error = "Empty list and '%(class_name)s.allow_empty' is False."
-
def list(self, request, *args, **kwargs):
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
page = self.paginate_queryset(self.object_list)
if page is not None:
--
cgit v1.2.3
From e5e6329a222def3b0745f90fc55ee36de95ada83 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Aug 2014 11:29:26 +0100
Subject: Remove `pk_url_field`, `slug_url_field`, `slug_field`.
Closes #1773.
---
rest_framework/generics.py | 36 ++------------
rest_framework/mixins.py | 36 +++-----------
rest_framework/relations.py | 117 ++------------------------------------------
3 files changed, 15 insertions(+), 174 deletions(-)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index cb8061b7..e21dc5c7 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -76,13 +76,6 @@ class GenericAPIView(views.APIView):
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'
-
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
@@ -270,7 +263,7 @@ class GenericAPIView(views.APIView):
error_format = "'%s' must define 'queryset' or 'model'"
raise ImproperlyConfigured(error_format % self.__class__.__name__)
- def get_object(self, queryset=None):
+ def get_object(self):
"""
Returns the object the view is displaying.
@@ -278,36 +271,14 @@ 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.
# Note that `pk` and `slug` are deprecated styles of lookup filtering.
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:
- filter_kwargs = {self.lookup_field: lookup}
- elif pk is not None and self.lookup_field == 'pk':
- warnings.warn(
- 'The `pk_url_kwarg` attribute is deprecated. '
- '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:
+ if lookup is None:
raise ImproperlyConfigured(
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
@@ -315,6 +286,7 @@ class GenericAPIView(views.APIView):
(self.__class__.__name__, self.lookup_field)
)
+ filter_kwargs = {self.lookup_field: lookup}
obj = get_object_or_404(queryset, **filter_kwargs)
# May raise a permission denied
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index dc4c9f35..ac59d979 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -12,10 +12,9 @@ from rest_framework import status
from rest_framework.response import Response
from rest_framework.request import clone_request
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,
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,
so we only clean the required fields.
"""
- include = []
-
- if pk:
- # Deprecated
+ if lookup_field == 'pk':
pk_field = obj._meta.pk
while pk_field.rel:
pk_field = pk_field.rel.to._meta.pk
- include.append(pk_field.name)
-
- if slug_field:
- # Deprecated
- include.append(slug_field)
-
- if lookup_field and lookup_field != 'pk':
- include.append(lookup_field)
+ lookup_field = pk_field.name
- return [field.name for field in obj._meta.fields if field.name not in include]
+ return [field.name for field in obj._meta.fields if field.name != lookup_field]
class CreateModelMixin(object):
@@ -146,26 +135,15 @@ class UpdateModelMixin(object):
"""
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 = 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)
- slug_field = slug and self.slug_field or None
-
- if lookup:
- setattr(obj, self.lookup_field, lookup)
-
- if pk:
- setattr(obj, 'pk', pk)
+ lookup_value = self.kwargs[lookup_url_kwarg]
- if slug:
- setattr(obj, slug_field, slug)
+ setattr(obj, self.lookup_field, lookup_value)
# 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.
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)
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 1acbdce2..56870b40 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -16,7 +16,6 @@ from rest_framework.fields import Field, WritableField, get_component, is_simple
from rest_framework.reverse import reverse
from rest_framework.compat import urlparse
from rest_framework.compat import smart_text
-import warnings
# Relational fields
@@ -320,11 +319,6 @@ class HyperlinkedRelatedField(RelatedField):
'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')
@@ -334,22 +328,6 @@ class HyperlinkedRelatedField(RelatedField):
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):
@@ -361,39 +339,7 @@ class HyperlinkedRelatedField(RelatedField):
"""
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()
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
def get_object(self, queryset, view_name, view_args, view_kwargs):
"""
@@ -402,19 +348,8 @@ class HyperlinkedRelatedField(RelatedField):
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()
-
+ lookup_value = view_kwargs[self.lookup_field]
+ filter_kwargs = {self.lookup_field: lookup_value}
return queryset.get(**filter_kwargs)
def to_native(self, obj):
@@ -486,11 +421,6 @@ class HyperlinkedIdentityField(Field):
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')
@@ -502,22 +432,6 @@ class HyperlinkedIdentityField(Field):
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):
@@ -569,27 +483,4 @@ class HyperlinkedIdentityField(Field):
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()
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
--
cgit v1.2.3
From b8c8d10a18741b76355ed7035655d0101c1d778a Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Aug 2014 11:38:54 +0100
Subject: Remove `page_size` argument.
`paginate_queryset` no longer takes an optional `page_size` argument.
---
rest_framework/generics.py | 22 ++++------------------
1 file changed, 4 insertions(+), 18 deletions(-)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index e21dc5c7..09035303 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -111,26 +111,14 @@ class GenericAPIView(views.APIView):
context = self.get_serializer_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,
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 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
+ page_size = self.get_paginate_by()
+ if not page_size:
+ return None
paginator = self.paginator_class(queryset, page_size)
page_kwarg = self.kwargs.get(self.page_kwarg)
@@ -152,8 +140,6 @@ class GenericAPIView(views.APIView):
'message': str(exc)
})
- if deprecated_style:
- return (paginator, page, page.object_list, page.has_other_pages())
return page
def filter_queryset(self, queryset):
--
cgit v1.2.3
From b3253b42836acd123224e88c0927f1ee6a031d94 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Aug 2014 12:35:53 +0100
Subject: Remove `.model` usage in tests.
Remove the shortcut `.model` view attribute usage from test cases.
---
rest_framework/generics.py | 49 +++++++--------------------
tests/serializers.py | 7 ----
tests/test_filters.py | 65 +++++++++++++++++++++++++-----------
tests/test_generics.py | 34 ++++++++++++++-----
tests/test_hyperlinkedserializers.py | 62 ++++++++++++++++++++++++----------
tests/test_nullable_fields.py | 13 ++++++--
tests/test_pagination.py | 32 +++++++++++++-----
tests/test_permissions.py | 24 ++++++++++---
tests/test_response.py | 5 +--
tests/test_validation.py | 4 +--
tests/views.py | 8 -----
11 files changed, 185 insertions(+), 118 deletions(-)
delete mode 100644 tests/serializers.py
delete mode 100644 tests/views.py
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 09035303..68222864 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -51,11 +51,6 @@ class GenericAPIView(views.APIView):
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.
# For more complex lookup requirements override `get_object()`.
lookup_field = 'pk'
@@ -71,9 +66,8 @@ class GenericAPIView(views.APIView):
# The filter backend classes to use for queryset filtering
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.
- model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
paginator_class = Paginator
def get_serializer_context(self):
@@ -199,26 +193,13 @@ class GenericAPIView(views.APIView):
(Eg. admins get full serialization, others get basic serialization)
"""
- serializer_class = self.serializer_class
- if serializer_class is not None:
- return serializer_class
-
- 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, \
- "'%s' should either include a 'serializer_class' attribute, " \
- "or use the 'model' attribute as a shortcut for " \
- "automatically generating a serializer class." \
+ 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__
+ )
- class DefaultSerializer(self.model_serializer_class):
- class Meta:
- model = self.model
- return DefaultSerializer
+ return self.serializer_class
def get_queryset(self):
"""
@@ -235,19 +216,13 @@ class GenericAPIView(views.APIView):
(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:
- 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()
+ assert self.queryset is not None, (
+ "'%s' should either include a `queryset` attribute, "
+ "or override the `get_queryset()` method."
+ % self.__class__.__name__
+ )
- error_format = "'%s' must define 'queryset' or 'model'"
- raise ImproperlyConfigured(error_format % self.__class__.__name__)
+ return self.queryset._clone()
def get_object(self):
"""
diff --git a/tests/serializers.py b/tests/serializers.py
deleted file mode 100644
index be7b3772..00000000
--- a/tests/serializers.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from rest_framework import serializers
-from tests.models import NullableForeignKeySource
-
-
-class NullableFKSourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = NullableForeignKeySource
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 47bffd43..6f24b1ab 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -16,9 +16,14 @@ factory = APIRequestFactory()
if django_filters:
+ class FilterableItemSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = FilterableItem
+
# Basic filter on a list view.
class FilterFieldsRootView(generics.ListCreateAPIView):
- model = FilterableItem
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
filter_fields = ['decimal', 'date']
filter_backends = (filters.DjangoFilterBackend,)
@@ -33,7 +38,8 @@ if django_filters:
fields = ['text', 'decimal', 'date']
class FilterClassRootView(generics.ListCreateAPIView):
- model = FilterableItem
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
filter_class = SeveralFieldsFilter
filter_backends = (filters.DjangoFilterBackend,)
@@ -46,12 +52,14 @@ if django_filters:
fields = ['text']
class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
- model = FilterableItem
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
filter_class = MisconfiguredFilter
filter_backends = (filters.DjangoFilterBackend,)
class FilterClassDetailView(generics.RetrieveAPIView):
- model = FilterableItem
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
filter_class = SeveralFieldsFilter
filter_backends = (filters.DjangoFilterBackend,)
@@ -63,15 +71,12 @@ if django_filters:
model = BaseFilterableItem
class BaseFilterableItemFilterRootView(generics.ListCreateAPIView):
- model = FilterableItem
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
filter_class = BaseFilterableItemFilter
filter_backends = (filters.DjangoFilterBackend,)
# Regression test for #814
- class FilterableItemSerializer(serializers.ModelSerializer):
- class Meta:
- model = FilterableItem
-
class FilterFieldsQuerysetView(generics.ListCreateAPIView):
queryset = FilterableItem.objects.all()
serializer_class = FilterableItemSerializer
@@ -323,6 +328,11 @@ class SearchFilterModel(models.Model):
text = models.CharField(max_length=100)
+class SearchFilterSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = SearchFilterModel
+
+
class SearchFilterTests(TestCase):
def setUp(self):
# Sequence of title/text is:
@@ -342,7 +352,8 @@ class SearchFilterTests(TestCase):
def test_search(self):
class SearchListView(generics.ListAPIView):
- model = SearchFilterModel
+ queryset = SearchFilterModel.objects.all()
+ serializer_class = SearchFilterSerializer
filter_backends = (filters.SearchFilter,)
search_fields = ('title', 'text')
@@ -359,7 +370,8 @@ class SearchFilterTests(TestCase):
def test_exact_search(self):
class SearchListView(generics.ListAPIView):
- model = SearchFilterModel
+ queryset = SearchFilterModel.objects.all()
+ serializer_class = SearchFilterSerializer
filter_backends = (filters.SearchFilter,)
search_fields = ('=title', 'text')
@@ -375,7 +387,8 @@ class SearchFilterTests(TestCase):
def test_startswith_search(self):
class SearchListView(generics.ListAPIView):
- model = SearchFilterModel
+ queryset = SearchFilterModel.objects.all()
+ serializer_class = SearchFilterSerializer
filter_backends = (filters.SearchFilter,)
search_fields = ('title', '^text')
@@ -392,7 +405,8 @@ class SearchFilterTests(TestCase):
def test_search_with_nonstandard_search_param(self):
with temporary_setting('SEARCH_PARAM', 'query', module=filters):
class SearchListView(generics.ListAPIView):
- model = SearchFilterModel
+ queryset = SearchFilterModel.objects.all()
+ serializer_class = SearchFilterSerializer
filter_backends = (filters.SearchFilter,)
search_fields = ('title', 'text')
@@ -418,6 +432,11 @@ class OrderingFilterRelatedModel(models.Model):
related_name="relateds")
+class OrderingFilterSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = OrdringFilterModel
+
+
class OrderingFilterTests(TestCase):
def setUp(self):
# Sequence of title/text is:
@@ -440,7 +459,8 @@ class OrderingFilterTests(TestCase):
def test_ordering(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ queryset = OrdringFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
ordering_fields = ('text',)
@@ -459,7 +479,8 @@ class OrderingFilterTests(TestCase):
def test_reverse_ordering(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ queryset = OrdringFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
ordering_fields = ('text',)
@@ -478,7 +499,8 @@ class OrderingFilterTests(TestCase):
def test_incorrectfield_ordering(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ queryset = OrdringFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
ordering_fields = ('text',)
@@ -497,7 +519,8 @@ class OrderingFilterTests(TestCase):
def test_default_ordering(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ queryset = OrdringFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
oredering_fields = ('text',)
@@ -516,7 +539,8 @@ class OrderingFilterTests(TestCase):
def test_default_ordering_using_string(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ queryset = OrdringFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,)
ordering = 'title'
ordering_fields = ('text',)
@@ -545,7 +569,7 @@ class OrderingFilterTests(TestCase):
new_related.save()
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,)
ordering = 'title'
ordering_fields = '__all__'
@@ -567,7 +591,8 @@ class OrderingFilterTests(TestCase):
def test_ordering_with_nonstandard_ordering_param(self):
with temporary_setting('ORDERING_PARAM', 'order', filters):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ queryset = OrdringFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
ordering_fields = ('text',)
diff --git a/tests/test_generics.py b/tests/test_generics.py
index e9f5bebd..f50d53e9 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -11,18 +11,30 @@ from tests.models import ForeignKeySource, ForeignKeyTarget
factory = APIRequestFactory()
+class BasicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BasicModel
+
+
+class ForeignKeySerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ForeignKeySource
+
+
class RootView(generics.ListCreateAPIView):
"""
Example description for OPTIONS.
"""
- model = BasicModel
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
class InstanceView(generics.RetrieveUpdateDestroyAPIView):
"""
Example description for OPTIONS.
"""
- model = BasicModel
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
def get_queryset(self):
queryset = super(InstanceView, self).get_queryset()
@@ -33,7 +45,8 @@ class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
"""
FK: example description for OPTIONS.
"""
- model = ForeignKeySource
+ queryset = ForeignKeySource.objects.all()
+ serializer_class = ForeignKeySerializer
class SlugSerializer(serializers.ModelSerializer):
@@ -48,7 +61,7 @@ class SlugBasedInstanceView(InstanceView):
"""
A model with a slug-field.
"""
- model = SlugBasedModel
+ queryset = SlugBasedModel.objects.all()
serializer_class = SlugSerializer
lookup_field = 'slug'
@@ -503,7 +516,7 @@ class TestOverriddenGetObject(TestCase):
"""
Example detail view for override of get_object().
"""
- model = BasicModel
+ serializer_class = BasicSerializer
def get_object(self):
pk = int(self.kwargs['pk'])
@@ -573,7 +586,7 @@ class ClassASerializer(serializers.ModelSerializer):
class ExampleView(generics.ListCreateAPIView):
serializer_class = ClassASerializer
- model = ClassA
+ queryset = ClassA.objects.all()
class TestM2MBrowseableAPI(TestCase):
@@ -603,7 +616,7 @@ class TwoFieldModel(models.Model):
class DynamicSerializerView(generics.ListCreateAPIView):
- model = TwoFieldModel
+ queryset = TwoFieldModel.objects.all()
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
def get_serializer_class(self):
@@ -612,8 +625,11 @@ class DynamicSerializerView(generics.ListCreateAPIView):
class Meta:
model = TwoFieldModel
fields = ('field_b',)
- return DynamicSerializer
- return super(DynamicSerializerView, self).get_serializer_class()
+ else:
+ class DynamicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TwoFieldModel
+ return DynamicSerializer
class TestFilterBackendAppliedToViews(TestCase):
diff --git a/tests/test_hyperlinkedserializers.py b/tests/test_hyperlinkedserializers.py
index d4548539..0e8c1ed4 100644
--- a/tests/test_hyperlinkedserializers.py
+++ b/tests/test_hyperlinkedserializers.py
@@ -39,59 +39,85 @@ class AlbumSerializer(serializers.ModelSerializer):
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):
- model = BasicModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
class BasicDetail(generics.RetrieveUpdateDestroyAPIView):
- model = BasicModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
class AnchorDetail(generics.RetrieveAPIView):
- model = Anchor
- model_serializer_class = serializers.HyperlinkedModelSerializer
+ queryset = Anchor.objects.all()
+ serializer_class = AnchorSerializer
class ManyToManyList(generics.ListAPIView):
- model = ManyToManyModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
+ queryset = ManyToManyModel.objects.all()
+ serializer_class = ManyToManySerializer
class ManyToManyDetail(generics.RetrieveAPIView):
- model = ManyToManyModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
+ queryset = ManyToManyModel.objects.all()
+ serializer_class = ManyToManySerializer
class BlogPostCommentListCreate(generics.ListCreateAPIView):
- model = BlogPostComment
+ queryset = BlogPostComment.objects.all()
serializer_class = BlogPostCommentSerializer
class BlogPostCommentDetail(generics.RetrieveAPIView):
- model = BlogPostComment
+ queryset = BlogPostComment.objects.all()
serializer_class = BlogPostCommentSerializer
class BlogPostDetail(generics.RetrieveAPIView):
- model = BlogPost
+ queryset = BlogPost.objects.all()
+ serializer_class = BlogPostSerializer
class PhotoListCreate(generics.ListCreateAPIView):
- model = Photo
- model_serializer_class = PhotoSerializer
+ queryset = Photo.objects.all()
+ serializer_class = PhotoSerializer
class AlbumDetail(generics.RetrieveAPIView):
- model = Album
+ queryset = Album.objects.all()
serializer_class = AlbumSerializer
lookup_field = 'title'
class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
- model = OptionalRelationModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
+ queryset = OptionalRelationModel.objects.all()
+ serializer_class = OptionalRelationSerializer
urlpatterns = patterns(
diff --git a/tests/test_nullable_fields.py b/tests/test_nullable_fields.py
index 0c133fc2..8d0c84bb 100644
--- a/tests/test_nullable_fields.py
+++ b/tests/test_nullable_fields.py
@@ -1,10 +1,19 @@
from django.core.urlresolvers import reverse
from django.conf.urls import patterns, url
+from rest_framework import serializers, generics
from rest_framework.test import APITestCase
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(
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 80c33e2e..8f9e0005 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -4,7 +4,7 @@ from decimal import Decimal
from django.core.paginator import Paginator
from django.test import TestCase
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.test import APIRequestFactory
from .models import BasicModel, FilterableItem
@@ -22,11 +22,22 @@ def split_arguments_from_url(url):
return path, args
+class BasicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BasicModel
+
+
+class FilterableItemSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = FilterableItem
+
+
class RootView(generics.ListCreateAPIView):
"""
Example description for OPTIONS.
"""
- model = BasicModel
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
paginate_by = 10
@@ -34,14 +45,16 @@ class DefaultPageSizeKwargView(generics.ListAPIView):
"""
View for testing default paginate_by_param usage
"""
- model = BasicModel
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
class PaginateByParamView(generics.ListAPIView):
"""
View for testing custom paginate_by_param usage
"""
- model = BasicModel
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
paginate_by_param = 'page_size'
@@ -49,7 +62,8 @@ class MaxPaginateByView(generics.ListAPIView):
"""
View for testing custom max_paginate_by usage
"""
- model = BasicModel
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
paginate_by = 3
max_paginate_by = 5
paginate_by_param = 'page_size'
@@ -140,7 +154,8 @@ class IntegrationTestPaginationAndFiltering(TestCase):
fields = ['text', 'decimal', 'date']
class FilterFieldsRootView(generics.ListCreateAPIView):
- model = FilterableItem
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
paginate_by = 10
filter_class = DecimalFilter
filter_backends = (filters.DjangoFilterBackend,)
@@ -188,7 +203,8 @@ class IntegrationTestPaginationAndFiltering(TestCase):
return queryset.filter(decimal__lt=Decimal(request.GET['decimal']))
class BasicFilterFieldsRootView(generics.ListCreateAPIView):
- model = FilterableItem
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
paginate_by = 10
filter_backends = (DecimalFilterBackend,)
@@ -387,7 +403,7 @@ class TestContextPassedToCustomField(TestCase):
def test_with_pagination(self):
class ListView(generics.ListCreateAPIView):
- model = BasicModel
+ queryset = BasicModel.objects.all()
serializer_class = BasicModelSerializer
paginate_by = 1
diff --git a/tests/test_permissions.py b/tests/test_permissions.py
index 93f8020f..b90ba4f1 100644
--- a/tests/test_permissions.py
+++ b/tests/test_permissions.py
@@ -3,7 +3,7 @@ from django.contrib.auth.models import User, Permission, Group
from django.db import models
from django.test import TestCase
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.filters import DjangoObjectPermissionsFilter
from rest_framework.test import APIRequestFactory
@@ -13,14 +13,21 @@ import base64
factory = APIRequestFactory()
+class BasicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BasicModel
+
+
class RootView(generics.ListCreateAPIView):
- model = BasicModel
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
authentication_classes = [authentication.BasicAuthentication]
permission_classes = [permissions.DjangoModelPermissions]
class InstanceView(generics.RetrieveUpdateDestroyAPIView):
- model = BasicModel
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
authentication_classes = [authentication.BasicAuthentication]
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
class ViewObjectPermissions(permissions.DjangoObjectPermissions):
perms_map = {
@@ -181,7 +193,8 @@ class ViewObjectPermissions(permissions.DjangoObjectPermissions):
class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
- model = BasicPermModel
+ queryset = BasicPermModel.objects.all()
+ serializer_class = BasicPermSerializer
authentication_classes = [authentication.BasicAuthentication]
permission_classes = [ViewObjectPermissions]
@@ -189,7 +202,8 @@ object_permissions_view = ObjectPermissionInstanceView.as_view()
class ObjectPermissionListView(generics.ListAPIView):
- model = BasicPermModel
+ queryset = BasicPermModel.objects.all()
+ serializer_class = BasicPermSerializer
authentication_classes = [authentication.BasicAuthentication]
permission_classes = [ViewObjectPermissions]
diff --git a/tests/test_response.py b/tests/test_response.py
index 2eff83d3..004c565c 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -86,14 +86,15 @@ class HTMLView1(APIView):
class HTMLNewModelViewSet(viewsets.ModelViewSet):
- model = BasicModel
+ serializer_class = BasicModelSerializer
+ queryset = BasicModel.objects.all()
class HTMLNewModelView(generics.ListCreateAPIView):
renderer_classes = (BrowsableAPIRenderer,)
permission_classes = []
serializer_class = BasicModelSerializer
- model = BasicModel
+ queryset = BasicModel.objects.all()
new_model_viewset_router = routers.DefaultRouter()
diff --git a/tests/test_validation.py b/tests/test_validation.py
index e13e4078..f62d9068 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -22,7 +22,7 @@ class ValidationModelSerializer(serializers.ModelSerializer):
class UpdateValidationModel(generics.RetrieveUpdateDestroyAPIView):
- model = ValidationModel
+ queryset = ValidationModel.objects.all()
serializer_class = ValidationModelSerializer
@@ -117,7 +117,7 @@ class ValidationMaxValueValidatorModelSerializer(serializers.ModelSerializer):
class UpdateMaxValueValidationModel(generics.RetrieveUpdateDestroyAPIView):
- model = ValidationMaxValueValidatorModel
+ queryset = ValidationMaxValueValidatorModel.objects.all()
serializer_class = ValidationMaxValueValidatorModelSerializer
diff --git a/tests/views.py b/tests/views.py
deleted file mode 100644
index 55935e92..00000000
--- a/tests/views.py
+++ /dev/null
@@ -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
--
cgit v1.2.3
From 72c0811576feb89decf6fc6dc4ee5e25eca0aece Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Aug 2014 12:48:04 +0100
Subject: Minor tidy up.
---
rest_framework/generics.py | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 68222864..d0adeaec 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -3,7 +3,7 @@ Generic views that provide commonly needed behaviour.
"""
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.http import Http404
from django.shortcuts import get_object_or_404 as _get_object_or_404
@@ -235,19 +235,16 @@ class GenericAPIView(views.APIView):
queryset = self.filter_queryset(self.get_queryset())
# 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 = self.kwargs.get(lookup_url_kwarg, None)
- if lookup is None:
- 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)
- )
+ 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: lookup}
+ filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)
# May raise a permission denied
--
cgit v1.2.3
From ce7b2cded94abc12ae1be076642de96684d0927b Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Aug 2014 12:48:49 +0100
Subject: Remove deprecated generic views.
`MultipleObjectAPIView` and `SingleObjectAPIView` are no longer
required.
---
rest_framework/generics.py | 22 ----------------------
1 file changed, 22 deletions(-)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index d0adeaec..e6cbfca9 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -442,25 +442,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 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)
--
cgit v1.2.3
From f87d32558eb3b36f14a798ec48e4943d25380b92 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Aug 2014 12:53:45 +0100
Subject: Remove `.link()` and `.action()` decorators.
---
rest_framework/decorators.py | 34 ----------------------------------
1 file changed, 34 deletions(-)
diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py
index 449ba0a2..cc5d92c2 100644
--- a/rest_framework/decorators.py
+++ b/rest_framework/decorators.py
@@ -130,37 +130,3 @@ def list_route(methods=['get'], **kwargs):
func.kwargs = kwargs
return func
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
--
cgit v1.2.3
From b552b62540e5144272c9c13c28f120ffe5fcbe45 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Aug 2014 12:54:03 +0100
Subject: `get_paginate_by` no longer takes optional `.queryset`
---
rest_framework/generics.py | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index e6cbfca9..40c49844 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -158,7 +158,7 @@ class GenericAPIView(views.APIView):
# The following methods provide default implementations
# 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.
@@ -167,11 +167,6 @@ class GenericAPIView(views.APIView):
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:
try:
return strict_positive_int(
--
cgit v1.2.3
From 371d30aa8737c4b3aaf28ee10cc2b77a9c4d1fd9 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Aug 2014 12:54:52 +0100
Subject: Remove unused imports.
---
rest_framework/decorators.py | 1 -
rest_framework/generics.py | 1 -
2 files changed, 2 deletions(-)
diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py
index cc5d92c2..d28d6e22 100644
--- a/rest_framework/decorators.py
+++ b/rest_framework/decorators.py
@@ -10,7 +10,6 @@ from __future__ import unicode_literals
from django.utils import six
from rest_framework.views import APIView
import types
-import warnings
def api_view(http_method_names):
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 40c49844..b3bd6ce9 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -11,7 +11,6 @@ from django.utils.translation import ugettext as _
from rest_framework import views, mixins, exceptions
from rest_framework.request import clone_request
from rest_framework.settings import api_settings
-import warnings
def strict_positive_int(integer_string, cutoff=None):
--
cgit v1.2.3
From 4ac4676a40b121d27cfd1173ff548d96b8d3de2f Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 29 Aug 2014 16:46:26 +0100
Subject: First pass
---
rest_framework/fields.py | 1166 +++++++-------------------------------
rest_framework/generics.py | 10 +-
rest_framework/mixins.py | 24 +-
rest_framework/pagination.py | 22 +-
rest_framework/relations.py | 486 ----------------
rest_framework/renderers.py | 10 +-
rest_framework/serializers.py | 1096 ++++++++++-------------------------
rest_framework/utils/encoders.py | 18 +-
rest_framework/utils/html.py | 86 +++
9 files changed, 632 insertions(+), 2286 deletions(-)
create mode 100644 rest_framework/utils/html.py
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 9d707c9b..a83bf94c 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1,1038 +1,308 @@
-"""
-Serializer fields perform validation on incoming data.
+from rest_framework.utils import html
-They are very similar to Django's form fields.
-"""
-from __future__ import unicode_literals
-import copy
-import datetime
-import inspect
-import re
-import warnings
-from decimal import Decimal, DecimalException
-from django import forms
-from django.core import validators
-from django.core.exceptions import ValidationError
-from django.conf import settings
-from django.db.models.fields import BLANK_CHOICE_DASH
-from django.http import QueryDict
-from django.forms import widgets
-from django.utils import six, timezone
-from django.utils.encoding import is_protected_type
-from django.utils.translation import ugettext_lazy as _
-from django.utils.datastructures import SortedDict
-from django.utils.dateparse import parse_date, parse_datetime, parse_time
-from rest_framework import ISO_8601
-from rest_framework.compat import (
- BytesIO, smart_text,
- force_text, is_non_str_iterable
-)
-from rest_framework.settings import api_settings
-
-
-def is_simple_callable(obj):
+class empty:
"""
- True if the object is a callable that takes no arguments.
- """
- function = inspect.isfunction(obj)
- method = inspect.ismethod(obj)
-
- if not (function or method):
- return False
+ This class is used to represent no data being provided for a given input
+ or output value.
- args, _, _, defaults = inspect.getargspec(obj)
- len_args = len(args) if function else len(args) - 1
- len_defaults = len(defaults) if defaults else 0
- return len_args <= len_defaults
+ It is required because `None` may be a valid input or output value.
+ """
+ pass
-def get_component(obj, attr_name):
+def get_attribute(instance, attrs):
"""
- Given an object, and an attribute name,
- return that attribute on the object.
+ Similar to Python's built in `getattr(instance, attr)`,
+ but takes a list of nested attributes, instead of a single attribute.
"""
- if isinstance(obj, dict):
- val = obj.get(attr_name)
- else:
- val = getattr(obj, attr_name)
-
- if is_simple_callable(val):
- return val()
- return val
-
-
-def readable_datetime_formats(formats):
- format = ', '.join(formats).replace(
- ISO_8601,
- 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'
- )
- return humanize_strptime(format)
-
-
-def readable_date_formats(formats):
- format = ', '.join(formats).replace(ISO_8601, 'YYYY[-MM[-DD]]')
- return humanize_strptime(format)
+ for attr in attrs:
+ instance = getattr(instance, attr)
+ return instance
-def readable_time_formats(formats):
- format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]')
- return humanize_strptime(format)
-
-
-def humanize_strptime(format_string):
- # Note that we're missing some of the locale specific mappings that
- # don't really make sense.
- mapping = {
- "%Y": "YYYY",
- "%y": "YY",
- "%m": "MM",
- "%b": "[Jan-Dec]",
- "%B": "[January-December]",
- "%d": "DD",
- "%H": "hh",
- "%I": "hh", # Requires '%p' to differentiate from '%H'.
- "%M": "mm",
- "%S": "ss",
- "%f": "uuuuuu",
- "%a": "[Mon-Sun]",
- "%A": "[Monday-Sunday]",
- "%p": "[AM|PM]",
- "%z": "[+HHMM|-HHMM]"
- }
- for key, val in mapping.items():
- format_string = format_string.replace(key, val)
- return format_string
-
-
-def strip_multiple_choice_msg(help_text):
+def set_value(dictionary, keys, value):
"""
- Remove the 'Hold down "control" ...' message that is Django enforces in
- select multiple fields on ModelForms. (Required for 1.5 and earlier)
+ Similar to Python's built in `dictionary[key] = value`,
+ but takes a list of nested keys instead of a single key.
- See https://code.djangoproject.com/ticket/9321
+ set_value({'a': 1}, [], {'b': 2}) -> {'a': 1, 'b': 2}
+ set_value({'a': 1}, ['x'], 2) -> {'a': 1, 'x': 2}
+ set_value({'a': 1}, ['x', 'y'], 2) -> {'a': 1, 'x': {'y': 2}}
"""
- multiple_choice_msg = _(' Hold down "Control", or "Command" on a Mac, to select more than one.')
- multiple_choice_msg = force_text(multiple_choice_msg)
+ if not keys:
+ dictionary.update(value)
+ return
- return help_text.replace(multiple_choice_msg, '')
+ for key in keys[:-1]:
+ if key not in dictionary:
+ dictionary[key] = {}
+ dictionary = dictionary[key]
+ dictionary[keys[-1]] = value
-class Field(object):
- read_only = True
- creation_counter = 0
- empty = ''
- type_name = None
- partial = False
- use_files = False
- form_field_class = forms.CharField
- type_label = 'field'
- widget = None
- def __init__(self, source=None, label=None, help_text=None):
- self.parent = None
+class ValidationError(Exception):
+ pass
- self.creation_counter = Field.creation_counter
- Field.creation_counter += 1
- self.source = source
+class SkipField(Exception):
+ pass
- if label is not None:
- self.label = smart_text(label)
- else:
- self.label = None
- if help_text is not None:
- self.help_text = strip_multiple_choice_msg(smart_text(help_text))
- else:
- self.help_text = None
+class Field(object):
+ _creation_counter = 0
- self._errors = []
- self._value = None
- self._name = None
+ MESSAGES = {
+ 'required': 'This field is required.'
+ }
- @property
- def errors(self):
- return self._errors
+ _NOT_READ_ONLY_WRITE_ONLY = 'May not set both `read_only` and `write_only`'
+ _NOT_READ_ONLY_REQUIRED = 'May not set both `read_only` and `required`'
+ _NOT_READ_ONLY_DEFAULT = 'May not set both `read_only` and `default`'
+ _NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`'
+ _MISSING_ERROR_MESSAGE = (
+ 'ValidationError raised by `{class_name}`, but error key `{key}` does '
+ 'not exist in the `MESSAGES` dictionary.'
+ )
- def widget_html(self):
- if not self.widget:
- return ''
+ def __init__(self, read_only=False, write_only=False,
+ required=None, default=empty, initial=None, source=None,
+ label=None, style=None):
+ self._creation_counter = Field._creation_counter
+ Field._creation_counter += 1
- attrs = {}
- if 'id' not in self.widget.attrs:
- attrs['id'] = self._name
+ # If `required` is unset, then use `True` unless a default is provided.
+ if required is None:
+ required = default is empty and not read_only
- return self.widget.render(self._name, self._value, attrs=attrs)
+ # Some combinations of keyword arguments do not make sense.
+ assert not (read_only and write_only), self._NOT_READ_ONLY_WRITE_ONLY
+ assert not (read_only and required), self._NOT_READ_ONLY_REQUIRED
+ assert not (read_only and default is not empty), self._NOT_READ_ONLY_DEFAULT
+ assert not (required and default is not empty), self._NOT_REQUIRED_DEFAULT
- def label_tag(self):
- return '' % (self._name, self.label)
+ self.read_only = read_only
+ self.write_only = write_only
+ self.required = required
+ self.default = default
+ self.source = source
+ self.initial = initial
+ self.label = label
+ self.style = {} if style is None else style
- def initialize(self, parent, field_name):
+ def bind(self, field_name, parent, root):
"""
- Called to set up a field prior to field_to_native or field_from_native.
-
- parent - The parent serializer.
- field_name - The name of the field being initialized.
+ Setup the context for the field instance.
"""
+ self.field_name = field_name
self.parent = parent
- self.root = parent.root or parent
- self.context = self.root.context
- self.partial = self.root.partial
- if self.partial:
- self.required = False
+ self.root = root
- def field_from_native(self, data, files, field_name, into):
- """
- Given a dictionary and a field name, updates the dictionary `into`,
- with the field and it's deserialized value.
- """
- return
+ # `self.label` should deafult to being based on the field name.
+ if self.label is None:
+ self.label = self.field_name.replace('_', ' ').capitalize()
- def field_to_native(self, obj, field_name):
- """
- Given an object and a field name, returns the value that should be
- serialized for that field.
- """
- if obj is None:
- return self.empty
+ # self.source should default to being the same as the field name.
+ if self.source is None:
+ self.source = field_name
+ # self.source_attrs is a list of attributes that need to be looked up
+ # when serializing the instance, or populating the validated data.
if self.source == '*':
- return self.to_native(obj)
-
- source = self.source or field_name
- value = obj
-
- for component in source.split('.'):
- value = get_component(value, component)
- if value is None:
- break
-
- return self.to_native(value)
+ self.source_attrs = []
+ else:
+ self.source_attrs = self.source.split('.')
- def to_native(self, value):
+ def get_initial(self):
"""
- Converts the field's value into it's simple representation.
+ Return a value to use when the field is being returned as a primative
+ value, without any object instance.
"""
- if is_simple_callable(value):
- value = value()
-
- if is_protected_type(value):
- return value
- elif (is_non_str_iterable(value) and
- not isinstance(value, (dict, six.string_types))):
- return [self.to_native(item) for item in value]
- elif isinstance(value, dict):
- # Make sure we preserve field ordering, if it exists
- ret = SortedDict()
- for key, val in value.items():
- ret[key] = self.to_native(val)
- return ret
- return force_text(value)
+ return self.initial
- def attributes(self):
+ def get_value(self, dictionary):
"""
- Returns a dictionary of attributes to be used when serializing to xml.
+ Given the *incoming* primative data, return the value for this field
+ that should be validated and transformed to a native value.
"""
- if self.type_name:
- return {'type': self.type_name}
- return {}
-
- def metadata(self):
- metadata = SortedDict()
- metadata['type'] = self.type_label
- metadata['required'] = getattr(self, 'required', False)
- optional_attrs = ['read_only', 'label', 'help_text',
- 'min_length', 'max_length']
- for attr in optional_attrs:
- value = getattr(self, attr, None)
- if value is not None and value != '':
- metadata[attr] = force_text(value, strings_only=True)
- return metadata
-
-
-class WritableField(Field):
- """
- Base for read/write fields.
- """
- write_only = False
- default_validators = []
- default_error_messages = {
- 'required': _('This field is required.'),
- 'invalid': _('Invalid value.'),
- }
- widget = widgets.TextInput
- default = None
-
- def __init__(self, source=None, label=None, help_text=None,
- read_only=False, write_only=False, required=None,
- validators=[], error_messages=None, widget=None,
- default=None, blank=None):
-
- super(WritableField, self).__init__(source=source, label=label, help_text=help_text)
-
- self.read_only = read_only
- self.write_only = write_only
-
- assert not (read_only and write_only), "Cannot set read_only=True and write_only=True"
-
- if required is None:
- self.required = not(read_only)
- else:
- assert not (read_only and required), "Cannot set required=True and read_only=True"
- self.required = required
-
- messages = {}
- for c in reversed(self.__class__.__mro__):
- messages.update(getattr(c, 'default_error_messages', {}))
- messages.update(error_messages or {})
- self.error_messages = messages
+ return dictionary.get(self.field_name, empty)
- self.validators = self.default_validators + validators
- self.default = default if default is not None else self.default
-
- # Widgets are only used for HTML forms.
- widget = widget or self.widget
- if isinstance(widget, type):
- widget = widget()
- self.widget = widget
+ def get_attribute(self, instance):
+ """
+ Given the *outgoing* object instance, return the value for this field
+ that should be returned as a primative value.
+ """
+ return get_attribute(instance, self.source_attrs)
- def __deepcopy__(self, memo):
- result = copy.copy(self)
- memo[id(self)] = result
- result.validators = self.validators[:]
- return result
+ def get_default(self):
+ """
+ Return the default value to use when validating data if no input
+ is provided for this field.
- def get_default_value(self):
- if is_simple_callable(self.default):
- return self.default()
+ If a default has not been set for this field then this will simply
+ return `empty`, indicating that no value should be set in the
+ validated data for this field.
+ """
+ if self.default is empty:
+ raise SkipField()
return self.default
- def validate(self, value):
- if value in validators.EMPTY_VALUES and self.required:
- raise ValidationError(self.error_messages['required'])
+ def validate(self, data=empty):
+ """
+ Validate a simple representation and return the internal value.
- def run_validators(self, value):
- if value in validators.EMPTY_VALUES:
- return
- errors = []
- for v in self.validators:
- try:
- v(value)
- except ValidationError as e:
- if hasattr(e, 'code') and e.code in self.error_messages:
- message = self.error_messages[e.code]
- if e.params:
- message = message % e.params
- errors.append(message)
- else:
- errors.extend(e.messages)
- if errors:
- raise ValidationError(errors)
+ The provided data may be `empty` if no representation was included.
+ May return `empty` if the field should not be included in the
+ validated data.
+ """
+ if data is empty:
+ if self.required:
+ self.fail('required')
+ return self.get_default()
- def field_to_native(self, obj, field_name):
- if self.write_only:
- return None
- return super(WritableField, self).field_to_native(obj, field_name)
+ return self.to_native(data)
- def field_from_native(self, data, files, field_name, into):
+ def to_native(self, data):
"""
- Given a dictionary and a field name, updates the dictionary `into`,
- with the field and it's deserialized value.
+ Transform the *incoming* primative data into a native value.
"""
- if self.read_only:
- return
-
- try:
- data = data or {}
- if self.use_files:
- files = files or {}
- try:
- native = files[field_name]
- except KeyError:
- native = data[field_name]
- else:
- native = data[field_name]
- except KeyError:
- if self.default is not None and not self.partial:
- # Note: partial updates shouldn't set defaults
- native = self.get_default_value()
- else:
- if self.required:
- raise ValidationError(self.error_messages['required'])
- return
-
- value = self.from_native(native)
- if self.source == '*':
- if value:
- into.update(value)
- else:
- self.validate(value)
- self.run_validators(value)
- into[self.source or field_name] = value
+ return data
- def from_native(self, value):
+ def to_primative(self, value):
"""
- Reverts a simple representation back to the field's value.
+ Transform the *outgoing* native value into primative data.
"""
return value
-
-class ModelField(WritableField):
- """
- A generic field that can be used against an arbitrary model field.
- """
- def __init__(self, *args, **kwargs):
+ def fail(self, key, **kwargs):
+ """
+ A helper method that simply raises a validation error.
+ """
try:
- self.model_field = kwargs.pop('model_field')
+ raise ValidationError(self.MESSAGES[key].format(**kwargs))
except KeyError:
- raise ValueError("ModelField requires 'model_field' kwarg")
-
- self.min_length = kwargs.pop('min_length',
- getattr(self.model_field, 'min_length', None))
- self.max_length = kwargs.pop('max_length',
- getattr(self.model_field, 'max_length', None))
- self.min_value = kwargs.pop('min_value',
- getattr(self.model_field, 'min_value', None))
- self.max_value = kwargs.pop('max_value',
- getattr(self.model_field, 'max_value', None))
-
- super(ModelField, self).__init__(*args, **kwargs)
-
- if self.min_length is not None:
- self.validators.append(validators.MinLengthValidator(self.min_length))
- if self.max_length is not None:
- self.validators.append(validators.MaxLengthValidator(self.max_length))
- if self.min_value is not None:
- self.validators.append(validators.MinValueValidator(self.min_value))
- if self.max_value is not None:
- self.validators.append(validators.MaxValueValidator(self.max_value))
-
- def from_native(self, value):
- rel = getattr(self.model_field, "rel", None)
- if rel is not None:
- return rel.to._meta.get_field(rel.field_name).to_python(value)
- else:
- return self.model_field.to_python(value)
+ class_name = self.__class__.__name__
+ msg = self._MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)
+ raise AssertionError(msg)
- def field_to_native(self, obj, field_name):
- value = self.model_field._get_val_from_obj(obj)
- if is_protected_type(value):
- return value
- return self.model_field.value_to_string(obj)
- def attributes(self):
- return {
- "type": self.model_field.get_internal_type()
- }
-
-
-# Typed Fields
-
-class BooleanField(WritableField):
- type_name = 'BooleanField'
- type_label = 'boolean'
- form_field_class = forms.BooleanField
- widget = widgets.CheckboxInput
- default_error_messages = {
- 'invalid': _("'%s' value must be either True or False."),
+class BooleanField(Field):
+ MESSAGES = {
+ 'required': 'This field is required.',
+ 'invalid_value': '`{input}` is not a valid boolean.'
}
- empty = False
-
- def field_from_native(self, data, files, field_name, into):
- # HTML checkboxes do not explicitly represent unchecked as `False`
- # we deal with that here...
- if isinstance(data, QueryDict) and self.default is None:
- self.default = False
-
- return super(BooleanField, self).field_from_native(
- data, files, field_name, into
- )
-
- def from_native(self, value):
- if value in ('true', 't', 'True', '1'):
+ TRUE_VALUES = {'t', 'T', 'true', 'True', 'TRUE', '1', 1, True}
+ FALSE_VALUES = {'f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False}
+
+ def get_value(self, dictionary):
+ if html.is_html_input(dictionary):
+ # HTML forms do not send a `False` value on an empty checkbox,
+ # so we override the default empty value to be False.
+ return dictionary.get(self.field_name, False)
+ return dictionary.get(self.field_name, empty)
+
+ def to_native(self, data):
+ if data in self.TRUE_VALUES:
return True
- if value in ('false', 'f', 'False', '0'):
+ elif data in self.FALSE_VALUES:
return False
- return bool(value)
+ self.fail('invalid_value', input=data)
-class CharField(WritableField):
- type_name = 'CharField'
- type_label = 'string'
- form_field_class = forms.CharField
+class CharField(Field):
+ MESSAGES = {
+ 'required': 'This field is required.',
+ 'blank': 'This field may not be blank.'
+ }
- def __init__(self, max_length=None, min_length=None, allow_none=False, *args, **kwargs):
- self.max_length, self.min_length = max_length, min_length
- self.allow_none = allow_none
+ def __init__(self, *args, **kwargs):
+ self.allow_blank = kwargs.pop('allow_blank', False)
super(CharField, self).__init__(*args, **kwargs)
- if min_length is not None:
- self.validators.append(validators.MinLengthValidator(min_length))
- if max_length is not None:
- self.validators.append(validators.MaxLengthValidator(max_length))
-
- def from_native(self, value):
- if isinstance(value, six.string_types):
- return value
-
- if value is None and not self.allow_none:
- return ''
-
- return smart_text(value)
-
-class URLField(CharField):
- type_name = 'URLField'
- type_label = 'url'
-
- def __init__(self, **kwargs):
- if 'validators' not in kwargs:
- kwargs['validators'] = [validators.URLValidator()]
- super(URLField, self).__init__(**kwargs)
+ def to_native(self, data):
+ if data == '' and not self.allow_blank:
+ self.fail('blank')
+ return str(data)
-class SlugField(CharField):
- type_name = 'SlugField'
- type_label = 'slug'
- form_field_class = forms.SlugField
-
- default_error_messages = {
- 'invalid': _("Enter a valid 'slug' consisting of letters, numbers,"
- " underscores or hyphens."),
+class ChoiceField(Field):
+ MESSAGES = {
+ 'required': 'This field is required.',
+ 'invalid_choice': '`{input}` is not a valid choice.'
}
- default_validators = [validators.validate_slug]
+ coerce_to_type = str
def __init__(self, *args, **kwargs):
- super(SlugField, self).__init__(*args, **kwargs)
-
+ choices = kwargs.pop('choices')
+
+ assert choices, '`choices` argument is required and may not be empty'
+
+ # Allow either single or paired choices style:
+ # choices = [1, 2, 3]
+ # choices = [(1, 'First'), (2, 'Second'), (3, 'Third')]
+ pairs = [
+ isinstance(item, (list, tuple)) and len(item) == 2
+ for item in choices
+ ]
+ if all(pairs):
+ self.choices = {key: val for key, val in choices}
+ else:
+ self.choices = {item: item for item in choices}
-class ChoiceField(WritableField):
- type_name = 'ChoiceField'
- type_label = 'choice'
- form_field_class = forms.ChoiceField
- widget = widgets.Select
- default_error_messages = {
- 'invalid_choice': _('Select a valid choice. %(value)s is not one of '
- 'the available choices.'),
- }
+ # Map the string representation of choices to the underlying value.
+ # Allows us to deal with eg. integer choices while supporting either
+ # integer or string input, but still get the correct datatype out.
+ self.choice_strings_to_values = {
+ str(key): key for key in self.choices.keys()
+ }
- def __init__(self, choices=(), blank_display_value=None, *args, **kwargs):
- self.empty = kwargs.pop('empty', '')
super(ChoiceField, self).__init__(*args, **kwargs)
- self.choices = choices
- if not self.required:
- if blank_display_value is None:
- blank_choice = BLANK_CHOICE_DASH
- else:
- blank_choice = [('', blank_display_value)]
- self.choices = blank_choice + self.choices
-
- def _get_choices(self):
- return self._choices
-
- 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)
-
- def metadata(self):
- data = super(ChoiceField, self).metadata()
- data['choices'] = [{'value': v, 'display_name': n} for v, n in self.choices]
- return data
-
- def validate(self, value):
- """
- Validates that the input is in self.choices.
- """
- super(ChoiceField, self).validate(value)
- if value and not self.valid_value(value):
- raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
-
- def valid_value(self, value):
- """
- Check to see if the provided value is a valid choice.
- """
- for k, v in self.choices:
- if isinstance(v, (list, tuple)):
- # This is an optgroup, so look inside the group for options
- for k2, v2 in v:
- if value == smart_text(k2):
- return True
- else:
- if value == smart_text(k) or value == k:
- return True
- return False
-
- def from_native(self, value):
- value = super(ChoiceField, self).from_native(value)
- if value == self.empty or value in validators.EMPTY_VALUES:
- return self.empty
- return value
-
-
-class EmailField(CharField):
- type_name = 'EmailField'
- type_label = 'email'
- form_field_class = forms.EmailField
-
- default_error_messages = {
- 'invalid': _('Enter a valid email address.'),
- }
- default_validators = [validators.validate_email]
-
- def from_native(self, value):
- ret = super(EmailField, self).from_native(value)
- if ret is None:
- return None
- return ret.strip()
-
-
-class RegexField(CharField):
- type_name = 'RegexField'
- type_label = 'regex'
- form_field_class = forms.RegexField
-
- def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs):
- super(RegexField, self).__init__(max_length, min_length, *args, **kwargs)
- self.regex = regex
-
- def _get_regex(self):
- return self._regex
-
- def _set_regex(self, regex):
- if isinstance(regex, six.string_types):
- regex = re.compile(regex)
- self._regex = regex
- if hasattr(self, '_regex_validator') and self._regex_validator in self.validators:
- self.validators.remove(self._regex_validator)
- self._regex_validator = validators.RegexValidator(regex=regex)
- self.validators.append(self._regex_validator)
-
- regex = property(_get_regex, _set_regex)
-
-
-class DateField(WritableField):
- type_name = 'DateField'
- type_label = 'date'
- widget = widgets.DateInput
- form_field_class = forms.DateField
-
- default_error_messages = {
- 'invalid': _("Date has wrong format. Use one of these formats instead: %s"),
- }
- empty = None
- input_formats = api_settings.DATE_INPUT_FORMATS
- format = api_settings.DATE_FORMAT
-
- def __init__(self, input_formats=None, format=None, *args, **kwargs):
- self.input_formats = input_formats if input_formats is not None else self.input_formats
- self.format = format if format is not None else self.format
- super(DateField, self).__init__(*args, **kwargs)
-
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
-
- if isinstance(value, datetime.datetime):
- if timezone and settings.USE_TZ and timezone.is_aware(value):
- # Convert aware datetimes to the default time zone
- # before casting them to dates (#17742).
- default_timezone = timezone.get_default_timezone()
- value = timezone.make_naive(value, default_timezone)
- return value.date()
- if isinstance(value, datetime.date):
- return value
-
- for format in self.input_formats:
- if format.lower() == ISO_8601:
- try:
- parsed = parse_date(value)
- except (ValueError, TypeError):
- pass
- else:
- if parsed is not None:
- return parsed
- else:
- try:
- parsed = datetime.datetime.strptime(value, format)
- except (ValueError, TypeError):
- pass
- else:
- return parsed.date()
-
- msg = self.error_messages['invalid'] % readable_date_formats(self.input_formats)
- raise ValidationError(msg)
-
- def to_native(self, value):
- if value is None or self.format is None:
- return value
-
- if isinstance(value, datetime.datetime):
- value = value.date()
-
- if self.format.lower() == ISO_8601:
- return value.isoformat()
- return value.strftime(self.format)
-
-
-class DateTimeField(WritableField):
- type_name = 'DateTimeField'
- type_label = 'datetime'
- widget = widgets.DateTimeInput
- form_field_class = forms.DateTimeField
-
- default_error_messages = {
- 'invalid': _("Datetime has wrong format. Use one of these formats instead: %s"),
- }
- empty = None
- input_formats = api_settings.DATETIME_INPUT_FORMATS
- format = api_settings.DATETIME_FORMAT
-
- def __init__(self, input_formats=None, format=None, *args, **kwargs):
- self.input_formats = input_formats if input_formats is not None else self.input_formats
- self.format = format if format is not None else self.format
- super(DateTimeField, self).__init__(*args, **kwargs)
-
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
-
- if isinstance(value, datetime.datetime):
- return value
- if isinstance(value, datetime.date):
- value = datetime.datetime(value.year, value.month, value.day)
- if settings.USE_TZ:
- # For backwards compatibility, interpret naive datetimes in
- # local time. This won't work during DST change, but we can't
- # do much about it, so we let the exceptions percolate up the
- # call stack.
- warnings.warn("DateTimeField received a naive datetime (%s)"
- " while time zone support is active." % value,
- RuntimeWarning)
- default_timezone = timezone.get_default_timezone()
- value = timezone.make_aware(value, default_timezone)
- return value
-
- for format in self.input_formats:
- if format.lower() == ISO_8601:
- try:
- parsed = parse_datetime(value)
- except (ValueError, TypeError):
- pass
- else:
- if parsed is not None:
- return parsed
- else:
- try:
- parsed = datetime.datetime.strptime(value, format)
- except (ValueError, TypeError):
- pass
- else:
- return parsed
-
- msg = self.error_messages['invalid'] % readable_datetime_formats(self.input_formats)
- raise ValidationError(msg)
-
- def to_native(self, value):
- if value is None or self.format is None:
- return value
-
- if self.format.lower() == ISO_8601:
- ret = value.isoformat()
- if ret.endswith('+00:00'):
- ret = ret[:-6] + 'Z'
- return ret
- return value.strftime(self.format)
-
-
-class TimeField(WritableField):
- type_name = 'TimeField'
- type_label = 'time'
- widget = widgets.TimeInput
- form_field_class = forms.TimeField
-
- default_error_messages = {
- 'invalid': _("Time has wrong format. Use one of these formats instead: %s"),
- }
- empty = None
- input_formats = api_settings.TIME_INPUT_FORMATS
- format = api_settings.TIME_FORMAT
-
- def __init__(self, input_formats=None, format=None, *args, **kwargs):
- self.input_formats = input_formats if input_formats is not None else self.input_formats
- self.format = format if format is not None else self.format
- super(TimeField, self).__init__(*args, **kwargs)
-
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
-
- if isinstance(value, datetime.time):
- return value
-
- for format in self.input_formats:
- if format.lower() == ISO_8601:
- try:
- parsed = parse_time(value)
- except (ValueError, TypeError):
- pass
- else:
- if parsed is not None:
- return parsed
- else:
- try:
- parsed = datetime.datetime.strptime(value, format)
- except (ValueError, TypeError):
- pass
- else:
- return parsed.time()
-
- msg = self.error_messages['invalid'] % readable_time_formats(self.input_formats)
- raise ValidationError(msg)
-
- def to_native(self, value):
- if value is None or self.format is None:
- return value
-
- if isinstance(value, datetime.datetime):
- value = value.time()
-
- if self.format.lower() == ISO_8601:
- return value.isoformat()
- return value.strftime(self.format)
-
-
-class IntegerField(WritableField):
- type_name = 'IntegerField'
- type_label = 'integer'
- form_field_class = forms.IntegerField
- empty = 0
-
- default_error_messages = {
- 'invalid': _('Enter a whole number.'),
- 'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'),
- 'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'),
- }
-
- def __init__(self, max_value=None, min_value=None, *args, **kwargs):
- self.max_value, self.min_value = max_value, min_value
- super(IntegerField, self).__init__(*args, **kwargs)
-
- if max_value is not None:
- self.validators.append(validators.MaxValueValidator(max_value))
- if min_value is not None:
- self.validators.append(validators.MinValueValidator(min_value))
-
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
-
- try:
- value = int(str(value))
- except (ValueError, TypeError):
- raise ValidationError(self.error_messages['invalid'])
- return value
-
-
-class FloatField(WritableField):
- type_name = 'FloatField'
- type_label = 'float'
- form_field_class = forms.FloatField
- empty = 0
-
- default_error_messages = {
- 'invalid': _("'%s' value must be a float."),
- }
-
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
+ def to_native(self, data):
try:
- return float(value)
- except (TypeError, ValueError):
- msg = self.error_messages['invalid'] % value
- raise ValidationError(msg)
-
+ return self.choice_strings_to_values[str(data)]
+ except KeyError:
+ self.fail('invalid_choice', input=data)
-class DecimalField(WritableField):
- type_name = 'DecimalField'
- type_label = 'decimal'
- form_field_class = forms.DecimalField
- empty = Decimal('0')
- default_error_messages = {
- 'invalid': _('Enter a number.'),
- 'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'),
- 'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'),
- 'max_digits': _('Ensure that there are no more than %s digits in total.'),
- 'max_decimal_places': _('Ensure that there are no more than %s decimal places.'),
- 'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.')
+class MultipleChoiceField(ChoiceField):
+ MESSAGES = {
+ 'required': 'This field is required.',
+ 'invalid_choice': '`{input}` is not a valid choice.',
+ 'not_a_list': 'Expected a list of items but got type `{input_type}`'
}
- def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
- self.max_value, self.min_value = max_value, min_value
- self.max_digits, self.decimal_places = max_digits, decimal_places
- super(DecimalField, self).__init__(*args, **kwargs)
-
- if max_value is not None:
- self.validators.append(validators.MaxValueValidator(max_value))
- if min_value is not None:
- self.validators.append(validators.MinValueValidator(min_value))
-
- def from_native(self, value):
- """
- Validates that the input is a decimal number. Returns a Decimal
- instance. Returns None for empty values. Ensures that there are no more
- than max_digits in the number, and no more than decimal_places digits
- after the decimal point.
- """
- if value in validators.EMPTY_VALUES:
- return None
- value = smart_text(value).strip()
- try:
- value = Decimal(value)
- except DecimalException:
- raise ValidationError(self.error_messages['invalid'])
- return value
-
- def validate(self, value):
- super(DecimalField, self).validate(value)
- if value in validators.EMPTY_VALUES:
- return
- # Check for NaN, Inf and -Inf values. We can't compare directly for NaN,
- # since it is never equal to itself. However, NaN is the only value that
- # isn't equal to itself, so we can use this to identify NaN
- if value != value or value == Decimal("Inf") or value == Decimal("-Inf"):
- raise ValidationError(self.error_messages['invalid'])
- sign, digittuple, exponent = value.as_tuple()
- decimals = abs(exponent)
- # digittuple doesn't include any leading zeros.
- digits = len(digittuple)
- if decimals > digits:
- # We have leading zeros up to or past the decimal point. Count
- # everything past the decimal point as a digit. We do not count
- # 0 before the decimal point as a digit since that would mean
- # we would not allow max_digits = decimal_places.
- digits = decimals
- whole_digits = digits - decimals
-
- if self.max_digits is not None and digits > self.max_digits:
- raise ValidationError(self.error_messages['max_digits'] % self.max_digits)
- if self.decimal_places is not None and decimals > self.decimal_places:
- raise ValidationError(self.error_messages['max_decimal_places'] % self.decimal_places)
- if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places):
- raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places))
- return value
-
+ def to_native(self, data):
+ if not hasattr(data, '__iter__'):
+ self.fail('not_a_list', input_type=type(data).__name__)
+ return set([
+ super(MultipleChoiceField, self).to_native(item)
+ for item in data
+ ])
-class FileField(WritableField):
- use_files = True
- type_name = 'FileField'
- type_label = 'file upload'
- form_field_class = forms.FileField
- widget = widgets.FileInput
- default_error_messages = {
- 'invalid': _("No file was submitted. Check the encoding type on the form."),
- 'missing': _("No file was submitted."),
- 'empty': _("The submitted file is empty."),
- 'max_length': _('Ensure this filename has at most %(max)d characters (it has %(length)d).'),
- 'contradiction': _('Please either submit a file or check the clear checkbox, not both.')
+class IntegerField(Field):
+ MESSAGES = {
+ 'required': 'This field is required.',
+ 'invalid_integer': 'A valid integer is required.'
}
- def __init__(self, *args, **kwargs):
- self.max_length = kwargs.pop('max_length', None)
- self.allow_empty_file = kwargs.pop('allow_empty_file', False)
- super(FileField, self).__init__(*args, **kwargs)
-
- def from_native(self, data):
- if data in validators.EMPTY_VALUES:
- return None
-
- # UploadedFile objects should have name and size attributes.
+ def to_native(self, data):
try:
- file_name = data.name
- file_size = data.size
- except AttributeError:
- raise ValidationError(self.error_messages['invalid'])
-
- if self.max_length is not None and len(file_name) > self.max_length:
- error_values = {'max': self.max_length, 'length': len(file_name)}
- raise ValidationError(self.error_messages['max_length'] % error_values)
- if not file_name:
- raise ValidationError(self.error_messages['invalid'])
- if not self.allow_empty_file and not file_size:
- raise ValidationError(self.error_messages['empty'])
-
+ data = int(str(data))
+ except (ValueError, TypeError):
+ self.fail('invalid_integer')
return data
- def to_native(self, value):
- return value.name
-
-
-class ImageField(FileField):
- use_files = True
- type_name = 'ImageField'
- type_label = 'image upload'
- form_field_class = forms.ImageField
-
- default_error_messages = {
- 'invalid_image': _("Upload a valid image. The file you uploaded was "
- "either not an image or a corrupted image."),
- }
-
- def from_native(self, data):
- """
- Checks that the file-upload field data contains a valid image (GIF, JPG,
- PNG, possibly others -- whatever the Python Imaging Library supports).
- """
- f = super(ImageField, self).from_native(data)
- if f is None:
- return None
-
- from rest_framework.compat import Image
- assert Image is not None, 'Either Pillow or PIL must be installed for ImageField support.'
-
- # We need to get a file object for PIL. We might have a path or we might
- # have to read the data into memory.
- if hasattr(data, 'temporary_file_path'):
- file = data.temporary_file_path()
- else:
- if hasattr(data, 'read'):
- file = BytesIO(data.read())
- else:
- file = BytesIO(data['content'])
-
- try:
- # load() could spot a truncated JPEG, but it loads the entire
- # image in memory, which is a DoS vector. See #3848 and #18520.
- # verify() must be called immediately after the constructor.
- Image.open(file).verify()
- except ImportError:
- # Under PyPy, it is possible to import PIL. However, the underlying
- # _imaging C module isn't available, so an ImportError will be
- # raised. Catch and re-raise.
- raise
- except Exception: # Python Imaging Library doesn't recognize it as an image
- raise ValidationError(self.error_messages['invalid_image'])
- if hasattr(f, 'seek') and callable(f.seek):
- f.seek(0)
- return f
-
-class SerializerMethodField(Field):
- """
- A field that gets its value by calling a method on the serializer it's attached to.
- """
-
- def __init__(self, method_name, *args, **kwargs):
- self.method_name = method_name
- super(SerializerMethodField, self).__init__(*args, **kwargs)
-
- def field_to_native(self, obj, field_name):
- value = getattr(self.parent, self.method_name)(obj)
- return self.to_native(value)
+class MethodField(Field):
+ def __init__(self, **kwargs):
+ kwargs['source'] = '*'
+ kwargs['read_only'] = True
+ super(MethodField, self).__init__(**kwargs)
+
+ def to_primative(self, value):
+ attr = 'get_{field_name}'.format(field_name=self.field_name)
+ method = getattr(self.parent, attr)
+ return method(value)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index b3bd6ce9..6705cbb2 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -79,18 +79,16 @@ class GenericAPIView(views.APIView):
'view': self
}
- def get_serializer(self, instance=None, data=None, files=None, many=False,
- partial=False, allow_add_remove=False):
+ def get_serializer(self, instance=None, data=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,
- allow_add_remove=allow_add_remove,
- context=context)
+ return serializer_class(
+ instance, data=data, many=many, partial=partial, context=context
+ )
def get_pagination_serializer(self, page):
"""
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index ac59d979..ee01cabc 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -36,12 +36,10 @@ class CreateModelMixin(object):
Create a model instance.
"""
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():
- self.pre_save(serializer.object)
- self.object = serializer.save(force_insert=True)
- self.post_save(self.object, created=True)
+ self.object = serializer.save()
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
@@ -90,26 +88,20 @@ class UpdateModelMixin(object):
partial = kwargs.pop('partial', False)
self.object = self.get_object_or_none()
- serializer = self.get_serializer(self.object, data=request.DATA,
- files=request.FILES, partial=partial)
+ serializer = self.get_serializer(self.object, data=request.DATA, partial=partial)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- try:
- self.pre_save(serializer.object)
- except ValidationError as err:
- # 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)
+ lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
+ lookup_value = self.kwargs[lookup_url_kwarg]
+ extras = {self.lookup_field: lookup_value}
if self.object is None:
- self.object = serializer.save(force_insert=True)
- self.post_save(self.object, created=True)
+ self.object = serializer.save(extras=extras)
return Response(serializer.data, status=status.HTTP_201_CREATED)
- self.object = serializer.save(force_update=True)
- self.post_save(self.object, created=False)
+ self.object = serializer.save(extras=extras)
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, *args, **kwargs):
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index d51ea929..83ef97c5 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -48,17 +48,17 @@ class DefaultObjectSerializer(serializers.Field):
super(DefaultObjectSerializer, self).__init__(source=source)
-class PaginationSerializerOptions(serializers.SerializerOptions):
- """
- An object that stores the options that may be provided to a
- pagination serializer by using the inner `Meta` class.
+# class PaginationSerializerOptions(serializers.SerializerOptions):
+# """
+# An object that stores the options that may be provided to a
+# pagination serializer by using the inner `Meta` class.
- Accessible on the instance as `serializer.opts`.
- """
- def __init__(self, meta):
- super(PaginationSerializerOptions, self).__init__(meta)
- self.object_serializer_class = getattr(meta, 'object_serializer_class',
- DefaultObjectSerializer)
+# Accessible on the instance as `serializer.opts`.
+# """
+# def __init__(self, meta):
+# super(PaginationSerializerOptions, self).__init__(meta)
+# self.object_serializer_class = getattr(meta, 'object_serializer_class',
+# DefaultObjectSerializer)
class BasePaginationSerializer(serializers.Serializer):
@@ -66,7 +66,7 @@ class BasePaginationSerializer(serializers.Serializer):
A base class for pagination serializers to inherit from,
to make implementing custom serializers more easy.
"""
- _options_class = PaginationSerializerOptions
+ # _options_class = PaginationSerializerOptions
results_field = 'results'
def __init__(self, *args, **kwargs):
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 56870b40..e69de29b 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -1,486 +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
-
-
-# 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.'),
- }
-
- 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)
-
- 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}
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
-
- 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_value = view_kwargs[self.lookup_field]
- filter_kwargs = {self.lookup_field: lookup_value}
- 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
-
- 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
-
- 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
-
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 748ebac9..e8935b01 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -458,7 +458,7 @@ class BrowsableAPIRenderer(BaseRenderer):
):
return
- serializer = view.get_serializer(instance=obj, data=data, files=files)
+ serializer = view.get_serializer(instance=obj, data=data)
serializer.is_valid()
data = serializer.data
@@ -579,10 +579,10 @@ class BrowsableAPIRenderer(BaseRenderer):
'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
'response_headers': response_headers,
- 'put_form': self.get_rendered_html_form(view, 'PUT', request),
- 'post_form': self.get_rendered_html_form(view, 'POST', request),
- 'delete_form': self.get_rendered_html_form(view, 'DELETE', request),
- 'options_form': self.get_rendered_html_form(view, 'OPTIONS', request),
+ #'put_form': self.get_rendered_html_form(view, 'PUT', request),
+ #'post_form': self.get_rendered_html_form(view, 'POST', request),
+ #'delete_form': self.get_rendered_html_form(view, 'DELETE', request),
+ #'options_form': self.get_rendered_html_form(view, 'OPTIONS', request),
'raw_data_put_form': raw_data_put_form,
'raw_data_post_form': raw_data_post_form,
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index be8ad3f2..d121812d 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -10,21 +10,14 @@ python primitives.
2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
-from __future__ import unicode_literals
-import copy
-import datetime
-import inspect
-import types
-from decimal import Decimal
-from django.contrib.contenttypes.generic import GenericForeignKey
-from django.core.paginator import Page
from django.db import models
-from django.forms import widgets
from django.utils import six
-from django.utils.datastructures import SortedDict
-from django.core.exceptions import ObjectDoesNotExist
+from collections import namedtuple, OrderedDict
+from rest_framework.fields import empty, set_value, Field, SkipField, ValidationError
from rest_framework.settings import api_settings
-
+from rest_framework.utils import html
+import copy
+import inspect
# Note: We do the following so that users of the framework can use this style:
#
@@ -37,635 +30,339 @@ from rest_framework.relations import * # NOQA
from rest_framework.fields import * # NOQA
-def _resolve_model(obj):
- """
- Resolve supplied `obj` to a Django model class.
+FieldResult = namedtuple('FieldResult', ['field', 'value', 'error'])
- `obj` must be a Django model class itself, or a string
- representation of one. Useful in situtations like GH #1225 where
- Django may not have resolved a string-based reference to a model in
- another model's foreign key definition.
-
- String representations should have the format:
- 'appname.ModelName'
- """
- if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
- app_name, model_name = obj.split('.')
- return models.get_model(app_name, model_name)
- elif inspect.isclass(obj) and issubclass(obj, models.Model):
- return obj
- else:
- raise ValueError("{0} is not a Django model".format(obj))
-
-
-def pretty_name(name):
- """Converts 'first_name' to 'First name'"""
- if not name:
- return ''
- return name.replace('_', ' ').capitalize()
+class BaseSerializer(Field):
+ def __init__(self, instance=None, data=None, **kwargs):
+ super(BaseSerializer, self).__init__(**kwargs)
+ self.instance = instance
+ self._initial_data = data
-class RelationsList(list):
- _deleted = []
+ def to_native(self, data):
+ raise NotImplementedError()
+ def to_primative(self, instance):
+ raise NotImplementedError()
-class NestedValidationError(ValidationError):
- """
- The default ValidationError behavior is to stringify each item in the list
- if the messages are a list of error messages.
+ def update(self, instance):
+ raise NotImplementedError()
- In the case of nested serializers, where the parent has many children,
- then the child's `serializer.errors` will be a list of dicts. In the case
- of a single child, the `serializer.errors` will be a dict.
+ def create(self):
+ raise NotImplementedError()
- We need to override the default behavior to get properly nested error dicts.
- """
+ def save(self, extras=None):
+ if extras is not None:
+ self._validated_data.update(extras)
- def __init__(self, message):
- if isinstance(message, dict):
- self._messages = [message]
+ if self.instance is not None:
+ self.update(self.instance)
else:
- self._messages = message
-
- @property
- def messages(self):
- return self._messages
+ self.instance = self.create()
+ return self.instance
-class DictWithMetadata(dict):
- """
- A dict-like object, that can have additional properties attached.
- """
- def __getstate__(self):
- """
- Used by pickle (e.g., caching).
- Overridden to remove the metadata from the dict, since it shouldn't be
- pickled and may in some instances be unpickleable.
- """
- return dict(self)
-
+ def is_valid(self):
+ try:
+ self._validated_data = self.to_native(self._initial_data)
+ except ValidationError as exc:
+ self._validated_data = {}
+ self._errors = exc.args[0]
+ return False
+ self._errors = {}
+ return True
-class SortedDictWithMetadata(SortedDict):
- """
- A sorted dict-like object, that can have additional properties attached.
- """
- def __getstate__(self):
- """
- Used by pickle (e.g., caching).
- Overriden to remove the metadata from the dict, since it shouldn't be
- pickle and may in some instances be unpickleable.
- """
- return SortedDict(self).__dict__
+ @property
+ def data(self):
+ if not hasattr(self, '_data'):
+ if self.instance is not None:
+ self._data = self.to_primative(self.instance)
+ elif self._initial_data is not None:
+ self._data = {
+ field_name: field.get_value(self._initial_data)
+ for field_name, field in self.fields.items()
+ }
+ else:
+ self._data = self.get_initial()
+ return self._data
+ @property
+ def errors(self):
+ if not hasattr(self, '_errors'):
+ msg = 'You must call `.is_valid()` before accessing `.errors`.'
+ raise AssertionError(msg)
+ return self._errors
-def _is_protected_type(obj):
- """
- True if the object is a native datatype that does not need to
- be serialized further.
- """
- return isinstance(obj, (
- types.NoneType,
- int, long,
- datetime.datetime, datetime.date, datetime.time,
- float, Decimal,
- basestring)
- )
+ @property
+ def validated_data(self):
+ if not hasattr(self, '_validated_data'):
+ msg = 'You must call `.is_valid()` before accessing `.validated_data`.'
+ raise AssertionError(msg)
+ return self._validated_data
-def _get_declared_fields(bases, attrs):
+class SerializerMetaclass(type):
"""
- Create a list of serializer field instances from the passed in 'attrs',
- plus any fields on the base classes (in 'bases').
+ This metaclass sets a dictionary named `base_fields` on the class.
- Note that all fields from the base classes are used.
+ Any fields included as attributes on either the class or it's superclasses
+ will be include in the `base_fields` dictionary.
"""
- fields = [(field_name, attrs.pop(field_name))
- for field_name, obj in list(six.iteritems(attrs))
- if isinstance(obj, Field)]
- fields.sort(key=lambda x: x[1].creation_counter)
- # If this class is subclassing another Serializer, add that Serializer's
- # fields. Note that we loop over the bases in *reverse*. This is necessary
- # in order to maintain the correct order of fields.
- for base in bases[::-1]:
- if hasattr(base, 'base_fields'):
- fields = list(base.base_fields.items()) + fields
+ @classmethod
+ def _get_fields(cls, bases, attrs):
+ fields = [(field_name, attrs.pop(field_name))
+ for field_name, obj in list(attrs.items())
+ if isinstance(obj, Field)]
+ fields.sort(key=lambda x: x[1]._creation_counter)
- return SortedDict(fields)
+ # If this class is subclassing another Serializer, add that Serializer's
+ # fields. Note that we loop over the bases in *reverse*. This is necessary
+ # in order to maintain the correct order of fields.
+ for base in bases[::-1]:
+ if hasattr(base, 'base_fields'):
+ fields = list(base.base_fields.items()) + fields
+ return OrderedDict(fields)
-class SerializerMetaclass(type):
def __new__(cls, name, bases, attrs):
- attrs['base_fields'] = _get_declared_fields(bases, attrs)
+ attrs['base_fields'] = cls._get_fields(bases, attrs)
return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
-class SerializerOptions(object):
- """
- Meta class options for Serializer
- """
- def __init__(self, meta):
- self.depth = getattr(meta, 'depth', 0)
- self.fields = getattr(meta, 'fields', ())
- self.exclude = getattr(meta, 'exclude', ())
+@six.add_metaclass(SerializerMetaclass)
+class Serializer(BaseSerializer):
+ def __new__(cls, *args, **kwargs):
+ many = kwargs.pop('many', False)
+ if many:
+ class DynamicListSerializer(ListSerializer):
+ child = cls()
+ return DynamicListSerializer(*args, **kwargs)
+ return super(Serializer, cls).__new__(cls)
-class BaseSerializer(WritableField):
- """
- This is the Serializer implementation.
- We need to implement it as `BaseSerializer` due to metaclass magicks.
- """
- class Meta(object):
- pass
-
- _options_class = SerializerOptions
- _dict_class = SortedDictWithMetadata
-
- def __init__(self, instance=None, data=None, files=None,
- context=None, partial=False, many=False,
- allow_add_remove=False, **kwargs):
- super(BaseSerializer, self).__init__(**kwargs)
- self.opts = self._options_class(self.Meta)
- self.parent = None
- self.root = None
- self.partial = partial
- self.many = many
- self.allow_add_remove = allow_add_remove
+ def __init__(self, *args, **kwargs):
+ kwargs.pop('context', None)
+ kwargs.pop('partial', None)
+ kwargs.pop('many', False)
- self.context = context or {}
+ super(Serializer, self).__init__(*args, **kwargs)
- self.init_data = data
- self.init_files = files
- self.object = instance
+ # Every new serializer is created with a clone of the field instances.
+ # This allows users to dynamically modify the fields on a serializer
+ # instance without affecting every other serializer class.
self.fields = self.get_fields()
- self._data = None
- self._files = None
- self._errors = None
-
- if many and instance is not None and not hasattr(instance, '__iter__'):
- raise ValueError('instance should be a queryset or other iterable with many=True')
-
- if allow_add_remove and not many:
- raise ValueError('allow_add_remove should only be used for bulk updates, but you have not set many=True')
-
- #####
- # Methods to determine which fields to use when (de)serializing objects.
-
- def get_default_fields(self):
- """
- Return the complete set of default fields for the object, as a dict.
- """
- return {}
-
- def get_fields(self):
- """
- Returns the complete set of fields for the object as a dict.
-
- This will be the set of any explicitly declared fields,
- plus the set of fields returned by get_default_fields().
- """
- ret = SortedDict()
-
- # Get the explicitly declared fields
- base_fields = copy.deepcopy(self.base_fields)
- for key, field in base_fields.items():
- ret[key] = field
-
- # Add in the default fields
- default_fields = self.get_default_fields()
- for key, val in default_fields.items():
- if key not in ret:
- ret[key] = val
-
- # If 'fields' is specified, use those fields, in that order.
- if self.opts.fields:
- assert isinstance(self.opts.fields, (list, tuple)), '`fields` must be a list or tuple'
- new = SortedDict()
- for key in self.opts.fields:
- new[key] = ret[key]
- ret = new
-
- # Remove anything in 'exclude'
- if self.opts.exclude:
- assert isinstance(self.opts.exclude, (list, tuple)), '`exclude` must be a list or tuple'
- for key in self.opts.exclude:
- ret.pop(key, None)
-
- for key, field in ret.items():
- field.initialize(parent=self, field_name=key)
-
- return ret
-
- #####
- # Methods to convert or revert from objects <--> primitive representations.
-
- def get_field_key(self, field_name):
- """
- Return the key that should be used for a given field.
- """
- return field_name
-
- def restore_fields(self, data, files):
- """
- Core of deserialization, together with `restore_object`.
- Converts a dictionary of data into a dictionary of deserialized fields.
- """
- reverted_data = {}
-
- if data is not None and not isinstance(data, dict):
- self._errors['non_field_errors'] = ['Invalid data']
- return None
-
+ # Setup all the child fields, to provide them with the current context.
for field_name, field in self.fields.items():
- field.initialize(parent=self, field_name=field_name)
- try:
- field.field_from_native(data, files, field_name, reverted_data)
- except ValidationError as err:
- self._errors[field_name] = list(err.messages)
+ field.bind(field_name, self, self)
- return reverted_data
+ def get_fields(self):
+ return copy.deepcopy(self.base_fields)
- def perform_validation(self, attrs):
- """
- Run `validate_()` and `validate()` methods on the serializer
- """
+ def bind(self, field_name, parent, root):
+ # If the serializer is used as a field then when it becomes bound
+ # it also needs to bind all its child fields.
+ super(Serializer, self).bind(field_name, parent, root)
for field_name, field in self.fields.items():
- if field_name in self._errors:
- continue
+ field.bind(field_name, self, root)
- source = field.source or field_name
- if self.partial and source not in attrs:
- continue
- try:
- validate_method = getattr(self, 'validate_%s' % field_name, None)
- if validate_method:
- attrs = validate_method(attrs, source)
- except ValidationError as err:
- self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
-
- # If there are already errors, we don't run .validate() because
- # field-validation failed and thus `attrs` may not be complete.
- # which in turn can cause inconsistent validation errors.
- if not self._errors:
- try:
- attrs = self.validate(attrs)
- except ValidationError as err:
- if hasattr(err, 'message_dict'):
- for field_name, error_messages in err.message_dict.items():
- self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages)
- elif hasattr(err, 'messages'):
- self._errors['non_field_errors'] = err.messages
-
- return attrs
+ def get_initial(self):
+ return {
+ field.field_name: field.get_initial()
+ for field in self.fields.values()
+ }
- def validate(self, attrs):
- """
- Stub method, to be overridden in Serializer subclasses
- """
- return attrs
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # nested HTML forms.
+ if html.is_html_input(dictionary):
+ return html.parse_html_dict(dictionary, prefix=self.field_name)
+ return dictionary.get(self.field_name, empty)
- def restore_object(self, attrs, instance=None):
+ def to_native(self, data):
"""
- Deserialize a dictionary of attributes into an object instance.
- You should override this method to control how deserialized objects
- are instantiated.
+ Dict of native values <- Dict of primitive datatypes.
"""
- if instance is not None:
- instance.update(attrs)
- return instance
- return attrs
+ ret = {}
+ errors = {}
+ fields = [field for field in self.fields.values() if not field.read_only]
- def to_native(self, obj):
- """
- Serialize objects -> primitives.
- """
- ret = self._dict_class()
- ret.fields = self._dict_class()
+ for field in fields:
+ primitive_value = field.get_value(data)
+ try:
+ validated_value = field.validate(primitive_value)
+ except ValidationError as exc:
+ errors[field.field_name] = str(exc)
+ except SkipField:
+ pass
+ else:
+ set_value(ret, field.source_attrs, validated_value)
- for field_name, field in self.fields.items():
- if field.read_only and obj is None:
- continue
- field.initialize(parent=self, field_name=field_name)
- key = self.get_field_key(field_name)
- value = field.field_to_native(obj, field_name)
- method = getattr(self, 'transform_%s' % field_name, None)
- if callable(method):
- value = method(obj, value)
- if not getattr(field, 'write_only', False):
- ret[key] = value
- ret.fields[key] = self.augment_field(field, field_name, key, value)
+ if errors:
+ raise ValidationError(errors)
return ret
- def from_native(self, data, files=None):
- """
- Deserialize primitives -> objects.
- """
- self._errors = {}
-
- if data is not None or files is not None:
- attrs = self.restore_fields(data, files)
- if attrs is not None:
- attrs = self.perform_validation(attrs)
- else:
- self._errors['non_field_errors'] = ['No input provided']
-
- if not self._errors:
- return self.restore_object(attrs, instance=getattr(self, 'object', None))
-
- def augment_field(self, field, field_name, key, value):
- # This horrible stuff is to manage serializers rendering to HTML
- field._errors = self._errors.get(key) if self._errors else None
- field._name = field_name
- field._value = self.init_data.get(key) if self._errors and self.init_data else value
- if not field.label:
- field.label = pretty_name(key)
- return field
-
- def field_to_native(self, obj, field_name):
+ def to_primative(self, instance):
"""
- Override default so that the serializer can be used as a nested field
- across relationships.
+ Object instance -> Dict of primitive datatypes.
"""
- if self.write_only:
- return None
+ ret = OrderedDict()
+ fields = [field for field in self.fields.values() if not field.write_only]
- if self.source == '*':
- return self.to_native(obj)
+ for field in fields:
+ native_value = field.get_attribute(instance)
+ ret[field.field_name] = field.to_primative(native_value)
- # Get the raw field value
- try:
- 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
+ return ret
- if is_simple_callable(getattr(value, 'all', None)):
- return [self.to_native(item) for item in value.all()]
+ def __iter__(self):
+ errors = self.errors if hasattr(self, '_errors') else {}
+ for field in self.fields.values():
+ value = self.data.get(field.field_name) if self.data else None
+ error = errors.get(field.field_name)
+ yield FieldResult(field, value, error)
- if value is None:
- return None
- if self.many:
- return [self.to_native(item) for item in value]
- return self.to_native(value)
+class ListSerializer(BaseSerializer):
+ child = None
+ initial = []
- def field_from_native(self, data, files, field_name, into):
- """
- Override default so that the serializer can be used as a writable
- nested field across relationships.
- """
- if self.read_only:
- return
+ def __init__(self, *args, **kwargs):
+ self.child = kwargs.pop('child', copy.deepcopy(self.child))
+ assert self.child is not None, '`child` is a required argument.'
- try:
- value = data[field_name]
- except KeyError:
- if self.default is not None and not self.partial:
- # Note: partial updates shouldn't set defaults
- value = copy.deepcopy(self.default)
- else:
- if self.required:
- raise ValidationError(self.error_messages['required'])
- return
-
- if self.source == '*':
- if value:
- reverted_data = self.restore_fields(value, {})
- if not self._errors:
- into.update(reverted_data)
- else:
- if value in (None, ''):
- into[(self.source or field_name)] = None
- else:
- # Set the serializer object if it exists
- obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None
-
- # If we have a model manager or similar object then we need
- # to iterate through each instance.
- if (
- self.many and
- not hasattr(obj, '__iter__') and
- is_simple_callable(getattr(obj, 'all', None))
- ):
- obj = obj.all()
-
- kwargs = {
- 'instance': obj,
- 'data': value,
- 'context': self.context,
- 'partial': self.partial,
- 'many': self.many,
- 'allow_add_remove': self.allow_add_remove
- }
- serializer = self.__class__(**kwargs)
+ kwargs.pop('context', None)
+ kwargs.pop('partial', None)
- if serializer.is_valid():
- into[self.source or field_name] = serializer.object
- else:
- # Propagate errors up to our parent
- raise NestedValidationError(serializer.errors)
+ super(ListSerializer, self).__init__(*args, **kwargs)
+ self.child.bind('', self, self)
- def get_identity(self, data):
- """
- This hook is required for bulk update.
- It is used to determine the canonical identity of a given object.
+ def bind(self, field_name, parent, root):
+ # If the list is used as a field then it needs to provide
+ # the current context to the child serializer.
+ super(ListSerializer, self).bind(field_name, parent, root)
+ self.child.bind(field_name, self, root)
- Note that the data has not been validated at this point, so we need
- to make sure that we catch any cases of incorrect datatypes being
- passed to this method.
- """
- try:
- return data.get('id', None)
- except AttributeError:
- return None
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # lists in HTML forms.
+ if is_html_input(dictionary):
+ return html.parse_html_list(dictionary, prefix=self.field_name)
+ return dictionary.get(self.field_name, empty)
- @property
- def errors(self):
+ def to_native(self, data):
"""
- Run deserialization and return error data,
- setting self.object if no errors occurred.
+ List of dicts of native values <- List of dicts of primitive datatypes.
"""
- if self._errors is None:
- data, files = self.init_data, self.init_files
+ if html.is_html_input(data):
+ data = html.parse_html_list(data)
- if self.many is not None:
- many = self.many
- else:
- many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type))
- if many:
- warnings.warn('Implicit list/queryset serialization is deprecated. '
- 'Use the `many=True` flag when instantiating the serializer.',
- DeprecationWarning, stacklevel=3)
-
- if many:
- ret = RelationsList()
- errors = []
- update = self.object is not None
-
- if update:
- # If this is a bulk update we need to map all the objects
- # to a canonical identity so we can determine which
- # individual object is being updated for each item in the
- # incoming data
- objects = self.object
- identities = [self.get_identity(self.to_native(obj)) for obj in objects]
- identity_to_objects = dict(zip(identities, objects))
-
- if hasattr(data, '__iter__') and not isinstance(data, (dict, six.text_type)):
- for item in data:
- if update:
- # Determine which object we're updating
- identity = self.get_identity(item)
- self.object = identity_to_objects.pop(identity, None)
- if self.object is None and not self.allow_add_remove:
- ret.append(None)
- errors.append({'non_field_errors': ['Cannot create a new item, only existing items may be updated.']})
- continue
-
- ret.append(self.from_native(item, None))
- errors.append(self._errors)
-
- if update and self.allow_add_remove:
- ret._deleted = identity_to_objects.values()
-
- self._errors = any(errors) and errors or []
- else:
- self._errors = {'non_field_errors': ['Expected a list of items.']}
- else:
- ret = self.from_native(data, files)
-
- if not self._errors:
- self.object = ret
-
- return self._errors
-
- def is_valid(self):
- return not self.errors
+ return [self.child.validate(item) for item in data]
- @property
- def data(self):
+ def to_primative(self, data):
"""
- Returns the serialized data on the serializer.
+ List of object instances -> List of dicts of primitive datatypes.
"""
- if self._data is None:
- obj = self.object
+ return [self.child.to_primative(item) for item in data]
- if self.many is not None:
- many = self.many
- else:
- many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict))
- if many:
- warnings.warn('Implicit list/queryset serialization is deprecated. '
- 'Use the `many=True` flag when instantiating the serializer.',
- DeprecationWarning, stacklevel=2)
-
- if many:
- self._data = [self.to_native(item) for item in obj]
- else:
- self._data = self.to_native(obj)
+ def create(self, attrs_list):
+ return [self.child.create(attrs) for attrs in attrs_list]
- return self._data
+ def save(self):
+ if self.instance is not None:
+ self.update(self.instance, self.validated_data)
+ self.instance = self.create(self.validated_data)
+ return self.instance
- def save_object(self, obj, **kwargs):
- obj.save(**kwargs)
- def delete_object(self, obj):
- obj.delete()
-
- def save(self, **kwargs):
- """
- Save the deserialized object and return it.
- """
- # Clear cached _data, which may be invalidated by `save()`
- self._data = None
-
- if isinstance(self.object, list):
- [self.save_object(item, **kwargs) for item in self.object]
-
- if self.object._deleted:
- [self.delete_object(item) for item in self.object._deleted]
- else:
- self.save_object(self.object, **kwargs)
-
- return self.object
-
- def metadata(self):
- """
- Return a dictionary of metadata about the fields on the serializer.
- Useful for things like responding to OPTIONS requests, or generating
- API schemas for auto-documentation.
- """
- return SortedDict(
- [
- (field_name, field.metadata())
- for field_name, field in six.iteritems(self.fields)
- ]
- )
+def _resolve_model(obj):
+ """
+ Resolve supplied `obj` to a Django model class.
+ `obj` must be a Django model class itself, or a string
+ representation of one. Useful in situtations like GH #1225 where
+ Django may not have resolved a string-based reference to a model in
+ another model's foreign key definition.
-class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)):
- pass
+ String representations should have the format:
+ 'appname.ModelName'
+ """
+ if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
+ app_name, model_name = obj.split('.')
+ return models.get_model(app_name, model_name)
+ elif inspect.isclass(obj) and issubclass(obj, models.Model):
+ return obj
+ else:
+ raise ValueError("{0} is not a Django model".format(obj))
-class ModelSerializerOptions(SerializerOptions):
+class ModelSerializerOptions(object):
"""
Meta class options for ModelSerializer
"""
def __init__(self, meta):
- super(ModelSerializerOptions, self).__init__(meta)
- self.model = getattr(meta, 'model', None)
- self.read_only_fields = getattr(meta, 'read_only_fields', ())
- self.write_only_fields = getattr(meta, 'write_only_fields', ())
+ self.model = getattr(meta, 'model')
+ self.fields = getattr(meta, 'fields', ())
+ self.depth = getattr(meta, 'depth', 0)
class ModelSerializer(Serializer):
- """
- A serializer that deals with model instances and querysets.
- """
- _options_class = ModelSerializerOptions
-
field_mapping = {
models.AutoField: IntegerField,
- models.FloatField: FloatField,
+ # models.FloatField: FloatField,
models.IntegerField: IntegerField,
models.PositiveIntegerField: IntegerField,
models.SmallIntegerField: IntegerField,
models.PositiveSmallIntegerField: IntegerField,
- models.DateTimeField: DateTimeField,
- models.DateField: DateField,
- models.TimeField: TimeField,
- models.DecimalField: DecimalField,
- models.EmailField: EmailField,
+ # models.DateTimeField: DateTimeField,
+ # models.DateField: DateField,
+ # models.TimeField: TimeField,
+ # models.DecimalField: DecimalField,
+ # models.EmailField: EmailField,
models.CharField: CharField,
- models.URLField: URLField,
- models.SlugField: SlugField,
+ # models.URLField: URLField,
+ # models.SlugField: SlugField,
models.TextField: CharField,
models.CommaSeparatedIntegerField: CharField,
models.BooleanField: BooleanField,
models.NullBooleanField: BooleanField,
- models.FileField: FileField,
- models.ImageField: ImageField,
+ # models.FileField: FileField,
+ # models.ImageField: ImageField,
}
+ _options_class = ModelSerializerOptions
+
+ def __init__(self, *args, **kwargs):
+ self.opts = self._options_class(self.Meta)
+ super(ModelSerializer, self).__init__(*args, **kwargs)
+
+ def get_fields(self):
+ # Get the explicitly declared fields.
+ fields = copy.deepcopy(self.base_fields)
+
+ # Add in the default fields.
+ for key, val in self.get_default_fields().items():
+ if key not in fields:
+ fields[key] = val
+
+ # If `fields` is set on the `Meta` class,
+ # then use only those fields, and in that order.
+ if self.opts.fields:
+ fields = OrderedDict([
+ (key, fields[key]) for key in self.opts.fields
+ ])
+
+ return fields
+
def get_default_fields(self):
"""
Return all the fields that should be serialized for the model.
"""
-
cls = self.opts.model
- assert cls is not None, (
- "Serializer class '%s' is missing 'model' Meta option" %
- self.__class__.__name__
- )
opts = cls._meta.concrete_model._meta
- ret = SortedDict()
+ ret = OrderedDict()
nested = bool(self.opts.depth)
# Deal with adding the primary key field
@@ -694,29 +391,9 @@ class ModelSerializer(Serializer):
has_through_model = True
if model_field.rel and nested:
- if len(inspect.getargspec(self.get_nested_field).args) == 2:
- warnings.warn(
- 'The `get_nested_field(model_field)` call signature '
- 'is deprecated. '
- 'Use `get_nested_field(model_field, related_model, '
- 'to_many) instead',
- DeprecationWarning
- )
- field = self.get_nested_field(model_field)
- else:
- field = self.get_nested_field(model_field, related_model, to_many)
+ field = self.get_nested_field(model_field, related_model, to_many)
elif model_field.rel:
- if len(inspect.getargspec(self.get_nested_field).args) == 3:
- warnings.warn(
- 'The `get_related_field(model_field, to_many)` call '
- 'signature is deprecated. '
- 'Use `get_related_field(model_field, related_model, '
- 'to_many) instead',
- DeprecationWarning
- )
- field = self.get_related_field(model_field, to_many=to_many)
- else:
- field = self.get_related_field(model_field, related_model, to_many)
+ field = self.get_related_field(model_field, related_model, to_many)
else:
field = self.get_field(model_field)
@@ -763,38 +440,6 @@ class ModelSerializer(Serializer):
ret[accessor_name] = field
- # Ensure that 'read_only_fields' is an iterable
- assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple'
-
- # Add the `read_only` flag to any fields that have been specified
- # in the `read_only_fields` option
- for field_name in self.opts.read_only_fields:
- assert field_name not in self.base_fields.keys(), (
- "field '%s' on serializer '%s' specified in "
- "`read_only_fields`, but also added "
- "as an explicit field. Remove it from `read_only_fields`." %
- (field_name, self.__class__.__name__))
- assert field_name in ret, (
- "Non-existant field '%s' specified in `read_only_fields` "
- "on serializer '%s'." %
- (field_name, self.__class__.__name__))
- ret[field_name].read_only = True
-
- # Ensure that 'write_only_fields' is an iterable
- assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple'
-
- for field_name in self.opts.write_only_fields:
- assert field_name not in self.base_fields.keys(), (
- "field '%s' on serializer '%s' specified in "
- "`write_only_fields`, but also added "
- "as an explicit field. Remove it from `write_only_fields`." %
- (field_name, self.__class__.__name__))
- assert field_name in ret, (
- "Non-existant field '%s' specified in `write_only_fields` "
- "on serializer '%s'." %
- (field_name, self.__class__.__name__))
- ret[field_name].write_only = True
-
return ret
def get_pk_field(self, model_field):
@@ -825,28 +470,24 @@ class ModelSerializer(Serializer):
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
- kwargs = {
- 'queryset': related_model._default_manager,
- 'many': to_many
- }
+ kwargs = {}
+ # 'queryset': related_model._default_manager,
+ # 'many': to_many
+ # }
if model_field:
kwargs['required'] = not(model_field.null or model_field.blank)
- if model_field.help_text is not None:
- kwargs['help_text'] = model_field.help_text
+ # if model_field.help_text is not None:
+ # kwargs['help_text'] = model_field.help_text
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
-
if not model_field.editable:
kwargs['read_only'] = True
-
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
- if model_field.help_text is not None:
- kwargs['help_text'] = model_field.help_text
-
- return PrimaryKeyRelatedField(**kwargs)
+ return IntegerField(**kwargs)
+ # TODO: return PrimaryKeyRelatedField(**kwargs)
def get_field(self, model_field):
"""
@@ -869,8 +510,8 @@ class ModelSerializer(Serializer):
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
- if model_field.help_text is not None:
- kwargs['help_text'] = model_field.help_text
+ # if model_field.help_text is not None:
+ # kwargs['help_text'] = model_field.help_text
# TODO: TypedChoiceField?
if model_field.flatchoices: # This ModelField contains choices
@@ -880,7 +521,7 @@ class ModelSerializer(Serializer):
return ChoiceField(**kwargs)
# put this below the ChoiceField because min_value isn't a valid initializer
- if issubclass(model_field.__class__, models.PositiveIntegerField) or\
+ if issubclass(model_field.__class__, models.PositiveIntegerField) or \
issubclass(model_field.__class__, models.PositiveSmallIntegerField):
kwargs['min_value'] = 0
@@ -888,170 +529,27 @@ class ModelSerializer(Serializer):
issubclass(model_field.__class__, (models.CharField, models.TextField)):
kwargs['allow_none'] = True
- attribute_dict = {
- models.CharField: ['max_length'],
- models.CommaSeparatedIntegerField: ['max_length'],
- models.DecimalField: ['max_digits', 'decimal_places'],
- models.EmailField: ['max_length'],
- models.FileField: ['max_length'],
- models.ImageField: ['max_length'],
- models.SlugField: ['max_length'],
- models.URLField: ['max_length'],
- }
-
- if model_field.__class__ in attribute_dict:
- attributes = attribute_dict[model_field.__class__]
- for attribute in attributes:
- kwargs.update({attribute: getattr(model_field, attribute)})
+ # attribute_dict = {
+ # models.CharField: ['max_length'],
+ # models.CommaSeparatedIntegerField: ['max_length'],
+ # models.DecimalField: ['max_digits', 'decimal_places'],
+ # models.EmailField: ['max_length'],
+ # models.FileField: ['max_length'],
+ # models.ImageField: ['max_length'],
+ # models.SlugField: ['max_length'],
+ # models.URLField: ['max_length'],
+ # }
+
+ # if model_field.__class__ in attribute_dict:
+ # attributes = attribute_dict[model_field.__class__]
+ # for attribute in attributes:
+ # kwargs.update({attribute: getattr(model_field, attribute)})
try:
return self.field_mapping[model_field.__class__](**kwargs)
except KeyError:
- return ModelField(model_field=model_field, **kwargs)
-
- def get_validation_exclusions(self, instance=None):
- """
- Return a list of field names to exclude from model validation.
- """
- cls = self.opts.model
- opts = cls._meta.concrete_model._meta
- exclusions = [field.name for field in opts.fields + opts.many_to_many]
-
- for field_name, field in self.fields.items():
- field_name = field.source or field_name
- if (
- field_name in exclusions
- and not field.read_only
- and (field.required or hasattr(instance, field_name))
- and not isinstance(field, Serializer)
- ):
- exclusions.remove(field_name)
- return exclusions
-
- def full_clean(self, instance):
- """
- Perform Django's full_clean, and populate the `errors` dictionary
- if any validation errors occur.
-
- Note that we don't perform this inside the `.restore_object()` method,
- so that subclasses can override `.restore_object()`, and still get
- the full_clean validation checking.
- """
- try:
- instance.full_clean(exclude=self.get_validation_exclusions(instance))
- except ValidationError as err:
- self._errors = err.message_dict
- return None
- return instance
-
- def restore_object(self, attrs, instance=None):
- """
- Restore the model instance.
- """
- m2m_data = {}
- related_data = {}
- nested_forward_relations = {}
- meta = self.opts.model._meta
-
- # Reverse fk or one-to-one relations
- for (obj, model) in meta.get_all_related_objects_with_model():
- field_name = obj.get_accessor_name()
- if field_name in attrs:
- related_data[field_name] = attrs.pop(field_name)
-
- # Reverse m2m relations
- for (obj, model) in meta.get_all_related_m2m_objects_with_model():
- field_name = obj.get_accessor_name()
- if field_name in attrs:
- m2m_data[field_name] = attrs.pop(field_name)
-
- # Forward m2m relations
- for field in meta.many_to_many + meta.virtual_fields:
- if isinstance(field, GenericForeignKey):
- continue
- if field.name in attrs:
- m2m_data[field.name] = attrs.pop(field.name)
-
- # Nested forward relations - These need to be marked so we can save
- # them before saving the parent model instance.
- for field_name in attrs.keys():
- if isinstance(self.fields.get(field_name, None), Serializer):
- nested_forward_relations[field_name] = attrs[field_name]
-
- # Create an empty instance of the model
- if instance is None:
- instance = self.opts.model()
-
- for key, val in attrs.items():
- try:
- setattr(instance, key, val)
- except ValueError:
- self._errors[key] = [self.error_messages['required']]
-
- # Any relations that cannot be set until we've
- # saved the model get hidden away on these
- # private attributes, so we can deal with them
- # at the point of save.
- instance._related_data = related_data
- instance._m2m_data = m2m_data
- instance._nested_forward_relations = nested_forward_relations
-
- return instance
-
- def from_native(self, data, files):
- """
- Override the default method to also include model field validation.
- """
- instance = super(ModelSerializer, self).from_native(data, files)
- if not self._errors:
- return self.full_clean(instance)
-
- def save_object(self, obj, **kwargs):
- """
- Save the deserialized object.
- """
- if getattr(obj, '_nested_forward_relations', None):
- # Nested relationships need to be saved before we can save the
- # parent instance.
- for field_name, sub_object in obj._nested_forward_relations.items():
- if sub_object:
- self.save_object(sub_object)
- setattr(obj, field_name, sub_object)
-
- obj.save(**kwargs)
-
- if getattr(obj, '_m2m_data', None):
- for accessor_name, object_list in obj._m2m_data.items():
- setattr(obj, accessor_name, object_list)
- del(obj._m2m_data)
-
- if getattr(obj, '_related_data', None):
- related_fields = dict([
- (field.get_accessor_name(), field)
- for field, model
- in obj._meta.get_all_related_objects_with_model()
- ])
- for accessor_name, related in obj._related_data.items():
- if isinstance(related, RelationsList):
- # Nested reverse fk relationship
- for related_item in related:
- fk_field = related_fields[accessor_name].field.name
- setattr(related_item, fk_field, obj)
- self.save_object(related_item)
-
- # Delete any removed objects
- if related._deleted:
- [self.delete_object(item) for item in related._deleted]
-
- elif isinstance(related, models.Model):
- # Nested reverse one-one relationship
- fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name
- setattr(related, fk_field, obj)
- self.save_object(related)
- else:
- # Reverse FK or reverse one-one
- setattr(obj, accessor_name, related)
- del(obj._related_data)
+ # TODO: Change this to `return ModelField(model_field=model_field, **kwargs)`
+ return CharField(**kwargs)
class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
@@ -1066,14 +564,10 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
class HyperlinkedModelSerializer(ModelSerializer):
- """
- A subclass of ModelSerializer that uses hyperlinked relationships,
- instead of primary key relationships.
- """
_options_class = HyperlinkedModelSerializerOptions
_default_view_name = '%(model_name)s-detail'
- _hyperlink_field_class = HyperlinkedRelatedField
- _hyperlink_identify_field_class = HyperlinkedIdentityField
+ #_hyperlink_field_class = HyperlinkedRelatedField
+ #_hyperlink_identify_field_class = HyperlinkedIdentityField
def get_default_fields(self):
fields = super(HyperlinkedModelSerializer, self).get_default_fields()
@@ -1081,15 +575,15 @@ class HyperlinkedModelSerializer(ModelSerializer):
if self.opts.view_name is None:
self.opts.view_name = self._get_default_view_name(self.opts.model)
- if self.opts.url_field_name not in fields:
- url_field = self._hyperlink_identify_field_class(
- view_name=self.opts.view_name,
- lookup_field=self.opts.lookup_field
- )
- ret = self._dict_class()
- ret[self.opts.url_field_name] = url_field
- ret.update(fields)
- fields = ret
+ # if self.opts.url_field_name not in fields:
+ # url_field = self._hyperlink_identify_field_class(
+ # view_name=self.opts.view_name,
+ # lookup_field=self.opts.lookup_field
+ # )
+ # ret = self._dict_class()
+ # ret[self.opts.url_field_name] = url_field
+ # ret.update(fields)
+ # fields = ret
return fields
@@ -1103,33 +597,25 @@ class HyperlinkedModelSerializer(ModelSerializer):
"""
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
- kwargs = {
- 'queryset': related_model._default_manager,
- 'view_name': self._get_default_view_name(related_model),
- 'many': to_many
- }
+ # kwargs = {
+ # 'queryset': related_model._default_manager,
+ # 'view_name': self._get_default_view_name(related_model),
+ # 'many': to_many
+ # }
+ kwargs = {}
if model_field:
kwargs['required'] = not(model_field.null or model_field.blank)
- if model_field.help_text is not None:
- kwargs['help_text'] = model_field.help_text
+ # if model_field.help_text is not None:
+ # kwargs['help_text'] = model_field.help_text
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
- if self.opts.lookup_field:
- kwargs['lookup_field'] = self.opts.lookup_field
-
- return self._hyperlink_field_class(**kwargs)
+ return IntegerField(**kwargs)
+ # if self.opts.lookup_field:
+ # kwargs['lookup_field'] = self.opts.lookup_field
- def get_identity(self, data):
- """
- This hook is required for bulk update.
- We need to override the default, to use the url as the identity.
- """
- try:
- return data.get(self.opts.url_field_name, None)
- except AttributeError:
- return None
+ # return self._hyperlink_field_class(**kwargs)
def _get_default_view_name(self, model):
"""
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index 00ffdfba..6a2f6126 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -7,7 +7,7 @@ from django.db.models.query import QuerySet
from django.utils.datastructures import SortedDict
from django.utils.functional import Promise
from rest_framework.compat import force_text
-from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
+# from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
import datetime
import decimal
import types
@@ -106,14 +106,14 @@ else:
SortedDict,
yaml.representer.SafeRepresenter.represent_dict
)
- SafeDumper.add_representer(
- DictWithMetadata,
- yaml.representer.SafeRepresenter.represent_dict
- )
- SafeDumper.add_representer(
- SortedDictWithMetadata,
- yaml.representer.SafeRepresenter.represent_dict
- )
+ # SafeDumper.add_representer(
+ # DictWithMetadata,
+ # yaml.representer.SafeRepresenter.represent_dict
+ # )
+ # SafeDumper.add_representer(
+ # SortedDictWithMetadata,
+ # yaml.representer.SafeRepresenter.represent_dict
+ # )
SafeDumper.add_representer(
types.GeneratorType,
yaml.representer.SafeRepresenter.represent_list
diff --git a/rest_framework/utils/html.py b/rest_framework/utils/html.py
new file mode 100644
index 00000000..bf17050d
--- /dev/null
+++ b/rest_framework/utils/html.py
@@ -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
--
cgit v1.2.3
From ec096a1caceff6a4f5c75a152dd1c7bea9ed281d Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 2 Sep 2014 15:07:56 +0100
Subject: Add relations and get tests running
---
rest_framework/fields.py | 30 ++++++++++--
rest_framework/mixins.py | 1 -
rest_framework/relations.py | 111 ++++++++++++++++++++++++++++++++++++++++++
rest_framework/renderers.py | 14 +++---
rest_framework/serializers.py | 8 +--
rest_framework/utils/html.py | 2 +
tests/test_serializer.py | 4 +-
7 files changed, 151 insertions(+), 19 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index a83bf94c..3e0f7ca4 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -68,7 +68,7 @@ class Field(object):
def __init__(self, read_only=False, write_only=False,
required=None, default=empty, initial=None, source=None,
- label=None, style=None):
+ label=None, style=None, error_messages=None):
self._creation_counter = Field._creation_counter
Field._creation_counter += 1
@@ -216,9 +216,11 @@ class CharField(Field):
'blank': 'This field may not be blank.'
}
- def __init__(self, *args, **kwargs):
+ def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
- super(CharField, self).__init__(*args, **kwargs)
+ self.max_length = kwargs.pop('max_length', None)
+ self.min_length = kwargs.pop('min_length', None)
+ super(CharField, self).__init__(**kwargs)
def to_native(self, data):
if data == '' and not self.allow_blank:
@@ -233,7 +235,7 @@ class ChoiceField(Field):
}
coerce_to_type = str
- def __init__(self, *args, **kwargs):
+ def __init__(self, **kwargs):
choices = kwargs.pop('choices')
assert choices, '`choices` argument is required and may not be empty'
@@ -257,7 +259,7 @@ class ChoiceField(Field):
str(key): key for key in self.choices.keys()
}
- super(ChoiceField, self).__init__(*args, **kwargs)
+ super(ChoiceField, self).__init__(**kwargs)
def to_native(self, data):
try:
@@ -296,6 +298,24 @@ class IntegerField(Field):
return data
+class EmailField(CharField):
+ pass # TODO
+
+
+class RegexField(CharField):
+ def __init__(self, **kwargs):
+ self.regex = kwargs.pop('regex')
+ super(CharField, self).__init__(**kwargs)
+
+
+class DateTimeField(CharField):
+ pass # TODO
+
+
+class FileField(Field):
+ pass # TODO
+
+
class MethodField(Field):
def __init__(self, **kwargs):
kwargs['source'] = '*'
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index ee01cabc..3e9c9bb3 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -6,7 +6,6 @@ which allows mixin classes to be composed in interesting ways.
"""
from __future__ import unicode_literals
-from django.core.exceptions import ValidationError
from django.http import Http404
from rest_framework import status
from rest_framework.response import Response
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index e69de29b..42d2c121 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -0,0 +1,111 @@
+from rest_framework.fields import Field
+from django.core.exceptions import ObjectDoesNotExist
+from django.core.urlresolvers import resolve, get_script_prefix
+from rest_framework.compat import urlparse
+
+
+def get_default_queryset(serializer_class, field_name):
+ manager = getattr(serializer_class.opts.model, field_name)
+ if hasattr(manager, 'related'):
+ # Forward relationships
+ return manager.related.model._default_manager.all()
+ # Reverse relationships
+ return manager.field.rel.to._default_manager.all()
+
+
+class RelatedField(Field):
+ def __init__(self, **kwargs):
+ self.queryset = kwargs.pop('queryset', None)
+ self.many = kwargs.pop('many', False)
+ super(RelatedField, self).__init__(**kwargs)
+
+ def bind(self, field_name, parent, root):
+ super(RelatedField, self).bind(field_name, parent, root)
+ if self.queryset is None and not self.read_only:
+ self.queryset = get_default_queryset(parent, self.source)
+
+
+class PrimaryKeyRelatedField(RelatedField):
+ MESSAGES = {
+ 'required': 'This field is required.',
+ 'does_not_exist': "Invalid pk '{pk_value}' - object does not exist.",
+ 'incorrect_type': 'Incorrect type. Expected pk value, received {data_type}.',
+ }
+
+ def from_native(self, data):
+ try:
+ return self.queryset.get(pk=data)
+ except ObjectDoesNotExist:
+ self.fail('does_not_exist', pk_value=data)
+ except (TypeError, ValueError):
+ self.fail('incorrect_type', data_type=type(data).__name__)
+
+
+class HyperlinkedRelatedField(RelatedField):
+ lookup_field = 'pk'
+
+ MESSAGES = {
+ 'required': 'This field is required.',
+ 'no_match': 'Invalid hyperlink - No URL match',
+ 'incorrect_match': 'Invalid hyperlink - Incorrect URL match.',
+ 'does_not_exist': "Invalid hyperlink - Object does not exist.",
+ 'incorrect_type': 'Incorrect type. Expected URL string, received {data_type}.',
+ }
+
+ def __init__(self, **kwargs):
+ self.view_name = kwargs.pop('view_name')
+ self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
+ self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
+ super(HyperlinkedRelatedField, self).__init__(**kwargs)
+
+ def get_object(self, view_name, view_args, view_kwargs):
+ """
+ Return the object corresponding to a matched URL.
+
+ Takes the matched URL conf arguments, and should return an
+ object instance, or raise an `ObjectDoesNotExist` exception.
+ """
+ lookup_value = view_kwargs[self.lookup_url_kwarg]
+ lookup_kwargs = {self.lookup_field: lookup_value}
+ return self.queryset.get(**lookup_kwargs)
+
+ def from_native(self, value):
+ try:
+ http_prefix = value.startswith(('http:', 'https:'))
+ except AttributeError:
+ self.fail('incorrect_type', 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:
+ self.fail('no_match')
+
+ if match.view_name != self.view_name:
+ self.fail('incorrect_match')
+
+ try:
+ return self.get_object(match.view_name, match.args, match.kwargs)
+ except (ObjectDoesNotExist, TypeError, ValueError):
+ self.fail('does_not_exist')
+
+
+class HyperlinkedIdentityField(RelatedField):
+ lookup_field = 'pk'
+
+ def __init__(self, **kwargs):
+ self.view_name = kwargs.pop('view_name')
+ self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
+ self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
+ super(HyperlinkedIdentityField, self).__init__(**kwargs)
+
+
+class SlugRelatedField(RelatedField):
+ def __init__(self, **kwargs):
+ self.slug_field = kwargs.pop('slug_field', None)
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index e8935b01..dfc5a39f 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -436,13 +436,13 @@ class BrowsableAPIRenderer(BaseRenderer):
if request.method == method:
try:
data = request.DATA
- files = request.FILES
+ # files = request.FILES
except ParseError:
data = None
- files = None
+ # files = None
else:
data = None
- files = None
+ # files = None
with override_method(view, request, method) as request:
obj = getattr(view, 'object', None)
@@ -579,10 +579,10 @@ class BrowsableAPIRenderer(BaseRenderer):
'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
'response_headers': response_headers,
- #'put_form': self.get_rendered_html_form(view, 'PUT', request),
- #'post_form': self.get_rendered_html_form(view, 'POST', request),
- #'delete_form': self.get_rendered_html_form(view, 'DELETE', request),
- #'options_form': self.get_rendered_html_form(view, 'OPTIONS', request),
+ # 'put_form': self.get_rendered_html_form(view, 'PUT', request),
+ # 'post_form': self.get_rendered_html_form(view, 'POST', request),
+ # 'delete_form': self.get_rendered_html_form(view, 'DELETE', request),
+ # 'options_form': self.get_rendered_html_form(view, 'OPTIONS', request),
'raw_data_put_form': raw_data_put_form,
'raw_data_post_form': raw_data_post_form,
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index d121812d..2f23b4d9 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -477,8 +477,8 @@ class ModelSerializer(Serializer):
if model_field:
kwargs['required'] = not(model_field.null or model_field.blank)
- # if model_field.help_text is not None:
- # kwargs['help_text'] = model_field.help_text
+ # if model_field.help_text is not None:
+ # kwargs['help_text'] = model_field.help_text
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
if not model_field.editable:
@@ -566,8 +566,8 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
class HyperlinkedModelSerializer(ModelSerializer):
_options_class = HyperlinkedModelSerializerOptions
_default_view_name = '%(model_name)s-detail'
- #_hyperlink_field_class = HyperlinkedRelatedField
- #_hyperlink_identify_field_class = HyperlinkedIdentityField
+ # _hyperlink_field_class = HyperlinkedRelatedField
+ # _hyperlink_identify_field_class = HyperlinkedIdentityField
def get_default_fields(self):
fields = super(HyperlinkedModelSerializer, self).get_default_fields()
diff --git a/rest_framework/utils/html.py b/rest_framework/utils/html.py
index bf17050d..edc591e9 100644
--- a/rest_framework/utils/html.py
+++ b/rest_framework/utils/html.py
@@ -1,6 +1,8 @@
"""
Helpers for dealing with HTML input.
"""
+import re
+
def is_html_input(dictionary):
# MultiDict type datastructures are used to represent HTML form input,
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index 90f37cf2..fa5cafcf 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -1795,8 +1795,8 @@ class DefaultValuesOnAutogeneratedFieldsTests(TestCase):
class MetadataSerializer(serializers.Serializer):
- field1 = serializers.CharField(3, required=True)
- field2 = serializers.CharField(10, required=False)
+ field1 = serializers.CharField(max_length=3, required=True)
+ field2 = serializers.CharField(max_length=10, required=False)
class MetadataSerializerTestCase(TestCase):
--
cgit v1.2.3
From f2852811f93863f2eed04d51eeb7ef27716b2409 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 2 Sep 2014 17:41:23 +0100
Subject: Getting tests passing
---
rest_framework/authtoken/serializers.py | 5 +-
rest_framework/authtoken/views.py | 3 +-
rest_framework/fields.py | 50 +-
rest_framework/mixins.py | 41 +-
rest_framework/pagination.py | 36 +-
rest_framework/relations.py | 2 +-
rest_framework/serializers.py | 53 +-
tests/test_fields.py | 1920 +++++++++++++++----------------
tests/test_files.py | 184 +--
tests/test_genericrelations.py | 302 ++---
tests/test_generics.py | 358 +++---
tests/test_hyperlinkedserializers.py | 724 ++++++------
tests/test_nullable_fields.py | 54 +-
tests/test_pagination.py | 8 +-
tests/test_permissions.py | 106 +-
tests/test_relations.py | 298 ++---
tests/test_relations_hyperlink.py | 1050 ++++++++---------
tests/test_relations_nested.py | 652 +++++------
tests/test_relations_pk.py | 1102 +++++++++---------
tests/test_relations_slug.py | 514 ++++-----
20 files changed, 3737 insertions(+), 3725 deletions(-)
diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py
index 99e99ae3..edeae857 100644
--- a/rest_framework/authtoken/serializers.py
+++ b/rest_framework/authtoken/serializers.py
@@ -19,11 +19,12 @@ class AuthTokenSerializer(serializers.Serializer):
if not user.is_active:
msg = _('User account is disabled.')
raise serializers.ValidationError(msg)
- attrs['user'] = user
- return attrs
else:
msg = _('Unable to login with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "username" and "password"')
raise serializers.ValidationError(msg)
+
+ attrs['user'] = user
+ return attrs
diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py
index 7c03cb76..94e6f061 100644
--- a/rest_framework/authtoken/views.py
+++ b/rest_framework/authtoken/views.py
@@ -18,7 +18,8 @@ class ObtainAuthToken(APIView):
def post(self, request):
serializer = self.serializer_class(data=request.DATA)
if serializer.is_valid():
- token, created = Token.objects.get_or_create(user=serializer.object['user'])
+ user = serializer.validated_data['user']
+ token, created = Token.objects.get_or_create(user=user)
return Response({'token': token.key})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 3e0f7ca4..838aa3b0 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1,4 +1,5 @@
from rest_framework.utils import html
+import inspect
class empty:
@@ -11,6 +12,22 @@ class empty:
pass
+def is_simple_callable(obj):
+ """
+ True if the object is a callable that takes no arguments.
+ """
+ function = inspect.isfunction(obj)
+ method = inspect.ismethod(obj)
+
+ if not (function or method):
+ return False
+
+ args, _, _, defaults = inspect.getargspec(obj)
+ len_args = len(args) if function else len(args) - 1
+ len_defaults = len(defaults) if defaults else 0
+ return len_args <= len_defaults
+
+
def get_attribute(instance, attrs):
"""
Similar to Python's built in `getattr(instance, attr)`,
@@ -98,6 +115,7 @@ class Field(object):
self.field_name = field_name
self.parent = parent
self.root = root
+ self.context = parent.context
# `self.label` should deafult to being based on the field name.
if self.label is None:
@@ -297,25 +315,55 @@ class IntegerField(Field):
self.fail('invalid_integer')
return data
+ def to_primative(self, value):
+ if value is None:
+ return None
+ return int(value)
+
class EmailField(CharField):
pass # TODO
+class URLField(CharField):
+ pass # TODO
+
+
class RegexField(CharField):
def __init__(self, **kwargs):
self.regex = kwargs.pop('regex')
super(CharField, self).__init__(**kwargs)
+class DateField(CharField):
+ def __init__(self, **kwargs):
+ self.input_formats = kwargs.pop('input_formats', None)
+ super(DateField, self).__init__(**kwargs)
+
+
+class TimeField(CharField):
+ def __init__(self, **kwargs):
+ self.input_formats = kwargs.pop('input_formats', None)
+ super(TimeField, self).__init__(**kwargs)
+
+
class DateTimeField(CharField):
- pass # TODO
+ def __init__(self, **kwargs):
+ self.input_formats = kwargs.pop('input_formats', None)
+ super(DateTimeField, self).__init__(**kwargs)
class FileField(Field):
pass # TODO
+class ReadOnlyField(Field):
+ def to_primative(self, value):
+ if is_simple_callable(value):
+ return value()
+ return value
+
+
class MethodField(Field):
def __init__(self, **kwargs):
kwargs['source'] = '*'
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 3e9c9bb3..359740ce 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -13,23 +13,6 @@ from rest_framework.request import clone_request
from rest_framework.settings import api_settings
-def _get_validation_exclusions(obj, lookup_field=None):
- """
- Given a model instance, and an optional pk and slug field,
- return the full list of all other field names on that model.
-
- For use when performing full_clean on a model instance,
- so we only clean the required fields.
- """
- if lookup_field == 'pk':
- pk_field = obj._meta.pk
- while pk_field.rel:
- pk_field = pk_field.rel.to._meta.pk
- lookup_field = pk_field.name
-
- return [field.name for field in obj._meta.fields if field.name != lookup_field]
-
-
class CreateModelMixin(object):
"""
Create a model instance.
@@ -92,15 +75,14 @@ class UpdateModelMixin(object):
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
- lookup_value = self.kwargs[lookup_url_kwarg]
- extras = {self.lookup_field: lookup_value}
-
if self.object is None:
+ lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
+ lookup_value = self.kwargs[lookup_url_kwarg]
+ extras = {self.lookup_field: lookup_value}
self.object = serializer.save(extras=extras)
return Response(serializer.data, status=status.HTTP_201_CREATED)
- self.object = serializer.save(extras=extras)
+ self.object = serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, *args, **kwargs):
@@ -122,21 +104,6 @@ class UpdateModelMixin(object):
# return a 404 response.
raise
- def pre_save(self, obj):
- """
- Set any attributes on the object that are implicit in the request.
- """
- lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
- lookup_value = self.kwargs[lookup_url_kwarg]
-
- setattr(obj, self.lookup_field, lookup_value)
-
- # 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.
- if hasattr(obj, 'full_clean'):
- exclude = _get_validation_exclusions(obj, self.lookup_field)
- obj.full_clean(exclude)
-
class DestroyModelMixin(object):
"""
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index 83ef97c5..478d32b4 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -13,7 +13,7 @@ class NextPageField(serializers.Field):
"""
page_field = 'page'
- def to_native(self, value):
+ def to_primative(self, value):
if not value.has_next():
return None
page = value.next_page_number()
@@ -28,7 +28,7 @@ class PreviousPageField(serializers.Field):
"""
page_field = 'page'
- def to_native(self, value):
+ def to_primative(self, value):
if not value.has_previous():
return None
page = value.previous_page_number()
@@ -48,25 +48,11 @@ class DefaultObjectSerializer(serializers.Field):
super(DefaultObjectSerializer, self).__init__(source=source)
-# class PaginationSerializerOptions(serializers.SerializerOptions):
-# """
-# An object that stores the options that may be provided to a
-# pagination serializer by using the inner `Meta` class.
-
-# Accessible on the instance as `serializer.opts`.
-# """
-# def __init__(self, meta):
-# super(PaginationSerializerOptions, self).__init__(meta)
-# self.object_serializer_class = getattr(meta, 'object_serializer_class',
-# DefaultObjectSerializer)
-
-
class BasePaginationSerializer(serializers.Serializer):
"""
A base class for pagination serializers to inherit from,
to make implementing custom serializers more easy.
"""
- # _options_class = PaginationSerializerOptions
results_field = 'results'
def __init__(self, *args, **kwargs):
@@ -75,14 +61,16 @@ class BasePaginationSerializer(serializers.Serializer):
"""
super(BasePaginationSerializer, self).__init__(*args, **kwargs)
results_field = self.results_field
- object_serializer = self.opts.object_serializer_class
-
- if 'context' in kwargs:
- context_kwarg = {'context': kwargs['context']}
- else:
- context_kwarg = {}
-
- self.fields[results_field] = object_serializer(source='object_list', **context_kwarg)
+ try:
+ object_serializer = self.Meta.object_serializer_class
+ except AttributeError:
+ object_serializer = DefaultObjectSerializer
+
+ self.fields[results_field] = serializers.ListSerializer(
+ child=object_serializer(),
+ source='object_list'
+ )
+ self.fields[results_field].bind(results_field, self, self) # TODO: Support automatic binding
class PaginationSerializer(BasePaginationSerializer):
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 42d2c121..0b01394a 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -73,7 +73,7 @@ class HyperlinkedRelatedField(RelatedField):
try:
http_prefix = value.startswith(('http:', 'https:'))
except AttributeError:
- self.fail('incorrect_type', type(value).__name__)
+ self.fail('incorrect_type', data_type=type(value).__name__)
if http_prefix:
# If needed convert absolute URLs to relative path
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 2f23b4d9..c38d8968 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -142,7 +142,7 @@ class Serializer(BaseSerializer):
return super(Serializer, cls).__new__(cls)
def __init__(self, *args, **kwargs):
- kwargs.pop('context', None)
+ self.context = kwargs.pop('context', {})
kwargs.pop('partial', None)
kwargs.pop('many', False)
@@ -202,7 +202,7 @@ class Serializer(BaseSerializer):
if errors:
raise ValidationError(errors)
- return ret
+ return self.validate(ret)
def to_primative(self, instance):
"""
@@ -217,6 +217,9 @@ class Serializer(BaseSerializer):
return ret
+ def validate(self, attrs):
+ return attrs
+
def __iter__(self):
errors = self.errors if hasattr(self, '_errors') else {}
for field in self.fields.values():
@@ -232,8 +235,7 @@ class ListSerializer(BaseSerializer):
def __init__(self, *args, **kwargs):
self.child = kwargs.pop('child', copy.deepcopy(self.child))
assert self.child is not None, '`child` is a required argument.'
-
- kwargs.pop('context', None)
+ self.context = kwargs.pop('context', {})
kwargs.pop('partial', None)
super(ListSerializer, self).__init__(*args, **kwargs)
@@ -316,19 +318,19 @@ class ModelSerializer(Serializer):
models.PositiveIntegerField: IntegerField,
models.SmallIntegerField: IntegerField,
models.PositiveSmallIntegerField: IntegerField,
- # models.DateTimeField: DateTimeField,
- # models.DateField: DateField,
- # models.TimeField: TimeField,
+ models.DateTimeField: DateTimeField,
+ models.DateField: DateField,
+ models.TimeField: TimeField,
# models.DecimalField: DecimalField,
- # models.EmailField: EmailField,
+ models.EmailField: EmailField,
models.CharField: CharField,
- # models.URLField: URLField,
+ models.URLField: URLField,
# models.SlugField: SlugField,
models.TextField: CharField,
models.CommaSeparatedIntegerField: CharField,
models.BooleanField: BooleanField,
models.NullBooleanField: BooleanField,
- # models.FileField: FileField,
+ models.FileField: FileField,
# models.ImageField: ImageField,
}
@@ -338,6 +340,15 @@ class ModelSerializer(Serializer):
self.opts = self._options_class(self.Meta)
super(ModelSerializer, self).__init__(*args, **kwargs)
+ def create(self):
+ ModelClass = self.opts.model
+ return ModelClass.objects.create(**self.validated_data)
+
+ def update(self, obj):
+ for attr, value in self.validated_data.items():
+ setattr(obj, attr, value)
+ obj.save()
+
def get_fields(self):
# Get the explicitly declared fields.
fields = copy.deepcopy(self.base_fields)
@@ -566,8 +577,8 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
class HyperlinkedModelSerializer(ModelSerializer):
_options_class = HyperlinkedModelSerializerOptions
_default_view_name = '%(model_name)s-detail'
- # _hyperlink_field_class = HyperlinkedRelatedField
- # _hyperlink_identify_field_class = HyperlinkedIdentityField
+ _hyperlink_field_class = HyperlinkedRelatedField
+ _hyperlink_identify_field_class = HyperlinkedIdentityField
def get_default_fields(self):
fields = super(HyperlinkedModelSerializer, self).get_default_fields()
@@ -575,15 +586,15 @@ class HyperlinkedModelSerializer(ModelSerializer):
if self.opts.view_name is None:
self.opts.view_name = self._get_default_view_name(self.opts.model)
- # if self.opts.url_field_name not in fields:
- # url_field = self._hyperlink_identify_field_class(
- # view_name=self.opts.view_name,
- # lookup_field=self.opts.lookup_field
- # )
- # ret = self._dict_class()
- # ret[self.opts.url_field_name] = url_field
- # ret.update(fields)
- # fields = ret
+ if self.opts.url_field_name not in fields:
+ url_field = self._hyperlink_identify_field_class(
+ view_name=self.opts.view_name,
+ lookup_field=self.opts.lookup_field
+ )
+ ret = fields.__class__()
+ ret[self.opts.url_field_name] = url_field
+ ret.update(fields)
+ fields = ret
return fields
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 094ac1eb..53c60538 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1,1022 +1,1022 @@
-"""
-General serializer field tests.
-"""
-from __future__ import unicode_literals
+# """
+# General serializer field tests.
+# """
+# from __future__ import unicode_literals
-import datetime
-import re
-from decimal import Decimal
-from uuid import uuid4
-from django.core import validators
-from django.db import models
-from django.test import TestCase
-from django.utils.datastructures import SortedDict
-from rest_framework import serializers
-from tests.models import RESTFrameworkModel
+# import datetime
+# import re
+# from decimal import Decimal
+# from uuid import uuid4
+# from django.core import validators
+# from django.db import models
+# from django.test import TestCase
+# from django.utils.datastructures import SortedDict
+# from rest_framework import serializers
+# from tests.models import RESTFrameworkModel
-class TimestampedModel(models.Model):
- added = models.DateTimeField(auto_now_add=True)
- updated = models.DateTimeField(auto_now=True)
+# class TimestampedModel(models.Model):
+# added = models.DateTimeField(auto_now_add=True)
+# updated = models.DateTimeField(auto_now=True)
-class CharPrimaryKeyModel(models.Model):
- id = models.CharField(max_length=20, primary_key=True)
+# class CharPrimaryKeyModel(models.Model):
+# id = models.CharField(max_length=20, primary_key=True)
-class TimestampedModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = TimestampedModel
+# class TimestampedModelSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = TimestampedModel
-class CharPrimaryKeyModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = CharPrimaryKeyModel
+# class CharPrimaryKeyModelSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = CharPrimaryKeyModel
-class TimeFieldModel(models.Model):
- clock = models.TimeField()
+# class TimeFieldModel(models.Model):
+# clock = models.TimeField()
-class TimeFieldModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = TimeFieldModel
+# class TimeFieldModelSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = TimeFieldModel
-SAMPLE_CHOICES = [
- ('red', 'Red'),
- ('green', 'Green'),
- ('blue', 'Blue'),
-]
+# SAMPLE_CHOICES = [
+# ('red', 'Red'),
+# ('green', 'Green'),
+# ('blue', 'Blue'),
+# ]
-class ChoiceFieldModel(models.Model):
- choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, max_length=255)
+# class ChoiceFieldModel(models.Model):
+# choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, max_length=255)
-class ChoiceFieldModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = ChoiceFieldModel
-
-
-class ChoiceFieldModelWithNull(models.Model):
- choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, null=True, max_length=255)
-
-
-class ChoiceFieldModelWithNullSerializer(serializers.ModelSerializer):
- class Meta:
- model = ChoiceFieldModelWithNull
-
-
-class BasicFieldTests(TestCase):
- def test_auto_now_fields_read_only(self):
- """
- auto_now and auto_now_add fields should be read_only by default.
- """
- serializer = TimestampedModelSerializer()
- self.assertEqual(serializer.fields['added'].read_only, True)
-
- def test_auto_pk_fields_read_only(self):
- """
- AutoField fields should be read_only by default.
- """
- serializer = TimestampedModelSerializer()
- self.assertEqual(serializer.fields['id'].read_only, True)
-
- def test_non_auto_pk_fields_not_read_only(self):
- """
- PK fields other than AutoField fields should not be read_only by default.
- """
- serializer = CharPrimaryKeyModelSerializer()
- self.assertEqual(serializer.fields['id'].read_only, False)
-
- def test_dict_field_ordering(self):
- """
- Field should preserve dictionary ordering, if it exists.
- See: https://github.com/tomchristie/django-rest-framework/issues/832
- """
- ret = SortedDict()
- ret['c'] = 1
- ret['b'] = 1
- ret['a'] = 1
- ret['z'] = 1
- field = serializers.Field()
- keys = list(field.to_native(ret).keys())
- self.assertEqual(keys, ['c', 'b', 'a', 'z'])
-
- def test_widget_html_attributes(self):
- """
- Make sure widget_html() renders the correct attributes
- """
- r = re.compile('(\S+)=["\']?((?:.(?!["\']?\s+(?:\S+)=|[>"\']))+.)["\']?')
- form = TimeFieldModelSerializer().data
- attributes = r.findall(form.fields['clock'].widget_html())
- self.assertIn(('name', 'clock'), attributes)
- self.assertIn(('id', 'clock'), attributes)
+# class ChoiceFieldModelSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = ChoiceFieldModel
+
+
+# class ChoiceFieldModelWithNull(models.Model):
+# choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, null=True, max_length=255)
+
+
+# class ChoiceFieldModelWithNullSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = ChoiceFieldModelWithNull
+
+
+# class BasicFieldTests(TestCase):
+# def test_auto_now_fields_read_only(self):
+# """
+# auto_now and auto_now_add fields should be read_only by default.
+# """
+# serializer = TimestampedModelSerializer()
+# self.assertEqual(serializer.fields['added'].read_only, True)
+
+# def test_auto_pk_fields_read_only(self):
+# """
+# AutoField fields should be read_only by default.
+# """
+# serializer = TimestampedModelSerializer()
+# self.assertEqual(serializer.fields['id'].read_only, True)
+
+# def test_non_auto_pk_fields_not_read_only(self):
+# """
+# PK fields other than AutoField fields should not be read_only by default.
+# """
+# serializer = CharPrimaryKeyModelSerializer()
+# self.assertEqual(serializer.fields['id'].read_only, False)
+
+# def test_dict_field_ordering(self):
+# """
+# Field should preserve dictionary ordering, if it exists.
+# See: https://github.com/tomchristie/django-rest-framework/issues/832
+# """
+# ret = SortedDict()
+# ret['c'] = 1
+# ret['b'] = 1
+# ret['a'] = 1
+# ret['z'] = 1
+# field = serializers.Field()
+# keys = list(field.to_native(ret).keys())
+# self.assertEqual(keys, ['c', 'b', 'a', 'z'])
+
+# def test_widget_html_attributes(self):
+# """
+# Make sure widget_html() renders the correct attributes
+# """
+# r = re.compile('(\S+)=["\']?((?:.(?!["\']?\s+(?:\S+)=|[>"\']))+.)["\']?')
+# form = TimeFieldModelSerializer().data
+# attributes = r.findall(form.fields['clock'].widget_html())
+# self.assertIn(('name', 'clock'), attributes)
+# self.assertIn(('id', 'clock'), attributes)
-class DateFieldTest(TestCase):
- """
- Tests for the DateFieldTest from_native() and to_native() behavior
- """
-
- def test_from_native_string(self):
- """
- Make sure from_native() accepts default iso input formats.
- """
- f = serializers.DateField()
- result_1 = f.from_native('1984-07-31')
-
- self.assertEqual(datetime.date(1984, 7, 31), result_1)
-
- def test_from_native_datetime_date(self):
- """
- Make sure from_native() accepts a datetime.date instance.
- """
- f = serializers.DateField()
- result_1 = f.from_native(datetime.date(1984, 7, 31))
-
- self.assertEqual(result_1, datetime.date(1984, 7, 31))
-
- def test_from_native_custom_format(self):
- """
- Make sure from_native() accepts custom input formats.
- """
- f = serializers.DateField(input_formats=['%Y -- %d'])
- result = f.from_native('1984 -- 31')
-
- self.assertEqual(datetime.date(1984, 1, 31), result)
-
- def test_from_native_invalid_default_on_custom_format(self):
- """
- Make sure from_native() don't accept default formats if custom format is preset
- """
- f = serializers.DateField(input_formats=['%Y -- %d'])
-
- try:
- f.from_native('1984-07-31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY -- DD"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_empty(self):
- """
- Make sure from_native() returns None on empty param.
- """
- f = serializers.DateField()
- result = f.from_native('')
-
- self.assertEqual(result, None)
-
- def test_from_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DateField()
- result = f.from_native(None)
-
- self.assertEqual(result, None)
-
- def test_from_native_invalid_date(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid date.
- """
- f = serializers.DateField()
-
- try:
- f.from_native('1984-13-31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_invalid_format(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid format.
- """
- f = serializers.DateField()
-
- try:
- f.from_native('1984 -- 31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_to_native(self):
- """
- Make sure to_native() returns datetime as default.
- """
- f = serializers.DateField()
-
- result_1 = f.to_native(datetime.date(1984, 7, 31))
-
- self.assertEqual(datetime.date(1984, 7, 31), result_1)
-
- def test_to_native_iso(self):
- """
- Make sure to_native() with 'iso-8601' returns iso formated date.
- """
- f = serializers.DateField(format='iso-8601')
-
- result_1 = f.to_native(datetime.date(1984, 7, 31))
-
- self.assertEqual('1984-07-31', result_1)
-
- def test_to_native_custom_format(self):
- """
- Make sure to_native() returns correct custom format.
- """
- f = serializers.DateField(format="%Y - %m.%d")
-
- result_1 = f.to_native(datetime.date(1984, 7, 31))
-
- self.assertEqual('1984 - 07.31', result_1)
-
- def test_to_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DateField(required=False)
- self.assertEqual(None, f.to_native(None))
-
-
-class DateTimeFieldTest(TestCase):
- """
- Tests for the DateTimeField from_native() and to_native() behavior
- """
-
- def test_from_native_string(self):
- """
- Make sure from_native() accepts default iso input formats.
- """
- f = serializers.DateTimeField()
- result_1 = f.from_native('1984-07-31 04:31')
- result_2 = f.from_native('1984-07-31 04:31:59')
- result_3 = f.from_native('1984-07-31 04:31:59.000200')
-
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result_1)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59), result_2)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59, 200), result_3)
-
- def test_from_native_datetime_datetime(self):
- """
- Make sure from_native() accepts a datetime.datetime instance.
- """
- f = serializers.DateTimeField()
- result_1 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31))
- result_2 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
- result_3 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- self.assertEqual(result_1, datetime.datetime(1984, 7, 31, 4, 31))
- self.assertEqual(result_2, datetime.datetime(1984, 7, 31, 4, 31, 59))
- self.assertEqual(result_3, datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- def test_from_native_custom_format(self):
- """
- Make sure from_native() accepts custom input formats.
- """
- f = serializers.DateTimeField(input_formats=['%Y -- %H:%M'])
- result = f.from_native('1984 -- 04:59')
-
- self.assertEqual(datetime.datetime(1984, 1, 1, 4, 59), result)
-
- def test_from_native_invalid_default_on_custom_format(self):
- """
- Make sure from_native() don't accept default formats if custom format is preset
- """
- f = serializers.DateTimeField(input_formats=['%Y -- %H:%M'])
-
- try:
- f.from_native('1984-07-31 04:31:59')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: YYYY -- hh:mm"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_empty(self):
- """
- Make sure from_native() returns None on empty param.
- """
- f = serializers.DateTimeField()
- result = f.from_native('')
-
- self.assertEqual(result, None)
-
- def test_from_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DateTimeField()
- result = f.from_native(None)
-
- self.assertEqual(result, None)
-
- def test_from_native_invalid_datetime(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid datetime.
- """
- f = serializers.DateTimeField()
-
- try:
- f.from_native('04:61:59')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
- "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_invalid_format(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid format.
- """
- f = serializers.DateTimeField()
-
- try:
- f.from_native('04 -- 31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
- "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_to_native(self):
- """
- Make sure to_native() returns isoformat as default.
- """
- f = serializers.DateTimeField()
-
- result_1 = f.to_native(datetime.datetime(1984, 7, 31))
- result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
- result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
- result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- self.assertEqual(datetime.datetime(1984, 7, 31), result_1)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result_2)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59), result_3)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59, 200), result_4)
-
- def test_to_native_iso(self):
- """
- Make sure to_native() with format=iso-8601 returns iso formatted datetime.
- """
- f = serializers.DateTimeField(format='iso-8601')
-
- result_1 = f.to_native(datetime.datetime(1984, 7, 31))
- result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
- result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
- result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- self.assertEqual('1984-07-31T00:00:00', result_1)
- self.assertEqual('1984-07-31T04:31:00', result_2)
- self.assertEqual('1984-07-31T04:31:59', result_3)
- self.assertEqual('1984-07-31T04:31:59.000200', result_4)
-
- def test_to_native_custom_format(self):
- """
- Make sure to_native() returns correct custom format.
- """
- f = serializers.DateTimeField(format="%Y - %H:%M")
-
- result_1 = f.to_native(datetime.datetime(1984, 7, 31))
- result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
- result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
- result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- self.assertEqual('1984 - 00:00', result_1)
- self.assertEqual('1984 - 04:31', result_2)
- self.assertEqual('1984 - 04:31', result_3)
- self.assertEqual('1984 - 04:31', result_4)
-
- def test_to_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DateTimeField(required=False)
- self.assertEqual(None, f.to_native(None))
-
-
-class TimeFieldTest(TestCase):
- """
- Tests for the TimeField from_native() and to_native() behavior
- """
-
- def test_from_native_string(self):
- """
- Make sure from_native() accepts default iso input formats.
- """
- f = serializers.TimeField()
- result_1 = f.from_native('04:31')
- result_2 = f.from_native('04:31:59')
- result_3 = f.from_native('04:31:59.000200')
-
- self.assertEqual(datetime.time(4, 31), result_1)
- self.assertEqual(datetime.time(4, 31, 59), result_2)
- self.assertEqual(datetime.time(4, 31, 59, 200), result_3)
-
- def test_from_native_datetime_time(self):
- """
- Make sure from_native() accepts a datetime.time instance.
- """
- f = serializers.TimeField()
- result_1 = f.from_native(datetime.time(4, 31))
- result_2 = f.from_native(datetime.time(4, 31, 59))
- result_3 = f.from_native(datetime.time(4, 31, 59, 200))
-
- self.assertEqual(result_1, datetime.time(4, 31))
- self.assertEqual(result_2, datetime.time(4, 31, 59))
- self.assertEqual(result_3, datetime.time(4, 31, 59, 200))
-
- def test_from_native_custom_format(self):
- """
- Make sure from_native() accepts custom input formats.
- """
- f = serializers.TimeField(input_formats=['%H -- %M'])
- result = f.from_native('04 -- 31')
-
- self.assertEqual(datetime.time(4, 31), result)
-
- def test_from_native_invalid_default_on_custom_format(self):
- """
- Make sure from_native() don't accept default formats if custom format is preset
- """
- f = serializers.TimeField(input_formats=['%H -- %M'])
-
- try:
- f.from_native('04:31:59')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: hh -- mm"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_empty(self):
- """
- Make sure from_native() returns None on empty param.
- """
- f = serializers.TimeField()
- result = f.from_native('')
-
- self.assertEqual(result, None)
-
- def test_from_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.TimeField()
- result = f.from_native(None)
-
- self.assertEqual(result, None)
-
- def test_from_native_invalid_time(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid time.
- """
- f = serializers.TimeField()
-
- try:
- f.from_native('04:61:59')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: "
- "hh:mm[:ss[.uuuuuu]]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_invalid_format(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid format.
- """
- f = serializers.TimeField()
-
- try:
- f.from_native('04 -- 31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: "
- "hh:mm[:ss[.uuuuuu]]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_to_native(self):
- """
- Make sure to_native() returns time object as default.
- """
- f = serializers.TimeField()
- result_1 = f.to_native(datetime.time(4, 31))
- result_2 = f.to_native(datetime.time(4, 31, 59))
- result_3 = f.to_native(datetime.time(4, 31, 59, 200))
-
- self.assertEqual(datetime.time(4, 31), result_1)
- self.assertEqual(datetime.time(4, 31, 59), result_2)
- self.assertEqual(datetime.time(4, 31, 59, 200), result_3)
-
- def test_to_native_iso(self):
- """
- Make sure to_native() with format='iso-8601' returns iso formatted time.
- """
- f = serializers.TimeField(format='iso-8601')
- result_1 = f.to_native(datetime.time(4, 31))
- result_2 = f.to_native(datetime.time(4, 31, 59))
- result_3 = f.to_native(datetime.time(4, 31, 59, 200))
-
- self.assertEqual('04:31:00', result_1)
- self.assertEqual('04:31:59', result_2)
- self.assertEqual('04:31:59.000200', result_3)
-
- def test_to_native_custom_format(self):
- """
- Make sure to_native() returns correct custom format.
- """
- f = serializers.TimeField(format="%H - %S [%f]")
- result_1 = f.to_native(datetime.time(4, 31))
- result_2 = f.to_native(datetime.time(4, 31, 59))
- result_3 = f.to_native(datetime.time(4, 31, 59, 200))
-
- self.assertEqual('04 - 00 [000000]', result_1)
- self.assertEqual('04 - 59 [000000]', result_2)
- self.assertEqual('04 - 59 [000200]', result_3)
-
-
-class DecimalFieldTest(TestCase):
- """
- Tests for the DecimalField from_native() and to_native() behavior
- """
-
- def test_from_native_string(self):
- """
- Make sure from_native() accepts string values
- """
- f = serializers.DecimalField()
- result_1 = f.from_native('9000')
- result_2 = f.from_native('1.00000001')
-
- self.assertEqual(Decimal('9000'), result_1)
- self.assertEqual(Decimal('1.00000001'), result_2)
-
- def test_from_native_invalid_string(self):
- """
- Make sure from_native() raises ValidationError on passing invalid string
- """
- f = serializers.DecimalField()
-
- try:
- f.from_native('123.45.6')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Enter a number."])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_integer(self):
- """
- Make sure from_native() accepts integer values
- """
- f = serializers.DecimalField()
- result = f.from_native(9000)
-
- self.assertEqual(Decimal('9000'), result)
-
- def test_from_native_float(self):
- """
- Make sure from_native() accepts float values
- """
- f = serializers.DecimalField()
- result = f.from_native(1.00000001)
-
- self.assertEqual(Decimal('1.00000001'), result)
-
- def test_from_native_empty(self):
- """
- Make sure from_native() returns None on empty param.
- """
- f = serializers.DecimalField()
- result = f.from_native('')
-
- self.assertEqual(result, None)
-
- def test_from_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DecimalField()
- result = f.from_native(None)
-
- self.assertEqual(result, None)
-
- def test_to_native(self):
- """
- Make sure to_native() returns Decimal as string.
- """
- f = serializers.DecimalField()
-
- result_1 = f.to_native(Decimal('9000'))
- result_2 = f.to_native(Decimal('1.00000001'))
-
- self.assertEqual(Decimal('9000'), result_1)
- self.assertEqual(Decimal('1.00000001'), result_2)
-
- def test_to_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DecimalField(required=False)
- self.assertEqual(None, f.to_native(None))
-
- def test_valid_serialization(self):
- """
- Make sure the serializer works correctly
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(max_value=9010,
- min_value=9000,
- max_digits=6,
- decimal_places=2)
-
- self.assertTrue(DecimalSerializer(data={'decimal_field': '9001'}).is_valid())
- self.assertTrue(DecimalSerializer(data={'decimal_field': '9001.2'}).is_valid())
- self.assertTrue(DecimalSerializer(data={'decimal_field': '9001.23'}).is_valid())
-
- self.assertFalse(DecimalSerializer(data={'decimal_field': '8000'}).is_valid())
- self.assertFalse(DecimalSerializer(data={'decimal_field': '9900'}).is_valid())
- self.assertFalse(DecimalSerializer(data={'decimal_field': '9001.234'}).is_valid())
-
- def test_raise_max_value(self):
- """
- Make sure max_value violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(max_value=100)
-
- s = DecimalSerializer(data={'decimal_field': '123'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is less than or equal to 100.']})
-
- def test_raise_min_value(self):
- """
- Make sure min_value violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(min_value=100)
-
- s = DecimalSerializer(data={'decimal_field': '99'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is greater than or equal to 100.']})
-
- def test_raise_max_digits(self):
- """
- Make sure max_digits violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(max_digits=5)
-
- s = DecimalSerializer(data={'decimal_field': '123.456'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 5 digits in total.']})
-
- def test_raise_max_decimal_places(self):
- """
- Make sure max_decimal_places violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(decimal_places=3)
-
- s = DecimalSerializer(data={'decimal_field': '123.4567'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 3 decimal places.']})
-
- def test_raise_max_whole_digits(self):
- """
- Make sure max_whole_digits violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(max_digits=4, decimal_places=3)
-
- s = DecimalSerializer(data={'decimal_field': '12345.6'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
-
-
-class ChoiceFieldTests(TestCase):
- """
- Tests for the ChoiceField options generator
- """
- def test_choices_required(self):
- """
- Make sure proper choices are rendered if field is required
- """
- f = serializers.ChoiceField(required=True, choices=SAMPLE_CHOICES)
- self.assertEqual(f.choices, SAMPLE_CHOICES)
-
- def test_choices_not_required(self):
- """
- Make sure proper choices (plus blank) are rendered if the field isn't required
- """
- f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
- self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
-
- def test_blank_choice_display(self):
- blank = 'No Preference'
- f = serializers.ChoiceField(
- required=False,
- choices=SAMPLE_CHOICES,
- blank_display_value=blank,
- )
- self.assertEqual(f.choices, [('', blank)] + SAMPLE_CHOICES)
-
- def test_invalid_choice_model(self):
- s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']})
- self.assertEqual(s.data['choice'], '')
-
- def test_empty_choice_model(self):
- """
- Test that the 'empty' value is correctly passed and used depending on
- the 'null' property on the model field.
- """
- s = ChoiceFieldModelSerializer(data={'choice': ''})
- self.assertTrue(s.is_valid())
- self.assertEqual(s.data['choice'], '')
-
- s = ChoiceFieldModelWithNullSerializer(data={'choice': ''})
- self.assertTrue(s.is_valid())
- self.assertEqual(s.data['choice'], None)
-
- def test_from_native_empty(self):
- """
- Make sure from_native() returns an empty string on empty param by default.
- """
- f = serializers.ChoiceField(choices=SAMPLE_CHOICES)
- self.assertEqual(f.from_native(''), '')
- self.assertEqual(f.from_native(None), '')
-
- def test_from_native_empty_override(self):
- """
- Make sure you can override from_native() behavior regarding empty values.
- """
- f = serializers.ChoiceField(choices=SAMPLE_CHOICES, empty=None)
- self.assertEqual(f.from_native(''), None)
- self.assertEqual(f.from_native(None), None)
-
- def test_metadata_choices(self):
- """
- Make sure proper choices are included in the field's metadata.
- """
- choices = [{'value': v, 'display_name': n} for v, n in SAMPLE_CHOICES]
- f = serializers.ChoiceField(choices=SAMPLE_CHOICES)
- self.assertEqual(f.metadata()['choices'], choices)
-
- def test_metadata_choices_not_required(self):
- """
- Make sure proper choices are included in the field's metadata.
- """
- choices = [{'value': v, 'display_name': n}
- for v, n in models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES]
- f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
- self.assertEqual(f.metadata()['choices'], choices)
-
-
-class EmailFieldTests(TestCase):
- """
- Tests for EmailField attribute values
- """
-
- class EmailFieldModel(RESTFrameworkModel):
- email_field = models.EmailField(blank=True)
-
- class EmailFieldWithGivenMaxLengthModel(RESTFrameworkModel):
- email_field = models.EmailField(max_length=150, blank=True)
-
- def test_default_model_value(self):
- class EmailFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.EmailFieldModel
-
- serializer = EmailFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 75)
-
- def test_given_model_value(self):
- class EmailFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.EmailFieldWithGivenMaxLengthModel
-
- serializer = EmailFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 150)
-
- def test_given_serializer_value(self):
- class EmailFieldSerializer(serializers.ModelSerializer):
- email_field = serializers.EmailField(source='email_field', max_length=20, required=False)
-
- class Meta:
- model = self.EmailFieldModel
-
- serializer = EmailFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 20)
-
-
-class SlugFieldTests(TestCase):
- """
- Tests for SlugField attribute values
- """
-
- class SlugFieldModel(RESTFrameworkModel):
- slug_field = models.SlugField(blank=True)
-
- class SlugFieldWithGivenMaxLengthModel(RESTFrameworkModel):
- slug_field = models.SlugField(max_length=84, blank=True)
-
- def test_default_model_value(self):
- class SlugFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.SlugFieldModel
-
- serializer = SlugFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 50)
-
- def test_given_model_value(self):
- class SlugFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.SlugFieldWithGivenMaxLengthModel
-
- serializer = SlugFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 84)
-
- def test_given_serializer_value(self):
- class SlugFieldSerializer(serializers.ModelSerializer):
- slug_field = serializers.SlugField(source='slug_field',
- max_length=20, required=False)
-
- class Meta:
- model = self.SlugFieldModel
-
- serializer = SlugFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['slug_field'],
- 'max_length'), 20)
-
- def test_invalid_slug(self):
- """
- Make sure an invalid slug raises ValidationError
- """
- class SlugFieldSerializer(serializers.ModelSerializer):
- slug_field = serializers.SlugField(source='slug_field', max_length=20, required=True)
+# class DateFieldTest(TestCase):
+# """
+# Tests for the DateFieldTest from_native() and to_native() behavior
+# """
+
+# def test_from_native_string(self):
+# """
+# Make sure from_native() accepts default iso input formats.
+# """
+# f = serializers.DateField()
+# result_1 = f.from_native('1984-07-31')
+
+# self.assertEqual(datetime.date(1984, 7, 31), result_1)
+
+# def test_from_native_datetime_date(self):
+# """
+# Make sure from_native() accepts a datetime.date instance.
+# """
+# f = serializers.DateField()
+# result_1 = f.from_native(datetime.date(1984, 7, 31))
+
+# self.assertEqual(result_1, datetime.date(1984, 7, 31))
+
+# def test_from_native_custom_format(self):
+# """
+# Make sure from_native() accepts custom input formats.
+# """
+# f = serializers.DateField(input_formats=['%Y -- %d'])
+# result = f.from_native('1984 -- 31')
+
+# self.assertEqual(datetime.date(1984, 1, 31), result)
+
+# def test_from_native_invalid_default_on_custom_format(self):
+# """
+# Make sure from_native() don't accept default formats if custom format is preset
+# """
+# f = serializers.DateField(input_formats=['%Y -- %d'])
+
+# try:
+# f.from_native('1984-07-31')
+# except validators.ValidationError as e:
+# self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY -- DD"])
+# else:
+# self.fail("ValidationError was not properly raised")
+
+# def test_from_native_empty(self):
+# """
+# Make sure from_native() returns None on empty param.
+# """
+# f = serializers.DateField()
+# result = f.from_native('')
+
+# self.assertEqual(result, None)
+
+# def test_from_native_none(self):
+# """
+# Make sure from_native() returns None on None param.
+# """
+# f = serializers.DateField()
+# result = f.from_native(None)
+
+# self.assertEqual(result, None)
+
+# def test_from_native_invalid_date(self):
+# """
+# Make sure from_native() raises a ValidationError on passing an invalid date.
+# """
+# f = serializers.DateField()
+
+# try:
+# f.from_native('1984-13-31')
+# except validators.ValidationError as e:
+# self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]"])
+# else:
+# self.fail("ValidationError was not properly raised")
+
+# def test_from_native_invalid_format(self):
+# """
+# Make sure from_native() raises a ValidationError on passing an invalid format.
+# """
+# f = serializers.DateField()
+
+# try:
+# f.from_native('1984 -- 31')
+# except validators.ValidationError as e:
+# self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]"])
+# else:
+# self.fail("ValidationError was not properly raised")
+
+# def test_to_native(self):
+# """
+# Make sure to_native() returns datetime as default.
+# """
+# f = serializers.DateField()
+
+# result_1 = f.to_native(datetime.date(1984, 7, 31))
+
+# self.assertEqual(datetime.date(1984, 7, 31), result_1)
+
+# def test_to_native_iso(self):
+# """
+# Make sure to_native() with 'iso-8601' returns iso formated date.
+# """
+# f = serializers.DateField(format='iso-8601')
+
+# result_1 = f.to_native(datetime.date(1984, 7, 31))
+
+# self.assertEqual('1984-07-31', result_1)
+
+# def test_to_native_custom_format(self):
+# """
+# Make sure to_native() returns correct custom format.
+# """
+# f = serializers.DateField(format="%Y - %m.%d")
+
+# result_1 = f.to_native(datetime.date(1984, 7, 31))
+
+# self.assertEqual('1984 - 07.31', result_1)
+
+# def test_to_native_none(self):
+# """
+# Make sure from_native() returns None on None param.
+# """
+# f = serializers.DateField(required=False)
+# self.assertEqual(None, f.to_native(None))
+
+
+# class DateTimeFieldTest(TestCase):
+# """
+# Tests for the DateTimeField from_native() and to_native() behavior
+# """
+
+# def test_from_native_string(self):
+# """
+# Make sure from_native() accepts default iso input formats.
+# """
+# f = serializers.DateTimeField()
+# result_1 = f.from_native('1984-07-31 04:31')
+# result_2 = f.from_native('1984-07-31 04:31:59')
+# result_3 = f.from_native('1984-07-31 04:31:59.000200')
+
+# self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result_1)
+# self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59), result_2)
+# self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59, 200), result_3)
+
+# def test_from_native_datetime_datetime(self):
+# """
+# Make sure from_native() accepts a datetime.datetime instance.
+# """
+# f = serializers.DateTimeField()
+# result_1 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31))
+# result_2 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
+# result_3 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
+
+# self.assertEqual(result_1, datetime.datetime(1984, 7, 31, 4, 31))
+# self.assertEqual(result_2, datetime.datetime(1984, 7, 31, 4, 31, 59))
+# self.assertEqual(result_3, datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
+
+# def test_from_native_custom_format(self):
+# """
+# Make sure from_native() accepts custom input formats.
+# """
+# f = serializers.DateTimeField(input_formats=['%Y -- %H:%M'])
+# result = f.from_native('1984 -- 04:59')
+
+# self.assertEqual(datetime.datetime(1984, 1, 1, 4, 59), result)
+
+# def test_from_native_invalid_default_on_custom_format(self):
+# """
+# Make sure from_native() don't accept default formats if custom format is preset
+# """
+# f = serializers.DateTimeField(input_formats=['%Y -- %H:%M'])
+
+# try:
+# f.from_native('1984-07-31 04:31:59')
+# except validators.ValidationError as e:
+# self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: YYYY -- hh:mm"])
+# else:
+# self.fail("ValidationError was not properly raised")
+
+# def test_from_native_empty(self):
+# """
+# Make sure from_native() returns None on empty param.
+# """
+# f = serializers.DateTimeField()
+# result = f.from_native('')
+
+# self.assertEqual(result, None)
+
+# def test_from_native_none(self):
+# """
+# Make sure from_native() returns None on None param.
+# """
+# f = serializers.DateTimeField()
+# result = f.from_native(None)
+
+# self.assertEqual(result, None)
+
+# def test_from_native_invalid_datetime(self):
+# """
+# Make sure from_native() raises a ValidationError on passing an invalid datetime.
+# """
+# f = serializers.DateTimeField()
+
+# try:
+# f.from_native('04:61:59')
+# except validators.ValidationError as e:
+# self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
+# "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
+# else:
+# self.fail("ValidationError was not properly raised")
+
+# def test_from_native_invalid_format(self):
+# """
+# Make sure from_native() raises a ValidationError on passing an invalid format.
+# """
+# f = serializers.DateTimeField()
+
+# try:
+# f.from_native('04 -- 31')
+# except validators.ValidationError as e:
+# self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
+# "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
+# else:
+# self.fail("ValidationError was not properly raised")
+
+# def test_to_native(self):
+# """
+# Make sure to_native() returns isoformat as default.
+# """
+# f = serializers.DateTimeField()
+
+# result_1 = f.to_native(datetime.datetime(1984, 7, 31))
+# result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
+# result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
+# result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
+
+# self.assertEqual(datetime.datetime(1984, 7, 31), result_1)
+# self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result_2)
+# self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59), result_3)
+# self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59, 200), result_4)
+
+# def test_to_native_iso(self):
+# """
+# Make sure to_native() with format=iso-8601 returns iso formatted datetime.
+# """
+# f = serializers.DateTimeField(format='iso-8601')
+
+# result_1 = f.to_native(datetime.datetime(1984, 7, 31))
+# result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
+# result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
+# result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
+
+# self.assertEqual('1984-07-31T00:00:00', result_1)
+# self.assertEqual('1984-07-31T04:31:00', result_2)
+# self.assertEqual('1984-07-31T04:31:59', result_3)
+# self.assertEqual('1984-07-31T04:31:59.000200', result_4)
+
+# def test_to_native_custom_format(self):
+# """
+# Make sure to_native() returns correct custom format.
+# """
+# f = serializers.DateTimeField(format="%Y - %H:%M")
+
+# result_1 = f.to_native(datetime.datetime(1984, 7, 31))
+# result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
+# result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
+# result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
+
+# self.assertEqual('1984 - 00:00', result_1)
+# self.assertEqual('1984 - 04:31', result_2)
+# self.assertEqual('1984 - 04:31', result_3)
+# self.assertEqual('1984 - 04:31', result_4)
+
+# def test_to_native_none(self):
+# """
+# Make sure from_native() returns None on None param.
+# """
+# f = serializers.DateTimeField(required=False)
+# self.assertEqual(None, f.to_native(None))
+
+
+# class TimeFieldTest(TestCase):
+# """
+# Tests for the TimeField from_native() and to_native() behavior
+# """
+
+# def test_from_native_string(self):
+# """
+# Make sure from_native() accepts default iso input formats.
+# """
+# f = serializers.TimeField()
+# result_1 = f.from_native('04:31')
+# result_2 = f.from_native('04:31:59')
+# result_3 = f.from_native('04:31:59.000200')
+
+# self.assertEqual(datetime.time(4, 31), result_1)
+# self.assertEqual(datetime.time(4, 31, 59), result_2)
+# self.assertEqual(datetime.time(4, 31, 59, 200), result_3)
+
+# def test_from_native_datetime_time(self):
+# """
+# Make sure from_native() accepts a datetime.time instance.
+# """
+# f = serializers.TimeField()
+# result_1 = f.from_native(datetime.time(4, 31))
+# result_2 = f.from_native(datetime.time(4, 31, 59))
+# result_3 = f.from_native(datetime.time(4, 31, 59, 200))
+
+# self.assertEqual(result_1, datetime.time(4, 31))
+# self.assertEqual(result_2, datetime.time(4, 31, 59))
+# self.assertEqual(result_3, datetime.time(4, 31, 59, 200))
+
+# def test_from_native_custom_format(self):
+# """
+# Make sure from_native() accepts custom input formats.
+# """
+# f = serializers.TimeField(input_formats=['%H -- %M'])
+# result = f.from_native('04 -- 31')
+
+# self.assertEqual(datetime.time(4, 31), result)
+
+# def test_from_native_invalid_default_on_custom_format(self):
+# """
+# Make sure from_native() don't accept default formats if custom format is preset
+# """
+# f = serializers.TimeField(input_formats=['%H -- %M'])
+
+# try:
+# f.from_native('04:31:59')
+# except validators.ValidationError as e:
+# self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: hh -- mm"])
+# else:
+# self.fail("ValidationError was not properly raised")
+
+# def test_from_native_empty(self):
+# """
+# Make sure from_native() returns None on empty param.
+# """
+# f = serializers.TimeField()
+# result = f.from_native('')
+
+# self.assertEqual(result, None)
+
+# def test_from_native_none(self):
+# """
+# Make sure from_native() returns None on None param.
+# """
+# f = serializers.TimeField()
+# result = f.from_native(None)
+
+# self.assertEqual(result, None)
+
+# def test_from_native_invalid_time(self):
+# """
+# Make sure from_native() raises a ValidationError on passing an invalid time.
+# """
+# f = serializers.TimeField()
+
+# try:
+# f.from_native('04:61:59')
+# except validators.ValidationError as e:
+# self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: "
+# "hh:mm[:ss[.uuuuuu]]"])
+# else:
+# self.fail("ValidationError was not properly raised")
+
+# def test_from_native_invalid_format(self):
+# """
+# Make sure from_native() raises a ValidationError on passing an invalid format.
+# """
+# f = serializers.TimeField()
+
+# try:
+# f.from_native('04 -- 31')
+# except validators.ValidationError as e:
+# self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: "
+# "hh:mm[:ss[.uuuuuu]]"])
+# else:
+# self.fail("ValidationError was not properly raised")
+
+# def test_to_native(self):
+# """
+# Make sure to_native() returns time object as default.
+# """
+# f = serializers.TimeField()
+# result_1 = f.to_native(datetime.time(4, 31))
+# result_2 = f.to_native(datetime.time(4, 31, 59))
+# result_3 = f.to_native(datetime.time(4, 31, 59, 200))
+
+# self.assertEqual(datetime.time(4, 31), result_1)
+# self.assertEqual(datetime.time(4, 31, 59), result_2)
+# self.assertEqual(datetime.time(4, 31, 59, 200), result_3)
+
+# def test_to_native_iso(self):
+# """
+# Make sure to_native() with format='iso-8601' returns iso formatted time.
+# """
+# f = serializers.TimeField(format='iso-8601')
+# result_1 = f.to_native(datetime.time(4, 31))
+# result_2 = f.to_native(datetime.time(4, 31, 59))
+# result_3 = f.to_native(datetime.time(4, 31, 59, 200))
+
+# self.assertEqual('04:31:00', result_1)
+# self.assertEqual('04:31:59', result_2)
+# self.assertEqual('04:31:59.000200', result_3)
+
+# def test_to_native_custom_format(self):
+# """
+# Make sure to_native() returns correct custom format.
+# """
+# f = serializers.TimeField(format="%H - %S [%f]")
+# result_1 = f.to_native(datetime.time(4, 31))
+# result_2 = f.to_native(datetime.time(4, 31, 59))
+# result_3 = f.to_native(datetime.time(4, 31, 59, 200))
+
+# self.assertEqual('04 - 00 [000000]', result_1)
+# self.assertEqual('04 - 59 [000000]', result_2)
+# self.assertEqual('04 - 59 [000200]', result_3)
+
+
+# class DecimalFieldTest(TestCase):
+# """
+# Tests for the DecimalField from_native() and to_native() behavior
+# """
+
+# def test_from_native_string(self):
+# """
+# Make sure from_native() accepts string values
+# """
+# f = serializers.DecimalField()
+# result_1 = f.from_native('9000')
+# result_2 = f.from_native('1.00000001')
+
+# self.assertEqual(Decimal('9000'), result_1)
+# self.assertEqual(Decimal('1.00000001'), result_2)
+
+# def test_from_native_invalid_string(self):
+# """
+# Make sure from_native() raises ValidationError on passing invalid string
+# """
+# f = serializers.DecimalField()
+
+# try:
+# f.from_native('123.45.6')
+# except validators.ValidationError as e:
+# self.assertEqual(e.messages, ["Enter a number."])
+# else:
+# self.fail("ValidationError was not properly raised")
+
+# def test_from_native_integer(self):
+# """
+# Make sure from_native() accepts integer values
+# """
+# f = serializers.DecimalField()
+# result = f.from_native(9000)
+
+# self.assertEqual(Decimal('9000'), result)
+
+# def test_from_native_float(self):
+# """
+# Make sure from_native() accepts float values
+# """
+# f = serializers.DecimalField()
+# result = f.from_native(1.00000001)
+
+# self.assertEqual(Decimal('1.00000001'), result)
+
+# def test_from_native_empty(self):
+# """
+# Make sure from_native() returns None on empty param.
+# """
+# f = serializers.DecimalField()
+# result = f.from_native('')
+
+# self.assertEqual(result, None)
+
+# def test_from_native_none(self):
+# """
+# Make sure from_native() returns None on None param.
+# """
+# f = serializers.DecimalField()
+# result = f.from_native(None)
+
+# self.assertEqual(result, None)
+
+# def test_to_native(self):
+# """
+# Make sure to_native() returns Decimal as string.
+# """
+# f = serializers.DecimalField()
+
+# result_1 = f.to_native(Decimal('9000'))
+# result_2 = f.to_native(Decimal('1.00000001'))
+
+# self.assertEqual(Decimal('9000'), result_1)
+# self.assertEqual(Decimal('1.00000001'), result_2)
+
+# def test_to_native_none(self):
+# """
+# Make sure from_native() returns None on None param.
+# """
+# f = serializers.DecimalField(required=False)
+# self.assertEqual(None, f.to_native(None))
+
+# def test_valid_serialization(self):
+# """
+# Make sure the serializer works correctly
+# """
+# class DecimalSerializer(serializers.Serializer):
+# decimal_field = serializers.DecimalField(max_value=9010,
+# min_value=9000,
+# max_digits=6,
+# decimal_places=2)
+
+# self.assertTrue(DecimalSerializer(data={'decimal_field': '9001'}).is_valid())
+# self.assertTrue(DecimalSerializer(data={'decimal_field': '9001.2'}).is_valid())
+# self.assertTrue(DecimalSerializer(data={'decimal_field': '9001.23'}).is_valid())
+
+# self.assertFalse(DecimalSerializer(data={'decimal_field': '8000'}).is_valid())
+# self.assertFalse(DecimalSerializer(data={'decimal_field': '9900'}).is_valid())
+# self.assertFalse(DecimalSerializer(data={'decimal_field': '9001.234'}).is_valid())
+
+# def test_raise_max_value(self):
+# """
+# Make sure max_value violations raises ValidationError
+# """
+# class DecimalSerializer(serializers.Serializer):
+# decimal_field = serializers.DecimalField(max_value=100)
+
+# s = DecimalSerializer(data={'decimal_field': '123'})
+
+# self.assertFalse(s.is_valid())
+# self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is less than or equal to 100.']})
+
+# def test_raise_min_value(self):
+# """
+# Make sure min_value violations raises ValidationError
+# """
+# class DecimalSerializer(serializers.Serializer):
+# decimal_field = serializers.DecimalField(min_value=100)
+
+# s = DecimalSerializer(data={'decimal_field': '99'})
+
+# self.assertFalse(s.is_valid())
+# self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is greater than or equal to 100.']})
+
+# def test_raise_max_digits(self):
+# """
+# Make sure max_digits violations raises ValidationError
+# """
+# class DecimalSerializer(serializers.Serializer):
+# decimal_field = serializers.DecimalField(max_digits=5)
+
+# s = DecimalSerializer(data={'decimal_field': '123.456'})
+
+# self.assertFalse(s.is_valid())
+# self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 5 digits in total.']})
+
+# def test_raise_max_decimal_places(self):
+# """
+# Make sure max_decimal_places violations raises ValidationError
+# """
+# class DecimalSerializer(serializers.Serializer):
+# decimal_field = serializers.DecimalField(decimal_places=3)
+
+# s = DecimalSerializer(data={'decimal_field': '123.4567'})
+
+# self.assertFalse(s.is_valid())
+# self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 3 decimal places.']})
+
+# def test_raise_max_whole_digits(self):
+# """
+# Make sure max_whole_digits violations raises ValidationError
+# """
+# class DecimalSerializer(serializers.Serializer):
+# decimal_field = serializers.DecimalField(max_digits=4, decimal_places=3)
+
+# s = DecimalSerializer(data={'decimal_field': '12345.6'})
+
+# self.assertFalse(s.is_valid())
+# self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
+
+
+# class ChoiceFieldTests(TestCase):
+# """
+# Tests for the ChoiceField options generator
+# """
+# def test_choices_required(self):
+# """
+# Make sure proper choices are rendered if field is required
+# """
+# f = serializers.ChoiceField(required=True, choices=SAMPLE_CHOICES)
+# self.assertEqual(f.choices, SAMPLE_CHOICES)
+
+# def test_choices_not_required(self):
+# """
+# Make sure proper choices (plus blank) are rendered if the field isn't required
+# """
+# f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
+# self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
+
+# def test_blank_choice_display(self):
+# blank = 'No Preference'
+# f = serializers.ChoiceField(
+# required=False,
+# choices=SAMPLE_CHOICES,
+# blank_display_value=blank,
+# )
+# self.assertEqual(f.choices, [('', blank)] + SAMPLE_CHOICES)
+
+# def test_invalid_choice_model(self):
+# s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
+# self.assertFalse(s.is_valid())
+# self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']})
+# self.assertEqual(s.data['choice'], '')
+
+# def test_empty_choice_model(self):
+# """
+# Test that the 'empty' value is correctly passed and used depending on
+# the 'null' property on the model field.
+# """
+# s = ChoiceFieldModelSerializer(data={'choice': ''})
+# self.assertTrue(s.is_valid())
+# self.assertEqual(s.data['choice'], '')
+
+# s = ChoiceFieldModelWithNullSerializer(data={'choice': ''})
+# self.assertTrue(s.is_valid())
+# self.assertEqual(s.data['choice'], None)
+
+# def test_from_native_empty(self):
+# """
+# Make sure from_native() returns an empty string on empty param by default.
+# """
+# f = serializers.ChoiceField(choices=SAMPLE_CHOICES)
+# self.assertEqual(f.from_native(''), '')
+# self.assertEqual(f.from_native(None), '')
+
+# def test_from_native_empty_override(self):
+# """
+# Make sure you can override from_native() behavior regarding empty values.
+# """
+# f = serializers.ChoiceField(choices=SAMPLE_CHOICES, empty=None)
+# self.assertEqual(f.from_native(''), None)
+# self.assertEqual(f.from_native(None), None)
+
+# def test_metadata_choices(self):
+# """
+# Make sure proper choices are included in the field's metadata.
+# """
+# choices = [{'value': v, 'display_name': n} for v, n in SAMPLE_CHOICES]
+# f = serializers.ChoiceField(choices=SAMPLE_CHOICES)
+# self.assertEqual(f.metadata()['choices'], choices)
+
+# def test_metadata_choices_not_required(self):
+# """
+# Make sure proper choices are included in the field's metadata.
+# """
+# choices = [{'value': v, 'display_name': n}
+# for v, n in models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES]
+# f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
+# self.assertEqual(f.metadata()['choices'], choices)
+
+
+# class EmailFieldTests(TestCase):
+# """
+# Tests for EmailField attribute values
+# """
+
+# class EmailFieldModel(RESTFrameworkModel):
+# email_field = models.EmailField(blank=True)
+
+# class EmailFieldWithGivenMaxLengthModel(RESTFrameworkModel):
+# email_field = models.EmailField(max_length=150, blank=True)
+
+# def test_default_model_value(self):
+# class EmailFieldSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = self.EmailFieldModel
+
+# serializer = EmailFieldSerializer(data={})
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 75)
+
+# def test_given_model_value(self):
+# class EmailFieldSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = self.EmailFieldWithGivenMaxLengthModel
+
+# serializer = EmailFieldSerializer(data={})
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 150)
+
+# def test_given_serializer_value(self):
+# class EmailFieldSerializer(serializers.ModelSerializer):
+# email_field = serializers.EmailField(source='email_field', max_length=20, required=False)
+
+# class Meta:
+# model = self.EmailFieldModel
+
+# serializer = EmailFieldSerializer(data={})
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 20)
+
+
+# class SlugFieldTests(TestCase):
+# """
+# Tests for SlugField attribute values
+# """
+
+# class SlugFieldModel(RESTFrameworkModel):
+# slug_field = models.SlugField(blank=True)
+
+# class SlugFieldWithGivenMaxLengthModel(RESTFrameworkModel):
+# slug_field = models.SlugField(max_length=84, blank=True)
+
+# def test_default_model_value(self):
+# class SlugFieldSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = self.SlugFieldModel
+
+# serializer = SlugFieldSerializer(data={})
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 50)
+
+# def test_given_model_value(self):
+# class SlugFieldSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = self.SlugFieldWithGivenMaxLengthModel
+
+# serializer = SlugFieldSerializer(data={})
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 84)
+
+# def test_given_serializer_value(self):
+# class SlugFieldSerializer(serializers.ModelSerializer):
+# slug_field = serializers.SlugField(source='slug_field',
+# max_length=20, required=False)
+
+# class Meta:
+# model = self.SlugFieldModel
+
+# serializer = SlugFieldSerializer(data={})
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(getattr(serializer.fields['slug_field'],
+# 'max_length'), 20)
+
+# def test_invalid_slug(self):
+# """
+# Make sure an invalid slug raises ValidationError
+# """
+# class SlugFieldSerializer(serializers.ModelSerializer):
+# slug_field = serializers.SlugField(source='slug_field', max_length=20, required=True)
- class Meta:
- model = self.SlugFieldModel
+# class Meta:
+# model = self.SlugFieldModel
- s = SlugFieldSerializer(data={'slug_field': 'a b'})
+# s = SlugFieldSerializer(data={'slug_field': 'a b'})
- self.assertEqual(s.is_valid(), False)
- self.assertEqual(s.errors, {'slug_field': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]})
+# self.assertEqual(s.is_valid(), False)
+# self.assertEqual(s.errors, {'slug_field': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]})
-class URLFieldTests(TestCase):
- """
- Tests for URLField attribute values.
+# class URLFieldTests(TestCase):
+# """
+# Tests for URLField attribute values.
- (Includes test for #1210, checking that validators can be overridden.)
- """
+# (Includes test for #1210, checking that validators can be overridden.)
+# """
- class URLFieldModel(RESTFrameworkModel):
- url_field = models.URLField(blank=True)
+# class URLFieldModel(RESTFrameworkModel):
+# url_field = models.URLField(blank=True)
- class URLFieldWithGivenMaxLengthModel(RESTFrameworkModel):
- url_field = models.URLField(max_length=128, blank=True)
+# class URLFieldWithGivenMaxLengthModel(RESTFrameworkModel):
+# url_field = models.URLField(max_length=128, blank=True)
- def test_default_model_value(self):
- class URLFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.URLFieldModel
+# def test_default_model_value(self):
+# class URLFieldSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = self.URLFieldModel
- serializer = URLFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['url_field'],
- 'max_length'), 200)
+# serializer = URLFieldSerializer(data={})
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(getattr(serializer.fields['url_field'],
+# 'max_length'), 200)
- def test_given_model_value(self):
- class URLFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.URLFieldWithGivenMaxLengthModel
+# def test_given_model_value(self):
+# class URLFieldSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = self.URLFieldWithGivenMaxLengthModel
- serializer = URLFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['url_field'],
- 'max_length'), 128)
+# serializer = URLFieldSerializer(data={})
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(getattr(serializer.fields['url_field'],
+# 'max_length'), 128)
- def test_given_serializer_value(self):
- class URLFieldSerializer(serializers.ModelSerializer):
- url_field = serializers.URLField(source='url_field',
- max_length=20, required=False)
+# def test_given_serializer_value(self):
+# class URLFieldSerializer(serializers.ModelSerializer):
+# url_field = serializers.URLField(source='url_field',
+# max_length=20, required=False)
- class Meta:
- model = self.URLFieldWithGivenMaxLengthModel
+# class Meta:
+# model = self.URLFieldWithGivenMaxLengthModel
- serializer = URLFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['url_field'],
- 'max_length'), 20)
+# serializer = URLFieldSerializer(data={})
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(getattr(serializer.fields['url_field'],
+# 'max_length'), 20)
- def test_validators_can_be_overridden(self):
- url_field = serializers.URLField(validators=[])
- validators = url_field.validators
- self.assertEqual([], validators, 'Passing `validators` kwarg should have overridden default validators')
+# def test_validators_can_be_overridden(self):
+# url_field = serializers.URLField(validators=[])
+# validators = url_field.validators
+# self.assertEqual([], validators, 'Passing `validators` kwarg should have overridden default validators')
-class FieldMetadata(TestCase):
- def setUp(self):
- self.required_field = serializers.Field()
- self.required_field.label = uuid4().hex
- self.required_field.required = True
+# class FieldMetadata(TestCase):
+# def setUp(self):
+# self.required_field = serializers.Field()
+# self.required_field.label = uuid4().hex
+# self.required_field.required = True
- self.optional_field = serializers.Field()
- self.optional_field.label = uuid4().hex
- self.optional_field.required = False
+# self.optional_field = serializers.Field()
+# self.optional_field.label = uuid4().hex
+# self.optional_field.required = False
- def test_required(self):
- self.assertEqual(self.required_field.metadata()['required'], True)
+# def test_required(self):
+# self.assertEqual(self.required_field.metadata()['required'], True)
- def test_optional(self):
- self.assertEqual(self.optional_field.metadata()['required'], False)
+# def test_optional(self):
+# self.assertEqual(self.optional_field.metadata()['required'], False)
- def test_label(self):
- for field in (self.required_field, self.optional_field):
- self.assertEqual(field.metadata()['label'], field.label)
+# def test_label(self):
+# for field in (self.required_field, self.optional_field):
+# self.assertEqual(field.metadata()['label'], field.label)
-class FieldCallableDefault(TestCase):
- def setUp(self):
- self.simple_callable = lambda: 'foo bar'
+# class FieldCallableDefault(TestCase):
+# def setUp(self):
+# self.simple_callable = lambda: 'foo bar'
- def test_default_can_be_simple_callable(self):
- """
- Ensure that the 'default' argument can also be a simple callable.
- """
- field = serializers.WritableField(default=self.simple_callable)
- into = {}
- field.field_from_native({}, {}, 'field', into)
- self.assertEqual(into, {'field': 'foo bar'})
+# def test_default_can_be_simple_callable(self):
+# """
+# Ensure that the 'default' argument can also be a simple callable.
+# """
+# field = serializers.WritableField(default=self.simple_callable)
+# into = {}
+# field.field_from_native({}, {}, 'field', into)
+# self.assertEqual(into, {'field': 'foo bar'})
-class CustomIntegerField(TestCase):
- """
- Test that custom fields apply min_value and max_value constraints
- """
- def test_custom_fields_can_be_validated_for_value(self):
+# class CustomIntegerField(TestCase):
+# """
+# Test that custom fields apply min_value and max_value constraints
+# """
+# def test_custom_fields_can_be_validated_for_value(self):
- class MoneyField(models.PositiveIntegerField):
- pass
+# class MoneyField(models.PositiveIntegerField):
+# pass
- class EntryModel(models.Model):
- bank = MoneyField(validators=[validators.MaxValueValidator(100)])
+# class EntryModel(models.Model):
+# bank = MoneyField(validators=[validators.MaxValueValidator(100)])
- class EntrySerializer(serializers.ModelSerializer):
- class Meta:
- model = EntryModel
+# class EntrySerializer(serializers.ModelSerializer):
+# class Meta:
+# model = EntryModel
- entry = EntryModel(bank=1)
+# entry = EntryModel(bank=1)
- serializer = EntrySerializer(entry, data={"bank": 11})
- self.assertTrue(serializer.is_valid())
+# serializer = EntrySerializer(entry, data={"bank": 11})
+# self.assertTrue(serializer.is_valid())
- serializer = EntrySerializer(entry, data={"bank": -1})
- self.assertFalse(serializer.is_valid())
+# serializer = EntrySerializer(entry, data={"bank": -1})
+# self.assertFalse(serializer.is_valid())
- serializer = EntrySerializer(entry, data={"bank": 101})
- self.assertFalse(serializer.is_valid())
+# serializer = EntrySerializer(entry, data={"bank": 101})
+# self.assertFalse(serializer.is_valid())
-class BooleanField(TestCase):
- """
- Tests for BooleanField
- """
- def test_boolean_required(self):
- class BooleanRequiredSerializer(serializers.Serializer):
- bool_field = serializers.BooleanField(required=True)
+# class BooleanField(TestCase):
+# """
+# Tests for BooleanField
+# """
+# def test_boolean_required(self):
+# class BooleanRequiredSerializer(serializers.Serializer):
+# bool_field = serializers.BooleanField(required=True)
- self.assertFalse(BooleanRequiredSerializer(data={}).is_valid())
+# self.assertFalse(BooleanRequiredSerializer(data={}).is_valid())
-class SerializerMethodFieldTest(TestCase):
- """
- Tests for the SerializerMethodField field_to_native() behavior
- """
- class SerializerTest(serializers.Serializer):
- def get_my_test(self, obj):
- return obj.my_test[0:5]
+# class SerializerMethodFieldTest(TestCase):
+# """
+# Tests for the SerializerMethodField field_to_native() behavior
+# """
+# class SerializerTest(serializers.Serializer):
+# def get_my_test(self, obj):
+# return obj.my_test[0:5]
- class Example():
- my_test = 'Hey, this is a test !'
+# class Example():
+# my_test = 'Hey, this is a test !'
- def test_field_to_native(self):
- s = serializers.SerializerMethodField('get_my_test')
- s.initialize(self.SerializerTest(), 'name')
- result = s.field_to_native(self.Example(), None)
- self.assertEqual(result, 'Hey, ')
+# def test_field_to_native(self):
+# s = serializers.SerializerMethodField('get_my_test')
+# s.initialize(self.SerializerTest(), 'name')
+# result = s.field_to_native(self.Example(), None)
+# self.assertEqual(result, 'Hey, ')
diff --git a/tests/test_files.py b/tests/test_files.py
index de4f71d1..a5613fcb 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -1,92 +1,92 @@
-from __future__ import unicode_literals
-from django.test import TestCase
-from django.utils import six
-from rest_framework import serializers
-from rest_framework.compat import BytesIO
-import datetime
-
-
-class UploadedFile(object):
- def __init__(self, file=None, created=None):
- self.file = file
- self.created = created or datetime.datetime.now()
-
-
-class UploadedFileSerializer(serializers.Serializer):
- file = serializers.FileField(required=False)
- created = serializers.DateTimeField()
-
- def restore_object(self, attrs, instance=None):
- if instance:
- instance.file = attrs['file']
- instance.created = attrs['created']
- return instance
- return UploadedFile(**attrs)
-
-
-class FileSerializerTests(TestCase):
- def test_create(self):
- now = datetime.datetime.now()
- file = BytesIO(six.b('stuff'))
- file.name = 'stuff.txt'
- file.size = len(file.getvalue())
- serializer = UploadedFileSerializer(data={'created': now}, files={'file': file})
- uploaded_file = UploadedFile(file=file, created=now)
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.object.created, uploaded_file.created)
- self.assertEqual(serializer.object.file, uploaded_file.file)
- self.assertFalse(serializer.object is uploaded_file)
-
- def test_creation_failure(self):
- """
- Passing files=None should result in an ValidationError
-
- Regression test for:
- https://github.com/tomchristie/django-rest-framework/issues/542
- """
- now = datetime.datetime.now()
-
- serializer = UploadedFileSerializer(data={'created': now})
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.object.created, now)
- self.assertIsNone(serializer.object.file)
-
- def test_remove_with_empty_string(self):
- """
- Passing empty string as data should cause file to be removed
-
- Test for:
- https://github.com/tomchristie/django-rest-framework/issues/937
- """
- now = datetime.datetime.now()
- file = BytesIO(six.b('stuff'))
- file.name = 'stuff.txt'
- file.size = len(file.getvalue())
-
- uploaded_file = UploadedFile(file=file, created=now)
-
- serializer = UploadedFileSerializer(instance=uploaded_file, data={'created': now, 'file': ''})
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.object.created, uploaded_file.created)
- self.assertIsNone(serializer.object.file)
-
- def test_validation_error_with_non_file(self):
- """
- Passing non-files should raise a validation error.
- """
- now = datetime.datetime.now()
- errmsg = 'No file was submitted. Check the encoding type on the form.'
-
- serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'})
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'file': [errmsg]})
-
- def test_validation_with_no_data(self):
- """
- Validation should still function when no data dictionary is provided.
- """
- uploaded_file = BytesIO(six.b('stuff'))
- uploaded_file.name = 'stuff.txt'
- uploaded_file.size = len(uploaded_file.getvalue())
- serializer = UploadedFileSerializer(files={'file': uploaded_file})
- self.assertFalse(serializer.is_valid())
+# from __future__ import unicode_literals
+# from django.test import TestCase
+# from django.utils import six
+# from rest_framework import serializers
+# from rest_framework.compat import BytesIO
+# import datetime
+
+
+# class UploadedFile(object):
+# def __init__(self, file=None, created=None):
+# self.file = file
+# self.created = created or datetime.datetime.now()
+
+
+# class UploadedFileSerializer(serializers.Serializer):
+# file = serializers.FileField(required=False)
+# created = serializers.DateTimeField()
+
+# def restore_object(self, attrs, instance=None):
+# if instance:
+# instance.file = attrs['file']
+# instance.created = attrs['created']
+# return instance
+# return UploadedFile(**attrs)
+
+
+# class FileSerializerTests(TestCase):
+# def test_create(self):
+# now = datetime.datetime.now()
+# file = BytesIO(six.b('stuff'))
+# file.name = 'stuff.txt'
+# file.size = len(file.getvalue())
+# serializer = UploadedFileSerializer(data={'created': now}, files={'file': file})
+# uploaded_file = UploadedFile(file=file, created=now)
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.object.created, uploaded_file.created)
+# self.assertEqual(serializer.object.file, uploaded_file.file)
+# self.assertFalse(serializer.object is uploaded_file)
+
+# def test_creation_failure(self):
+# """
+# Passing files=None should result in an ValidationError
+
+# Regression test for:
+# https://github.com/tomchristie/django-rest-framework/issues/542
+# """
+# now = datetime.datetime.now()
+
+# serializer = UploadedFileSerializer(data={'created': now})
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.object.created, now)
+# self.assertIsNone(serializer.object.file)
+
+# def test_remove_with_empty_string(self):
+# """
+# Passing empty string as data should cause file to be removed
+
+# Test for:
+# https://github.com/tomchristie/django-rest-framework/issues/937
+# """
+# now = datetime.datetime.now()
+# file = BytesIO(six.b('stuff'))
+# file.name = 'stuff.txt'
+# file.size = len(file.getvalue())
+
+# uploaded_file = UploadedFile(file=file, created=now)
+
+# serializer = UploadedFileSerializer(instance=uploaded_file, data={'created': now, 'file': ''})
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.object.created, uploaded_file.created)
+# self.assertIsNone(serializer.object.file)
+
+# def test_validation_error_with_non_file(self):
+# """
+# Passing non-files should raise a validation error.
+# """
+# now = datetime.datetime.now()
+# errmsg = 'No file was submitted. Check the encoding type on the form.'
+
+# serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'})
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'file': [errmsg]})
+
+# def test_validation_with_no_data(self):
+# """
+# Validation should still function when no data dictionary is provided.
+# """
+# uploaded_file = BytesIO(six.b('stuff'))
+# uploaded_file.name = 'stuff.txt'
+# uploaded_file.size = len(uploaded_file.getvalue())
+# serializer = UploadedFileSerializer(files={'file': uploaded_file})
+# self.assertFalse(serializer.is_valid())
diff --git a/tests/test_genericrelations.py b/tests/test_genericrelations.py
index 95295eaa..a87ea3fd 100644
--- a/tests/test_genericrelations.py
+++ b/tests/test_genericrelations.py
@@ -1,151 +1,151 @@
-from __future__ import unicode_literals
-from django.contrib.contenttypes.models import ContentType
-from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey
-from django.db import models
-from django.test import TestCase
-from rest_framework import serializers
-from rest_framework.compat import python_2_unicode_compatible
-
-
-@python_2_unicode_compatible
-class Tag(models.Model):
- """
- Tags have a descriptive slug, and are attached to an arbitrary object.
- """
- tag = models.SlugField()
- content_type = models.ForeignKey(ContentType)
- object_id = models.PositiveIntegerField()
- tagged_item = GenericForeignKey('content_type', 'object_id')
-
- def __str__(self):
- return self.tag
-
-
-@python_2_unicode_compatible
-class Bookmark(models.Model):
- """
- A URL bookmark that may have multiple tags attached.
- """
- url = models.URLField()
- tags = GenericRelation(Tag)
-
- def __str__(self):
- return 'Bookmark: %s' % self.url
-
-
-@python_2_unicode_compatible
-class Note(models.Model):
- """
- A textual note that may have multiple tags attached.
- """
- text = models.TextField()
- tags = GenericRelation(Tag)
-
- def __str__(self):
- return 'Note: %s' % self.text
-
-
-class TestGenericRelations(TestCase):
- def setUp(self):
- self.bookmark = Bookmark.objects.create(url='https://www.djangoproject.com/')
- Tag.objects.create(tagged_item=self.bookmark, tag='django')
- Tag.objects.create(tagged_item=self.bookmark, tag='python')
- self.note = Note.objects.create(text='Remember the milk')
- Tag.objects.create(tagged_item=self.note, tag='reminder')
-
- def test_generic_relation(self):
- """
- Test a relationship that spans a GenericRelation field.
- IE. A reverse generic relationship.
- """
-
- class BookmarkSerializer(serializers.ModelSerializer):
- tags = serializers.RelatedField(many=True)
-
- class Meta:
- model = Bookmark
- exclude = ('id',)
-
- serializer = BookmarkSerializer(self.bookmark)
- expected = {
- 'tags': ['django', 'python'],
- 'url': 'https://www.djangoproject.com/'
- }
- self.assertEqual(serializer.data, expected)
-
- def test_generic_nested_relation(self):
- """
- Test saving a GenericRelation field via a nested serializer.
- """
-
- class TagSerializer(serializers.ModelSerializer):
- class Meta:
- model = Tag
- exclude = ('content_type', 'object_id')
-
- class BookmarkSerializer(serializers.ModelSerializer):
- tags = TagSerializer(many=True)
-
- class Meta:
- model = Bookmark
- exclude = ('id',)
-
- data = {
- 'url': 'https://docs.djangoproject.com/',
- 'tags': [
- {'tag': 'contenttypes'},
- {'tag': 'genericrelations'},
- ]
- }
- serializer = BookmarkSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- serializer.save()
- self.assertEqual(serializer.object.tags.count(), 2)
-
- def test_generic_fk(self):
- """
- Test a relationship that spans a GenericForeignKey field.
- IE. A forward generic relationship.
- """
-
- class TagSerializer(serializers.ModelSerializer):
- tagged_item = serializers.RelatedField()
-
- class Meta:
- model = Tag
- exclude = ('id', 'content_type', 'object_id')
-
- serializer = TagSerializer(Tag.objects.all(), many=True)
- expected = [
- {
- 'tag': 'django',
- 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
- },
- {
- 'tag': 'python',
- 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
- },
- {
- 'tag': 'reminder',
- 'tagged_item': 'Note: Remember the milk'
- }
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_restore_object_generic_fk(self):
- """
- Ensure an object with a generic foreign key can be restored.
- """
-
- class TagSerializer(serializers.ModelSerializer):
- class Meta:
- model = Tag
- exclude = ('content_type', 'object_id')
-
- serializer = TagSerializer()
-
- bookmark = Bookmark(url='http://example.com')
- attrs = {'tagged_item': bookmark, 'tag': 'example'}
-
- tag = serializer.restore_object(attrs)
- self.assertEqual(tag.tagged_item, bookmark)
+# from __future__ import unicode_literals
+# from django.contrib.contenttypes.models import ContentType
+# from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey
+# from django.db import models
+# from django.test import TestCase
+# from rest_framework import serializers
+# from rest_framework.compat import python_2_unicode_compatible
+
+
+# @python_2_unicode_compatible
+# class Tag(models.Model):
+# """
+# Tags have a descriptive slug, and are attached to an arbitrary object.
+# """
+# tag = models.SlugField()
+# content_type = models.ForeignKey(ContentType)
+# object_id = models.PositiveIntegerField()
+# tagged_item = GenericForeignKey('content_type', 'object_id')
+
+# def __str__(self):
+# return self.tag
+
+
+# @python_2_unicode_compatible
+# class Bookmark(models.Model):
+# """
+# A URL bookmark that may have multiple tags attached.
+# """
+# url = models.URLField()
+# tags = GenericRelation(Tag)
+
+# def __str__(self):
+# return 'Bookmark: %s' % self.url
+
+
+# @python_2_unicode_compatible
+# class Note(models.Model):
+# """
+# A textual note that may have multiple tags attached.
+# """
+# text = models.TextField()
+# tags = GenericRelation(Tag)
+
+# def __str__(self):
+# return 'Note: %s' % self.text
+
+
+# class TestGenericRelations(TestCase):
+# def setUp(self):
+# self.bookmark = Bookmark.objects.create(url='https://www.djangoproject.com/')
+# Tag.objects.create(tagged_item=self.bookmark, tag='django')
+# Tag.objects.create(tagged_item=self.bookmark, tag='python')
+# self.note = Note.objects.create(text='Remember the milk')
+# Tag.objects.create(tagged_item=self.note, tag='reminder')
+
+# def test_generic_relation(self):
+# """
+# Test a relationship that spans a GenericRelation field.
+# IE. A reverse generic relationship.
+# """
+
+# class BookmarkSerializer(serializers.ModelSerializer):
+# tags = serializers.RelatedField(many=True)
+
+# class Meta:
+# model = Bookmark
+# exclude = ('id',)
+
+# serializer = BookmarkSerializer(self.bookmark)
+# expected = {
+# 'tags': ['django', 'python'],
+# 'url': 'https://www.djangoproject.com/'
+# }
+# self.assertEqual(serializer.data, expected)
+
+# def test_generic_nested_relation(self):
+# """
+# Test saving a GenericRelation field via a nested serializer.
+# """
+
+# class TagSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = Tag
+# exclude = ('content_type', 'object_id')
+
+# class BookmarkSerializer(serializers.ModelSerializer):
+# tags = TagSerializer(many=True)
+
+# class Meta:
+# model = Bookmark
+# exclude = ('id',)
+
+# data = {
+# 'url': 'https://docs.djangoproject.com/',
+# 'tags': [
+# {'tag': 'contenttypes'},
+# {'tag': 'genericrelations'},
+# ]
+# }
+# serializer = BookmarkSerializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# serializer.save()
+# self.assertEqual(serializer.object.tags.count(), 2)
+
+# def test_generic_fk(self):
+# """
+# Test a relationship that spans a GenericForeignKey field.
+# IE. A forward generic relationship.
+# """
+
+# class TagSerializer(serializers.ModelSerializer):
+# tagged_item = serializers.RelatedField()
+
+# class Meta:
+# model = Tag
+# exclude = ('id', 'content_type', 'object_id')
+
+# serializer = TagSerializer(Tag.objects.all(), many=True)
+# expected = [
+# {
+# 'tag': 'django',
+# 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
+# },
+# {
+# 'tag': 'python',
+# 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
+# },
+# {
+# 'tag': 'reminder',
+# 'tagged_item': 'Note: Remember the milk'
+# }
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_restore_object_generic_fk(self):
+# """
+# Ensure an object with a generic foreign key can be restored.
+# """
+
+# class TagSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = Tag
+# exclude = ('content_type', 'object_id')
+
+# serializer = TagSerializer()
+
+# bookmark = Bookmark(url='http://example.com')
+# attrs = {'tagged_item': bookmark, 'tag': 'example'}
+
+# tag = serializer.restore_object(attrs)
+# self.assertEqual(tag.tagged_item, bookmark)
diff --git a/tests/test_generics.py b/tests/test_generics.py
index f50d53e9..55f361b2 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -33,13 +33,9 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView):
"""
Example description for OPTIONS.
"""
- queryset = BasicModel.objects.all()
+ queryset = BasicModel.objects.exclude(text='filtered out')
serializer_class = BasicSerializer
- def get_queryset(self):
- queryset = super(InstanceView, self).get_queryset()
- return queryset.exclude(text='filtered out')
-
class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
"""
@@ -50,11 +46,11 @@ class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
class SlugSerializer(serializers.ModelSerializer):
- slug = serializers.Field() # read only
+ slug = serializers.Field(read_only=True)
class Meta:
model = SlugBasedModel
- exclude = ('id',)
+ fields = ('text', 'slug')
class SlugBasedInstanceView(InstanceView):
@@ -125,46 +121,46 @@ class TestRootView(TestCase):
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
self.assertEqual(response.data, {"detail": "Method 'DELETE' not allowed."})
- def test_options_root_view(self):
- """
- OPTIONS requests to ListCreateAPIView should return metadata
- """
- request = factory.options('/')
- with self.assertNumQueries(0):
- response = self.view(request).render()
- expected = {
- 'parses': [
- 'application/json',
- 'application/x-www-form-urlencoded',
- 'multipart/form-data'
- ],
- 'renders': [
- 'application/json',
- 'text/html'
- ],
- 'name': 'Root',
- 'description': 'Example description for OPTIONS.',
- 'actions': {
- 'POST': {
- 'text': {
- 'max_length': 100,
- 'read_only': False,
- 'required': True,
- 'type': 'string',
- "label": "Text comes here",
- "help_text": "Text description."
- },
- 'id': {
- 'read_only': True,
- 'required': False,
- 'type': 'integer',
- 'label': 'ID',
- },
- }
- }
- }
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, expected)
+ # def test_options_root_view(self):
+ # """
+ # OPTIONS requests to ListCreateAPIView should return metadata
+ # """
+ # request = factory.options('/')
+ # with self.assertNumQueries(0):
+ # response = self.view(request).render()
+ # expected = {
+ # 'parses': [
+ # 'application/json',
+ # 'application/x-www-form-urlencoded',
+ # 'multipart/form-data'
+ # ],
+ # 'renders': [
+ # 'application/json',
+ # 'text/html'
+ # ],
+ # 'name': 'Root',
+ # 'description': 'Example description for OPTIONS.',
+ # 'actions': {
+ # 'POST': {
+ # 'text': {
+ # 'max_length': 100,
+ # 'read_only': False,
+ # 'required': True,
+ # 'type': 'string',
+ # "label": "Text comes here",
+ # "help_text": "Text description."
+ # },
+ # 'id': {
+ # 'read_only': True,
+ # 'required': False,
+ # 'type': 'integer',
+ # 'label': 'ID',
+ # },
+ # }
+ # }
+ # }
+ # self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # self.assertEqual(response.data, expected)
def test_post_cannot_set_id(self):
"""
@@ -223,10 +219,10 @@ class TestInstanceView(TestCase):
"""
data = {'text': 'foobar'}
request = factory.put('/1', data, format='json')
- with self.assertNumQueries(2):
+ with self.assertNumQueries(3):
response = self.view(request, pk='1').render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
+ self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'})
updated = self.objects.get(id=1)
self.assertEqual(updated.text, 'foobar')
@@ -237,7 +233,7 @@ class TestInstanceView(TestCase):
data = {'text': 'foobar'}
request = factory.patch('/1', data, format='json')
- with self.assertNumQueries(2):
+ with self.assertNumQueries(3):
response = self.view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
@@ -256,88 +252,88 @@ class TestInstanceView(TestCase):
ids = [obj.id for obj in self.objects.all()]
self.assertEqual(ids, [2, 3])
- def test_options_instance_view(self):
- """
- OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
- """
- request = factory.options('/1')
- with self.assertNumQueries(1):
- response = self.view(request, pk=1).render()
- expected = {
- 'parses': [
- 'application/json',
- 'application/x-www-form-urlencoded',
- 'multipart/form-data'
- ],
- 'renders': [
- 'application/json',
- 'text/html'
- ],
- 'name': 'Instance',
- 'description': 'Example description for OPTIONS.',
- 'actions': {
- 'PUT': {
- 'text': {
- 'max_length': 100,
- 'read_only': False,
- 'required': True,
- 'type': 'string',
- 'label': 'Text comes here',
- 'help_text': 'Text description.'
- },
- 'id': {
- 'read_only': True,
- 'required': False,
- 'type': 'integer',
- 'label': 'ID',
- },
- }
- }
- }
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, expected)
-
- def test_options_before_instance_create(self):
- """
- OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
- before the instance has been created
- """
- request = factory.options('/999')
- with self.assertNumQueries(1):
- response = self.view(request, pk=999).render()
- expected = {
- 'parses': [
- 'application/json',
- 'application/x-www-form-urlencoded',
- 'multipart/form-data'
- ],
- 'renders': [
- 'application/json',
- 'text/html'
- ],
- 'name': 'Instance',
- 'description': 'Example description for OPTIONS.',
- 'actions': {
- 'PUT': {
- 'text': {
- 'max_length': 100,
- 'read_only': False,
- 'required': True,
- 'type': 'string',
- 'label': 'Text comes here',
- 'help_text': 'Text description.'
- },
- 'id': {
- 'read_only': True,
- 'required': False,
- 'type': 'integer',
- 'label': 'ID',
- },
- }
- }
- }
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, expected)
+ # def test_options_instance_view(self):
+ # """
+ # OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
+ # """
+ # request = factory.options('/1')
+ # with self.assertNumQueries(1):
+ # response = self.view(request, pk=1).render()
+ # expected = {
+ # 'parses': [
+ # 'application/json',
+ # 'application/x-www-form-urlencoded',
+ # 'multipart/form-data'
+ # ],
+ # 'renders': [
+ # 'application/json',
+ # 'text/html'
+ # ],
+ # 'name': 'Instance',
+ # 'description': 'Example description for OPTIONS.',
+ # 'actions': {
+ # 'PUT': {
+ # 'text': {
+ # 'max_length': 100,
+ # 'read_only': False,
+ # 'required': True,
+ # 'type': 'string',
+ # 'label': 'Text comes here',
+ # 'help_text': 'Text description.'
+ # },
+ # 'id': {
+ # 'read_only': True,
+ # 'required': False,
+ # 'type': 'integer',
+ # 'label': 'ID',
+ # },
+ # }
+ # }
+ # }
+ # self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # self.assertEqual(response.data, expected)
+
+ # def test_options_before_instance_create(self):
+ # """
+ # OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
+ # before the instance has been created
+ # """
+ # request = factory.options('/999')
+ # with self.assertNumQueries(1):
+ # response = self.view(request, pk=999).render()
+ # expected = {
+ # 'parses': [
+ # 'application/json',
+ # 'application/x-www-form-urlencoded',
+ # 'multipart/form-data'
+ # ],
+ # 'renders': [
+ # 'application/json',
+ # 'text/html'
+ # ],
+ # 'name': 'Instance',
+ # 'description': 'Example description for OPTIONS.',
+ # 'actions': {
+ # 'PUT': {
+ # 'text': {
+ # 'max_length': 100,
+ # 'read_only': False,
+ # 'required': True,
+ # 'type': 'string',
+ # 'label': 'Text comes here',
+ # 'help_text': 'Text description.'
+ # },
+ # 'id': {
+ # 'read_only': True,
+ # 'required': False,
+ # 'type': 'integer',
+ # 'label': 'ID',
+ # },
+ # }
+ # }
+ # }
+ # self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # self.assertEqual(response.data, expected)
def test_get_instance_view_incorrect_arg(self):
"""
@@ -355,7 +351,7 @@ class TestInstanceView(TestCase):
"""
data = {'id': 999, 'text': 'foobar'}
request = factory.put('/1', data, format='json')
- with self.assertNumQueries(2):
+ with self.assertNumQueries(3):
response = self.view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
@@ -370,7 +366,7 @@ class TestInstanceView(TestCase):
self.objects.get(id=1).delete()
data = {'text': 'foobar'}
request = factory.put('/1', data, format='json')
- with self.assertNumQueries(3):
+ with self.assertNumQueries(2):
response = self.view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
@@ -396,7 +392,7 @@ class TestInstanceView(TestCase):
data = {'text': 'foobar'}
# pk fields can not be created on demand, only the database can set the pk for a new object
request = factory.put('/5', data, format='json')
- with self.assertNumQueries(3):
+ with self.assertNumQueries(2):
response = self.view(request, pk=5).render()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
new_obj = self.objects.get(pk=5)
@@ -446,52 +442,52 @@ class TestFKInstanceView(TestCase):
]
self.view = FKInstanceView.as_view()
- def test_options_root_view(self):
- """
- OPTIONS requests to ListCreateAPIView should return metadata
- """
- request = factory.options('/999')
- with self.assertNumQueries(1):
- response = self.view(request, pk=999).render()
- expected = {
- 'name': 'Fk Instance',
- 'description': 'FK: example description for OPTIONS.',
- 'renders': [
- 'application/json',
- 'text/html'
- ],
- 'parses': [
- 'application/json',
- 'application/x-www-form-urlencoded',
- 'multipart/form-data'
- ],
- 'actions': {
- 'PUT': {
- 'id': {
- 'type': 'integer',
- 'required': False,
- 'read_only': True,
- 'label': 'ID'
- },
- 'name': {
- 'type': 'string',
- 'required': True,
- 'read_only': False,
- 'label': 'name',
- 'max_length': 100
- },
- 'target': {
- 'type': 'field',
- 'required': True,
- 'read_only': False,
- 'label': 'Target',
- 'help_text': 'Target'
- }
- }
- }
- }
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, expected)
+ # def test_options_root_view(self):
+ # """
+ # OPTIONS requests to ListCreateAPIView should return metadata
+ # """
+ # request = factory.options('/999')
+ # with self.assertNumQueries(1):
+ # response = self.view(request, pk=999).render()
+ # expected = {
+ # 'name': 'Fk Instance',
+ # 'description': 'FK: example description for OPTIONS.',
+ # 'renders': [
+ # 'application/json',
+ # 'text/html'
+ # ],
+ # 'parses': [
+ # 'application/json',
+ # 'application/x-www-form-urlencoded',
+ # 'multipart/form-data'
+ # ],
+ # 'actions': {
+ # 'PUT': {
+ # 'id': {
+ # 'type': 'integer',
+ # 'required': False,
+ # 'read_only': True,
+ # 'label': 'ID'
+ # },
+ # 'name': {
+ # 'type': 'string',
+ # 'required': True,
+ # 'read_only': False,
+ # 'label': 'name',
+ # 'max_length': 100
+ # },
+ # 'target': {
+ # 'type': 'field',
+ # 'required': True,
+ # 'read_only': False,
+ # 'label': 'Target',
+ # 'help_text': 'Target'
+ # }
+ # }
+ # }
+ # }
+ # self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # self.assertEqual(response.data, expected)
class TestOverriddenGetObject(TestCase):
diff --git a/tests/test_hyperlinkedserializers.py b/tests/test_hyperlinkedserializers.py
index 0e8c1ed4..ff3663dd 100644
--- a/tests/test_hyperlinkedserializers.py
+++ b/tests/test_hyperlinkedserializers.py
@@ -1,406 +1,406 @@
-from __future__ import unicode_literals
-import json
-from django.test import TestCase
-from rest_framework import generics, status, serializers
-from django.conf.urls import patterns, url
-from rest_framework.settings import api_settings
-from rest_framework.test import APIRequestFactory
-from tests.models import (
- Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment,
- Album, Photo, OptionalRelationModel
-)
+# from __future__ import unicode_literals
+# import json
+# from django.test import TestCase
+# from rest_framework import generics, status, serializers
+# from django.conf.urls import patterns, url
+# from rest_framework.settings import api_settings
+# from rest_framework.test import APIRequestFactory
+# from tests.models import (
+# Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment,
+# Album, Photo, OptionalRelationModel
+# )
-factory = APIRequestFactory()
+# factory = APIRequestFactory()
-class BlogPostCommentSerializer(serializers.ModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='blogpostcomment-detail')
- text = serializers.CharField()
- blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail')
+# class BlogPostCommentSerializer(serializers.ModelSerializer):
+# url = serializers.HyperlinkedIdentityField(view_name='blogpostcomment-detail')
+# text = serializers.CharField()
+# blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail')
- class Meta:
- model = BlogPostComment
- fields = ('text', 'blog_post_url', 'url')
+# class Meta:
+# model = BlogPostComment
+# fields = ('text', 'blog_post_url', 'url')
-class PhotoSerializer(serializers.Serializer):
- description = serializers.CharField()
- album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), lookup_field='title')
+# class PhotoSerializer(serializers.Serializer):
+# description = serializers.CharField()
+# album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), lookup_field='title')
- def restore_object(self, attrs, instance=None):
- return Photo(**attrs)
+# def restore_object(self, attrs, instance=None):
+# return Photo(**attrs)
-class AlbumSerializer(serializers.ModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='album-detail', lookup_field='title')
+# class AlbumSerializer(serializers.ModelSerializer):
+# url = serializers.HyperlinkedIdentityField(view_name='album-detail', lookup_field='title')
- class Meta:
- model = Album
- fields = ('title', 'url')
+# class Meta:
+# model = Album
+# fields = ('title', 'url')
-class BasicSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = BasicModel
+# class BasicSerializer(serializers.HyperlinkedModelSerializer):
+# class Meta:
+# model = BasicModel
-class AnchorSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = Anchor
+# class AnchorSerializer(serializers.HyperlinkedModelSerializer):
+# class Meta:
+# model = Anchor
-class ManyToManySerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = ManyToManyModel
+# class ManyToManySerializer(serializers.HyperlinkedModelSerializer):
+# class Meta:
+# model = ManyToManyModel
-class BlogPostSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlogPost
+# class BlogPostSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = BlogPost
-class OptionalRelationSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = OptionalRelationModel
+# class OptionalRelationSerializer(serializers.HyperlinkedModelSerializer):
+# class Meta:
+# model = OptionalRelationModel
-class BasicList(generics.ListCreateAPIView):
- queryset = BasicModel.objects.all()
- serializer_class = BasicSerializer
+# class BasicList(generics.ListCreateAPIView):
+# queryset = BasicModel.objects.all()
+# serializer_class = BasicSerializer
-class BasicDetail(generics.RetrieveUpdateDestroyAPIView):
- queryset = BasicModel.objects.all()
- serializer_class = BasicSerializer
+# class BasicDetail(generics.RetrieveUpdateDestroyAPIView):
+# queryset = BasicModel.objects.all()
+# serializer_class = BasicSerializer
-class AnchorDetail(generics.RetrieveAPIView):
- queryset = Anchor.objects.all()
- serializer_class = AnchorSerializer
+# class AnchorDetail(generics.RetrieveAPIView):
+# queryset = Anchor.objects.all()
+# serializer_class = AnchorSerializer
-class ManyToManyList(generics.ListAPIView):
- queryset = ManyToManyModel.objects.all()
- serializer_class = ManyToManySerializer
+# class ManyToManyList(generics.ListAPIView):
+# queryset = ManyToManyModel.objects.all()
+# serializer_class = ManyToManySerializer
-class ManyToManyDetail(generics.RetrieveAPIView):
- queryset = ManyToManyModel.objects.all()
- serializer_class = ManyToManySerializer
+# class ManyToManyDetail(generics.RetrieveAPIView):
+# queryset = ManyToManyModel.objects.all()
+# serializer_class = ManyToManySerializer
-class BlogPostCommentListCreate(generics.ListCreateAPIView):
- queryset = BlogPostComment.objects.all()
- serializer_class = BlogPostCommentSerializer
+# class BlogPostCommentListCreate(generics.ListCreateAPIView):
+# queryset = BlogPostComment.objects.all()
+# serializer_class = BlogPostCommentSerializer
-class BlogPostCommentDetail(generics.RetrieveAPIView):
- queryset = BlogPostComment.objects.all()
- serializer_class = BlogPostCommentSerializer
+# class BlogPostCommentDetail(generics.RetrieveAPIView):
+# queryset = BlogPostComment.objects.all()
+# serializer_class = BlogPostCommentSerializer
-class BlogPostDetail(generics.RetrieveAPIView):
- queryset = BlogPost.objects.all()
- serializer_class = BlogPostSerializer
+# class BlogPostDetail(generics.RetrieveAPIView):
+# queryset = BlogPost.objects.all()
+# serializer_class = BlogPostSerializer
-class PhotoListCreate(generics.ListCreateAPIView):
- queryset = Photo.objects.all()
- serializer_class = PhotoSerializer
+# class PhotoListCreate(generics.ListCreateAPIView):
+# queryset = Photo.objects.all()
+# serializer_class = PhotoSerializer
-class AlbumDetail(generics.RetrieveAPIView):
- queryset = Album.objects.all()
- serializer_class = AlbumSerializer
- lookup_field = 'title'
+# class AlbumDetail(generics.RetrieveAPIView):
+# queryset = Album.objects.all()
+# serializer_class = AlbumSerializer
+# lookup_field = 'title'
-class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
- queryset = OptionalRelationModel.objects.all()
- serializer_class = OptionalRelationSerializer
+# class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
+# queryset = OptionalRelationModel.objects.all()
+# serializer_class = OptionalRelationSerializer
-urlpatterns = patterns(
- '',
- url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
- url(r'^basic/(?P\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),
- url(r'^anchor/(?P\d+)/$', AnchorDetail.as_view(), name='anchor-detail'),
- url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'),
- url(r'^manytomany/(?P\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
- url(r'^posts/(?P\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'),
- url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
- url(r'^comments/(?P\d+)/$', BlogPostCommentDetail.as_view(), name='blogpostcomment-detail'),
- url(r'^albums/(?P\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
- url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'),
- url(r'^optionalrelation/(?P\d+)/$', OptionalRelationDetail.as_view(), name='optionalrelationmodel-detail'),
-)
-
-
-class TestBasicHyperlinkedView(TestCase):
- urls = 'tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create 3 BasicModel instances.
- """
- items = ['foo', 'bar', 'baz']
- for item in items:
- BasicModel(text=item).save()
- self.objects = BasicModel.objects
- self.data = [
- {'url': 'http://testserver/basic/%d/' % obj.id, 'text': obj.text}
- for obj in self.objects.all()
- ]
- self.list_view = BasicList.as_view()
- self.detail_view = BasicDetail.as_view()
-
- def test_get_list_view(self):
- """
- GET requests to ListCreateAPIView should return list of objects.
- """
- request = factory.get('/basic/')
- response = self.list_view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data)
-
- def test_get_detail_view(self):
- """
- GET requests to ListCreateAPIView should return list of objects.
- """
- request = factory.get('/basic/1')
- response = self.detail_view(request, pk=1).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data[0])
-
-
-class TestManyToManyHyperlinkedView(TestCase):
- urls = 'tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create 3 BasicModel instances.
- """
- items = ['foo', 'bar', 'baz']
- anchors = []
- for item in items:
- anchor = Anchor(text=item)
- anchor.save()
- anchors.append(anchor)
-
- manytomany = ManyToManyModel()
- manytomany.save()
- manytomany.rel.add(*anchors)
-
- self.data = [{
- 'url': 'http://testserver/manytomany/1/',
- 'rel': [
- 'http://testserver/anchor/1/',
- 'http://testserver/anchor/2/',
- 'http://testserver/anchor/3/',
- ]
- }]
- self.list_view = ManyToManyList.as_view()
- self.detail_view = ManyToManyDetail.as_view()
-
- def test_get_list_view(self):
- """
- GET requests to ListCreateAPIView should return list of objects.
- """
- request = factory.get('/manytomany/')
- response = self.list_view(request)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data)
-
- def test_get_detail_view(self):
- """
- GET requests to ListCreateAPIView should return list of objects.
- """
- request = factory.get('/manytomany/1/')
- response = self.detail_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data[0])
-
-
-class TestHyperlinkedIdentityFieldLookup(TestCase):
- urls = 'tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create 3 Album instances.
- """
- titles = ['foo', 'bar', 'baz']
- for title in titles:
- album = Album(title=title)
- album.save()
- self.detail_view = AlbumDetail.as_view()
- self.data = {
- 'foo': {'title': 'foo', 'url': 'http://testserver/albums/foo/'},
- 'bar': {'title': 'bar', 'url': 'http://testserver/albums/bar/'},
- 'baz': {'title': 'baz', 'url': 'http://testserver/albums/baz/'}
- }
-
- def test_lookup_field(self):
- """
- GET requests to AlbumDetail view should return serialized Albums
- with a url field keyed by `title`.
- """
- for album in Album.objects.all():
- request = factory.get('/albums/{0}/'.format(album.title))
- response = self.detail_view(request, title=album.title)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data[album.title])
-
-
-class TestCreateWithForeignKeys(TestCase):
- urls = 'tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create a blog post
- """
- self.post = BlogPost.objects.create(title="Test post")
- self.create_view = BlogPostCommentListCreate.as_view()
-
- def test_create_comment(self):
-
- data = {
- 'text': 'A test comment',
- 'blog_post_url': 'http://testserver/posts/1/'
- }
-
- request = factory.post('/comments/', data=data)
- response = self.create_view(request)
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(response['Location'], 'http://testserver/comments/1/')
- self.assertEqual(self.post.blogpostcomment_set.count(), 1)
- self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment')
-
-
-class TestCreateWithForeignKeysAndCustomSlug(TestCase):
- urls = 'tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create an Album
- """
- self.post = Album.objects.create(title='test-album')
- self.list_create_view = PhotoListCreate.as_view()
-
- def test_create_photo(self):
-
- data = {
- 'description': 'A test photo',
- 'album_url': 'http://testserver/albums/test-album/'
- }
-
- request = factory.post('/photos/', data=data)
- response = self.list_create_view(request)
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertNotIn('Location', response, msg='Location should only be included if there is a "url" field on the serializer')
- self.assertEqual(self.post.photo_set.count(), 1)
- self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo')
-
-
-class TestOptionalRelationHyperlinkedView(TestCase):
- urls = 'tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create 1 OptionalRelationModel instances.
- """
- OptionalRelationModel().save()
- self.objects = OptionalRelationModel.objects
- self.detail_view = OptionalRelationDetail.as_view()
- self.data = {"url": "http://testserver/optionalrelation/1/", "other": None}
-
- def test_get_detail_view(self):
- """
- GET requests to RetrieveAPIView with optional relations should return None
- for non existing relations.
- """
- request = factory.get('/optionalrelationmodel-detail/1')
- response = self.detail_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data)
-
- def test_put_detail_view(self):
- """
- PUT requests to RetrieveUpdateDestroyAPIView with optional relations
- should accept None for non existing relations.
- """
- response = self.client.put('/optionalrelation/1/',
- data=json.dumps(self.data),
- content_type='application/json')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-
-class TestOverriddenURLField(TestCase):
- def setUp(self):
- class OverriddenURLSerializer(serializers.HyperlinkedModelSerializer):
- url = serializers.SerializerMethodField('get_url')
-
- class Meta:
- model = BlogPost
- fields = ('title', 'url')
-
- def get_url(self, obj):
- return 'foo bar'
-
- self.Serializer = OverriddenURLSerializer
- self.obj = BlogPost.objects.create(title='New blog post')
-
- def test_overridden_url_field(self):
- """
- The 'url' field should respect overriding.
- Regression test for #936.
- """
- serializer = self.Serializer(self.obj)
- self.assertEqual(
- serializer.data,
- {'title': 'New blog post', 'url': 'foo bar'}
- )
-
-
-class TestURLFieldNameBySettings(TestCase):
- urls = 'tests.test_hyperlinkedserializers'
-
- def setUp(self):
- self.saved_url_field_name = api_settings.URL_FIELD_NAME
- api_settings.URL_FIELD_NAME = 'global_url_field'
-
- class Serializer(serializers.HyperlinkedModelSerializer):
-
- class Meta:
- model = BlogPost
- fields = ('title', api_settings.URL_FIELD_NAME)
-
- self.Serializer = Serializer
- self.obj = BlogPost.objects.create(title="New blog post")
-
- def tearDown(self):
- api_settings.URL_FIELD_NAME = self.saved_url_field_name
-
- def test_overridden_url_field_name(self):
- request = factory.get('/posts/')
- serializer = self.Serializer(self.obj, context={'request': request})
- self.assertIn(api_settings.URL_FIELD_NAME, serializer.data)
-
-
-class TestURLFieldNameByOptions(TestCase):
- urls = 'tests.test_hyperlinkedserializers'
-
- def setUp(self):
- class Serializer(serializers.HyperlinkedModelSerializer):
-
- class Meta:
- model = BlogPost
- fields = ('title', 'serializer_url_field')
- url_field_name = 'serializer_url_field'
-
- self.Serializer = Serializer
- self.obj = BlogPost.objects.create(title="New blog post")
-
- def test_overridden_url_field_name(self):
- request = factory.get('/posts/')
- serializer = self.Serializer(self.obj, context={'request': request})
- self.assertIn(self.Serializer.Meta.url_field_name, serializer.data)
+# urlpatterns = patterns(
+# '',
+# url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
+# url(r'^basic/(?P\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),
+# url(r'^anchor/(?P\d+)/$', AnchorDetail.as_view(), name='anchor-detail'),
+# url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'),
+# url(r'^manytomany/(?P\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
+# url(r'^posts/(?P\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'),
+# url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
+# url(r'^comments/(?P\d+)/$', BlogPostCommentDetail.as_view(), name='blogpostcomment-detail'),
+# url(r'^albums/(?P\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
+# url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'),
+# url(r'^optionalrelation/(?P\d+)/$', OptionalRelationDetail.as_view(), name='optionalrelationmodel-detail'),
+# )
+
+
+# class TestBasicHyperlinkedView(TestCase):
+# urls = 'tests.test_hyperlinkedserializers'
+
+# def setUp(self):
+# """
+# Create 3 BasicModel instances.
+# """
+# items = ['foo', 'bar', 'baz']
+# for item in items:
+# BasicModel(text=item).save()
+# self.objects = BasicModel.objects
+# self.data = [
+# {'url': 'http://testserver/basic/%d/' % obj.id, 'text': obj.text}
+# for obj in self.objects.all()
+# ]
+# self.list_view = BasicList.as_view()
+# self.detail_view = BasicDetail.as_view()
+
+# def test_get_list_view(self):
+# """
+# GET requests to ListCreateAPIView should return list of objects.
+# """
+# request = factory.get('/basic/')
+# response = self.list_view(request).render()
+# self.assertEqual(response.status_code, status.HTTP_200_OK)
+# self.assertEqual(response.data, self.data)
+
+# def test_get_detail_view(self):
+# """
+# GET requests to ListCreateAPIView should return list of objects.
+# """
+# request = factory.get('/basic/1')
+# response = self.detail_view(request, pk=1).render()
+# self.assertEqual(response.status_code, status.HTTP_200_OK)
+# self.assertEqual(response.data, self.data[0])
+
+
+# class TestManyToManyHyperlinkedView(TestCase):
+# urls = 'tests.test_hyperlinkedserializers'
+
+# def setUp(self):
+# """
+# Create 3 BasicModel instances.
+# """
+# items = ['foo', 'bar', 'baz']
+# anchors = []
+# for item in items:
+# anchor = Anchor(text=item)
+# anchor.save()
+# anchors.append(anchor)
+
+# manytomany = ManyToManyModel()
+# manytomany.save()
+# manytomany.rel.add(*anchors)
+
+# self.data = [{
+# 'url': 'http://testserver/manytomany/1/',
+# 'rel': [
+# 'http://testserver/anchor/1/',
+# 'http://testserver/anchor/2/',
+# 'http://testserver/anchor/3/',
+# ]
+# }]
+# self.list_view = ManyToManyList.as_view()
+# self.detail_view = ManyToManyDetail.as_view()
+
+# def test_get_list_view(self):
+# """
+# GET requests to ListCreateAPIView should return list of objects.
+# """
+# request = factory.get('/manytomany/')
+# response = self.list_view(request)
+# self.assertEqual(response.status_code, status.HTTP_200_OK)
+# self.assertEqual(response.data, self.data)
+
+# def test_get_detail_view(self):
+# """
+# GET requests to ListCreateAPIView should return list of objects.
+# """
+# request = factory.get('/manytomany/1/')
+# response = self.detail_view(request, pk=1)
+# self.assertEqual(response.status_code, status.HTTP_200_OK)
+# self.assertEqual(response.data, self.data[0])
+
+
+# class TestHyperlinkedIdentityFieldLookup(TestCase):
+# urls = 'tests.test_hyperlinkedserializers'
+
+# def setUp(self):
+# """
+# Create 3 Album instances.
+# """
+# titles = ['foo', 'bar', 'baz']
+# for title in titles:
+# album = Album(title=title)
+# album.save()
+# self.detail_view = AlbumDetail.as_view()
+# self.data = {
+# 'foo': {'title': 'foo', 'url': 'http://testserver/albums/foo/'},
+# 'bar': {'title': 'bar', 'url': 'http://testserver/albums/bar/'},
+# 'baz': {'title': 'baz', 'url': 'http://testserver/albums/baz/'}
+# }
+
+# def test_lookup_field(self):
+# """
+# GET requests to AlbumDetail view should return serialized Albums
+# with a url field keyed by `title`.
+# """
+# for album in Album.objects.all():
+# request = factory.get('/albums/{0}/'.format(album.title))
+# response = self.detail_view(request, title=album.title)
+# self.assertEqual(response.status_code, status.HTTP_200_OK)
+# self.assertEqual(response.data, self.data[album.title])
+
+
+# class TestCreateWithForeignKeys(TestCase):
+# urls = 'tests.test_hyperlinkedserializers'
+
+# def setUp(self):
+# """
+# Create a blog post
+# """
+# self.post = BlogPost.objects.create(title="Test post")
+# self.create_view = BlogPostCommentListCreate.as_view()
+
+# def test_create_comment(self):
+
+# data = {
+# 'text': 'A test comment',
+# 'blog_post_url': 'http://testserver/posts/1/'
+# }
+
+# request = factory.post('/comments/', data=data)
+# response = self.create_view(request)
+# self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+# self.assertEqual(response['Location'], 'http://testserver/comments/1/')
+# self.assertEqual(self.post.blogpostcomment_set.count(), 1)
+# self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment')
+
+
+# class TestCreateWithForeignKeysAndCustomSlug(TestCase):
+# urls = 'tests.test_hyperlinkedserializers'
+
+# def setUp(self):
+# """
+# Create an Album
+# """
+# self.post = Album.objects.create(title='test-album')
+# self.list_create_view = PhotoListCreate.as_view()
+
+# def test_create_photo(self):
+
+# data = {
+# 'description': 'A test photo',
+# 'album_url': 'http://testserver/albums/test-album/'
+# }
+
+# request = factory.post('/photos/', data=data)
+# response = self.list_create_view(request)
+# self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+# self.assertNotIn('Location', response, msg='Location should only be included if there is a "url" field on the serializer')
+# self.assertEqual(self.post.photo_set.count(), 1)
+# self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo')
+
+
+# class TestOptionalRelationHyperlinkedView(TestCase):
+# urls = 'tests.test_hyperlinkedserializers'
+
+# def setUp(self):
+# """
+# Create 1 OptionalRelationModel instances.
+# """
+# OptionalRelationModel().save()
+# self.objects = OptionalRelationModel.objects
+# self.detail_view = OptionalRelationDetail.as_view()
+# self.data = {"url": "http://testserver/optionalrelation/1/", "other": None}
+
+# def test_get_detail_view(self):
+# """
+# GET requests to RetrieveAPIView with optional relations should return None
+# for non existing relations.
+# """
+# request = factory.get('/optionalrelationmodel-detail/1')
+# response = self.detail_view(request, pk=1)
+# self.assertEqual(response.status_code, status.HTTP_200_OK)
+# self.assertEqual(response.data, self.data)
+
+# def test_put_detail_view(self):
+# """
+# PUT requests to RetrieveUpdateDestroyAPIView with optional relations
+# should accept None for non existing relations.
+# """
+# response = self.client.put('/optionalrelation/1/',
+# data=json.dumps(self.data),
+# content_type='application/json')
+# self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+
+# class TestOverriddenURLField(TestCase):
+# def setUp(self):
+# class OverriddenURLSerializer(serializers.HyperlinkedModelSerializer):
+# url = serializers.SerializerMethodField('get_url')
+
+# class Meta:
+# model = BlogPost
+# fields = ('title', 'url')
+
+# def get_url(self, obj):
+# return 'foo bar'
+
+# self.Serializer = OverriddenURLSerializer
+# self.obj = BlogPost.objects.create(title='New blog post')
+
+# def test_overridden_url_field(self):
+# """
+# The 'url' field should respect overriding.
+# Regression test for #936.
+# """
+# serializer = self.Serializer(self.obj)
+# self.assertEqual(
+# serializer.data,
+# {'title': 'New blog post', 'url': 'foo bar'}
+# )
+
+
+# class TestURLFieldNameBySettings(TestCase):
+# urls = 'tests.test_hyperlinkedserializers'
+
+# def setUp(self):
+# self.saved_url_field_name = api_settings.URL_FIELD_NAME
+# api_settings.URL_FIELD_NAME = 'global_url_field'
+
+# class Serializer(serializers.HyperlinkedModelSerializer):
+
+# class Meta:
+# model = BlogPost
+# fields = ('title', api_settings.URL_FIELD_NAME)
+
+# self.Serializer = Serializer
+# self.obj = BlogPost.objects.create(title="New blog post")
+
+# def tearDown(self):
+# api_settings.URL_FIELD_NAME = self.saved_url_field_name
+
+# def test_overridden_url_field_name(self):
+# request = factory.get('/posts/')
+# serializer = self.Serializer(self.obj, context={'request': request})
+# self.assertIn(api_settings.URL_FIELD_NAME, serializer.data)
+
+
+# class TestURLFieldNameByOptions(TestCase):
+# urls = 'tests.test_hyperlinkedserializers'
+
+# def setUp(self):
+# class Serializer(serializers.HyperlinkedModelSerializer):
+
+# class Meta:
+# model = BlogPost
+# fields = ('title', 'serializer_url_field')
+# url_field_name = 'serializer_url_field'
+
+# self.Serializer = Serializer
+# self.obj = BlogPost.objects.create(title="New blog post")
+
+# def test_overridden_url_field_name(self):
+# request = factory.get('/posts/')
+# serializer = self.Serializer(self.obj, context={'request': request})
+# self.assertIn(self.Serializer.Meta.url_field_name, serializer.data)
diff --git a/tests/test_nullable_fields.py b/tests/test_nullable_fields.py
index 8d0c84bb..9843182a 100644
--- a/tests/test_nullable_fields.py
+++ b/tests/test_nullable_fields.py
@@ -1,39 +1,39 @@
-from django.core.urlresolvers import reverse
+# from django.core.urlresolvers import reverse
-from django.conf.urls import patterns, url
-from rest_framework import serializers, generics
-from rest_framework.test import APITestCase
-from tests.models import NullableForeignKeySource
+# from django.conf.urls import patterns, url
+# from rest_framework import serializers, generics
+# from rest_framework.test import APITestCase
+# from tests.models import NullableForeignKeySource
-class NullableFKSourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = NullableForeignKeySource
+# class NullableFKSourceSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = NullableForeignKeySource
-class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView):
- queryset = NullableForeignKeySource.objects.all()
- serializer_class = NullableFKSourceSerializer
+# class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView):
+# queryset = NullableForeignKeySource.objects.all()
+# serializer_class = NullableFKSourceSerializer
-urlpatterns = patterns(
- '',
- url(r'^objects/(?P\d+)/$', NullableFKSourceDetail.as_view(), name='object-detail'),
-)
+# urlpatterns = patterns(
+# '',
+# url(r'^objects/(?P\d+)/$', NullableFKSourceDetail.as_view(), name='object-detail'),
+# )
-class NullableForeignKeyTests(APITestCase):
- """
- DRF should be able to handle nullable foreign keys when a test
- Client POST/PUT request is made with its own serialized object.
- """
- urls = 'tests.test_nullable_fields'
+# class NullableForeignKeyTests(APITestCase):
+# """
+# DRF should be able to handle nullable foreign keys when a test
+# Client POST/PUT request is made with its own serialized object.
+# """
+# urls = 'tests.test_nullable_fields'
- def test_updating_object_with_null_fk(self):
- obj = NullableForeignKeySource(name='example', target=None)
- obj.save()
- serialized_data = NullableFKSourceSerializer(obj).data
+# def test_updating_object_with_null_fk(self):
+# obj = NullableForeignKeySource(name='example', target=None)
+# obj.save()
+# serialized_data = NullableFKSourceSerializer(obj).data
- response = self.client.put(reverse('object-detail', args=[obj.pk]), serialized_data)
+# response = self.client.put(reverse('object-detail', args=[obj.pk]), serialized_data)
- self.assertEqual(response.data, serialized_data)
+# self.assertEqual(response.data, serialized_data)
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 8f9e0005..2e56d970 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -391,10 +391,10 @@ class CustomField(serializers.Field):
class BasicModelSerializer(serializers.Serializer):
text = CustomField()
- def __init__(self, *args, **kwargs):
- super(BasicModelSerializer, self).__init__(*args, **kwargs)
+ def to_native(self, value):
if 'view' not in self.context:
- raise RuntimeError("context isn't getting passed into serializer init")
+ raise RuntimeError("context isn't getting passed into serializer")
+ return super(BasicSerializer, self).to_native(value)
class TestContextPassedToCustomField(TestCase):
@@ -423,7 +423,7 @@ class LinksSerializer(serializers.Serializer):
class CustomPaginationSerializer(pagination.BasePaginationSerializer):
links = LinksSerializer(source='*') # Takes the page object as the source
- total_results = serializers.Field(source='paginator.count')
+ total_results = serializers.ReadOnlyField(source='paginator.count')
results_field = 'objects'
diff --git a/tests/test_permissions.py b/tests/test_permissions.py
index b90ba4f1..d5568c55 100644
--- a/tests/test_permissions.py
+++ b/tests/test_permissions.py
@@ -108,59 +108,59 @@ class ModelPermissionsIntegrationTests(TestCase):
response = instance_view(request, pk='2')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
- def test_options_permitted(self):
- request = factory.options(
- '/',
- HTTP_AUTHORIZATION=self.permitted_credentials
- )
- response = root_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn('actions', response.data)
- self.assertEqual(list(response.data['actions'].keys()), ['POST'])
-
- request = factory.options(
- '/1',
- HTTP_AUTHORIZATION=self.permitted_credentials
- )
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn('actions', response.data)
- self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
-
- def test_options_disallowed(self):
- request = factory.options(
- '/',
- HTTP_AUTHORIZATION=self.disallowed_credentials
- )
- response = root_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertNotIn('actions', response.data)
-
- request = factory.options(
- '/1',
- HTTP_AUTHORIZATION=self.disallowed_credentials
- )
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertNotIn('actions', response.data)
-
- def test_options_updateonly(self):
- request = factory.options(
- '/',
- HTTP_AUTHORIZATION=self.updateonly_credentials
- )
- response = root_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertNotIn('actions', response.data)
-
- request = factory.options(
- '/1',
- HTTP_AUTHORIZATION=self.updateonly_credentials
- )
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn('actions', response.data)
- self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
+ # def test_options_permitted(self):
+ # request = factory.options(
+ # '/',
+ # HTTP_AUTHORIZATION=self.permitted_credentials
+ # )
+ # response = root_view(request, pk='1')
+ # self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # self.assertIn('actions', response.data)
+ # self.assertEqual(list(response.data['actions'].keys()), ['POST'])
+
+ # request = factory.options(
+ # '/1',
+ # HTTP_AUTHORIZATION=self.permitted_credentials
+ # )
+ # response = instance_view(request, pk='1')
+ # self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # self.assertIn('actions', response.data)
+ # self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
+
+ # def test_options_disallowed(self):
+ # request = factory.options(
+ # '/',
+ # HTTP_AUTHORIZATION=self.disallowed_credentials
+ # )
+ # response = root_view(request, pk='1')
+ # self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # self.assertNotIn('actions', response.data)
+
+ # request = factory.options(
+ # '/1',
+ # HTTP_AUTHORIZATION=self.disallowed_credentials
+ # )
+ # response = instance_view(request, pk='1')
+ # self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # self.assertNotIn('actions', response.data)
+
+ # def test_options_updateonly(self):
+ # request = factory.options(
+ # '/',
+ # HTTP_AUTHORIZATION=self.updateonly_credentials
+ # )
+ # response = root_view(request, pk='1')
+ # self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # self.assertNotIn('actions', response.data)
+
+ # request = factory.options(
+ # '/1',
+ # HTTP_AUTHORIZATION=self.updateonly_credentials
+ # )
+ # response = instance_view(request, pk='1')
+ # self.assertEqual(response.status_code, status.HTTP_200_OK)
+ # self.assertIn('actions', response.data)
+ # self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
class BasicPermModel(models.Model):
diff --git a/tests/test_relations.py b/tests/test_relations.py
index bc1db69f..a30b12e6 100644
--- a/tests/test_relations.py
+++ b/tests/test_relations.py
@@ -1,149 +1,149 @@
-"""
-General tests for relational fields.
-"""
-from __future__ import unicode_literals
-from django import get_version
-from django.db import models
-from django.test import TestCase
-from django.utils import unittest
-from rest_framework import serializers
-from tests.models import BlogPost
-
-
-class NullModel(models.Model):
- pass
-
-
-class FieldTests(TestCase):
- def test_pk_related_field_with_empty_string(self):
- """
- Regression test for #446
-
- https://github.com/tomchristie/django-rest-framework/issues/446
- """
- field = serializers.PrimaryKeyRelatedField(queryset=NullModel.objects.all())
- self.assertRaises(serializers.ValidationError, field.from_native, '')
- self.assertRaises(serializers.ValidationError, field.from_native, [])
-
- def test_hyperlinked_related_field_with_empty_string(self):
- field = serializers.HyperlinkedRelatedField(queryset=NullModel.objects.all(), view_name='')
- self.assertRaises(serializers.ValidationError, field.from_native, '')
- self.assertRaises(serializers.ValidationError, field.from_native, [])
-
- def test_slug_related_field_with_empty_string(self):
- field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
- self.assertRaises(serializers.ValidationError, field.from_native, '')
- self.assertRaises(serializers.ValidationError, field.from_native, [])
-
-
-class TestManyRelatedMixin(TestCase):
- def test_missing_many_to_many_related_field(self):
- '''
- Regression test for #632
-
- https://github.com/tomchristie/django-rest-framework/pull/632
- '''
- field = serializers.RelatedField(many=True, read_only=False)
-
- into = {}
- field.field_from_native({}, None, 'field_name', into)
- self.assertEqual(into['field_name'], [])
-
-
-# Regression tests for #694 (`source` attribute on related fields)
-
-class RelatedFieldSourceTests(TestCase):
- def test_related_manager_source(self):
- """
- Relational fields should be able to use manager-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.RelatedField(many=True, source='get_blogposts_manager')
-
- class ClassWithManagerMethod(object):
- def get_blogposts_manager(self):
- return BlogPost.objects
-
- obj = ClassWithManagerMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['BlogPost object'])
-
- def test_related_queryset_source(self):
- """
- Relational fields should be able to use queryset-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.RelatedField(many=True, source='get_blogposts_queryset')
-
- class ClassWithQuerysetMethod(object):
- def get_blogposts_queryset(self):
- return BlogPost.objects.all()
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['BlogPost object'])
-
- def test_dotted_source(self):
- """
- Source argument should support dotted.source notation.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.RelatedField(many=True, source='a.b.c')
-
- class ClassWithQuerysetMethod(object):
- a = {
- 'b': {
- 'c': BlogPost.objects.all()
- }
- }
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['BlogPost object'])
-
- # Regression for #1129
- def test_exception_for_incorect_fk(self):
- """
- Check that the exception message are correct if the source field
- doesn't exist.
- """
- from tests.models import ManyToManySource
-
- class Meta:
- model = ManyToManySource
-
- attrs = {
- 'name': serializers.SlugRelatedField(
- slug_field='name', source='banzai'),
- 'Meta': Meta,
- }
-
- TestSerializer = type(
- str('TestSerializer'),
- (serializers.ModelSerializer,),
- attrs
- )
- with self.assertRaises(AttributeError):
- TestSerializer(data={'name': 'foo'})
-
-
-@unittest.skipIf(get_version() < '1.6.0', 'Upstream behaviour changed in v1.6')
-class RelatedFieldChoicesTests(TestCase):
- """
- Tests for #1408 "Web browseable API doesn't have blank option on drop down list box"
- https://github.com/tomchristie/django-rest-framework/issues/1408
- """
- def test_blank_option_is_added_to_choice_if_required_equals_false(self):
- """
-
- """
- post = BlogPost(title="Checking blank option is added")
- post.save()
-
- queryset = BlogPost.objects.all()
- field = serializers.RelatedField(required=False, queryset=queryset)
-
- choice_count = BlogPost.objects.count()
- widget_count = len(field.widget.choices)
-
- self.assertEqual(widget_count, choice_count + 1, 'BLANK_CHOICE_DASH option should have been added')
+# """
+# General tests for relational fields.
+# """
+# from __future__ import unicode_literals
+# from django import get_version
+# from django.db import models
+# from django.test import TestCase
+# from django.utils import unittest
+# from rest_framework import serializers
+# from tests.models import BlogPost
+
+
+# class NullModel(models.Model):
+# pass
+
+
+# class FieldTests(TestCase):
+# def test_pk_related_field_with_empty_string(self):
+# """
+# Regression test for #446
+
+# https://github.com/tomchristie/django-rest-framework/issues/446
+# """
+# field = serializers.PrimaryKeyRelatedField(queryset=NullModel.objects.all())
+# self.assertRaises(serializers.ValidationError, field.from_native, '')
+# self.assertRaises(serializers.ValidationError, field.from_native, [])
+
+# def test_hyperlinked_related_field_with_empty_string(self):
+# field = serializers.HyperlinkedRelatedField(queryset=NullModel.objects.all(), view_name='')
+# self.assertRaises(serializers.ValidationError, field.from_native, '')
+# self.assertRaises(serializers.ValidationError, field.from_native, [])
+
+# def test_slug_related_field_with_empty_string(self):
+# field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
+# self.assertRaises(serializers.ValidationError, field.from_native, '')
+# self.assertRaises(serializers.ValidationError, field.from_native, [])
+
+
+# class TestManyRelatedMixin(TestCase):
+# def test_missing_many_to_many_related_field(self):
+# '''
+# Regression test for #632
+
+# https://github.com/tomchristie/django-rest-framework/pull/632
+# '''
+# field = serializers.RelatedField(many=True, read_only=False)
+
+# into = {}
+# field.field_from_native({}, None, 'field_name', into)
+# self.assertEqual(into['field_name'], [])
+
+
+# # Regression tests for #694 (`source` attribute on related fields)
+
+# class RelatedFieldSourceTests(TestCase):
+# def test_related_manager_source(self):
+# """
+# Relational fields should be able to use manager-returning methods as their source.
+# """
+# BlogPost.objects.create(title='blah')
+# field = serializers.RelatedField(many=True, source='get_blogposts_manager')
+
+# class ClassWithManagerMethod(object):
+# def get_blogposts_manager(self):
+# return BlogPost.objects
+
+# obj = ClassWithManagerMethod()
+# value = field.field_to_native(obj, 'field_name')
+# self.assertEqual(value, ['BlogPost object'])
+
+# def test_related_queryset_source(self):
+# """
+# Relational fields should be able to use queryset-returning methods as their source.
+# """
+# BlogPost.objects.create(title='blah')
+# field = serializers.RelatedField(many=True, source='get_blogposts_queryset')
+
+# class ClassWithQuerysetMethod(object):
+# def get_blogposts_queryset(self):
+# return BlogPost.objects.all()
+
+# obj = ClassWithQuerysetMethod()
+# value = field.field_to_native(obj, 'field_name')
+# self.assertEqual(value, ['BlogPost object'])
+
+# def test_dotted_source(self):
+# """
+# Source argument should support dotted.source notation.
+# """
+# BlogPost.objects.create(title='blah')
+# field = serializers.RelatedField(many=True, source='a.b.c')
+
+# class ClassWithQuerysetMethod(object):
+# a = {
+# 'b': {
+# 'c': BlogPost.objects.all()
+# }
+# }
+
+# obj = ClassWithQuerysetMethod()
+# value = field.field_to_native(obj, 'field_name')
+# self.assertEqual(value, ['BlogPost object'])
+
+# # Regression for #1129
+# def test_exception_for_incorect_fk(self):
+# """
+# Check that the exception message are correct if the source field
+# doesn't exist.
+# """
+# from tests.models import ManyToManySource
+
+# class Meta:
+# model = ManyToManySource
+
+# attrs = {
+# 'name': serializers.SlugRelatedField(
+# slug_field='name', source='banzai'),
+# 'Meta': Meta,
+# }
+
+# TestSerializer = type(
+# str('TestSerializer'),
+# (serializers.ModelSerializer,),
+# attrs
+# )
+# with self.assertRaises(AttributeError):
+# TestSerializer(data={'name': 'foo'})
+
+
+# @unittest.skipIf(get_version() < '1.6.0', 'Upstream behaviour changed in v1.6')
+# class RelatedFieldChoicesTests(TestCase):
+# """
+# Tests for #1408 "Web browseable API doesn't have blank option on drop down list box"
+# https://github.com/tomchristie/django-rest-framework/issues/1408
+# """
+# def test_blank_option_is_added_to_choice_if_required_equals_false(self):
+# """
+
+# """
+# post = BlogPost(title="Checking blank option is added")
+# post.save()
+
+# queryset = BlogPost.objects.all()
+# field = serializers.RelatedField(required=False, queryset=queryset)
+
+# choice_count = BlogPost.objects.count()
+# widget_count = len(field.widget.choices)
+
+# self.assertEqual(widget_count, choice_count + 1, 'BLANK_CHOICE_DASH option should have been added')
diff --git a/tests/test_relations_hyperlink.py b/tests/test_relations_hyperlink.py
index 0c8eb254..315d1abf 100644
--- a/tests/test_relations_hyperlink.py
+++ b/tests/test_relations_hyperlink.py
@@ -1,525 +1,525 @@
-from __future__ import unicode_literals
-from django.conf.urls import patterns, url
-from django.test import TestCase
-from rest_framework import serializers
-from rest_framework.test import APIRequestFactory
-from tests.models import (
- BlogPost,
- ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
- NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
-)
-
-factory = APIRequestFactory()
-request = factory.get('/') # Just to ensure we have a request in the serializer context
-
-
-def dummy_view(request, pk):
- pass
-
-urlpatterns = patterns(
- '',
- url(r'^dummyurl/(?P[0-9]+)/$', dummy_view, name='dummy-url'),
- url(r'^manytomanysource/(?P[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
- url(r'^manytomanytarget/(?P[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
- url(r'^foreignkeysource/(?P[0-9]+)/$', dummy_view, name='foreignkeysource-detail'),
- url(r'^foreignkeytarget/(?P[0-9]+)/$', dummy_view, name='foreignkeytarget-detail'),
- url(r'^nullableforeignkeysource/(?P[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'),
- url(r'^onetoonetarget/(?P[0-9]+)/$', dummy_view, name='onetoonetarget-detail'),
- url(r'^nullableonetoonesource/(?P[0-9]+)/$', dummy_view, name='nullableonetoonesource-detail'),
-)
-
-
-# ManyToMany
-class ManyToManyTargetSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = ManyToManyTarget
- fields = ('url', 'name', 'sources')
-
-
-class ManyToManySourceSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = ManyToManySource
- fields = ('url', 'name', 'targets')
-
-
-# ForeignKey
-class ForeignKeyTargetSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = ForeignKeyTarget
- fields = ('url', 'name', 'sources')
-
-
-class ForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = ForeignKeySource
- fields = ('url', 'name', 'target')
-
-
-# Nullable ForeignKey
-class NullableForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = NullableForeignKeySource
- fields = ('url', 'name', 'target')
-
-
-# Nullable OneToOne
-class NullableOneToOneTargetSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = OneToOneTarget
- fields = ('url', 'name', 'nullable_source')
-
-
-# TODO: Add test that .data cannot be accessed prior to .is_valid
-
-class HyperlinkedManyToManyTests(TestCase):
- urls = 'tests.test_relations_hyperlink'
-
- def setUp(self):
- for idx in range(1, 4):
- target = ManyToManyTarget(name='target-%d' % idx)
- target.save()
- source = ManyToManySource(name='source-%d' % idx)
- source.save()
- for target in ManyToManyTarget.objects.all():
- source.targets.add(target)
-
- def test_many_to_many_retrieve(self):
- queryset = ManyToManySource.objects.all()
- serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']},
- {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
- {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_many_to_many_retrieve(self):
- queryset = ManyToManyTarget.objects.all()
- serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
- {'url': 'http://testserver/manytomanytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
- {'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_many_to_many_update(self):
- data = {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
- instance = ManyToManySource.objects.get(pk=1)
- serializer = ManyToManySourceSerializer(instance, data=data, context={'request': request})
- self.assertTrue(serializer.is_valid())
- serializer.save()
- self.assertEqual(serializer.data, data)
-
- # Ensure source 1 is updated, and everything else is as expected
- queryset = ManyToManySource.objects.all()
- serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
- {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
- {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_many_to_many_update(self):
- data = {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/']}
- instance = ManyToManyTarget.objects.get(pk=1)
- serializer = ManyToManyTargetSerializer(instance, data=data, context={'request': request})
- self.assertTrue(serializer.is_valid())
- serializer.save()
- self.assertEqual(serializer.data, data)
-
- # Ensure target 1 is updated, and everything else is as expected
- queryset = ManyToManyTarget.objects.all()
- serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/']},
- {'url': 'http://testserver/manytomanytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
- {'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']}
-
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_many_to_many_create(self):
- data = {'url': 'http://testserver/manytomanysource/4/', 'name': 'source-4', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/3/']}
- serializer = ManyToManySourceSerializer(data=data, context={'request': request})
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'source-4')
-
- # Ensure source 4 is added, and everything else is as expected
- queryset = ManyToManySource.objects.all()
- serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']},
- {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
- {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
- {'url': 'http://testserver/manytomanysource/4/', 'name': 'source-4', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/3/']}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_many_to_many_create(self):
- data = {'url': 'http://testserver/manytomanytarget/4/', 'name': 'target-4', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/3/']}
- serializer = ManyToManyTargetSerializer(data=data, context={'request': request})
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'target-4')
-
- # Ensure target 4 is added, and everything else is as expected
- queryset = ManyToManyTarget.objects.all()
- serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
- {'url': 'http://testserver/manytomanytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
- {'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']},
- {'url': 'http://testserver/manytomanytarget/4/', 'name': 'target-4', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/3/']}
- ]
- self.assertEqual(serializer.data, expected)
-
-
-class HyperlinkedForeignKeyTests(TestCase):
- urls = 'tests.test_relations_hyperlink'
-
- def setUp(self):
- target = ForeignKeyTarget(name='target-1')
- target.save()
- new_target = ForeignKeyTarget(name='target-2')
- new_target.save()
- for idx in range(1, 4):
- source = ForeignKeySource(name='source-%d' % idx, target=target)
- source.save()
-
- def test_foreign_key_retrieve(self):
- queryset = ForeignKeySource.objects.all()
- serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_foreign_key_retrieve(self):
- queryset = ForeignKeyTarget.objects.all()
- serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/2/', 'http://testserver/foreignkeysource/3/']},
- {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update(self):
- data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'}
- instance = ForeignKeySource.objects.get(pk=1)
- serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
- serializer.save()
-
- # Ensure source 1 is updated, and everything else is as expected
- queryset = ForeignKeySource.objects.all()
- serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'},
- {'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update_incorrect_type(self):
- data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 2}
- instance = ForeignKeySource.objects.get(pk=1)
- serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected url string, received int.']})
-
- def test_reverse_foreign_key_update(self):
- data = {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}
- instance = ForeignKeyTarget.objects.get(pk=2)
- serializer = ForeignKeyTargetSerializer(instance, data=data, context={'request': request})
- self.assertTrue(serializer.is_valid())
- # We shouldn't have saved anything to the db yet since save
- # hasn't been called.
- queryset = ForeignKeyTarget.objects.all()
- new_serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/2/', 'http://testserver/foreignkeysource/3/']},
- {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
- ]
- self.assertEqual(new_serializer.data, expected)
-
- serializer.save()
- self.assertEqual(serializer.data, data)
-
- # Ensure target 2 is update, and everything else is as expected
- queryset = ForeignKeyTarget.objects.all()
- serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/2/']},
- {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_create(self):
- data = {'url': 'http://testserver/foreignkeysource/4/', 'name': 'source-4', 'target': 'http://testserver/foreignkeytarget/2/'}
- serializer = ForeignKeySourceSerializer(data=data, context={'request': request})
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'source-4')
-
- # Ensure source 1 is updated, and everything else is as expected
- queryset = ForeignKeySource.objects.all()
- serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/foreignkeysource/4/', 'name': 'source-4', 'target': 'http://testserver/foreignkeytarget/2/'},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_foreign_key_create(self):
- data = {'url': 'http://testserver/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}
- serializer = ForeignKeyTargetSerializer(data=data, context={'request': request})
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'target-3')
-
- # Ensure target 4 is added, and everything else is as expected
- queryset = ForeignKeyTarget.objects.all()
- serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/2/']},
- {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
- {'url': 'http://testserver/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update_with_invalid_null(self):
- data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': None}
- instance = ForeignKeySource.objects.get(pk=1)
- serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['This field is required.']})
-
-
-class HyperlinkedNullableForeignKeyTests(TestCase):
- urls = 'tests.test_relations_hyperlink'
-
- def setUp(self):
- target = ForeignKeyTarget(name='target-1')
- target.save()
- for idx in range(1, 4):
- if idx == 3:
- target = None
- source = NullableForeignKeySource(name='source-%d' % idx, target=target)
- source.save()
-
- def test_foreign_key_retrieve_with_null(self):
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_create_with_valid_null(self):
- data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
- serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request})
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'source-4')
-
- # Ensure source 4 is created, and everything else is as expected
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
- {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_create_with_valid_emptystring(self):
- """
- The emptystring should be interpreted as null in the context
- of relationships.
- """
- data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': ''}
- expected_data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
- serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request})
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, expected_data)
- self.assertEqual(obj.name, 'source-4')
-
- # Ensure source 4 is created, and everything else is as expected
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
- {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update_with_valid_null(self):
- data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}
- instance = NullableForeignKeySource.objects.get(pk=1)
- serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
- serializer.save()
-
- # Ensure source 1 is updated, and everything else is as expected
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None},
- {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update_with_valid_emptystring(self):
- """
- The emptystring should be interpreted as null in the context
- of relationships.
- """
- data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': ''}
- expected_data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}
- instance = NullableForeignKeySource.objects.get(pk=1)
- serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, expected_data)
- serializer.save()
-
- # Ensure source 1 is updated, and everything else is as expected
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None},
- {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
- {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
- ]
- self.assertEqual(serializer.data, expected)
-
- # reverse foreign keys MUST be read_only
- # In the general case they do not provide .remove() or .clear()
- # and cannot be arbitrarily set.
-
- # def test_reverse_foreign_key_update(self):
- # data = {'id': 1, 'name': 'target-1', 'sources': [1]}
- # instance = ForeignKeyTarget.objects.get(pk=1)
- # serializer = ForeignKeyTargetSerializer(instance, data=data)
- # self.assertTrue(serializer.is_valid())
- # self.assertEqual(serializer.data, data)
- # serializer.save()
-
- # # Ensure target 1 is updated, and everything else is as expected
- # queryset = ForeignKeyTarget.objects.all()
- # serializer = ForeignKeyTargetSerializer(queryset, many=True)
- # expected = [
- # {'id': 1, 'name': 'target-1', 'sources': [1]},
- # {'id': 2, 'name': 'target-2', 'sources': []},
- # ]
- # self.assertEqual(serializer.data, expected)
-
-
-class HyperlinkedNullableOneToOneTests(TestCase):
- urls = 'tests.test_relations_hyperlink'
-
- def setUp(self):
- target = OneToOneTarget(name='target-1')
- target.save()
- new_target = OneToOneTarget(name='target-2')
- new_target.save()
- source = NullableOneToOneSource(name='source-1', target=target)
- source.save()
-
- def test_reverse_foreign_key_retrieve_with_null(self):
- queryset = OneToOneTarget.objects.all()
- serializer = NullableOneToOneTargetSerializer(queryset, many=True, context={'request': request})
- expected = [
- {'url': 'http://testserver/onetoonetarget/1/', 'name': 'target-1', 'nullable_source': 'http://testserver/nullableonetoonesource/1/'},
- {'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
- ]
- self.assertEqual(serializer.data, expected)
-
-
-# Regression tests for #694 (`source` attribute on related fields)
-
-class HyperlinkedRelatedFieldSourceTests(TestCase):
- urls = 'tests.test_relations_hyperlink'
-
- def test_related_manager_source(self):
- """
- Relational fields should be able to use manager-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.HyperlinkedRelatedField(
- many=True,
- source='get_blogposts_manager',
- view_name='dummy-url',
- )
- field.context = {'request': request}
-
- class ClassWithManagerMethod(object):
- def get_blogposts_manager(self):
- return BlogPost.objects
-
- obj = ClassWithManagerMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['http://testserver/dummyurl/1/'])
-
- def test_related_queryset_source(self):
- """
- Relational fields should be able to use queryset-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.HyperlinkedRelatedField(
- many=True,
- source='get_blogposts_queryset',
- view_name='dummy-url',
- )
- field.context = {'request': request}
-
- class ClassWithQuerysetMethod(object):
- def get_blogposts_queryset(self):
- return BlogPost.objects.all()
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['http://testserver/dummyurl/1/'])
-
- def test_dotted_source(self):
- """
- Source argument should support dotted.source notation.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.HyperlinkedRelatedField(
- many=True,
- source='a.b.c',
- view_name='dummy-url',
- )
- field.context = {'request': request}
-
- class ClassWithQuerysetMethod(object):
- a = {
- 'b': {
- 'c': BlogPost.objects.all()
- }
- }
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['http://testserver/dummyurl/1/'])
+# from __future__ import unicode_literals
+# from django.conf.urls import patterns, url
+# from django.test import TestCase
+# from rest_framework import serializers
+# from rest_framework.test import APIRequestFactory
+# from tests.models import (
+# BlogPost,
+# ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
+# NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
+# )
+
+# factory = APIRequestFactory()
+# request = factory.get('/') # Just to ensure we have a request in the serializer context
+
+
+# def dummy_view(request, pk):
+# pass
+
+# urlpatterns = patterns(
+# '',
+# url(r'^dummyurl/(?P[0-9]+)/$', dummy_view, name='dummy-url'),
+# url(r'^manytomanysource/(?P[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
+# url(r'^manytomanytarget/(?P[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
+# url(r'^foreignkeysource/(?P[0-9]+)/$', dummy_view, name='foreignkeysource-detail'),
+# url(r'^foreignkeytarget/(?P[0-9]+)/$', dummy_view, name='foreignkeytarget-detail'),
+# url(r'^nullableforeignkeysource/(?P[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'),
+# url(r'^onetoonetarget/(?P[0-9]+)/$', dummy_view, name='onetoonetarget-detail'),
+# url(r'^nullableonetoonesource/(?P[0-9]+)/$', dummy_view, name='nullableonetoonesource-detail'),
+# )
+
+
+# # ManyToMany
+# class ManyToManyTargetSerializer(serializers.HyperlinkedModelSerializer):
+# class Meta:
+# model = ManyToManyTarget
+# fields = ('url', 'name', 'sources')
+
+
+# class ManyToManySourceSerializer(serializers.HyperlinkedModelSerializer):
+# class Meta:
+# model = ManyToManySource
+# fields = ('url', 'name', 'targets')
+
+
+# # ForeignKey
+# class ForeignKeyTargetSerializer(serializers.HyperlinkedModelSerializer):
+# class Meta:
+# model = ForeignKeyTarget
+# fields = ('url', 'name', 'sources')
+
+
+# class ForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
+# class Meta:
+# model = ForeignKeySource
+# fields = ('url', 'name', 'target')
+
+
+# # Nullable ForeignKey
+# class NullableForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
+# class Meta:
+# model = NullableForeignKeySource
+# fields = ('url', 'name', 'target')
+
+
+# # Nullable OneToOne
+# class NullableOneToOneTargetSerializer(serializers.HyperlinkedModelSerializer):
+# class Meta:
+# model = OneToOneTarget
+# fields = ('url', 'name', 'nullable_source')
+
+
+# # TODO: Add test that .data cannot be accessed prior to .is_valid
+
+# class HyperlinkedManyToManyTests(TestCase):
+# urls = 'tests.test_relations_hyperlink'
+
+# def setUp(self):
+# for idx in range(1, 4):
+# target = ManyToManyTarget(name='target-%d' % idx)
+# target.save()
+# source = ManyToManySource(name='source-%d' % idx)
+# source.save()
+# for target in ManyToManyTarget.objects.all():
+# source.targets.add(target)
+
+# def test_many_to_many_retrieve(self):
+# queryset = ManyToManySource.objects.all()
+# serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']},
+# {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
+# {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_reverse_many_to_many_retrieve(self):
+# queryset = ManyToManyTarget.objects.all()
+# serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
+# {'url': 'http://testserver/manytomanytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
+# {'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_many_to_many_update(self):
+# data = {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
+# instance = ManyToManySource.objects.get(pk=1)
+# serializer = ManyToManySourceSerializer(instance, data=data, context={'request': request})
+# self.assertTrue(serializer.is_valid())
+# serializer.save()
+# self.assertEqual(serializer.data, data)
+
+# # Ensure source 1 is updated, and everything else is as expected
+# queryset = ManyToManySource.objects.all()
+# serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
+# {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
+# {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_reverse_many_to_many_update(self):
+# data = {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/']}
+# instance = ManyToManyTarget.objects.get(pk=1)
+# serializer = ManyToManyTargetSerializer(instance, data=data, context={'request': request})
+# self.assertTrue(serializer.is_valid())
+# serializer.save()
+# self.assertEqual(serializer.data, data)
+
+# # Ensure target 1 is updated, and everything else is as expected
+# queryset = ManyToManyTarget.objects.all()
+# serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/']},
+# {'url': 'http://testserver/manytomanytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
+# {'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']}
+
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_many_to_many_create(self):
+# data = {'url': 'http://testserver/manytomanysource/4/', 'name': 'source-4', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/3/']}
+# serializer = ManyToManySourceSerializer(data=data, context={'request': request})
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'source-4')
+
+# # Ensure source 4 is added, and everything else is as expected
+# queryset = ManyToManySource.objects.all()
+# serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']},
+# {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
+# {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
+# {'url': 'http://testserver/manytomanysource/4/', 'name': 'source-4', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/3/']}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_reverse_many_to_many_create(self):
+# data = {'url': 'http://testserver/manytomanytarget/4/', 'name': 'target-4', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/3/']}
+# serializer = ManyToManyTargetSerializer(data=data, context={'request': request})
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'target-4')
+
+# # Ensure target 4 is added, and everything else is as expected
+# queryset = ManyToManyTarget.objects.all()
+# serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
+# {'url': 'http://testserver/manytomanytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
+# {'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']},
+# {'url': 'http://testserver/manytomanytarget/4/', 'name': 'target-4', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/3/']}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+
+# class HyperlinkedForeignKeyTests(TestCase):
+# urls = 'tests.test_relations_hyperlink'
+
+# def setUp(self):
+# target = ForeignKeyTarget(name='target-1')
+# target.save()
+# new_target = ForeignKeyTarget(name='target-2')
+# new_target.save()
+# for idx in range(1, 4):
+# source = ForeignKeySource(name='source-%d' % idx, target=target)
+# source.save()
+
+# def test_foreign_key_retrieve(self):
+# queryset = ForeignKeySource.objects.all()
+# serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_reverse_foreign_key_retrieve(self):
+# queryset = ForeignKeyTarget.objects.all()
+# serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/2/', 'http://testserver/foreignkeysource/3/']},
+# {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update(self):
+# data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'}
+# instance = ForeignKeySource.objects.get(pk=1)
+# serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.data, data)
+# serializer.save()
+
+# # Ensure source 1 is updated, and everything else is as expected
+# queryset = ForeignKeySource.objects.all()
+# serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'},
+# {'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update_incorrect_type(self):
+# data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 2}
+# instance = ForeignKeySource.objects.get(pk=1)
+# serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected url string, received int.']})
+
+# def test_reverse_foreign_key_update(self):
+# data = {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}
+# instance = ForeignKeyTarget.objects.get(pk=2)
+# serializer = ForeignKeyTargetSerializer(instance, data=data, context={'request': request})
+# self.assertTrue(serializer.is_valid())
+# # We shouldn't have saved anything to the db yet since save
+# # hasn't been called.
+# queryset = ForeignKeyTarget.objects.all()
+# new_serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/2/', 'http://testserver/foreignkeysource/3/']},
+# {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
+# ]
+# self.assertEqual(new_serializer.data, expected)
+
+# serializer.save()
+# self.assertEqual(serializer.data, data)
+
+# # Ensure target 2 is update, and everything else is as expected
+# queryset = ForeignKeyTarget.objects.all()
+# serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/2/']},
+# {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_create(self):
+# data = {'url': 'http://testserver/foreignkeysource/4/', 'name': 'source-4', 'target': 'http://testserver/foreignkeytarget/2/'}
+# serializer = ForeignKeySourceSerializer(data=data, context={'request': request})
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'source-4')
+
+# # Ensure source 1 is updated, and everything else is as expected
+# queryset = ForeignKeySource.objects.all()
+# serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/foreignkeysource/4/', 'name': 'source-4', 'target': 'http://testserver/foreignkeytarget/2/'},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_reverse_foreign_key_create(self):
+# data = {'url': 'http://testserver/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}
+# serializer = ForeignKeyTargetSerializer(data=data, context={'request': request})
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'target-3')
+
+# # Ensure target 4 is added, and everything else is as expected
+# queryset = ForeignKeyTarget.objects.all()
+# serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/2/']},
+# {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
+# {'url': 'http://testserver/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update_with_invalid_null(self):
+# data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': None}
+# instance = ForeignKeySource.objects.get(pk=1)
+# serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'target': ['This field is required.']})
+
+
+# class HyperlinkedNullableForeignKeyTests(TestCase):
+# urls = 'tests.test_relations_hyperlink'
+
+# def setUp(self):
+# target = ForeignKeyTarget(name='target-1')
+# target.save()
+# for idx in range(1, 4):
+# if idx == 3:
+# target = None
+# source = NullableForeignKeySource(name='source-%d' % idx, target=target)
+# source.save()
+
+# def test_foreign_key_retrieve_with_null(self):
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_create_with_valid_null(self):
+# data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
+# serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request})
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'source-4')
+
+# # Ensure source 4 is created, and everything else is as expected
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
+# {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_create_with_valid_emptystring(self):
+# """
+# The emptystring should be interpreted as null in the context
+# of relationships.
+# """
+# data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': ''}
+# expected_data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
+# serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request})
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, expected_data)
+# self.assertEqual(obj.name, 'source-4')
+
+# # Ensure source 4 is created, and everything else is as expected
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
+# {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update_with_valid_null(self):
+# data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}
+# instance = NullableForeignKeySource.objects.get(pk=1)
+# serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.data, data)
+# serializer.save()
+
+# # Ensure source 1 is updated, and everything else is as expected
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None},
+# {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update_with_valid_emptystring(self):
+# """
+# The emptystring should be interpreted as null in the context
+# of relationships.
+# """
+# data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': ''}
+# expected_data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}
+# instance = NullableForeignKeySource.objects.get(pk=1)
+# serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.data, expected_data)
+# serializer.save()
+
+# # Ensure source 1 is updated, and everything else is as expected
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None},
+# {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
+# {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# # reverse foreign keys MUST be read_only
+# # In the general case they do not provide .remove() or .clear()
+# # and cannot be arbitrarily set.
+
+# # def test_reverse_foreign_key_update(self):
+# # data = {'id': 1, 'name': 'target-1', 'sources': [1]}
+# # instance = ForeignKeyTarget.objects.get(pk=1)
+# # serializer = ForeignKeyTargetSerializer(instance, data=data)
+# # self.assertTrue(serializer.is_valid())
+# # self.assertEqual(serializer.data, data)
+# # serializer.save()
+
+# # # Ensure target 1 is updated, and everything else is as expected
+# # queryset = ForeignKeyTarget.objects.all()
+# # serializer = ForeignKeyTargetSerializer(queryset, many=True)
+# # expected = [
+# # {'id': 1, 'name': 'target-1', 'sources': [1]},
+# # {'id': 2, 'name': 'target-2', 'sources': []},
+# # ]
+# # self.assertEqual(serializer.data, expected)
+
+
+# class HyperlinkedNullableOneToOneTests(TestCase):
+# urls = 'tests.test_relations_hyperlink'
+
+# def setUp(self):
+# target = OneToOneTarget(name='target-1')
+# target.save()
+# new_target = OneToOneTarget(name='target-2')
+# new_target.save()
+# source = NullableOneToOneSource(name='source-1', target=target)
+# source.save()
+
+# def test_reverse_foreign_key_retrieve_with_null(self):
+# queryset = OneToOneTarget.objects.all()
+# serializer = NullableOneToOneTargetSerializer(queryset, many=True, context={'request': request})
+# expected = [
+# {'url': 'http://testserver/onetoonetarget/1/', 'name': 'target-1', 'nullable_source': 'http://testserver/nullableonetoonesource/1/'},
+# {'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+
+# # Regression tests for #694 (`source` attribute on related fields)
+
+# class HyperlinkedRelatedFieldSourceTests(TestCase):
+# urls = 'tests.test_relations_hyperlink'
+
+# def test_related_manager_source(self):
+# """
+# Relational fields should be able to use manager-returning methods as their source.
+# """
+# BlogPost.objects.create(title='blah')
+# field = serializers.HyperlinkedRelatedField(
+# many=True,
+# source='get_blogposts_manager',
+# view_name='dummy-url',
+# )
+# field.context = {'request': request}
+
+# class ClassWithManagerMethod(object):
+# def get_blogposts_manager(self):
+# return BlogPost.objects
+
+# obj = ClassWithManagerMethod()
+# value = field.field_to_native(obj, 'field_name')
+# self.assertEqual(value, ['http://testserver/dummyurl/1/'])
+
+# def test_related_queryset_source(self):
+# """
+# Relational fields should be able to use queryset-returning methods as their source.
+# """
+# BlogPost.objects.create(title='blah')
+# field = serializers.HyperlinkedRelatedField(
+# many=True,
+# source='get_blogposts_queryset',
+# view_name='dummy-url',
+# )
+# field.context = {'request': request}
+
+# class ClassWithQuerysetMethod(object):
+# def get_blogposts_queryset(self):
+# return BlogPost.objects.all()
+
+# obj = ClassWithQuerysetMethod()
+# value = field.field_to_native(obj, 'field_name')
+# self.assertEqual(value, ['http://testserver/dummyurl/1/'])
+
+# def test_dotted_source(self):
+# """
+# Source argument should support dotted.source notation.
+# """
+# BlogPost.objects.create(title='blah')
+# field = serializers.HyperlinkedRelatedField(
+# many=True,
+# source='a.b.c',
+# view_name='dummy-url',
+# )
+# field.context = {'request': request}
+
+# class ClassWithQuerysetMethod(object):
+# a = {
+# 'b': {
+# 'c': BlogPost.objects.all()
+# }
+# }
+
+# obj = ClassWithQuerysetMethod()
+# value = field.field_to_native(obj, 'field_name')
+# self.assertEqual(value, ['http://testserver/dummyurl/1/'])
diff --git a/tests/test_relations_nested.py b/tests/test_relations_nested.py
index 4d9da489..4a99fee9 100644
--- a/tests/test_relations_nested.py
+++ b/tests/test_relations_nested.py
@@ -1,326 +1,326 @@
-from __future__ import unicode_literals
-from django.db import models
-from django.test import TestCase
-from rest_framework import serializers
-
-from .models import OneToOneTarget
-
-
-class OneToOneSource(models.Model):
- name = models.CharField(max_length=100)
- target = models.OneToOneField(OneToOneTarget, related_name='source',
- null=True, blank=True)
-
-
-class OneToManyTarget(models.Model):
- name = models.CharField(max_length=100)
-
-
-class OneToManySource(models.Model):
- name = models.CharField(max_length=100)
- target = models.ForeignKey(OneToManyTarget, related_name='sources')
-
-
-class ReverseNestedOneToOneTests(TestCase):
- def setUp(self):
- class OneToOneSourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = OneToOneSource
- fields = ('id', 'name')
-
- class OneToOneTargetSerializer(serializers.ModelSerializer):
- source = OneToOneSourceSerializer()
-
- class Meta:
- model = OneToOneTarget
- fields = ('id', 'name', 'source')
-
- self.Serializer = OneToOneTargetSerializer
-
- for idx in range(1, 4):
- target = OneToOneTarget(name='target-%d' % idx)
- target.save()
- source = OneToOneSource(name='source-%d' % idx, target=target)
- source.save()
-
- def test_one_to_one_retrieve(self):
- queryset = OneToOneTarget.objects.all()
- serializer = self.Serializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}},
- {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}},
- {'id': 3, 'name': 'target-3', 'source': {'id': 3, 'name': 'source-3'}}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_one_to_one_create(self):
- data = {'id': 4, 'name': 'target-4', 'source': {'id': 4, 'name': 'source-4'}}
- serializer = self.Serializer(data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'target-4')
-
- # Ensure (target 4, target_source 4, source 4) are added, and
- # everything else is as expected.
- queryset = OneToOneTarget.objects.all()
- serializer = self.Serializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}},
- {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}},
- {'id': 3, 'name': 'target-3', 'source': {'id': 3, 'name': 'source-3'}},
- {'id': 4, 'name': 'target-4', 'source': {'id': 4, 'name': 'source-4'}}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_one_to_one_create_with_invalid_data(self):
- data = {'id': 4, 'name': 'target-4', 'source': {'id': 4}}
- serializer = self.Serializer(data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'source': [{'name': ['This field is required.']}]})
-
- def test_one_to_one_update(self):
- data = {'id': 3, 'name': 'target-3-updated', 'source': {'id': 3, 'name': 'source-3-updated'}}
- instance = OneToOneTarget.objects.get(pk=3)
- serializer = self.Serializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'target-3-updated')
-
- # Ensure (target 3, target_source 3, source 3) are updated,
- # and everything else is as expected.
- queryset = OneToOneTarget.objects.all()
- serializer = self.Serializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}},
- {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}},
- {'id': 3, 'name': 'target-3-updated', 'source': {'id': 3, 'name': 'source-3-updated'}}
- ]
- self.assertEqual(serializer.data, expected)
-
-
-class ForwardNestedOneToOneTests(TestCase):
- def setUp(self):
- class OneToOneTargetSerializer(serializers.ModelSerializer):
- class Meta:
- model = OneToOneTarget
- fields = ('id', 'name')
-
- class OneToOneSourceSerializer(serializers.ModelSerializer):
- target = OneToOneTargetSerializer()
-
- class Meta:
- model = OneToOneSource
- fields = ('id', 'name', 'target')
-
- self.Serializer = OneToOneSourceSerializer
-
- for idx in range(1, 4):
- target = OneToOneTarget(name='target-%d' % idx)
- target.save()
- source = OneToOneSource(name='source-%d' % idx, target=target)
- source.save()
-
- def test_one_to_one_retrieve(self):
- queryset = OneToOneSource.objects.all()
- serializer = self.Serializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
- {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}},
- {'id': 3, 'name': 'source-3', 'target': {'id': 3, 'name': 'target-3'}}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_one_to_one_create(self):
- data = {'id': 4, 'name': 'source-4', 'target': {'id': 4, 'name': 'target-4'}}
- serializer = self.Serializer(data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'source-4')
-
- # Ensure (target 4, target_source 4, source 4) are added, and
- # everything else is as expected.
- queryset = OneToOneSource.objects.all()
- serializer = self.Serializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
- {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}},
- {'id': 3, 'name': 'source-3', 'target': {'id': 3, 'name': 'target-3'}},
- {'id': 4, 'name': 'source-4', 'target': {'id': 4, 'name': 'target-4'}}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_one_to_one_create_with_invalid_data(self):
- data = {'id': 4, 'name': 'source-4', 'target': {'id': 4}}
- serializer = self.Serializer(data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': [{'name': ['This field is required.']}]})
-
- def test_one_to_one_update(self):
- data = {'id': 3, 'name': 'source-3-updated', 'target': {'id': 3, 'name': 'target-3-updated'}}
- instance = OneToOneSource.objects.get(pk=3)
- serializer = self.Serializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'source-3-updated')
-
- # Ensure (target 3, target_source 3, source 3) are updated,
- # and everything else is as expected.
- queryset = OneToOneSource.objects.all()
- serializer = self.Serializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
- {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}},
- {'id': 3, 'name': 'source-3-updated', 'target': {'id': 3, 'name': 'target-3-updated'}}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_one_to_one_update_to_null(self):
- data = {'id': 3, 'name': 'source-3-updated', 'target': None}
- instance = OneToOneSource.objects.get(pk=3)
- serializer = self.Serializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
-
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'source-3-updated')
- self.assertEqual(obj.target, None)
-
- queryset = OneToOneSource.objects.all()
- serializer = self.Serializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
- {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}},
- {'id': 3, 'name': 'source-3-updated', 'target': None}
- ]
- self.assertEqual(serializer.data, expected)
-
- # TODO: Nullable 1-1 tests
- # def test_one_to_one_delete(self):
- # data = {'id': 3, 'name': 'target-3', 'target_source': None}
- # instance = OneToOneTarget.objects.get(pk=3)
- # serializer = self.Serializer(instance, data=data)
- # self.assertTrue(serializer.is_valid())
- # serializer.save()
-
- # # Ensure (target_source 3, source 3) are deleted,
- # # and everything else is as expected.
- # queryset = OneToOneTarget.objects.all()
- # serializer = self.Serializer(queryset)
- # expected = [
- # {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}},
- # {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}},
- # {'id': 3, 'name': 'target-3', 'source': None}
- # ]
- # self.assertEqual(serializer.data, expected)
-
-
-class ReverseNestedOneToManyTests(TestCase):
- def setUp(self):
- class OneToManySourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = OneToManySource
- fields = ('id', 'name')
-
- class OneToManyTargetSerializer(serializers.ModelSerializer):
- sources = OneToManySourceSerializer(many=True, allow_add_remove=True)
-
- class Meta:
- model = OneToManyTarget
- fields = ('id', 'name', 'sources')
-
- self.Serializer = OneToManyTargetSerializer
-
- target = OneToManyTarget(name='target-1')
- target.save()
- for idx in range(1, 4):
- source = OneToManySource(name='source-%d' % idx, target=target)
- source.save()
-
- def test_one_to_many_retrieve(self):
- queryset = OneToManyTarget.objects.all()
- serializer = self.Serializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
- {'id': 2, 'name': 'source-2'},
- {'id': 3, 'name': 'source-3'}]},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_one_to_many_create(self):
- data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
- {'id': 2, 'name': 'source-2'},
- {'id': 3, 'name': 'source-3'},
- {'id': 4, 'name': 'source-4'}]}
- instance = OneToManyTarget.objects.get(pk=1)
- serializer = self.Serializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'target-1')
-
- # Ensure source 4 is added, and everything else is as
- # expected.
- queryset = OneToManyTarget.objects.all()
- serializer = self.Serializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
- {'id': 2, 'name': 'source-2'},
- {'id': 3, 'name': 'source-3'},
- {'id': 4, 'name': 'source-4'}]}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_one_to_many_create_with_invalid_data(self):
- data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
- {'id': 2, 'name': 'source-2'},
- {'id': 3, 'name': 'source-3'},
- {'id': 4}]}
- serializer = self.Serializer(data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'sources': [{}, {}, {}, {'name': ['This field is required.']}]})
-
- def test_one_to_many_update(self):
- data = {'id': 1, 'name': 'target-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'},
- {'id': 2, 'name': 'source-2'},
- {'id': 3, 'name': 'source-3'}]}
- instance = OneToManyTarget.objects.get(pk=1)
- serializer = self.Serializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'target-1-updated')
-
- # Ensure (target 1, source 1) are updated,
- # and everything else is as expected.
- queryset = OneToManyTarget.objects.all()
- serializer = self.Serializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'},
- {'id': 2, 'name': 'source-2'},
- {'id': 3, 'name': 'source-3'}]}
-
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_one_to_many_delete(self):
- data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
- {'id': 3, 'name': 'source-3'}]}
- instance = OneToManyTarget.objects.get(pk=1)
- serializer = self.Serializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- serializer.save()
-
- # Ensure source 2 is deleted, and everything else is as
- # expected.
- queryset = OneToManyTarget.objects.all()
- serializer = self.Serializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
- {'id': 3, 'name': 'source-3'}]}
-
- ]
- self.assertEqual(serializer.data, expected)
+# from __future__ import unicode_literals
+# from django.db import models
+# from django.test import TestCase
+# from rest_framework import serializers
+
+# from .models import OneToOneTarget
+
+
+# class OneToOneSource(models.Model):
+# name = models.CharField(max_length=100)
+# target = models.OneToOneField(OneToOneTarget, related_name='source',
+# null=True, blank=True)
+
+
+# class OneToManyTarget(models.Model):
+# name = models.CharField(max_length=100)
+
+
+# class OneToManySource(models.Model):
+# name = models.CharField(max_length=100)
+# target = models.ForeignKey(OneToManyTarget, related_name='sources')
+
+
+# class ReverseNestedOneToOneTests(TestCase):
+# def setUp(self):
+# class OneToOneSourceSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = OneToOneSource
+# fields = ('id', 'name')
+
+# class OneToOneTargetSerializer(serializers.ModelSerializer):
+# source = OneToOneSourceSerializer()
+
+# class Meta:
+# model = OneToOneTarget
+# fields = ('id', 'name', 'source')
+
+# self.Serializer = OneToOneTargetSerializer
+
+# for idx in range(1, 4):
+# target = OneToOneTarget(name='target-%d' % idx)
+# target.save()
+# source = OneToOneSource(name='source-%d' % idx, target=target)
+# source.save()
+
+# def test_one_to_one_retrieve(self):
+# queryset = OneToOneTarget.objects.all()
+# serializer = self.Serializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}},
+# {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}},
+# {'id': 3, 'name': 'target-3', 'source': {'id': 3, 'name': 'source-3'}}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_one_to_one_create(self):
+# data = {'id': 4, 'name': 'target-4', 'source': {'id': 4, 'name': 'source-4'}}
+# serializer = self.Serializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'target-4')
+
+# # Ensure (target 4, target_source 4, source 4) are added, and
+# # everything else is as expected.
+# queryset = OneToOneTarget.objects.all()
+# serializer = self.Serializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}},
+# {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}},
+# {'id': 3, 'name': 'target-3', 'source': {'id': 3, 'name': 'source-3'}},
+# {'id': 4, 'name': 'target-4', 'source': {'id': 4, 'name': 'source-4'}}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_one_to_one_create_with_invalid_data(self):
+# data = {'id': 4, 'name': 'target-4', 'source': {'id': 4}}
+# serializer = self.Serializer(data=data)
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'source': [{'name': ['This field is required.']}]})
+
+# def test_one_to_one_update(self):
+# data = {'id': 3, 'name': 'target-3-updated', 'source': {'id': 3, 'name': 'source-3-updated'}}
+# instance = OneToOneTarget.objects.get(pk=3)
+# serializer = self.Serializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'target-3-updated')
+
+# # Ensure (target 3, target_source 3, source 3) are updated,
+# # and everything else is as expected.
+# queryset = OneToOneTarget.objects.all()
+# serializer = self.Serializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}},
+# {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}},
+# {'id': 3, 'name': 'target-3-updated', 'source': {'id': 3, 'name': 'source-3-updated'}}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+
+# class ForwardNestedOneToOneTests(TestCase):
+# def setUp(self):
+# class OneToOneTargetSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = OneToOneTarget
+# fields = ('id', 'name')
+
+# class OneToOneSourceSerializer(serializers.ModelSerializer):
+# target = OneToOneTargetSerializer()
+
+# class Meta:
+# model = OneToOneSource
+# fields = ('id', 'name', 'target')
+
+# self.Serializer = OneToOneSourceSerializer
+
+# for idx in range(1, 4):
+# target = OneToOneTarget(name='target-%d' % idx)
+# target.save()
+# source = OneToOneSource(name='source-%d' % idx, target=target)
+# source.save()
+
+# def test_one_to_one_retrieve(self):
+# queryset = OneToOneSource.objects.all()
+# serializer = self.Serializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
+# {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}},
+# {'id': 3, 'name': 'source-3', 'target': {'id': 3, 'name': 'target-3'}}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_one_to_one_create(self):
+# data = {'id': 4, 'name': 'source-4', 'target': {'id': 4, 'name': 'target-4'}}
+# serializer = self.Serializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'source-4')
+
+# # Ensure (target 4, target_source 4, source 4) are added, and
+# # everything else is as expected.
+# queryset = OneToOneSource.objects.all()
+# serializer = self.Serializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
+# {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}},
+# {'id': 3, 'name': 'source-3', 'target': {'id': 3, 'name': 'target-3'}},
+# {'id': 4, 'name': 'source-4', 'target': {'id': 4, 'name': 'target-4'}}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_one_to_one_create_with_invalid_data(self):
+# data = {'id': 4, 'name': 'source-4', 'target': {'id': 4}}
+# serializer = self.Serializer(data=data)
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'target': [{'name': ['This field is required.']}]})
+
+# def test_one_to_one_update(self):
+# data = {'id': 3, 'name': 'source-3-updated', 'target': {'id': 3, 'name': 'target-3-updated'}}
+# instance = OneToOneSource.objects.get(pk=3)
+# serializer = self.Serializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'source-3-updated')
+
+# # Ensure (target 3, target_source 3, source 3) are updated,
+# # and everything else is as expected.
+# queryset = OneToOneSource.objects.all()
+# serializer = self.Serializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
+# {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}},
+# {'id': 3, 'name': 'source-3-updated', 'target': {'id': 3, 'name': 'target-3-updated'}}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_one_to_one_update_to_null(self):
+# data = {'id': 3, 'name': 'source-3-updated', 'target': None}
+# instance = OneToOneSource.objects.get(pk=3)
+# serializer = self.Serializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'source-3-updated')
+# self.assertEqual(obj.target, None)
+
+# queryset = OneToOneSource.objects.all()
+# serializer = self.Serializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
+# {'id': 2, 'name': 'source-2', 'target': {'id': 2, 'name': 'target-2'}},
+# {'id': 3, 'name': 'source-3-updated', 'target': None}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# # TODO: Nullable 1-1 tests
+# # def test_one_to_one_delete(self):
+# # data = {'id': 3, 'name': 'target-3', 'target_source': None}
+# # instance = OneToOneTarget.objects.get(pk=3)
+# # serializer = self.Serializer(instance, data=data)
+# # self.assertTrue(serializer.is_valid())
+# # serializer.save()
+
+# # # Ensure (target_source 3, source 3) are deleted,
+# # # and everything else is as expected.
+# # queryset = OneToOneTarget.objects.all()
+# # serializer = self.Serializer(queryset)
+# # expected = [
+# # {'id': 1, 'name': 'target-1', 'source': {'id': 1, 'name': 'source-1'}},
+# # {'id': 2, 'name': 'target-2', 'source': {'id': 2, 'name': 'source-2'}},
+# # {'id': 3, 'name': 'target-3', 'source': None}
+# # ]
+# # self.assertEqual(serializer.data, expected)
+
+
+# class ReverseNestedOneToManyTests(TestCase):
+# def setUp(self):
+# class OneToManySourceSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = OneToManySource
+# fields = ('id', 'name')
+
+# class OneToManyTargetSerializer(serializers.ModelSerializer):
+# sources = OneToManySourceSerializer(many=True, allow_add_remove=True)
+
+# class Meta:
+# model = OneToManyTarget
+# fields = ('id', 'name', 'sources')
+
+# self.Serializer = OneToManyTargetSerializer
+
+# target = OneToManyTarget(name='target-1')
+# target.save()
+# for idx in range(1, 4):
+# source = OneToManySource(name='source-%d' % idx, target=target)
+# source.save()
+
+# def test_one_to_many_retrieve(self):
+# queryset = OneToManyTarget.objects.all()
+# serializer = self.Serializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
+# {'id': 2, 'name': 'source-2'},
+# {'id': 3, 'name': 'source-3'}]},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_one_to_many_create(self):
+# data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
+# {'id': 2, 'name': 'source-2'},
+# {'id': 3, 'name': 'source-3'},
+# {'id': 4, 'name': 'source-4'}]}
+# instance = OneToManyTarget.objects.get(pk=1)
+# serializer = self.Serializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'target-1')
+
+# # Ensure source 4 is added, and everything else is as
+# # expected.
+# queryset = OneToManyTarget.objects.all()
+# serializer = self.Serializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
+# {'id': 2, 'name': 'source-2'},
+# {'id': 3, 'name': 'source-3'},
+# {'id': 4, 'name': 'source-4'}]}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_one_to_many_create_with_invalid_data(self):
+# data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
+# {'id': 2, 'name': 'source-2'},
+# {'id': 3, 'name': 'source-3'},
+# {'id': 4}]}
+# serializer = self.Serializer(data=data)
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'sources': [{}, {}, {}, {'name': ['This field is required.']}]})
+
+# def test_one_to_many_update(self):
+# data = {'id': 1, 'name': 'target-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'},
+# {'id': 2, 'name': 'source-2'},
+# {'id': 3, 'name': 'source-3'}]}
+# instance = OneToManyTarget.objects.get(pk=1)
+# serializer = self.Serializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'target-1-updated')
+
+# # Ensure (target 1, source 1) are updated,
+# # and everything else is as expected.
+# queryset = OneToManyTarget.objects.all()
+# serializer = self.Serializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1-updated', 'sources': [{'id': 1, 'name': 'source-1-updated'},
+# {'id': 2, 'name': 'source-2'},
+# {'id': 3, 'name': 'source-3'}]}
+
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_one_to_many_delete(self):
+# data = {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
+# {'id': 3, 'name': 'source-3'}]}
+# instance = OneToManyTarget.objects.get(pk=1)
+# serializer = self.Serializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# serializer.save()
+
+# # Ensure source 2 is deleted, and everything else is as
+# # expected.
+# queryset = OneToManyTarget.objects.all()
+# serializer = self.Serializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': [{'id': 1, 'name': 'source-1'},
+# {'id': 3, 'name': 'source-3'}]}
+
+# ]
+# self.assertEqual(serializer.data, expected)
diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py
index e3f836ed..031a79b3 100644
--- a/tests/test_relations_pk.py
+++ b/tests/test_relations_pk.py
@@ -1,551 +1,551 @@
-from __future__ import unicode_literals
-from django.db import models
-from django.test import TestCase
-from django.utils import six
-from rest_framework import serializers
-from tests.models import (
- BlogPost, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
- NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource,
-)
-
-
-# ManyToMany
-class ManyToManyTargetSerializer(serializers.ModelSerializer):
- class Meta:
- model = ManyToManyTarget
- fields = ('id', 'name', 'sources')
-
-
-class ManyToManySourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = ManyToManySource
- fields = ('id', 'name', 'targets')
-
-
-# ForeignKey
-class ForeignKeyTargetSerializer(serializers.ModelSerializer):
- class Meta:
- model = ForeignKeyTarget
- fields = ('id', 'name', 'sources')
-
-
-class ForeignKeySourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = ForeignKeySource
- fields = ('id', 'name', 'target')
-
-
-# Nullable ForeignKey
-class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = NullableForeignKeySource
- fields = ('id', 'name', 'target')
-
-
-# Nullable OneToOne
-class NullableOneToOneTargetSerializer(serializers.ModelSerializer):
- class Meta:
- model = OneToOneTarget
- fields = ('id', 'name', 'nullable_source')
-
-
-# TODO: Add test that .data cannot be accessed prior to .is_valid
-
-class PKManyToManyTests(TestCase):
- def setUp(self):
- for idx in range(1, 4):
- target = ManyToManyTarget(name='target-%d' % idx)
- target.save()
- source = ManyToManySource(name='source-%d' % idx)
- source.save()
- for target in ManyToManyTarget.objects.all():
- source.targets.add(target)
-
- def test_many_to_many_retrieve(self):
- queryset = ManyToManySource.objects.all()
- serializer = ManyToManySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'targets': [1]},
- {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
- {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_many_to_many_retrieve(self):
- queryset = ManyToManyTarget.objects.all()
- serializer = ManyToManyTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]},
- {'id': 2, 'name': 'target-2', 'sources': [2, 3]},
- {'id': 3, 'name': 'target-3', 'sources': [3]}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_many_to_many_update(self):
- data = {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]}
- instance = ManyToManySource.objects.get(pk=1)
- serializer = ManyToManySourceSerializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- serializer.save()
- self.assertEqual(serializer.data, data)
-
- # Ensure source 1 is updated, and everything else is as expected
- queryset = ManyToManySource.objects.all()
- serializer = ManyToManySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]},
- {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
- {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_many_to_many_update(self):
- data = {'id': 1, 'name': 'target-1', 'sources': [1]}
- instance = ManyToManyTarget.objects.get(pk=1)
- serializer = ManyToManyTargetSerializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- serializer.save()
- self.assertEqual(serializer.data, data)
-
- # Ensure target 1 is updated, and everything else is as expected
- queryset = ManyToManyTarget.objects.all()
- serializer = ManyToManyTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': [1]},
- {'id': 2, 'name': 'target-2', 'sources': [2, 3]},
- {'id': 3, 'name': 'target-3', 'sources': [3]}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_many_to_many_create(self):
- data = {'id': 4, 'name': 'source-4', 'targets': [1, 3]}
- serializer = ManyToManySourceSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'source-4')
-
- # Ensure source 4 is added, and everything else is as expected
- queryset = ManyToManySource.objects.all()
- serializer = ManyToManySourceSerializer(queryset, many=True)
- self.assertFalse(serializer.fields['targets'].read_only)
- expected = [
- {'id': 1, 'name': 'source-1', 'targets': [1]},
- {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
- {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]},
- {'id': 4, 'name': 'source-4', 'targets': [1, 3]},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_many_to_many_create(self):
- data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
- serializer = ManyToManyTargetSerializer(data=data)
- self.assertFalse(serializer.fields['sources'].read_only)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'target-4')
-
- # Ensure target 4 is added, and everything else is as expected
- queryset = ManyToManyTarget.objects.all()
- serializer = ManyToManyTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]},
- {'id': 2, 'name': 'target-2', 'sources': [2, 3]},
- {'id': 3, 'name': 'target-3', 'sources': [3]},
- {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
- ]
- self.assertEqual(serializer.data, expected)
-
-
-class PKForeignKeyTests(TestCase):
- def setUp(self):
- target = ForeignKeyTarget(name='target-1')
- target.save()
- new_target = ForeignKeyTarget(name='target-2')
- new_target.save()
- for idx in range(1, 4):
- source = ForeignKeySource(name='source-%d' % idx, target=target)
- source.save()
-
- def test_foreign_key_retrieve(self):
- queryset = ForeignKeySource.objects.all()
- serializer = ForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': 1},
- {'id': 2, 'name': 'source-2', 'target': 1},
- {'id': 3, 'name': 'source-3', 'target': 1}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_foreign_key_retrieve(self):
- queryset = ForeignKeyTarget.objects.all()
- serializer = ForeignKeyTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]},
- {'id': 2, 'name': 'target-2', 'sources': []},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update(self):
- data = {'id': 1, 'name': 'source-1', 'target': 2}
- instance = ForeignKeySource.objects.get(pk=1)
- serializer = ForeignKeySourceSerializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
- serializer.save()
-
- # Ensure source 1 is updated, and everything else is as expected
- queryset = ForeignKeySource.objects.all()
- serializer = ForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': 2},
- {'id': 2, 'name': 'source-2', 'target': 1},
- {'id': 3, 'name': 'source-3', 'target': 1}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update_incorrect_type(self):
- data = {'id': 1, 'name': 'source-1', 'target': 'foo'}
- instance = ForeignKeySource.objects.get(pk=1)
- serializer = ForeignKeySourceSerializer(instance, data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]})
-
- def test_reverse_foreign_key_update(self):
- data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]}
- instance = ForeignKeyTarget.objects.get(pk=2)
- serializer = ForeignKeyTargetSerializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- # We shouldn't have saved anything to the db yet since save
- # hasn't been called.
- queryset = ForeignKeyTarget.objects.all()
- new_serializer = ForeignKeyTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]},
- {'id': 2, 'name': 'target-2', 'sources': []},
- ]
- self.assertEqual(new_serializer.data, expected)
-
- serializer.save()
- self.assertEqual(serializer.data, data)
-
- # Ensure target 2 is update, and everything else is as expected
- queryset = ForeignKeyTarget.objects.all()
- serializer = ForeignKeyTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': [2]},
- {'id': 2, 'name': 'target-2', 'sources': [1, 3]},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_create(self):
- data = {'id': 4, 'name': 'source-4', 'target': 2}
- serializer = ForeignKeySourceSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'source-4')
-
- # Ensure source 4 is added, and everything else is as expected
- queryset = ForeignKeySource.objects.all()
- serializer = ForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': 1},
- {'id': 2, 'name': 'source-2', 'target': 1},
- {'id': 3, 'name': 'source-3', 'target': 1},
- {'id': 4, 'name': 'source-4', 'target': 2},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_foreign_key_create(self):
- data = {'id': 3, 'name': 'target-3', 'sources': [1, 3]}
- serializer = ForeignKeyTargetSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'target-3')
-
- # Ensure target 3 is added, and everything else is as expected
- queryset = ForeignKeyTarget.objects.all()
- serializer = ForeignKeyTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': [2]},
- {'id': 2, 'name': 'target-2', 'sources': []},
- {'id': 3, 'name': 'target-3', 'sources': [1, 3]},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update_with_invalid_null(self):
- data = {'id': 1, 'name': 'source-1', 'target': None}
- instance = ForeignKeySource.objects.get(pk=1)
- serializer = ForeignKeySourceSerializer(instance, data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['This field is required.']})
-
- def test_foreign_key_with_empty(self):
- """
- Regression test for #1072
-
- https://github.com/tomchristie/django-rest-framework/issues/1072
- """
- serializer = NullableForeignKeySourceSerializer()
- self.assertEqual(serializer.data['target'], None)
-
-
-class PKNullableForeignKeyTests(TestCase):
- def setUp(self):
- target = ForeignKeyTarget(name='target-1')
- target.save()
- for idx in range(1, 4):
- if idx == 3:
- target = None
- source = NullableForeignKeySource(name='source-%d' % idx, target=target)
- source.save()
-
- def test_foreign_key_retrieve_with_null(self):
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': 1},
- {'id': 2, 'name': 'source-2', 'target': 1},
- {'id': 3, 'name': 'source-3', 'target': None},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_create_with_valid_null(self):
- data = {'id': 4, 'name': 'source-4', 'target': None}
- serializer = NullableForeignKeySourceSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'source-4')
-
- # Ensure source 4 is created, and everything else is as expected
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': 1},
- {'id': 2, 'name': 'source-2', 'target': 1},
- {'id': 3, 'name': 'source-3', 'target': None},
- {'id': 4, 'name': 'source-4', 'target': None}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_create_with_valid_emptystring(self):
- """
- The emptystring should be interpreted as null in the context
- of relationships.
- """
- data = {'id': 4, 'name': 'source-4', 'target': ''}
- expected_data = {'id': 4, 'name': 'source-4', 'target': None}
- serializer = NullableForeignKeySourceSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, expected_data)
- self.assertEqual(obj.name, 'source-4')
-
- # Ensure source 4 is created, and everything else is as expected
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': 1},
- {'id': 2, 'name': 'source-2', 'target': 1},
- {'id': 3, 'name': 'source-3', 'target': None},
- {'id': 4, 'name': 'source-4', 'target': None}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update_with_valid_null(self):
- data = {'id': 1, 'name': 'source-1', 'target': None}
- instance = NullableForeignKeySource.objects.get(pk=1)
- serializer = NullableForeignKeySourceSerializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
- serializer.save()
-
- # Ensure source 1 is updated, and everything else is as expected
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': None},
- {'id': 2, 'name': 'source-2', 'target': 1},
- {'id': 3, 'name': 'source-3', 'target': None}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update_with_valid_emptystring(self):
- """
- The emptystring should be interpreted as null in the context
- of relationships.
- """
- data = {'id': 1, 'name': 'source-1', 'target': ''}
- expected_data = {'id': 1, 'name': 'source-1', 'target': None}
- instance = NullableForeignKeySource.objects.get(pk=1)
- serializer = NullableForeignKeySourceSerializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, expected_data)
- serializer.save()
-
- # Ensure source 1 is updated, and everything else is as expected
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': None},
- {'id': 2, 'name': 'source-2', 'target': 1},
- {'id': 3, 'name': 'source-3', 'target': None}
- ]
- self.assertEqual(serializer.data, expected)
-
- # reverse foreign keys MUST be read_only
- # In the general case they do not provide .remove() or .clear()
- # and cannot be arbitrarily set.
-
- # def test_reverse_foreign_key_update(self):
- # data = {'id': 1, 'name': 'target-1', 'sources': [1]}
- # instance = ForeignKeyTarget.objects.get(pk=1)
- # serializer = ForeignKeyTargetSerializer(instance, data=data)
- # self.assertTrue(serializer.is_valid())
- # self.assertEqual(serializer.data, data)
- # serializer.save()
-
- # # Ensure target 1 is updated, and everything else is as expected
- # queryset = ForeignKeyTarget.objects.all()
- # serializer = ForeignKeyTargetSerializer(queryset, many=True)
- # expected = [
- # {'id': 1, 'name': 'target-1', 'sources': [1]},
- # {'id': 2, 'name': 'target-2', 'sources': []},
- # ]
- # self.assertEqual(serializer.data, expected)
-
-
-class PKNullableOneToOneTests(TestCase):
- def setUp(self):
- target = OneToOneTarget(name='target-1')
- target.save()
- new_target = OneToOneTarget(name='target-2')
- new_target.save()
- source = NullableOneToOneSource(name='source-1', target=new_target)
- source.save()
-
- def test_reverse_foreign_key_retrieve_with_null(self):
- queryset = OneToOneTarget.objects.all()
- serializer = NullableOneToOneTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'nullable_source': None},
- {'id': 2, 'name': 'target-2', 'nullable_source': 1},
- ]
- self.assertEqual(serializer.data, expected)
-
-
-# The below models and tests ensure that serializer fields corresponding
-# to a ManyToManyField field with a user-specified ``through`` model are
-# set to read only
-
-
-class ManyToManyThroughTarget(models.Model):
- name = models.CharField(max_length=100)
-
-
-class ManyToManyThrough(models.Model):
- source = models.ForeignKey('ManyToManyThroughSource')
- target = models.ForeignKey(ManyToManyThroughTarget)
-
-
-class ManyToManyThroughSource(models.Model):
- name = models.CharField(max_length=100)
- targets = models.ManyToManyField(ManyToManyThroughTarget,
- related_name='sources',
- through='ManyToManyThrough')
-
-
-class ManyToManyThroughTargetSerializer(serializers.ModelSerializer):
- class Meta:
- model = ManyToManyThroughTarget
- fields = ('id', 'name', 'sources')
-
-
-class ManyToManyThroughSourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = ManyToManyThroughSource
- fields = ('id', 'name', 'targets')
-
-
-class PKManyToManyThroughTests(TestCase):
- def setUp(self):
- self.source = ManyToManyThroughSource.objects.create(
- name='through-source-1')
- self.target = ManyToManyThroughTarget.objects.create(
- name='through-target-1')
-
- def test_many_to_many_create(self):
- data = {'id': 2, 'name': 'source-2', 'targets': [self.target.pk]}
- serializer = ManyToManyThroughSourceSerializer(data=data)
- self.assertTrue(serializer.fields['targets'].read_only)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(obj.name, 'source-2')
- self.assertEqual(obj.targets.count(), 0)
-
- def test_many_to_many_reverse_create(self):
- data = {'id': 2, 'name': 'target-2', 'sources': [self.source.pk]}
- serializer = ManyToManyThroughTargetSerializer(data=data)
- self.assertTrue(serializer.fields['sources'].read_only)
- self.assertTrue(serializer.is_valid())
- serializer.save()
- obj = serializer.save()
- self.assertEqual(obj.name, 'target-2')
- self.assertEqual(obj.sources.count(), 0)
-
-
-# Regression tests for #694 (`source` attribute on related fields)
-
-
-class PrimaryKeyRelatedFieldSourceTests(TestCase):
- def test_related_manager_source(self):
- """
- Relational fields should be able to use manager-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_manager')
-
- class ClassWithManagerMethod(object):
- def get_blogposts_manager(self):
- return BlogPost.objects
-
- obj = ClassWithManagerMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, [1])
-
- def test_related_queryset_source(self):
- """
- Relational fields should be able to use queryset-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_queryset')
-
- class ClassWithQuerysetMethod(object):
- def get_blogposts_queryset(self):
- return BlogPost.objects.all()
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, [1])
-
- def test_dotted_source(self):
- """
- Source argument should support dotted.source notation.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.PrimaryKeyRelatedField(many=True, source='a.b.c')
-
- class ClassWithQuerysetMethod(object):
- a = {
- 'b': {
- 'c': BlogPost.objects.all()
- }
- }
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, [1])
+# from __future__ import unicode_literals
+# from django.db import models
+# from django.test import TestCase
+# from django.utils import six
+# from rest_framework import serializers
+# from tests.models import (
+# BlogPost, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
+# NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource,
+# )
+
+
+# # ManyToMany
+# class ManyToManyTargetSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = ManyToManyTarget
+# fields = ('id', 'name', 'sources')
+
+
+# class ManyToManySourceSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = ManyToManySource
+# fields = ('id', 'name', 'targets')
+
+
+# # ForeignKey
+# class ForeignKeyTargetSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = ForeignKeyTarget
+# fields = ('id', 'name', 'sources')
+
+
+# class ForeignKeySourceSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = ForeignKeySource
+# fields = ('id', 'name', 'target')
+
+
+# # Nullable ForeignKey
+# class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = NullableForeignKeySource
+# fields = ('id', 'name', 'target')
+
+
+# # Nullable OneToOne
+# class NullableOneToOneTargetSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = OneToOneTarget
+# fields = ('id', 'name', 'nullable_source')
+
+
+# # TODO: Add test that .data cannot be accessed prior to .is_valid
+
+# class PKManyToManyTests(TestCase):
+# def setUp(self):
+# for idx in range(1, 4):
+# target = ManyToManyTarget(name='target-%d' % idx)
+# target.save()
+# source = ManyToManySource(name='source-%d' % idx)
+# source.save()
+# for target in ManyToManyTarget.objects.all():
+# source.targets.add(target)
+
+# def test_many_to_many_retrieve(self):
+# queryset = ManyToManySource.objects.all()
+# serializer = ManyToManySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'targets': [1]},
+# {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
+# {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_reverse_many_to_many_retrieve(self):
+# queryset = ManyToManyTarget.objects.all()
+# serializer = ManyToManyTargetSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]},
+# {'id': 2, 'name': 'target-2', 'sources': [2, 3]},
+# {'id': 3, 'name': 'target-3', 'sources': [3]}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_many_to_many_update(self):
+# data = {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]}
+# instance = ManyToManySource.objects.get(pk=1)
+# serializer = ManyToManySourceSerializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# serializer.save()
+# self.assertEqual(serializer.data, data)
+
+# # Ensure source 1 is updated, and everything else is as expected
+# queryset = ManyToManySource.objects.all()
+# serializer = ManyToManySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]},
+# {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
+# {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_reverse_many_to_many_update(self):
+# data = {'id': 1, 'name': 'target-1', 'sources': [1]}
+# instance = ManyToManyTarget.objects.get(pk=1)
+# serializer = ManyToManyTargetSerializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# serializer.save()
+# self.assertEqual(serializer.data, data)
+
+# # Ensure target 1 is updated, and everything else is as expected
+# queryset = ManyToManyTarget.objects.all()
+# serializer = ManyToManyTargetSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': [1]},
+# {'id': 2, 'name': 'target-2', 'sources': [2, 3]},
+# {'id': 3, 'name': 'target-3', 'sources': [3]}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_many_to_many_create(self):
+# data = {'id': 4, 'name': 'source-4', 'targets': [1, 3]}
+# serializer = ManyToManySourceSerializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'source-4')
+
+# # Ensure source 4 is added, and everything else is as expected
+# queryset = ManyToManySource.objects.all()
+# serializer = ManyToManySourceSerializer(queryset, many=True)
+# self.assertFalse(serializer.fields['targets'].read_only)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'targets': [1]},
+# {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
+# {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]},
+# {'id': 4, 'name': 'source-4', 'targets': [1, 3]},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_reverse_many_to_many_create(self):
+# data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
+# serializer = ManyToManyTargetSerializer(data=data)
+# self.assertFalse(serializer.fields['sources'].read_only)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'target-4')
+
+# # Ensure target 4 is added, and everything else is as expected
+# queryset = ManyToManyTarget.objects.all()
+# serializer = ManyToManyTargetSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]},
+# {'id': 2, 'name': 'target-2', 'sources': [2, 3]},
+# {'id': 3, 'name': 'target-3', 'sources': [3]},
+# {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+
+# class PKForeignKeyTests(TestCase):
+# def setUp(self):
+# target = ForeignKeyTarget(name='target-1')
+# target.save()
+# new_target = ForeignKeyTarget(name='target-2')
+# new_target.save()
+# for idx in range(1, 4):
+# source = ForeignKeySource(name='source-%d' % idx, target=target)
+# source.save()
+
+# def test_foreign_key_retrieve(self):
+# queryset = ForeignKeySource.objects.all()
+# serializer = ForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': 1},
+# {'id': 2, 'name': 'source-2', 'target': 1},
+# {'id': 3, 'name': 'source-3', 'target': 1}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_reverse_foreign_key_retrieve(self):
+# queryset = ForeignKeyTarget.objects.all()
+# serializer = ForeignKeyTargetSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]},
+# {'id': 2, 'name': 'target-2', 'sources': []},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update(self):
+# data = {'id': 1, 'name': 'source-1', 'target': 2}
+# instance = ForeignKeySource.objects.get(pk=1)
+# serializer = ForeignKeySourceSerializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.data, data)
+# serializer.save()
+
+# # Ensure source 1 is updated, and everything else is as expected
+# queryset = ForeignKeySource.objects.all()
+# serializer = ForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': 2},
+# {'id': 2, 'name': 'source-2', 'target': 1},
+# {'id': 3, 'name': 'source-3', 'target': 1}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update_incorrect_type(self):
+# data = {'id': 1, 'name': 'source-1', 'target': 'foo'}
+# instance = ForeignKeySource.objects.get(pk=1)
+# serializer = ForeignKeySourceSerializer(instance, data=data)
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]})
+
+# def test_reverse_foreign_key_update(self):
+# data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]}
+# instance = ForeignKeyTarget.objects.get(pk=2)
+# serializer = ForeignKeyTargetSerializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# # We shouldn't have saved anything to the db yet since save
+# # hasn't been called.
+# queryset = ForeignKeyTarget.objects.all()
+# new_serializer = ForeignKeyTargetSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]},
+# {'id': 2, 'name': 'target-2', 'sources': []},
+# ]
+# self.assertEqual(new_serializer.data, expected)
+
+# serializer.save()
+# self.assertEqual(serializer.data, data)
+
+# # Ensure target 2 is update, and everything else is as expected
+# queryset = ForeignKeyTarget.objects.all()
+# serializer = ForeignKeyTargetSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': [2]},
+# {'id': 2, 'name': 'target-2', 'sources': [1, 3]},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_create(self):
+# data = {'id': 4, 'name': 'source-4', 'target': 2}
+# serializer = ForeignKeySourceSerializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'source-4')
+
+# # Ensure source 4 is added, and everything else is as expected
+# queryset = ForeignKeySource.objects.all()
+# serializer = ForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': 1},
+# {'id': 2, 'name': 'source-2', 'target': 1},
+# {'id': 3, 'name': 'source-3', 'target': 1},
+# {'id': 4, 'name': 'source-4', 'target': 2},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_reverse_foreign_key_create(self):
+# data = {'id': 3, 'name': 'target-3', 'sources': [1, 3]}
+# serializer = ForeignKeyTargetSerializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'target-3')
+
+# # Ensure target 3 is added, and everything else is as expected
+# queryset = ForeignKeyTarget.objects.all()
+# serializer = ForeignKeyTargetSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': [2]},
+# {'id': 2, 'name': 'target-2', 'sources': []},
+# {'id': 3, 'name': 'target-3', 'sources': [1, 3]},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update_with_invalid_null(self):
+# data = {'id': 1, 'name': 'source-1', 'target': None}
+# instance = ForeignKeySource.objects.get(pk=1)
+# serializer = ForeignKeySourceSerializer(instance, data=data)
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'target': ['This field is required.']})
+
+# def test_foreign_key_with_empty(self):
+# """
+# Regression test for #1072
+
+# https://github.com/tomchristie/django-rest-framework/issues/1072
+# """
+# serializer = NullableForeignKeySourceSerializer()
+# self.assertEqual(serializer.data['target'], None)
+
+
+# class PKNullableForeignKeyTests(TestCase):
+# def setUp(self):
+# target = ForeignKeyTarget(name='target-1')
+# target.save()
+# for idx in range(1, 4):
+# if idx == 3:
+# target = None
+# source = NullableForeignKeySource(name='source-%d' % idx, target=target)
+# source.save()
+
+# def test_foreign_key_retrieve_with_null(self):
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': 1},
+# {'id': 2, 'name': 'source-2', 'target': 1},
+# {'id': 3, 'name': 'source-3', 'target': None},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_create_with_valid_null(self):
+# data = {'id': 4, 'name': 'source-4', 'target': None}
+# serializer = NullableForeignKeySourceSerializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'source-4')
+
+# # Ensure source 4 is created, and everything else is as expected
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': 1},
+# {'id': 2, 'name': 'source-2', 'target': 1},
+# {'id': 3, 'name': 'source-3', 'target': None},
+# {'id': 4, 'name': 'source-4', 'target': None}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_create_with_valid_emptystring(self):
+# """
+# The emptystring should be interpreted as null in the context
+# of relationships.
+# """
+# data = {'id': 4, 'name': 'source-4', 'target': ''}
+# expected_data = {'id': 4, 'name': 'source-4', 'target': None}
+# serializer = NullableForeignKeySourceSerializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, expected_data)
+# self.assertEqual(obj.name, 'source-4')
+
+# # Ensure source 4 is created, and everything else is as expected
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': 1},
+# {'id': 2, 'name': 'source-2', 'target': 1},
+# {'id': 3, 'name': 'source-3', 'target': None},
+# {'id': 4, 'name': 'source-4', 'target': None}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update_with_valid_null(self):
+# data = {'id': 1, 'name': 'source-1', 'target': None}
+# instance = NullableForeignKeySource.objects.get(pk=1)
+# serializer = NullableForeignKeySourceSerializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.data, data)
+# serializer.save()
+
+# # Ensure source 1 is updated, and everything else is as expected
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': None},
+# {'id': 2, 'name': 'source-2', 'target': 1},
+# {'id': 3, 'name': 'source-3', 'target': None}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update_with_valid_emptystring(self):
+# """
+# The emptystring should be interpreted as null in the context
+# of relationships.
+# """
+# data = {'id': 1, 'name': 'source-1', 'target': ''}
+# expected_data = {'id': 1, 'name': 'source-1', 'target': None}
+# instance = NullableForeignKeySource.objects.get(pk=1)
+# serializer = NullableForeignKeySourceSerializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.data, expected_data)
+# serializer.save()
+
+# # Ensure source 1 is updated, and everything else is as expected
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': None},
+# {'id': 2, 'name': 'source-2', 'target': 1},
+# {'id': 3, 'name': 'source-3', 'target': None}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# # reverse foreign keys MUST be read_only
+# # In the general case they do not provide .remove() or .clear()
+# # and cannot be arbitrarily set.
+
+# # def test_reverse_foreign_key_update(self):
+# # data = {'id': 1, 'name': 'target-1', 'sources': [1]}
+# # instance = ForeignKeyTarget.objects.get(pk=1)
+# # serializer = ForeignKeyTargetSerializer(instance, data=data)
+# # self.assertTrue(serializer.is_valid())
+# # self.assertEqual(serializer.data, data)
+# # serializer.save()
+
+# # # Ensure target 1 is updated, and everything else is as expected
+# # queryset = ForeignKeyTarget.objects.all()
+# # serializer = ForeignKeyTargetSerializer(queryset, many=True)
+# # expected = [
+# # {'id': 1, 'name': 'target-1', 'sources': [1]},
+# # {'id': 2, 'name': 'target-2', 'sources': []},
+# # ]
+# # self.assertEqual(serializer.data, expected)
+
+
+# class PKNullableOneToOneTests(TestCase):
+# def setUp(self):
+# target = OneToOneTarget(name='target-1')
+# target.save()
+# new_target = OneToOneTarget(name='target-2')
+# new_target.save()
+# source = NullableOneToOneSource(name='source-1', target=new_target)
+# source.save()
+
+# def test_reverse_foreign_key_retrieve_with_null(self):
+# queryset = OneToOneTarget.objects.all()
+# serializer = NullableOneToOneTargetSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'nullable_source': None},
+# {'id': 2, 'name': 'target-2', 'nullable_source': 1},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+
+# # The below models and tests ensure that serializer fields corresponding
+# # to a ManyToManyField field with a user-specified ``through`` model are
+# # set to read only
+
+
+# class ManyToManyThroughTarget(models.Model):
+# name = models.CharField(max_length=100)
+
+
+# class ManyToManyThrough(models.Model):
+# source = models.ForeignKey('ManyToManyThroughSource')
+# target = models.ForeignKey(ManyToManyThroughTarget)
+
+
+# class ManyToManyThroughSource(models.Model):
+# name = models.CharField(max_length=100)
+# targets = models.ManyToManyField(ManyToManyThroughTarget,
+# related_name='sources',
+# through='ManyToManyThrough')
+
+
+# class ManyToManyThroughTargetSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = ManyToManyThroughTarget
+# fields = ('id', 'name', 'sources')
+
+
+# class ManyToManyThroughSourceSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = ManyToManyThroughSource
+# fields = ('id', 'name', 'targets')
+
+
+# class PKManyToManyThroughTests(TestCase):
+# def setUp(self):
+# self.source = ManyToManyThroughSource.objects.create(
+# name='through-source-1')
+# self.target = ManyToManyThroughTarget.objects.create(
+# name='through-target-1')
+
+# def test_many_to_many_create(self):
+# data = {'id': 2, 'name': 'source-2', 'targets': [self.target.pk]}
+# serializer = ManyToManyThroughSourceSerializer(data=data)
+# self.assertTrue(serializer.fields['targets'].read_only)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(obj.name, 'source-2')
+# self.assertEqual(obj.targets.count(), 0)
+
+# def test_many_to_many_reverse_create(self):
+# data = {'id': 2, 'name': 'target-2', 'sources': [self.source.pk]}
+# serializer = ManyToManyThroughTargetSerializer(data=data)
+# self.assertTrue(serializer.fields['sources'].read_only)
+# self.assertTrue(serializer.is_valid())
+# serializer.save()
+# obj = serializer.save()
+# self.assertEqual(obj.name, 'target-2')
+# self.assertEqual(obj.sources.count(), 0)
+
+
+# # Regression tests for #694 (`source` attribute on related fields)
+
+
+# class PrimaryKeyRelatedFieldSourceTests(TestCase):
+# def test_related_manager_source(self):
+# """
+# Relational fields should be able to use manager-returning methods as their source.
+# """
+# BlogPost.objects.create(title='blah')
+# field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_manager')
+
+# class ClassWithManagerMethod(object):
+# def get_blogposts_manager(self):
+# return BlogPost.objects
+
+# obj = ClassWithManagerMethod()
+# value = field.field_to_native(obj, 'field_name')
+# self.assertEqual(value, [1])
+
+# def test_related_queryset_source(self):
+# """
+# Relational fields should be able to use queryset-returning methods as their source.
+# """
+# BlogPost.objects.create(title='blah')
+# field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_queryset')
+
+# class ClassWithQuerysetMethod(object):
+# def get_blogposts_queryset(self):
+# return BlogPost.objects.all()
+
+# obj = ClassWithQuerysetMethod()
+# value = field.field_to_native(obj, 'field_name')
+# self.assertEqual(value, [1])
+
+# def test_dotted_source(self):
+# """
+# Source argument should support dotted.source notation.
+# """
+# BlogPost.objects.create(title='blah')
+# field = serializers.PrimaryKeyRelatedField(many=True, source='a.b.c')
+
+# class ClassWithQuerysetMethod(object):
+# a = {
+# 'b': {
+# 'c': BlogPost.objects.all()
+# }
+# }
+
+# obj = ClassWithQuerysetMethod()
+# value = field.field_to_native(obj, 'field_name')
+# self.assertEqual(value, [1])
diff --git a/tests/test_relations_slug.py b/tests/test_relations_slug.py
index 97ebf23a..f7a59a95 100644
--- a/tests/test_relations_slug.py
+++ b/tests/test_relations_slug.py
@@ -1,257 +1,257 @@
-from django.test import TestCase
-from rest_framework import serializers
-from tests.models import NullableForeignKeySource, ForeignKeySource, ForeignKeyTarget
-
-
-class ForeignKeyTargetSerializer(serializers.ModelSerializer):
- sources = serializers.SlugRelatedField(many=True, slug_field='name')
-
- class Meta:
- model = ForeignKeyTarget
-
-
-class ForeignKeySourceSerializer(serializers.ModelSerializer):
- target = serializers.SlugRelatedField(slug_field='name')
-
- class Meta:
- model = ForeignKeySource
-
-
-class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
- target = serializers.SlugRelatedField(slug_field='name', required=False)
-
- class Meta:
- model = NullableForeignKeySource
-
-
-# TODO: M2M Tests, FKTests (Non-nullable), One2One
-class SlugForeignKeyTests(TestCase):
- def setUp(self):
- target = ForeignKeyTarget(name='target-1')
- target.save()
- new_target = ForeignKeyTarget(name='target-2')
- new_target.save()
- for idx in range(1, 4):
- source = ForeignKeySource(name='source-%d' % idx, target=target)
- source.save()
-
- def test_foreign_key_retrieve(self):
- queryset = ForeignKeySource.objects.all()
- serializer = ForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': 'target-1'},
- {'id': 2, 'name': 'source-2', 'target': 'target-1'},
- {'id': 3, 'name': 'source-3', 'target': 'target-1'}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_foreign_key_retrieve(self):
- queryset = ForeignKeyTarget.objects.all()
- serializer = ForeignKeyTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']},
- {'id': 2, 'name': 'target-2', 'sources': []},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update(self):
- data = {'id': 1, 'name': 'source-1', 'target': 'target-2'}
- instance = ForeignKeySource.objects.get(pk=1)
- serializer = ForeignKeySourceSerializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
- serializer.save()
-
- # Ensure source 1 is updated, and everything else is as expected
- queryset = ForeignKeySource.objects.all()
- serializer = ForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': 'target-2'},
- {'id': 2, 'name': 'source-2', 'target': 'target-1'},
- {'id': 3, 'name': 'source-3', 'target': 'target-1'}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update_incorrect_type(self):
- data = {'id': 1, 'name': 'source-1', 'target': 123}
- instance = ForeignKeySource.objects.get(pk=1)
- serializer = ForeignKeySourceSerializer(instance, data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['Object with name=123 does not exist.']})
-
- def test_reverse_foreign_key_update(self):
- data = {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']}
- instance = ForeignKeyTarget.objects.get(pk=2)
- serializer = ForeignKeyTargetSerializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- # We shouldn't have saved anything to the db yet since save
- # hasn't been called.
- queryset = ForeignKeyTarget.objects.all()
- new_serializer = ForeignKeyTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']},
- {'id': 2, 'name': 'target-2', 'sources': []},
- ]
- self.assertEqual(new_serializer.data, expected)
-
- serializer.save()
- self.assertEqual(serializer.data, data)
-
- # Ensure target 2 is update, and everything else is as expected
- queryset = ForeignKeyTarget.objects.all()
- serializer = ForeignKeyTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': ['source-2']},
- {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_create(self):
- data = {'id': 4, 'name': 'source-4', 'target': 'target-2'}
- serializer = ForeignKeySourceSerializer(data=data)
- serializer.is_valid()
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'source-4')
-
- # Ensure source 4 is added, and everything else is as expected
- queryset = ForeignKeySource.objects.all()
- serializer = ForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': 'target-1'},
- {'id': 2, 'name': 'source-2', 'target': 'target-1'},
- {'id': 3, 'name': 'source-3', 'target': 'target-1'},
- {'id': 4, 'name': 'source-4', 'target': 'target-2'},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_foreign_key_create(self):
- data = {'id': 3, 'name': 'target-3', 'sources': ['source-1', 'source-3']}
- serializer = ForeignKeyTargetSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'target-3')
-
- # Ensure target 3 is added, and everything else is as expected
- queryset = ForeignKeyTarget.objects.all()
- serializer = ForeignKeyTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': ['source-2']},
- {'id': 2, 'name': 'target-2', 'sources': []},
- {'id': 3, 'name': 'target-3', 'sources': ['source-1', 'source-3']},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update_with_invalid_null(self):
- data = {'id': 1, 'name': 'source-1', 'target': None}
- instance = ForeignKeySource.objects.get(pk=1)
- serializer = ForeignKeySourceSerializer(instance, data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['This field is required.']})
-
-
-class SlugNullableForeignKeyTests(TestCase):
- def setUp(self):
- target = ForeignKeyTarget(name='target-1')
- target.save()
- for idx in range(1, 4):
- if idx == 3:
- target = None
- source = NullableForeignKeySource(name='source-%d' % idx, target=target)
- source.save()
-
- def test_foreign_key_retrieve_with_null(self):
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': 'target-1'},
- {'id': 2, 'name': 'source-2', 'target': 'target-1'},
- {'id': 3, 'name': 'source-3', 'target': None},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_create_with_valid_null(self):
- data = {'id': 4, 'name': 'source-4', 'target': None}
- serializer = NullableForeignKeySourceSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, data)
- self.assertEqual(obj.name, 'source-4')
-
- # Ensure source 4 is created, and everything else is as expected
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': 'target-1'},
- {'id': 2, 'name': 'source-2', 'target': 'target-1'},
- {'id': 3, 'name': 'source-3', 'target': None},
- {'id': 4, 'name': 'source-4', 'target': None}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_create_with_valid_emptystring(self):
- """
- The emptystring should be interpreted as null in the context
- of relationships.
- """
- data = {'id': 4, 'name': 'source-4', 'target': ''}
- expected_data = {'id': 4, 'name': 'source-4', 'target': None}
- serializer = NullableForeignKeySourceSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(serializer.data, expected_data)
- self.assertEqual(obj.name, 'source-4')
-
- # Ensure source 4 is created, and everything else is as expected
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': 'target-1'},
- {'id': 2, 'name': 'source-2', 'target': 'target-1'},
- {'id': 3, 'name': 'source-3', 'target': None},
- {'id': 4, 'name': 'source-4', 'target': None}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update_with_valid_null(self):
- data = {'id': 1, 'name': 'source-1', 'target': None}
- instance = NullableForeignKeySource.objects.get(pk=1)
- serializer = NullableForeignKeySourceSerializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
- serializer.save()
-
- # Ensure source 1 is updated, and everything else is as expected
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': None},
- {'id': 2, 'name': 'source-2', 'target': 'target-1'},
- {'id': 3, 'name': 'source-3', 'target': None}
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_foreign_key_update_with_valid_emptystring(self):
- """
- The emptystring should be interpreted as null in the context
- of relationships.
- """
- data = {'id': 1, 'name': 'source-1', 'target': ''}
- expected_data = {'id': 1, 'name': 'source-1', 'target': None}
- instance = NullableForeignKeySource.objects.get(pk=1)
- serializer = NullableForeignKeySourceSerializer(instance, data=data)
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, expected_data)
- serializer.save()
-
- # Ensure source 1 is updated, and everything else is as expected
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': None},
- {'id': 2, 'name': 'source-2', 'target': 'target-1'},
- {'id': 3, 'name': 'source-3', 'target': None}
- ]
- self.assertEqual(serializer.data, expected)
+# from django.test import TestCase
+# from rest_framework import serializers
+# from tests.models import NullableForeignKeySource, ForeignKeySource, ForeignKeyTarget
+
+
+# class ForeignKeyTargetSerializer(serializers.ModelSerializer):
+# sources = serializers.SlugRelatedField(many=True, slug_field='name')
+
+# class Meta:
+# model = ForeignKeyTarget
+
+
+# class ForeignKeySourceSerializer(serializers.ModelSerializer):
+# target = serializers.SlugRelatedField(slug_field='name')
+
+# class Meta:
+# model = ForeignKeySource
+
+
+# class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
+# target = serializers.SlugRelatedField(slug_field='name', required=False)
+
+# class Meta:
+# model = NullableForeignKeySource
+
+
+# # TODO: M2M Tests, FKTests (Non-nullable), One2One
+# class SlugForeignKeyTests(TestCase):
+# def setUp(self):
+# target = ForeignKeyTarget(name='target-1')
+# target.save()
+# new_target = ForeignKeyTarget(name='target-2')
+# new_target.save()
+# for idx in range(1, 4):
+# source = ForeignKeySource(name='source-%d' % idx, target=target)
+# source.save()
+
+# def test_foreign_key_retrieve(self):
+# queryset = ForeignKeySource.objects.all()
+# serializer = ForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': 'target-1'},
+# {'id': 2, 'name': 'source-2', 'target': 'target-1'},
+# {'id': 3, 'name': 'source-3', 'target': 'target-1'}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_reverse_foreign_key_retrieve(self):
+# queryset = ForeignKeyTarget.objects.all()
+# serializer = ForeignKeyTargetSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']},
+# {'id': 2, 'name': 'target-2', 'sources': []},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update(self):
+# data = {'id': 1, 'name': 'source-1', 'target': 'target-2'}
+# instance = ForeignKeySource.objects.get(pk=1)
+# serializer = ForeignKeySourceSerializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.data, data)
+# serializer.save()
+
+# # Ensure source 1 is updated, and everything else is as expected
+# queryset = ForeignKeySource.objects.all()
+# serializer = ForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': 'target-2'},
+# {'id': 2, 'name': 'source-2', 'target': 'target-1'},
+# {'id': 3, 'name': 'source-3', 'target': 'target-1'}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update_incorrect_type(self):
+# data = {'id': 1, 'name': 'source-1', 'target': 123}
+# instance = ForeignKeySource.objects.get(pk=1)
+# serializer = ForeignKeySourceSerializer(instance, data=data)
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'target': ['Object with name=123 does not exist.']})
+
+# def test_reverse_foreign_key_update(self):
+# data = {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']}
+# instance = ForeignKeyTarget.objects.get(pk=2)
+# serializer = ForeignKeyTargetSerializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# # We shouldn't have saved anything to the db yet since save
+# # hasn't been called.
+# queryset = ForeignKeyTarget.objects.all()
+# new_serializer = ForeignKeyTargetSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']},
+# {'id': 2, 'name': 'target-2', 'sources': []},
+# ]
+# self.assertEqual(new_serializer.data, expected)
+
+# serializer.save()
+# self.assertEqual(serializer.data, data)
+
+# # Ensure target 2 is update, and everything else is as expected
+# queryset = ForeignKeyTarget.objects.all()
+# serializer = ForeignKeyTargetSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': ['source-2']},
+# {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_create(self):
+# data = {'id': 4, 'name': 'source-4', 'target': 'target-2'}
+# serializer = ForeignKeySourceSerializer(data=data)
+# serializer.is_valid()
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'source-4')
+
+# # Ensure source 4 is added, and everything else is as expected
+# queryset = ForeignKeySource.objects.all()
+# serializer = ForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': 'target-1'},
+# {'id': 2, 'name': 'source-2', 'target': 'target-1'},
+# {'id': 3, 'name': 'source-3', 'target': 'target-1'},
+# {'id': 4, 'name': 'source-4', 'target': 'target-2'},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_reverse_foreign_key_create(self):
+# data = {'id': 3, 'name': 'target-3', 'sources': ['source-1', 'source-3']}
+# serializer = ForeignKeyTargetSerializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'target-3')
+
+# # Ensure target 3 is added, and everything else is as expected
+# queryset = ForeignKeyTarget.objects.all()
+# serializer = ForeignKeyTargetSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'target-1', 'sources': ['source-2']},
+# {'id': 2, 'name': 'target-2', 'sources': []},
+# {'id': 3, 'name': 'target-3', 'sources': ['source-1', 'source-3']},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update_with_invalid_null(self):
+# data = {'id': 1, 'name': 'source-1', 'target': None}
+# instance = ForeignKeySource.objects.get(pk=1)
+# serializer = ForeignKeySourceSerializer(instance, data=data)
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'target': ['This field is required.']})
+
+
+# class SlugNullableForeignKeyTests(TestCase):
+# def setUp(self):
+# target = ForeignKeyTarget(name='target-1')
+# target.save()
+# for idx in range(1, 4):
+# if idx == 3:
+# target = None
+# source = NullableForeignKeySource(name='source-%d' % idx, target=target)
+# source.save()
+
+# def test_foreign_key_retrieve_with_null(self):
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': 'target-1'},
+# {'id': 2, 'name': 'source-2', 'target': 'target-1'},
+# {'id': 3, 'name': 'source-3', 'target': None},
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_create_with_valid_null(self):
+# data = {'id': 4, 'name': 'source-4', 'target': None}
+# serializer = NullableForeignKeySourceSerializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, data)
+# self.assertEqual(obj.name, 'source-4')
+
+# # Ensure source 4 is created, and everything else is as expected
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': 'target-1'},
+# {'id': 2, 'name': 'source-2', 'target': 'target-1'},
+# {'id': 3, 'name': 'source-3', 'target': None},
+# {'id': 4, 'name': 'source-4', 'target': None}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_create_with_valid_emptystring(self):
+# """
+# The emptystring should be interpreted as null in the context
+# of relationships.
+# """
+# data = {'id': 4, 'name': 'source-4', 'target': ''}
+# expected_data = {'id': 4, 'name': 'source-4', 'target': None}
+# serializer = NullableForeignKeySourceSerializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# obj = serializer.save()
+# self.assertEqual(serializer.data, expected_data)
+# self.assertEqual(obj.name, 'source-4')
+
+# # Ensure source 4 is created, and everything else is as expected
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': 'target-1'},
+# {'id': 2, 'name': 'source-2', 'target': 'target-1'},
+# {'id': 3, 'name': 'source-3', 'target': None},
+# {'id': 4, 'name': 'source-4', 'target': None}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update_with_valid_null(self):
+# data = {'id': 1, 'name': 'source-1', 'target': None}
+# instance = NullableForeignKeySource.objects.get(pk=1)
+# serializer = NullableForeignKeySourceSerializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.data, data)
+# serializer.save()
+
+# # Ensure source 1 is updated, and everything else is as expected
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': None},
+# {'id': 2, 'name': 'source-2', 'target': 'target-1'},
+# {'id': 3, 'name': 'source-3', 'target': None}
+# ]
+# self.assertEqual(serializer.data, expected)
+
+# def test_foreign_key_update_with_valid_emptystring(self):
+# """
+# The emptystring should be interpreted as null in the context
+# of relationships.
+# """
+# data = {'id': 1, 'name': 'source-1', 'target': ''}
+# expected_data = {'id': 1, 'name': 'source-1', 'target': None}
+# instance = NullableForeignKeySource.objects.get(pk=1)
+# serializer = NullableForeignKeySourceSerializer(instance, data=data)
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.data, expected_data)
+# serializer.save()
+
+# # Ensure source 1 is updated, and everything else is as expected
+# queryset = NullableForeignKeySource.objects.all()
+# serializer = NullableForeignKeySourceSerializer(queryset, many=True)
+# expected = [
+# {'id': 1, 'name': 'source-1', 'target': None},
+# {'id': 2, 'name': 'source-2', 'target': 'target-1'},
+# {'id': 3, 'name': 'source-3', 'target': None}
+# ]
+# self.assertEqual(serializer.data, expected)
--
cgit v1.2.3
From c1036c17533a3091401ff90f825571f0e6125eca Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 3 Sep 2014 16:34:09 +0100
Subject: More test passing
---
rest_framework/relations.py | 56 +-
tests/test_serializer.py | 3748 +++++++++++++++++-----------------
tests/test_serializer_bulk_update.py | 556 ++---
tests/test_serializer_empty.py | 22 +-
tests/test_serializer_import.py | 32 +-
tests/test_serializer_nested.py | 698 +++----
tests/test_serializers.py | 50 +-
7 files changed, 2608 insertions(+), 2554 deletions(-)
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 0b01394a..661a1249 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -1,6 +1,7 @@
from rest_framework.fields import Field
+from rest_framework.reverse import reverse
from django.core.exceptions import ObjectDoesNotExist
-from django.core.urlresolvers import resolve, get_script_prefix
+from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
from rest_framework.compat import urlparse
@@ -100,11 +101,64 @@ class HyperlinkedIdentityField(RelatedField):
lookup_field = 'pk'
def __init__(self, **kwargs):
+ kwargs['read_only'] = True
self.view_name = kwargs.pop('view_name')
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
super(HyperlinkedIdentityField, self).__init__(**kwargs)
+ def get_attribute(self, instance):
+ return instance
+
+ def to_primative(self, value):
+ request = self.context.get('request', None)
+ format = self.context.get('format', None)
+
+ 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(value, self.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 % self.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.
+ """
+ # Unsaved objects will not yet have a valid URL.
+ if obj.pk is None:
+ return None
+
+ lookup_value = getattr(obj, self.lookup_field)
+ kwargs = {self.lookup_url_kwarg: lookup_value}
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
+
class SlugRelatedField(RelatedField):
def __init__(self, **kwargs):
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index fa5cafcf..b0eb4e27 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -1,2004 +1,2004 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-from django.db import models
-from django.db.models.fields import BLANK_CHOICE_DASH
-from django.test import TestCase
-from django.utils import unittest
-from django.utils.datastructures import MultiValueDict
-from django.utils.translation import ugettext_lazy as _
-from rest_framework import serializers, fields, relations
-from tests.models import (
- HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
- BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel,
- DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo,
- RESTFrameworkModel, ForeignKeySource
-)
-from tests.models import BasicModelSerializer
-import datetime
-import pickle
-try:
- import PIL
-except:
- PIL = None
-
-
-if PIL is not None:
- class AMOAFModel(RESTFrameworkModel):
- char_field = models.CharField(max_length=1024, blank=True)
- comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True)
- decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True)
- email_field = models.EmailField(max_length=1024, blank=True)
- file_field = models.FileField(upload_to='test', max_length=1024, blank=True)
- image_field = models.ImageField(upload_to='test', max_length=1024, blank=True)
- slug_field = models.SlugField(max_length=1024, blank=True)
- url_field = models.URLField(max_length=1024, blank=True)
- nullable_char_field = models.CharField(max_length=1024, blank=True, null=True)
-
- class DVOAFModel(RESTFrameworkModel):
- positive_integer_field = models.PositiveIntegerField(blank=True)
- positive_small_integer_field = models.PositiveSmallIntegerField(blank=True)
- email_field = models.EmailField(blank=True)
- file_field = models.FileField(upload_to='test', blank=True)
- image_field = models.ImageField(upload_to='test', blank=True)
- slug_field = models.SlugField(blank=True)
- url_field = models.URLField(blank=True)
+# # -*- coding: utf-8 -*-
+# from __future__ import unicode_literals
+# from django.db import models
+# from django.db.models.fields import BLANK_CHOICE_DASH
+# from django.test import TestCase
+# from django.utils import unittest
+# from django.utils.datastructures import MultiValueDict
+# from django.utils.translation import ugettext_lazy as _
+# from rest_framework import serializers, fields, relations
+# from tests.models import (
+# HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
+# BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel,
+# DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo,
+# RESTFrameworkModel, ForeignKeySource
+# )
+# from tests.models import BasicModelSerializer
+# import datetime
+# import pickle
+# try:
+# import PIL
+# except:
+# PIL = None
+
+
+# if PIL is not None:
+# class AMOAFModel(RESTFrameworkModel):
+# char_field = models.CharField(max_length=1024, blank=True)
+# comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True)
+# decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True)
+# email_field = models.EmailField(max_length=1024, blank=True)
+# file_field = models.FileField(upload_to='test', max_length=1024, blank=True)
+# image_field = models.ImageField(upload_to='test', max_length=1024, blank=True)
+# slug_field = models.SlugField(max_length=1024, blank=True)
+# url_field = models.URLField(max_length=1024, blank=True)
+# nullable_char_field = models.CharField(max_length=1024, blank=True, null=True)
+
+# class DVOAFModel(RESTFrameworkModel):
+# positive_integer_field = models.PositiveIntegerField(blank=True)
+# positive_small_integer_field = models.PositiveSmallIntegerField(blank=True)
+# email_field = models.EmailField(blank=True)
+# file_field = models.FileField(upload_to='test', blank=True)
+# image_field = models.ImageField(upload_to='test', blank=True)
+# slug_field = models.SlugField(blank=True)
+# url_field = models.URLField(blank=True)
-class SubComment(object):
- def __init__(self, sub_comment):
- self.sub_comment = sub_comment
+# class SubComment(object):
+# def __init__(self, sub_comment):
+# self.sub_comment = sub_comment
-class Comment(object):
- def __init__(self, email, content, created):
- self.email = email
- self.content = content
- self.created = created or datetime.datetime.now()
+# class Comment(object):
+# def __init__(self, email, content, created):
+# self.email = email
+# self.content = content
+# self.created = created or datetime.datetime.now()
- def __eq__(self, other):
- return all([getattr(self, attr) == getattr(other, attr)
- for attr in ('email', 'content', 'created')])
+# def __eq__(self, other):
+# return all([getattr(self, attr) == getattr(other, attr)
+# for attr in ('email', 'content', 'created')])
- def get_sub_comment(self):
- sub_comment = SubComment('And Merry Christmas!')
- return sub_comment
+# def get_sub_comment(self):
+# sub_comment = SubComment('And Merry Christmas!')
+# return sub_comment
-class CommentSerializer(serializers.Serializer):
- email = serializers.EmailField()
- content = serializers.CharField(max_length=1000)
- created = serializers.DateTimeField()
- sub_comment = serializers.Field(source='get_sub_comment.sub_comment')
+# class CommentSerializer(serializers.Serializer):
+# email = serializers.EmailField()
+# content = serializers.CharField(max_length=1000)
+# created = serializers.DateTimeField()
+# sub_comment = serializers.Field(source='get_sub_comment.sub_comment')
- def restore_object(self, data, instance=None):
- if instance is None:
- return Comment(**data)
- for key, val in data.items():
- setattr(instance, key, val)
- return instance
+# def restore_object(self, data, instance=None):
+# if instance is None:
+# return Comment(**data)
+# for key, val in data.items():
+# setattr(instance, key, val)
+# return instance
-class NamesSerializer(serializers.Serializer):
- first = serializers.CharField()
- last = serializers.CharField(required=False, default='')
- initials = serializers.CharField(required=False, default='')
+# class NamesSerializer(serializers.Serializer):
+# first = serializers.CharField()
+# last = serializers.CharField(required=False, default='')
+# initials = serializers.CharField(required=False, default='')
-class PersonIdentifierSerializer(serializers.Serializer):
- ssn = serializers.CharField()
- names = NamesSerializer(source='names', required=False)
+# class PersonIdentifierSerializer(serializers.Serializer):
+# ssn = serializers.CharField()
+# names = NamesSerializer(source='names', required=False)
-class BookSerializer(serializers.ModelSerializer):
- isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'})
+# class BookSerializer(serializers.ModelSerializer):
+# isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'})
- class Meta:
- model = Book
+# class Meta:
+# model = Book
-class ActionItemSerializer(serializers.ModelSerializer):
+# class ActionItemSerializer(serializers.ModelSerializer):
- class Meta:
- model = ActionItem
+# class Meta:
+# model = ActionItem
-class ActionItemSerializerOptionalFields(serializers.ModelSerializer):
- """
- Intended to test that fields with `required=False` are excluded from validation.
- """
- title = serializers.CharField(required=False)
+# class ActionItemSerializerOptionalFields(serializers.ModelSerializer):
+# """
+# Intended to test that fields with `required=False` are excluded from validation.
+# """
+# title = serializers.CharField(required=False)
- class Meta:
- model = ActionItem
- fields = ('title',)
+# class Meta:
+# model = ActionItem
+# fields = ('title',)
-class ActionItemSerializerCustomRestore(serializers.ModelSerializer):
+# class ActionItemSerializerCustomRestore(serializers.ModelSerializer):
- class Meta:
- model = ActionItem
+# class Meta:
+# model = ActionItem
- def restore_object(self, data, instance=None):
- if instance is None:
- return ActionItem(**data)
- for key, val in data.items():
- setattr(instance, key, val)
- return instance
+# def restore_object(self, data, instance=None):
+# if instance is None:
+# return ActionItem(**data)
+# for key, val in data.items():
+# setattr(instance, key, val)
+# return instance
-class PersonSerializer(serializers.ModelSerializer):
- info = serializers.Field(source='info')
+# class PersonSerializer(serializers.ModelSerializer):
+# info = serializers.Field(source='info')
- class Meta:
- model = Person
- fields = ('name', 'age', 'info')
- read_only_fields = ('age',)
+# class Meta:
+# model = Person
+# fields = ('name', 'age', 'info')
+# read_only_fields = ('age',)
-class NestedSerializer(serializers.Serializer):
- info = serializers.Field()
+# class NestedSerializer(serializers.Serializer):
+# info = serializers.Field()
-class ModelSerializerWithNestedSerializer(serializers.ModelSerializer):
- nested = NestedSerializer(source='*')
+# class ModelSerializerWithNestedSerializer(serializers.ModelSerializer):
+# nested = NestedSerializer(source='*')
- class Meta:
- model = Person
+# class Meta:
+# model = Person
-class NestedSerializerWithRenamedField(serializers.Serializer):
- renamed_info = serializers.Field(source='info')
+# class NestedSerializerWithRenamedField(serializers.Serializer):
+# renamed_info = serializers.Field(source='info')
-class ModelSerializerWithNestedSerializerWithRenamedField(serializers.ModelSerializer):
- nested = NestedSerializerWithRenamedField(source='*')
+# class ModelSerializerWithNestedSerializerWithRenamedField(serializers.ModelSerializer):
+# nested = NestedSerializerWithRenamedField(source='*')
- class Meta:
- model = Person
+# class Meta:
+# model = Person
-class PersonSerializerInvalidReadOnly(serializers.ModelSerializer):
- """
- Testing for #652.
- """
- info = serializers.Field(source='info')
+# class PersonSerializerInvalidReadOnly(serializers.ModelSerializer):
+# """
+# Testing for #652.
+# """
+# info = serializers.Field(source='info')
- class Meta:
- model = Person
- fields = ('name', 'age', 'info')
- read_only_fields = ('age', 'info')
+# class Meta:
+# model = Person
+# fields = ('name', 'age', 'info')
+# read_only_fields = ('age', 'info')
-class AlbumsSerializer(serializers.ModelSerializer):
+# class AlbumsSerializer(serializers.ModelSerializer):
- class Meta:
- model = Album
- fields = ['title', 'ref'] # lists are also valid options
+# class Meta:
+# model = Album
+# fields = ['title', 'ref'] # lists are also valid options
-class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
- class Meta:
- model = HasPositiveIntegerAsChoice
- fields = ['some_integer']
+# class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = HasPositiveIntegerAsChoice
+# fields = ['some_integer']
-class ForeignKeySourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = ForeignKeySource
+# class ForeignKeySourceSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = ForeignKeySource
-class HyperlinkedForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = ForeignKeySource
+# class HyperlinkedForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
+# class Meta:
+# model = ForeignKeySource
-class BasicTests(TestCase):
- def setUp(self):
- self.comment = Comment(
- 'tom@example.com',
- 'Happy new year!',
- datetime.datetime(2012, 1, 1)
- )
- self.actionitem = ActionItem(title='Some to do item',)
- self.data = {
- 'email': 'tom@example.com',
- 'content': 'Happy new year!',
- 'created': datetime.datetime(2012, 1, 1),
- 'sub_comment': 'This wont change'
- }
- self.expected = {
- 'email': 'tom@example.com',
- 'content': 'Happy new year!',
- 'created': datetime.datetime(2012, 1, 1),
- 'sub_comment': 'And Merry Christmas!'
- }
- self.person_data = {'name': 'dwight', 'age': 35}
- self.person = Person(**self.person_data)
- self.person.save()
-
- def test_empty(self):
- serializer = CommentSerializer()
- expected = {
- 'email': '',
- 'content': '',
- 'created': None
- }
- self.assertEqual(serializer.data, expected)
-
- def test_retrieve(self):
- serializer = CommentSerializer(self.comment)
- self.assertEqual(serializer.data, self.expected)
-
- def test_create(self):
- serializer = CommentSerializer(data=self.data)
- expected = self.comment
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected)
- self.assertFalse(serializer.object is expected)
- self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
-
- def test_create_nested(self):
- """Test a serializer with nested data."""
- names = {'first': 'John', 'last': 'Doe', 'initials': 'jd'}
- data = {'ssn': '1234567890', 'names': names}
- serializer = PersonIdentifierSerializer(data=data)
-
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
- self.assertFalse(serializer.object is data)
- self.assertEqual(serializer.data['names'], names)
-
- def test_create_partial_nested(self):
- """Test a serializer with nested data which has missing fields."""
- names = {'first': 'John'}
- data = {'ssn': '1234567890', 'names': names}
- serializer = PersonIdentifierSerializer(data=data)
-
- expected_names = {'first': 'John', 'last': '', 'initials': ''}
- data['names'] = expected_names
-
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
- self.assertFalse(serializer.object is expected_names)
- self.assertEqual(serializer.data['names'], expected_names)
-
- def test_null_nested(self):
- """Test a serializer with a nonexistent nested field"""
- data = {'ssn': '1234567890'}
- serializer = PersonIdentifierSerializer(data=data)
-
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
- self.assertFalse(serializer.object is data)
- expected = {'ssn': '1234567890', 'names': None}
- self.assertEqual(serializer.data, expected)
-
- def test_update(self):
- serializer = CommentSerializer(self.comment, data=self.data)
- expected = self.comment
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected)
- self.assertTrue(serializer.object is expected)
- self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
-
- def test_partial_update(self):
- msg = 'Merry New Year!'
- partial_data = {'content': msg}
- serializer = CommentSerializer(self.comment, data=partial_data)
- self.assertEqual(serializer.is_valid(), False)
- serializer = CommentSerializer(self.comment, data=partial_data, partial=True)
- expected = self.comment
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected)
- self.assertTrue(serializer.object is expected)
- self.assertEqual(serializer.data['content'], msg)
-
- def test_model_fields_as_expected(self):
- """
- Make sure that the fields returned are the same as defined
- in the Meta data
- """
- serializer = PersonSerializer(self.person)
- self.assertEqual(
- set(serializer.data.keys()),
- set(['name', 'age', 'info'])
- )
-
- def test_field_with_dictionary(self):
- """
- Make sure that dictionaries from fields are left intact
- """
- serializer = PersonSerializer(self.person)
- expected = self.person_data
- self.assertEqual(serializer.data['info'], expected)
-
- def test_read_only_fields(self):
- """
- Attempting to update fields set as read_only should have no effect.
- """
- serializer = PersonSerializer(self.person, data={'name': 'dwight', 'age': 99})
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(serializer.errors, {})
- # Assert age is unchanged (35)
- self.assertEqual(instance.age, self.person_data['age'])
-
- def test_invalid_read_only_fields(self):
- """
- Regression test for #652.
- """
- self.assertRaises(AssertionError, PersonSerializerInvalidReadOnly, [])
-
- def test_serializer_data_is_cleared_on_save(self):
- """
- Check _data attribute is cleared on `save()`
-
- Regression test for #1116
- — id field is not populated if `data` is accessed prior to `save()`
- """
- serializer = ActionItemSerializer(self.actionitem)
- self.assertIsNone(serializer.data.get('id', None), 'New instance. `id` should not be set.')
- serializer.save()
- self.assertIsNotNone(serializer.data.get('id', None), 'Model is saved. `id` should be set.')
-
- def test_fields_marked_as_not_required_are_excluded_from_validation(self):
- """
- Check that fields with `required=False` are included in list of exclusions.
- """
- serializer = ActionItemSerializerOptionalFields(self.actionitem)
- exclusions = serializer.get_validation_exclusions()
- self.assertTrue('title' in exclusions, '`title` field was marked `required=False` and should be excluded')
-
-
-class DictStyleSerializer(serializers.Serializer):
- """
- Note that we don't have any `restore_object` method, so the default
- case of simply returning a dict will apply.
- """
- email = serializers.EmailField()
-
-
-class DictStyleSerializerTests(TestCase):
- def test_dict_style_deserialize(self):
- """
- Ensure serializers can deserialize into a dict.
- """
- data = {'email': 'foo@example.com'}
- serializer = DictStyleSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
-
- def test_dict_style_serialize(self):
- """
- Ensure serializers can serialize dict objects.
- """
- data = {'email': 'foo@example.com'}
- serializer = DictStyleSerializer(data)
- self.assertEqual(serializer.data, data)
-
-
-class ValidationTests(TestCase):
- def setUp(self):
- self.comment = Comment(
- 'tom@example.com',
- 'Happy new year!',
- datetime.datetime(2012, 1, 1)
- )
- self.data = {
- 'email': 'tom@example.com',
- 'content': 'x' * 1001,
- 'created': datetime.datetime(2012, 1, 1)
- }
- self.actionitem = ActionItem(title='Some to do item',)
-
- def test_create(self):
- serializer = CommentSerializer(data=self.data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'content': ['Ensure this value has at most 1000 characters (it has 1001).']})
-
- def test_update(self):
- serializer = CommentSerializer(self.comment, data=self.data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'content': ['Ensure this value has at most 1000 characters (it has 1001).']})
-
- def test_update_missing_field(self):
- data = {
- 'content': 'xxx',
- 'created': datetime.datetime(2012, 1, 1)
- }
- serializer = CommentSerializer(self.comment, data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'email': ['This field is required.']})
-
- def test_missing_bool_with_default(self):
- """Make sure that a boolean value with a 'False' value is not
- mistaken for not having a default."""
- data = {
- 'title': 'Some action item',
- # No 'done' value.
- }
- serializer = ActionItemSerializer(self.actionitem, data=data)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.errors, {})
-
- def test_cross_field_validation(self):
-
- class CommentSerializerWithCrossFieldValidator(CommentSerializer):
-
- def validate(self, attrs):
- if attrs["email"] not in attrs["content"]:
- raise serializers.ValidationError("Email address not in content")
- return attrs
-
- data = {
- 'email': 'tom@example.com',
- 'content': 'A comment from tom@example.com',
- 'created': datetime.datetime(2012, 1, 1)
- }
-
- serializer = CommentSerializerWithCrossFieldValidator(data=data)
- self.assertTrue(serializer.is_valid())
-
- data['content'] = 'A comment from foo@bar.com'
-
- serializer = CommentSerializerWithCrossFieldValidator(data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'non_field_errors': ['Email address not in content']})
-
- def test_null_is_true_fields(self):
- """
- Omitting a value for null-field should validate.
- """
- serializer = PersonSerializer(data={'name': 'marko'})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.errors, {})
-
- def test_modelserializer_max_length_exceeded(self):
- data = {
- 'title': 'x' * 201,
- }
- serializer = ActionItemSerializer(data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'title': ['Ensure this value has at most 200 characters (it has 201).']})
-
- def test_modelserializer_max_length_exceeded_with_custom_restore(self):
- """
- When overriding ModelSerializer.restore_object, validation tests should still apply.
- Regression test for #623.
-
- https://github.com/tomchristie/django-rest-framework/pull/623
- """
- data = {
- 'title': 'x' * 201,
- }
- serializer = ActionItemSerializerCustomRestore(data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'title': ['Ensure this value has at most 200 characters (it has 201).']})
-
- def test_default_modelfield_max_length_exceeded(self):
- data = {
- 'title': 'Testing "info" field...',
- 'info': 'x' * 13,
- }
- serializer = ActionItemSerializer(data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'info': ['Ensure this value has at most 12 characters (it has 13).']})
-
- def test_datetime_validation_failure(self):
- """
- Test DateTimeField validation errors on non-str values.
- Regression test for #669.
-
- https://github.com/tomchristie/django-rest-framework/issues/669
- """
- data = self.data
- data['created'] = 0
-
- serializer = CommentSerializer(data=data)
- self.assertEqual(serializer.is_valid(), False)
-
- self.assertIn('created', serializer.errors)
-
- def test_missing_model_field_exception_msg(self):
- """
- Assert that a meaningful exception message is outputted when the model
- field is missing (e.g. when mistyping ``model``).
- """
- class BrokenModelSerializer(serializers.ModelSerializer):
- class Meta:
- fields = ['some_field']
-
- try:
- BrokenModelSerializer()
- except AssertionError as e:
- self.assertEqual(e.args[0], "Serializer class 'BrokenModelSerializer' is missing 'model' Meta option")
- except:
- self.fail('Wrong exception type thrown.')
-
- def test_writable_star_source_on_nested_serializer(self):
- """
- Assert that a nested serializer instantiated with source='*' correctly
- expands the data into the outer serializer.
- """
- serializer = ModelSerializerWithNestedSerializer(data={
- 'name': 'marko',
- 'nested': {'info': 'hi'}},
- )
- self.assertEqual(serializer.is_valid(), True)
-
- def test_writable_star_source_on_nested_serializer_with_parent_object(self):
- class TitleSerializer(serializers.Serializer):
- title = serializers.WritableField(source='title')
-
- class AlbumSerializer(serializers.ModelSerializer):
- nested = TitleSerializer(source='*')
-
- class Meta:
- model = Album
- fields = ('nested',)
-
- class PhotoSerializer(serializers.ModelSerializer):
- album = AlbumSerializer(source='album')
-
- class Meta:
- model = Photo
- fields = ('album', )
-
- photo = Photo(album=Album())
-
- data = {'album': {'nested': {'title': 'test'}}}
-
- serializer = PhotoSerializer(photo, data=data)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.data, data)
-
- def test_writable_star_source_with_inner_source_fields(self):
- """
- Tests that a serializer with source="*" correctly expands the
- it's fields into the outer serializer even if they have their
- own 'source' parameters.
- """
-
- serializer = ModelSerializerWithNestedSerializerWithRenamedField(data={
- 'name': 'marko',
- 'nested': {'renamed_info': 'hi'}},
- )
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.errors, {})
-
-
-class CustomValidationTests(TestCase):
- class CommentSerializerWithFieldValidator(CommentSerializer):
-
- def validate_email(self, attrs, source):
- attrs[source]
- return attrs
-
- def validate_content(self, attrs, source):
- value = attrs[source]
- if "test" not in value:
- raise serializers.ValidationError("Test not in value")
- return attrs
-
- def test_field_validation(self):
- data = {
- 'email': 'tom@example.com',
- 'content': 'A test comment',
- 'created': datetime.datetime(2012, 1, 1)
- }
-
- serializer = self.CommentSerializerWithFieldValidator(data=data)
- self.assertTrue(serializer.is_valid())
-
- data['content'] = 'This should not validate'
-
- serializer = self.CommentSerializerWithFieldValidator(data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'content': ['Test not in value']})
-
- def test_missing_data(self):
- """
- Make sure that validate_content isn't called if the field is missing
- """
- incomplete_data = {
- 'email': 'tom@example.com',
- 'created': datetime.datetime(2012, 1, 1)
- }
- serializer = self.CommentSerializerWithFieldValidator(data=incomplete_data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'content': ['This field is required.']})
-
- def test_wrong_data(self):
- """
- Make sure that validate_content isn't called if the field input is wrong
- """
- wrong_data = {
- 'email': 'not an email',
- 'content': 'A test comment',
- 'created': datetime.datetime(2012, 1, 1)
- }
- serializer = self.CommentSerializerWithFieldValidator(data=wrong_data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'email': ['Enter a valid email address.']})
-
- def test_partial_update(self):
- """
- Make sure that validate_email isn't called when partial=True and email
- isn't found in data.
- """
- initial_data = {
- 'email': 'tom@example.com',
- 'content': 'A test comment',
- 'created': datetime.datetime(2012, 1, 1)
- }
-
- serializer = self.CommentSerializerWithFieldValidator(data=initial_data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.object
-
- new_content = 'An *updated* test comment'
- partial_data = {
- 'content': new_content
- }
-
- serializer = self.CommentSerializerWithFieldValidator(instance=instance,
- data=partial_data,
- partial=True)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.object
- self.assertEqual(instance.content, new_content)
-
-
-class PositiveIntegerAsChoiceTests(TestCase):
- def test_positive_integer_in_json_is_correctly_parsed(self):
- data = {'some_integer': 1}
- serializer = PositiveIntegerAsChoiceSerializer(data=data)
- self.assertEqual(serializer.is_valid(), True)
-
-
-class ModelValidationTests(TestCase):
- def test_validate_unique(self):
- """
- Just check if serializers.ModelSerializer handles unique checks via .full_clean()
- """
- serializer = AlbumsSerializer(data={'title': 'a', 'ref': '1'})
- serializer.is_valid()
- serializer.save()
- second_serializer = AlbumsSerializer(data={'title': 'a'})
- self.assertFalse(second_serializer.is_valid())
- self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']})
- third_serializer = AlbumsSerializer(data=[{'title': 'b', 'ref': '1'}, {'title': 'c'}], many=True)
- self.assertFalse(third_serializer.is_valid())
- self.assertEqual(third_serializer.errors, [{'ref': ['Album with this Ref already exists.']}, {}])
-
- def test_foreign_key_is_null_with_partial(self):
- """
- Test ModelSerializer validation with partial=True
-
- Specifically test that a null foreign key does not pass validation
- """
- album = Album(title='test')
- album.save()
-
- class PhotoSerializer(serializers.ModelSerializer):
- class Meta:
- model = Photo
-
- photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk})
- self.assertTrue(photo_serializer.is_valid())
- photo = photo_serializer.save()
-
- # Updating only the album (foreign key)
- photo_serializer = PhotoSerializer(instance=photo, data={'album': ''}, partial=True)
- self.assertFalse(photo_serializer.is_valid())
- self.assertTrue('album' in photo_serializer.errors)
- self.assertEqual(photo_serializer.errors['album'], [photo_serializer.error_messages['required']])
-
- def test_foreign_key_with_partial(self):
- """
- Test ModelSerializer validation with partial=True
-
- Specifically test foreign key validation.
- """
-
- album = Album(title='test')
- album.save()
-
- class PhotoSerializer(serializers.ModelSerializer):
- class Meta:
- model = Photo
-
- photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk})
- self.assertTrue(photo_serializer.is_valid())
- photo = photo_serializer.save()
-
- # Updating only the album (foreign key)
- photo_serializer = PhotoSerializer(instance=photo, data={'album': album.pk}, partial=True)
- self.assertTrue(photo_serializer.is_valid())
- self.assertTrue(photo_serializer.save())
-
- # Updating only the description
- photo_serializer = PhotoSerializer(instance=photo,
- data={'description': 'new'},
- partial=True)
-
- self.assertTrue(photo_serializer.is_valid())
- self.assertTrue(photo_serializer.save())
-
-
-class RegexValidationTest(TestCase):
- def test_create_failed(self):
- serializer = BookSerializer(data={'isbn': '1234567890'})
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']})
-
- serializer = BookSerializer(data={'isbn': '12345678901234'})
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']})
-
- serializer = BookSerializer(data={'isbn': 'abcdefghijklm'})
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']})
-
- def test_create_success(self):
- serializer = BookSerializer(data={'isbn': '1234567890123'})
- self.assertTrue(serializer.is_valid())
-
-
-class MetadataTests(TestCase):
- def test_empty(self):
- serializer = CommentSerializer()
- expected = {
- 'email': serializers.CharField,
- 'content': serializers.CharField,
- 'created': serializers.DateTimeField
- }
- for field_name, field in expected.items():
- self.assertTrue(isinstance(serializer.data.fields[field_name], field))
-
-
-class ManyToManyTests(TestCase):
- def setUp(self):
- class ManyToManySerializer(serializers.ModelSerializer):
- class Meta:
- model = ManyToManyModel
-
- self.serializer_class = ManyToManySerializer
-
- # An anchor instance to use for the relationship
- self.anchor = Anchor()
- self.anchor.save()
-
- # A model instance with a many to many relationship to the anchor
- self.instance = ManyToManyModel()
- self.instance.save()
- self.instance.rel.add(self.anchor)
-
- # A serialized representation of the model instance
- self.data = {'id': 1, 'rel': [self.anchor.id]}
-
- def test_retrieve(self):
- """
- Serialize an instance of a model with a ManyToMany relationship.
- """
- serializer = self.serializer_class(instance=self.instance)
- expected = self.data
- self.assertEqual(serializer.data, expected)
-
- def test_create(self):
- """
- Create an instance of a model with a ManyToMany relationship.
- """
- data = {'rel': [self.anchor.id]}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 2)
- self.assertEqual(instance.pk, 2)
- self.assertEqual(list(instance.rel.all()), [self.anchor])
-
- def test_update(self):
- """
- Update an instance of a model with a ManyToMany relationship.
- """
- new_anchor = Anchor()
- new_anchor.save()
- data = {'rel': [self.anchor.id, new_anchor.id]}
- serializer = self.serializer_class(self.instance, data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(list(instance.rel.all()), [self.anchor, new_anchor])
-
- def test_create_empty_relationship(self):
- """
- Create an instance of a model with a ManyToMany relationship,
- containing no items.
- """
- data = {'rel': []}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 2)
- self.assertEqual(instance.pk, 2)
- self.assertEqual(list(instance.rel.all()), [])
-
- def test_update_empty_relationship(self):
- """
- Update an instance of a model with a ManyToMany relationship,
- containing no items.
- """
- new_anchor = Anchor()
- new_anchor.save()
- data = {'rel': []}
- serializer = self.serializer_class(self.instance, data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(list(instance.rel.all()), [])
-
- def test_create_empty_relationship_flat_data(self):
- """
- Create an instance of a model with a ManyToMany relationship,
- containing no items, using a representation that does not support
- lists (eg form data).
- """
- data = MultiValueDict()
- data.setlist('rel', [''])
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 2)
- self.assertEqual(instance.pk, 2)
- self.assertEqual(list(instance.rel.all()), [])
-
-
-class ReadOnlyManyToManyTests(TestCase):
- def setUp(self):
- class ReadOnlyManyToManySerializer(serializers.ModelSerializer):
- rel = serializers.RelatedField(many=True, read_only=True)
-
- class Meta:
- model = ReadOnlyManyToManyModel
-
- self.serializer_class = ReadOnlyManyToManySerializer
-
- # An anchor instance to use for the relationship
- self.anchor = Anchor()
- self.anchor.save()
-
- # A model instance with a many to many relationship to the anchor
- self.instance = ReadOnlyManyToManyModel()
- self.instance.save()
- self.instance.rel.add(self.anchor)
-
- # A serialized representation of the model instance
- self.data = {'rel': [self.anchor.id], 'id': 1, 'text': 'anchor'}
-
- def test_update(self):
- """
- Attempt to update an instance of a model with a ManyToMany
- relationship. Not updated due to read_only=True
- """
- new_anchor = Anchor()
- new_anchor.save()
- data = {'rel': [self.anchor.id, new_anchor.id]}
- serializer = self.serializer_class(self.instance, data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ReadOnlyManyToManyModel.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- # rel is still as original (1 entry)
- self.assertEqual(list(instance.rel.all()), [self.anchor])
-
- def test_update_without_relationship(self):
- """
- Attempt to update an instance of a model where many to ManyToMany
- relationship is not supplied. Not updated due to read_only=True
- """
- new_anchor = Anchor()
- new_anchor.save()
- data = {}
- serializer = self.serializer_class(self.instance, data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ReadOnlyManyToManyModel.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- # rel is still as original (1 entry)
- self.assertEqual(list(instance.rel.all()), [self.anchor])
-
-
-class DefaultValueTests(TestCase):
- def setUp(self):
- class DefaultValueSerializer(serializers.ModelSerializer):
- class Meta:
- model = DefaultValueModel
-
- self.serializer_class = DefaultValueSerializer
- self.objects = DefaultValueModel.objects
-
- def test_create_using_default(self):
- data = {}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(self.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(instance.text, 'foobar')
-
- def test_create_overriding_default(self):
- data = {'text': 'overridden'}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(self.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(instance.text, 'overridden')
-
- def test_partial_update_default(self):
- """ Regression test for issue #532 """
- data = {'text': 'overridden'}
- serializer = self.serializer_class(data=data, partial=True)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
-
- data = {'extra': 'extra_value'}
- serializer = self.serializer_class(instance=instance, data=data, partial=True)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
-
- self.assertEqual(instance.extra, 'extra_value')
- self.assertEqual(instance.text, 'overridden')
-
-
-class WritableFieldDefaultValueTests(TestCase):
-
- def setUp(self):
- self.expected = {'default': 'value'}
- self.create_field = fields.WritableField
-
- def test_get_default_value_with_noncallable(self):
- field = self.create_field(default=self.expected)
- got = field.get_default_value()
- self.assertEqual(got, self.expected)
-
- def test_get_default_value_with_callable(self):
- field = self.create_field(default=lambda: self.expected)
- got = field.get_default_value()
- self.assertEqual(got, self.expected)
-
- def test_get_default_value_when_not_required(self):
- field = self.create_field(default=self.expected, required=False)
- got = field.get_default_value()
- self.assertEqual(got, self.expected)
-
- def test_get_default_value_returns_None(self):
- field = self.create_field()
- got = field.get_default_value()
- self.assertIsNone(got)
-
- def test_get_default_value_returns_non_True_values(self):
- values = [None, '', False, 0, [], (), {}] # values that assumed as 'False' in the 'if' clause
- for expected in values:
- field = self.create_field(default=expected)
- got = field.get_default_value()
- self.assertEqual(got, expected)
-
-
-class RelatedFieldDefaultValueTests(WritableFieldDefaultValueTests):
-
- def setUp(self):
- self.expected = {'foo': 'bar'}
- self.create_field = relations.RelatedField
-
- def test_get_default_value_returns_empty_list(self):
- field = self.create_field(many=True)
- got = field.get_default_value()
- self.assertListEqual(got, [])
-
- def test_get_default_value_returns_expected(self):
- expected = [1, 2, 3]
- field = self.create_field(many=True, default=expected)
- got = field.get_default_value()
- self.assertListEqual(got, expected)
-
-
-class CallableDefaultValueTests(TestCase):
- def setUp(self):
- class CallableDefaultValueSerializer(serializers.ModelSerializer):
- class Meta:
- model = CallableDefaultValueModel
-
- self.serializer_class = CallableDefaultValueSerializer
- self.objects = CallableDefaultValueModel.objects
-
- def test_create_using_default(self):
- data = {}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(self.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(instance.text, 'foobar')
-
- def test_create_overriding_default(self):
- data = {'text': 'overridden'}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(self.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(instance.text, 'overridden')
-
-
-class ManyRelatedTests(TestCase):
- def test_reverse_relations(self):
- post = BlogPost.objects.create(title="Test blog post")
- post.blogpostcomment_set.create(text="I hate this blog post")
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class BlogPostCommentSerializer(serializers.Serializer):
- text = serializers.CharField()
-
- class BlogPostSerializer(serializers.Serializer):
- title = serializers.CharField()
- comments = BlogPostCommentSerializer(source='blogpostcomment_set')
-
- serializer = BlogPostSerializer(instance=post)
- expected = {
- 'title': 'Test blog post',
- 'comments': [
- {'text': 'I hate this blog post'},
- {'text': 'I love this blog post'}
- ]
- }
-
- self.assertEqual(serializer.data, expected)
-
- def test_include_reverse_relations(self):
- post = BlogPost.objects.create(title="Test blog post")
- post.blogpostcomment_set.create(text="I hate this blog post")
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class BlogPostSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlogPost
- fields = ('id', 'title', 'blogpostcomment_set')
-
- serializer = BlogPostSerializer(instance=post)
- expected = {
- 'id': 1, 'title': 'Test blog post', 'blogpostcomment_set': [1, 2]
- }
- self.assertEqual(serializer.data, expected)
-
- def test_depth_include_reverse_relations(self):
- post = BlogPost.objects.create(title="Test blog post")
- post.blogpostcomment_set.create(text="I hate this blog post")
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class BlogPostSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlogPost
- fields = ('id', 'title', 'blogpostcomment_set')
- depth = 1
-
- serializer = BlogPostSerializer(instance=post)
- expected = {
- 'id': 1, 'title': 'Test blog post',
- 'blogpostcomment_set': [
- {'id': 1, 'text': 'I hate this blog post', 'blog_post': 1},
- {'id': 2, 'text': 'I love this blog post', 'blog_post': 1}
- ]
- }
- self.assertEqual(serializer.data, expected)
-
- def test_callable_source(self):
- post = BlogPost.objects.create(title="Test blog post")
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class BlogPostCommentSerializer(serializers.Serializer):
- text = serializers.CharField()
-
- class BlogPostSerializer(serializers.Serializer):
- title = serializers.CharField()
- first_comment = BlogPostCommentSerializer(source='get_first_comment')
-
- serializer = BlogPostSerializer(post)
-
- expected = {
- 'title': 'Test blog post',
- 'first_comment': {'text': 'I love this blog post'}
- }
- self.assertEqual(serializer.data, expected)
-
-
-class RelatedTraversalTest(TestCase):
- def test_nested_traversal(self):
- """
- Source argument should support dotted.source notation.
- """
- user = Person.objects.create(name="django")
- post = BlogPost.objects.create(title="Test blog post", writer=user)
- post.blogpostcomment_set.create(text="I love this blog post")
+# class BasicTests(TestCase):
+# def setUp(self):
+# self.comment = Comment(
+# 'tom@example.com',
+# 'Happy new year!',
+# datetime.datetime(2012, 1, 1)
+# )
+# self.actionitem = ActionItem(title='Some to do item',)
+# self.data = {
+# 'email': 'tom@example.com',
+# 'content': 'Happy new year!',
+# 'created': datetime.datetime(2012, 1, 1),
+# 'sub_comment': 'This wont change'
+# }
+# self.expected = {
+# 'email': 'tom@example.com',
+# 'content': 'Happy new year!',
+# 'created': datetime.datetime(2012, 1, 1),
+# 'sub_comment': 'And Merry Christmas!'
+# }
+# self.person_data = {'name': 'dwight', 'age': 35}
+# self.person = Person(**self.person_data)
+# self.person.save()
+
+# def test_empty(self):
+# serializer = CommentSerializer()
+# expected = {
+# 'email': '',
+# 'content': '',
+# 'created': None
+# }
+# self.assertEqual(serializer.data, expected)
+
+# def test_retrieve(self):
+# serializer = CommentSerializer(self.comment)
+# self.assertEqual(serializer.data, self.expected)
+
+# def test_create(self):
+# serializer = CommentSerializer(data=self.data)
+# expected = self.comment
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.object, expected)
+# self.assertFalse(serializer.object is expected)
+# self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
+
+# def test_create_nested(self):
+# """Test a serializer with nested data."""
+# names = {'first': 'John', 'last': 'Doe', 'initials': 'jd'}
+# data = {'ssn': '1234567890', 'names': names}
+# serializer = PersonIdentifierSerializer(data=data)
+
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.object, data)
+# self.assertFalse(serializer.object is data)
+# self.assertEqual(serializer.data['names'], names)
+
+# def test_create_partial_nested(self):
+# """Test a serializer with nested data which has missing fields."""
+# names = {'first': 'John'}
+# data = {'ssn': '1234567890', 'names': names}
+# serializer = PersonIdentifierSerializer(data=data)
+
+# expected_names = {'first': 'John', 'last': '', 'initials': ''}
+# data['names'] = expected_names
+
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.object, data)
+# self.assertFalse(serializer.object is expected_names)
+# self.assertEqual(serializer.data['names'], expected_names)
+
+# def test_null_nested(self):
+# """Test a serializer with a nonexistent nested field"""
+# data = {'ssn': '1234567890'}
+# serializer = PersonIdentifierSerializer(data=data)
+
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.object, data)
+# self.assertFalse(serializer.object is data)
+# expected = {'ssn': '1234567890', 'names': None}
+# self.assertEqual(serializer.data, expected)
+
+# def test_update(self):
+# serializer = CommentSerializer(self.comment, data=self.data)
+# expected = self.comment
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.object, expected)
+# self.assertTrue(serializer.object is expected)
+# self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
+
+# def test_partial_update(self):
+# msg = 'Merry New Year!'
+# partial_data = {'content': msg}
+# serializer = CommentSerializer(self.comment, data=partial_data)
+# self.assertEqual(serializer.is_valid(), False)
+# serializer = CommentSerializer(self.comment, data=partial_data, partial=True)
+# expected = self.comment
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.object, expected)
+# self.assertTrue(serializer.object is expected)
+# self.assertEqual(serializer.data['content'], msg)
+
+# def test_model_fields_as_expected(self):
+# """
+# Make sure that the fields returned are the same as defined
+# in the Meta data
+# """
+# serializer = PersonSerializer(self.person)
+# self.assertEqual(
+# set(serializer.data.keys()),
+# set(['name', 'age', 'info'])
+# )
+
+# def test_field_with_dictionary(self):
+# """
+# Make sure that dictionaries from fields are left intact
+# """
+# serializer = PersonSerializer(self.person)
+# expected = self.person_data
+# self.assertEqual(serializer.data['info'], expected)
+
+# def test_read_only_fields(self):
+# """
+# Attempting to update fields set as read_only should have no effect.
+# """
+# serializer = PersonSerializer(self.person, data={'name': 'dwight', 'age': 99})
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+# self.assertEqual(serializer.errors, {})
+# # Assert age is unchanged (35)
+# self.assertEqual(instance.age, self.person_data['age'])
+
+# def test_invalid_read_only_fields(self):
+# """
+# Regression test for #652.
+# """
+# self.assertRaises(AssertionError, PersonSerializerInvalidReadOnly, [])
+
+# def test_serializer_data_is_cleared_on_save(self):
+# """
+# Check _data attribute is cleared on `save()`
+
+# Regression test for #1116
+# — id field is not populated if `data` is accessed prior to `save()`
+# """
+# serializer = ActionItemSerializer(self.actionitem)
+# self.assertIsNone(serializer.data.get('id', None), 'New instance. `id` should not be set.')
+# serializer.save()
+# self.assertIsNotNone(serializer.data.get('id', None), 'Model is saved. `id` should be set.')
+
+# def test_fields_marked_as_not_required_are_excluded_from_validation(self):
+# """
+# Check that fields with `required=False` are included in list of exclusions.
+# """
+# serializer = ActionItemSerializerOptionalFields(self.actionitem)
+# exclusions = serializer.get_validation_exclusions()
+# self.assertTrue('title' in exclusions, '`title` field was marked `required=False` and should be excluded')
+
+
+# class DictStyleSerializer(serializers.Serializer):
+# """
+# Note that we don't have any `restore_object` method, so the default
+# case of simply returning a dict will apply.
+# """
+# email = serializers.EmailField()
+
+
+# class DictStyleSerializerTests(TestCase):
+# def test_dict_style_deserialize(self):
+# """
+# Ensure serializers can deserialize into a dict.
+# """
+# data = {'email': 'foo@example.com'}
+# serializer = DictStyleSerializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.data, data)
+
+# def test_dict_style_serialize(self):
+# """
+# Ensure serializers can serialize dict objects.
+# """
+# data = {'email': 'foo@example.com'}
+# serializer = DictStyleSerializer(data)
+# self.assertEqual(serializer.data, data)
+
+
+# class ValidationTests(TestCase):
+# def setUp(self):
+# self.comment = Comment(
+# 'tom@example.com',
+# 'Happy new year!',
+# datetime.datetime(2012, 1, 1)
+# )
+# self.data = {
+# 'email': 'tom@example.com',
+# 'content': 'x' * 1001,
+# 'created': datetime.datetime(2012, 1, 1)
+# }
+# self.actionitem = ActionItem(title='Some to do item',)
+
+# def test_create(self):
+# serializer = CommentSerializer(data=self.data)
+# self.assertEqual(serializer.is_valid(), False)
+# self.assertEqual(serializer.errors, {'content': ['Ensure this value has at most 1000 characters (it has 1001).']})
+
+# def test_update(self):
+# serializer = CommentSerializer(self.comment, data=self.data)
+# self.assertEqual(serializer.is_valid(), False)
+# self.assertEqual(serializer.errors, {'content': ['Ensure this value has at most 1000 characters (it has 1001).']})
+
+# def test_update_missing_field(self):
+# data = {
+# 'content': 'xxx',
+# 'created': datetime.datetime(2012, 1, 1)
+# }
+# serializer = CommentSerializer(self.comment, data=data)
+# self.assertEqual(serializer.is_valid(), False)
+# self.assertEqual(serializer.errors, {'email': ['This field is required.']})
+
+# def test_missing_bool_with_default(self):
+# """Make sure that a boolean value with a 'False' value is not
+# mistaken for not having a default."""
+# data = {
+# 'title': 'Some action item',
+# # No 'done' value.
+# }
+# serializer = ActionItemSerializer(self.actionitem, data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.errors, {})
+
+# def test_cross_field_validation(self):
+
+# class CommentSerializerWithCrossFieldValidator(CommentSerializer):
+
+# def validate(self, attrs):
+# if attrs["email"] not in attrs["content"]:
+# raise serializers.ValidationError("Email address not in content")
+# return attrs
+
+# data = {
+# 'email': 'tom@example.com',
+# 'content': 'A comment from tom@example.com',
+# 'created': datetime.datetime(2012, 1, 1)
+# }
+
+# serializer = CommentSerializerWithCrossFieldValidator(data=data)
+# self.assertTrue(serializer.is_valid())
+
+# data['content'] = 'A comment from foo@bar.com'
+
+# serializer = CommentSerializerWithCrossFieldValidator(data=data)
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'non_field_errors': ['Email address not in content']})
+
+# def test_null_is_true_fields(self):
+# """
+# Omitting a value for null-field should validate.
+# """
+# serializer = PersonSerializer(data={'name': 'marko'})
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.errors, {})
+
+# def test_modelserializer_max_length_exceeded(self):
+# data = {
+# 'title': 'x' * 201,
+# }
+# serializer = ActionItemSerializer(data=data)
+# self.assertEqual(serializer.is_valid(), False)
+# self.assertEqual(serializer.errors, {'title': ['Ensure this value has at most 200 characters (it has 201).']})
+
+# def test_modelserializer_max_length_exceeded_with_custom_restore(self):
+# """
+# When overriding ModelSerializer.restore_object, validation tests should still apply.
+# Regression test for #623.
+
+# https://github.com/tomchristie/django-rest-framework/pull/623
+# """
+# data = {
+# 'title': 'x' * 201,
+# }
+# serializer = ActionItemSerializerCustomRestore(data=data)
+# self.assertEqual(serializer.is_valid(), False)
+# self.assertEqual(serializer.errors, {'title': ['Ensure this value has at most 200 characters (it has 201).']})
+
+# def test_default_modelfield_max_length_exceeded(self):
+# data = {
+# 'title': 'Testing "info" field...',
+# 'info': 'x' * 13,
+# }
+# serializer = ActionItemSerializer(data=data)
+# self.assertEqual(serializer.is_valid(), False)
+# self.assertEqual(serializer.errors, {'info': ['Ensure this value has at most 12 characters (it has 13).']})
+
+# def test_datetime_validation_failure(self):
+# """
+# Test DateTimeField validation errors on non-str values.
+# Regression test for #669.
+
+# https://github.com/tomchristie/django-rest-framework/issues/669
+# """
+# data = self.data
+# data['created'] = 0
+
+# serializer = CommentSerializer(data=data)
+# self.assertEqual(serializer.is_valid(), False)
+
+# self.assertIn('created', serializer.errors)
+
+# def test_missing_model_field_exception_msg(self):
+# """
+# Assert that a meaningful exception message is outputted when the model
+# field is missing (e.g. when mistyping ``model``).
+# """
+# class BrokenModelSerializer(serializers.ModelSerializer):
+# class Meta:
+# fields = ['some_field']
+
+# try:
+# BrokenModelSerializer()
+# except AssertionError as e:
+# self.assertEqual(e.args[0], "Serializer class 'BrokenModelSerializer' is missing 'model' Meta option")
+# except:
+# self.fail('Wrong exception type thrown.')
+
+# def test_writable_star_source_on_nested_serializer(self):
+# """
+# Assert that a nested serializer instantiated with source='*' correctly
+# expands the data into the outer serializer.
+# """
+# serializer = ModelSerializerWithNestedSerializer(data={
+# 'name': 'marko',
+# 'nested': {'info': 'hi'}},
+# )
+# self.assertEqual(serializer.is_valid(), True)
+
+# def test_writable_star_source_on_nested_serializer_with_parent_object(self):
+# class TitleSerializer(serializers.Serializer):
+# title = serializers.WritableField(source='title')
+
+# class AlbumSerializer(serializers.ModelSerializer):
+# nested = TitleSerializer(source='*')
+
+# class Meta:
+# model = Album
+# fields = ('nested',)
+
+# class PhotoSerializer(serializers.ModelSerializer):
+# album = AlbumSerializer(source='album')
+
+# class Meta:
+# model = Photo
+# fields = ('album', )
+
+# photo = Photo(album=Album())
+
+# data = {'album': {'nested': {'title': 'test'}}}
+
+# serializer = PhotoSerializer(photo, data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.data, data)
+
+# def test_writable_star_source_with_inner_source_fields(self):
+# """
+# Tests that a serializer with source="*" correctly expands the
+# it's fields into the outer serializer even if they have their
+# own 'source' parameters.
+# """
+
+# serializer = ModelSerializerWithNestedSerializerWithRenamedField(data={
+# 'name': 'marko',
+# 'nested': {'renamed_info': 'hi'}},
+# )
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.errors, {})
+
+
+# class CustomValidationTests(TestCase):
+# class CommentSerializerWithFieldValidator(CommentSerializer):
+
+# def validate_email(self, attrs, source):
+# attrs[source]
+# return attrs
+
+# def validate_content(self, attrs, source):
+# value = attrs[source]
+# if "test" not in value:
+# raise serializers.ValidationError("Test not in value")
+# return attrs
+
+# def test_field_validation(self):
+# data = {
+# 'email': 'tom@example.com',
+# 'content': 'A test comment',
+# 'created': datetime.datetime(2012, 1, 1)
+# }
+
+# serializer = self.CommentSerializerWithFieldValidator(data=data)
+# self.assertTrue(serializer.is_valid())
+
+# data['content'] = 'This should not validate'
+
+# serializer = self.CommentSerializerWithFieldValidator(data=data)
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'content': ['Test not in value']})
+
+# def test_missing_data(self):
+# """
+# Make sure that validate_content isn't called if the field is missing
+# """
+# incomplete_data = {
+# 'email': 'tom@example.com',
+# 'created': datetime.datetime(2012, 1, 1)
+# }
+# serializer = self.CommentSerializerWithFieldValidator(data=incomplete_data)
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'content': ['This field is required.']})
+
+# def test_wrong_data(self):
+# """
+# Make sure that validate_content isn't called if the field input is wrong
+# """
+# wrong_data = {
+# 'email': 'not an email',
+# 'content': 'A test comment',
+# 'created': datetime.datetime(2012, 1, 1)
+# }
+# serializer = self.CommentSerializerWithFieldValidator(data=wrong_data)
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'email': ['Enter a valid email address.']})
+
+# def test_partial_update(self):
+# """
+# Make sure that validate_email isn't called when partial=True and email
+# isn't found in data.
+# """
+# initial_data = {
+# 'email': 'tom@example.com',
+# 'content': 'A test comment',
+# 'created': datetime.datetime(2012, 1, 1)
+# }
+
+# serializer = self.CommentSerializerWithFieldValidator(data=initial_data)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.object
+
+# new_content = 'An *updated* test comment'
+# partial_data = {
+# 'content': new_content
+# }
+
+# serializer = self.CommentSerializerWithFieldValidator(instance=instance,
+# data=partial_data,
+# partial=True)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.object
+# self.assertEqual(instance.content, new_content)
+
+
+# class PositiveIntegerAsChoiceTests(TestCase):
+# def test_positive_integer_in_json_is_correctly_parsed(self):
+# data = {'some_integer': 1}
+# serializer = PositiveIntegerAsChoiceSerializer(data=data)
+# self.assertEqual(serializer.is_valid(), True)
+
+
+# class ModelValidationTests(TestCase):
+# def test_validate_unique(self):
+# """
+# Just check if serializers.ModelSerializer handles unique checks via .full_clean()
+# """
+# serializer = AlbumsSerializer(data={'title': 'a', 'ref': '1'})
+# serializer.is_valid()
+# serializer.save()
+# second_serializer = AlbumsSerializer(data={'title': 'a'})
+# self.assertFalse(second_serializer.is_valid())
+# self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']})
+# third_serializer = AlbumsSerializer(data=[{'title': 'b', 'ref': '1'}, {'title': 'c'}], many=True)
+# self.assertFalse(third_serializer.is_valid())
+# self.assertEqual(third_serializer.errors, [{'ref': ['Album with this Ref already exists.']}, {}])
+
+# def test_foreign_key_is_null_with_partial(self):
+# """
+# Test ModelSerializer validation with partial=True
+
+# Specifically test that a null foreign key does not pass validation
+# """
+# album = Album(title='test')
+# album.save()
+
+# class PhotoSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = Photo
+
+# photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk})
+# self.assertTrue(photo_serializer.is_valid())
+# photo = photo_serializer.save()
+
+# # Updating only the album (foreign key)
+# photo_serializer = PhotoSerializer(instance=photo, data={'album': ''}, partial=True)
+# self.assertFalse(photo_serializer.is_valid())
+# self.assertTrue('album' in photo_serializer.errors)
+# self.assertEqual(photo_serializer.errors['album'], [photo_serializer.error_messages['required']])
+
+# def test_foreign_key_with_partial(self):
+# """
+# Test ModelSerializer validation with partial=True
+
+# Specifically test foreign key validation.
+# """
+
+# album = Album(title='test')
+# album.save()
+
+# class PhotoSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = Photo
+
+# photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk})
+# self.assertTrue(photo_serializer.is_valid())
+# photo = photo_serializer.save()
+
+# # Updating only the album (foreign key)
+# photo_serializer = PhotoSerializer(instance=photo, data={'album': album.pk}, partial=True)
+# self.assertTrue(photo_serializer.is_valid())
+# self.assertTrue(photo_serializer.save())
+
+# # Updating only the description
+# photo_serializer = PhotoSerializer(instance=photo,
+# data={'description': 'new'},
+# partial=True)
+
+# self.assertTrue(photo_serializer.is_valid())
+# self.assertTrue(photo_serializer.save())
+
+
+# class RegexValidationTest(TestCase):
+# def test_create_failed(self):
+# serializer = BookSerializer(data={'isbn': '1234567890'})
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']})
+
+# serializer = BookSerializer(data={'isbn': '12345678901234'})
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']})
+
+# serializer = BookSerializer(data={'isbn': 'abcdefghijklm'})
+# self.assertFalse(serializer.is_valid())
+# self.assertEqual(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']})
+
+# def test_create_success(self):
+# serializer = BookSerializer(data={'isbn': '1234567890123'})
+# self.assertTrue(serializer.is_valid())
+
+
+# class MetadataTests(TestCase):
+# def test_empty(self):
+# serializer = CommentSerializer()
+# expected = {
+# 'email': serializers.CharField,
+# 'content': serializers.CharField,
+# 'created': serializers.DateTimeField
+# }
+# for field_name, field in expected.items():
+# self.assertTrue(isinstance(serializer.data.fields[field_name], field))
+
+
+# class ManyToManyTests(TestCase):
+# def setUp(self):
+# class ManyToManySerializer(serializers.ModelSerializer):
+# class Meta:
+# model = ManyToManyModel
+
+# self.serializer_class = ManyToManySerializer
+
+# # An anchor instance to use for the relationship
+# self.anchor = Anchor()
+# self.anchor.save()
+
+# # A model instance with a many to many relationship to the anchor
+# self.instance = ManyToManyModel()
+# self.instance.save()
+# self.instance.rel.add(self.anchor)
+
+# # A serialized representation of the model instance
+# self.data = {'id': 1, 'rel': [self.anchor.id]}
+
+# def test_retrieve(self):
+# """
+# Serialize an instance of a model with a ManyToMany relationship.
+# """
+# serializer = self.serializer_class(instance=self.instance)
+# expected = self.data
+# self.assertEqual(serializer.data, expected)
+
+# def test_create(self):
+# """
+# Create an instance of a model with a ManyToMany relationship.
+# """
+# data = {'rel': [self.anchor.id]}
+# serializer = self.serializer_class(data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+# self.assertEqual(len(ManyToManyModel.objects.all()), 2)
+# self.assertEqual(instance.pk, 2)
+# self.assertEqual(list(instance.rel.all()), [self.anchor])
+
+# def test_update(self):
+# """
+# Update an instance of a model with a ManyToMany relationship.
+# """
+# new_anchor = Anchor()
+# new_anchor.save()
+# data = {'rel': [self.anchor.id, new_anchor.id]}
+# serializer = self.serializer_class(self.instance, data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+# self.assertEqual(len(ManyToManyModel.objects.all()), 1)
+# self.assertEqual(instance.pk, 1)
+# self.assertEqual(list(instance.rel.all()), [self.anchor, new_anchor])
+
+# def test_create_empty_relationship(self):
+# """
+# Create an instance of a model with a ManyToMany relationship,
+# containing no items.
+# """
+# data = {'rel': []}
+# serializer = self.serializer_class(data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+# self.assertEqual(len(ManyToManyModel.objects.all()), 2)
+# self.assertEqual(instance.pk, 2)
+# self.assertEqual(list(instance.rel.all()), [])
+
+# def test_update_empty_relationship(self):
+# """
+# Update an instance of a model with a ManyToMany relationship,
+# containing no items.
+# """
+# new_anchor = Anchor()
+# new_anchor.save()
+# data = {'rel': []}
+# serializer = self.serializer_class(self.instance, data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+# self.assertEqual(len(ManyToManyModel.objects.all()), 1)
+# self.assertEqual(instance.pk, 1)
+# self.assertEqual(list(instance.rel.all()), [])
+
+# def test_create_empty_relationship_flat_data(self):
+# """
+# Create an instance of a model with a ManyToMany relationship,
+# containing no items, using a representation that does not support
+# lists (eg form data).
+# """
+# data = MultiValueDict()
+# data.setlist('rel', [''])
+# serializer = self.serializer_class(data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+# self.assertEqual(len(ManyToManyModel.objects.all()), 2)
+# self.assertEqual(instance.pk, 2)
+# self.assertEqual(list(instance.rel.all()), [])
+
+
+# class ReadOnlyManyToManyTests(TestCase):
+# def setUp(self):
+# class ReadOnlyManyToManySerializer(serializers.ModelSerializer):
+# rel = serializers.RelatedField(many=True, read_only=True)
+
+# class Meta:
+# model = ReadOnlyManyToManyModel
+
+# self.serializer_class = ReadOnlyManyToManySerializer
+
+# # An anchor instance to use for the relationship
+# self.anchor = Anchor()
+# self.anchor.save()
+
+# # A model instance with a many to many relationship to the anchor
+# self.instance = ReadOnlyManyToManyModel()
+# self.instance.save()
+# self.instance.rel.add(self.anchor)
+
+# # A serialized representation of the model instance
+# self.data = {'rel': [self.anchor.id], 'id': 1, 'text': 'anchor'}
+
+# def test_update(self):
+# """
+# Attempt to update an instance of a model with a ManyToMany
+# relationship. Not updated due to read_only=True
+# """
+# new_anchor = Anchor()
+# new_anchor.save()
+# data = {'rel': [self.anchor.id, new_anchor.id]}
+# serializer = self.serializer_class(self.instance, data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+# self.assertEqual(len(ReadOnlyManyToManyModel.objects.all()), 1)
+# self.assertEqual(instance.pk, 1)
+# # rel is still as original (1 entry)
+# self.assertEqual(list(instance.rel.all()), [self.anchor])
+
+# def test_update_without_relationship(self):
+# """
+# Attempt to update an instance of a model where many to ManyToMany
+# relationship is not supplied. Not updated due to read_only=True
+# """
+# new_anchor = Anchor()
+# new_anchor.save()
+# data = {}
+# serializer = self.serializer_class(self.instance, data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+# self.assertEqual(len(ReadOnlyManyToManyModel.objects.all()), 1)
+# self.assertEqual(instance.pk, 1)
+# # rel is still as original (1 entry)
+# self.assertEqual(list(instance.rel.all()), [self.anchor])
+
+
+# class DefaultValueTests(TestCase):
+# def setUp(self):
+# class DefaultValueSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = DefaultValueModel
+
+# self.serializer_class = DefaultValueSerializer
+# self.objects = DefaultValueModel.objects
+
+# def test_create_using_default(self):
+# data = {}
+# serializer = self.serializer_class(data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+# self.assertEqual(len(self.objects.all()), 1)
+# self.assertEqual(instance.pk, 1)
+# self.assertEqual(instance.text, 'foobar')
+
+# def test_create_overriding_default(self):
+# data = {'text': 'overridden'}
+# serializer = self.serializer_class(data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+# self.assertEqual(len(self.objects.all()), 1)
+# self.assertEqual(instance.pk, 1)
+# self.assertEqual(instance.text, 'overridden')
+
+# def test_partial_update_default(self):
+# """ Regression test for issue #532 """
+# data = {'text': 'overridden'}
+# serializer = self.serializer_class(data=data, partial=True)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+
+# data = {'extra': 'extra_value'}
+# serializer = self.serializer_class(instance=instance, data=data, partial=True)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+
+# self.assertEqual(instance.extra, 'extra_value')
+# self.assertEqual(instance.text, 'overridden')
+
+
+# class WritableFieldDefaultValueTests(TestCase):
+
+# def setUp(self):
+# self.expected = {'default': 'value'}
+# self.create_field = fields.WritableField
+
+# def test_get_default_value_with_noncallable(self):
+# field = self.create_field(default=self.expected)
+# got = field.get_default_value()
+# self.assertEqual(got, self.expected)
+
+# def test_get_default_value_with_callable(self):
+# field = self.create_field(default=lambda: self.expected)
+# got = field.get_default_value()
+# self.assertEqual(got, self.expected)
+
+# def test_get_default_value_when_not_required(self):
+# field = self.create_field(default=self.expected, required=False)
+# got = field.get_default_value()
+# self.assertEqual(got, self.expected)
+
+# def test_get_default_value_returns_None(self):
+# field = self.create_field()
+# got = field.get_default_value()
+# self.assertIsNone(got)
+
+# def test_get_default_value_returns_non_True_values(self):
+# values = [None, '', False, 0, [], (), {}] # values that assumed as 'False' in the 'if' clause
+# for expected in values:
+# field = self.create_field(default=expected)
+# got = field.get_default_value()
+# self.assertEqual(got, expected)
+
+
+# class RelatedFieldDefaultValueTests(WritableFieldDefaultValueTests):
+
+# def setUp(self):
+# self.expected = {'foo': 'bar'}
+# self.create_field = relations.RelatedField
+
+# def test_get_default_value_returns_empty_list(self):
+# field = self.create_field(many=True)
+# got = field.get_default_value()
+# self.assertListEqual(got, [])
+
+# def test_get_default_value_returns_expected(self):
+# expected = [1, 2, 3]
+# field = self.create_field(many=True, default=expected)
+# got = field.get_default_value()
+# self.assertListEqual(got, expected)
+
+
+# class CallableDefaultValueTests(TestCase):
+# def setUp(self):
+# class CallableDefaultValueSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = CallableDefaultValueModel
+
+# self.serializer_class = CallableDefaultValueSerializer
+# self.objects = CallableDefaultValueModel.objects
+
+# def test_create_using_default(self):
+# data = {}
+# serializer = self.serializer_class(data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+# self.assertEqual(len(self.objects.all()), 1)
+# self.assertEqual(instance.pk, 1)
+# self.assertEqual(instance.text, 'foobar')
+
+# def test_create_overriding_default(self):
+# data = {'text': 'overridden'}
+# serializer = self.serializer_class(data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# instance = serializer.save()
+# self.assertEqual(len(self.objects.all()), 1)
+# self.assertEqual(instance.pk, 1)
+# self.assertEqual(instance.text, 'overridden')
+
+
+# class ManyRelatedTests(TestCase):
+# def test_reverse_relations(self):
+# post = BlogPost.objects.create(title="Test blog post")
+# post.blogpostcomment_set.create(text="I hate this blog post")
+# post.blogpostcomment_set.create(text="I love this blog post")
+
+# class BlogPostCommentSerializer(serializers.Serializer):
+# text = serializers.CharField()
+
+# class BlogPostSerializer(serializers.Serializer):
+# title = serializers.CharField()
+# comments = BlogPostCommentSerializer(source='blogpostcomment_set')
+
+# serializer = BlogPostSerializer(instance=post)
+# expected = {
+# 'title': 'Test blog post',
+# 'comments': [
+# {'text': 'I hate this blog post'},
+# {'text': 'I love this blog post'}
+# ]
+# }
+
+# self.assertEqual(serializer.data, expected)
+
+# def test_include_reverse_relations(self):
+# post = BlogPost.objects.create(title="Test blog post")
+# post.blogpostcomment_set.create(text="I hate this blog post")
+# post.blogpostcomment_set.create(text="I love this blog post")
+
+# class BlogPostSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = BlogPost
+# fields = ('id', 'title', 'blogpostcomment_set')
+
+# serializer = BlogPostSerializer(instance=post)
+# expected = {
+# 'id': 1, 'title': 'Test blog post', 'blogpostcomment_set': [1, 2]
+# }
+# self.assertEqual(serializer.data, expected)
+
+# def test_depth_include_reverse_relations(self):
+# post = BlogPost.objects.create(title="Test blog post")
+# post.blogpostcomment_set.create(text="I hate this blog post")
+# post.blogpostcomment_set.create(text="I love this blog post")
+
+# class BlogPostSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = BlogPost
+# fields = ('id', 'title', 'blogpostcomment_set')
+# depth = 1
+
+# serializer = BlogPostSerializer(instance=post)
+# expected = {
+# 'id': 1, 'title': 'Test blog post',
+# 'blogpostcomment_set': [
+# {'id': 1, 'text': 'I hate this blog post', 'blog_post': 1},
+# {'id': 2, 'text': 'I love this blog post', 'blog_post': 1}
+# ]
+# }
+# self.assertEqual(serializer.data, expected)
+
+# def test_callable_source(self):
+# post = BlogPost.objects.create(title="Test blog post")
+# post.blogpostcomment_set.create(text="I love this blog post")
+
+# class BlogPostCommentSerializer(serializers.Serializer):
+# text = serializers.CharField()
+
+# class BlogPostSerializer(serializers.Serializer):
+# title = serializers.CharField()
+# first_comment = BlogPostCommentSerializer(source='get_first_comment')
+
+# serializer = BlogPostSerializer(post)
+
+# expected = {
+# 'title': 'Test blog post',
+# 'first_comment': {'text': 'I love this blog post'}
+# }
+# self.assertEqual(serializer.data, expected)
+
+
+# class RelatedTraversalTest(TestCase):
+# def test_nested_traversal(self):
+# """
+# Source argument should support dotted.source notation.
+# """
+# user = Person.objects.create(name="django")
+# post = BlogPost.objects.create(title="Test blog post", writer=user)
+# post.blogpostcomment_set.create(text="I love this blog post")
- class PersonSerializer(serializers.ModelSerializer):
- class Meta:
- model = Person
- fields = ("name", "age")
+# class PersonSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = Person
+# fields = ("name", "age")
- class BlogPostCommentSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlogPostComment
- fields = ("text", "post_owner")
+# class BlogPostCommentSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = BlogPostComment
+# fields = ("text", "post_owner")
- text = serializers.CharField()
- post_owner = PersonSerializer(source='blog_post.writer')
+# text = serializers.CharField()
+# post_owner = PersonSerializer(source='blog_post.writer')
- class BlogPostSerializer(serializers.Serializer):
- title = serializers.CharField()
- comments = BlogPostCommentSerializer(source='blogpostcomment_set')
+# class BlogPostSerializer(serializers.Serializer):
+# title = serializers.CharField()
+# comments = BlogPostCommentSerializer(source='blogpostcomment_set')
- serializer = BlogPostSerializer(instance=post)
+# serializer = BlogPostSerializer(instance=post)
- expected = {
- 'title': 'Test blog post',
- 'comments': [{
- 'text': 'I love this blog post',
- 'post_owner': {
- "name": "django",
- "age": None
- }
- }]
- }
-
- self.assertEqual(serializer.data, expected)
+# expected = {
+# 'title': 'Test blog post',
+# 'comments': [{
+# 'text': 'I love this blog post',
+# 'post_owner': {
+# "name": "django",
+# "age": None
+# }
+# }]
+# }
+
+# self.assertEqual(serializer.data, expected)
- def test_nested_traversal_with_none(self):
- """
- If a component of the dotted.source is None, return None for the field.
- """
- from tests.models import NullableForeignKeySource
- instance = NullableForeignKeySource.objects.create(name='Source with null FK')
-
- class NullableSourceSerializer(serializers.Serializer):
- target_name = serializers.Field(source='target.name')
-
- serializer = NullableSourceSerializer(instance=instance)
-
- expected = {
- 'target_name': None,
- }
-
- self.assertEqual(serializer.data, expected)
-
-
-class SerializerMethodFieldTests(TestCase):
- def setUp(self):
-
- class BoopSerializer(serializers.Serializer):
- beep = serializers.SerializerMethodField('get_beep')
- boop = serializers.Field()
- boop_count = serializers.SerializerMethodField('get_boop_count')
-
- def get_beep(self, obj):
- return 'hello!'
-
- def get_boop_count(self, obj):
- return len(obj.boop)
-
- self.serializer_class = BoopSerializer
-
- def test_serializer_method_field(self):
-
- class MyModel(object):
- boop = ['a', 'b', 'c']
-
- source_data = MyModel()
-
- serializer = self.serializer_class(source_data)
-
- expected = {
- 'beep': 'hello!',
- 'boop': ['a', 'b', 'c'],
- 'boop_count': 3,
- }
-
- self.assertEqual(serializer.data, expected)
-
-
-# Test for issue #324
-class BlankFieldTests(TestCase):
- def setUp(self):
-
- class BlankFieldModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlankFieldModel
-
- class BlankFieldSerializer(serializers.Serializer):
- title = serializers.CharField(required=False)
-
- class NotBlankFieldModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = BasicModel
-
- class NotBlankFieldSerializer(serializers.Serializer):
- title = serializers.CharField()
-
- self.model_serializer_class = BlankFieldModelSerializer
- self.serializer_class = BlankFieldSerializer
- self.not_blank_model_serializer_class = NotBlankFieldModelSerializer
- self.not_blank_serializer_class = NotBlankFieldSerializer
- self.data = {'title': ''}
-
- def test_create_blank_field(self):
- serializer = self.serializer_class(data=self.data)
- self.assertEqual(serializer.is_valid(), True)
-
- def test_create_model_blank_field(self):
- serializer = self.model_serializer_class(data=self.data)
- self.assertEqual(serializer.is_valid(), True)
-
- def test_create_model_null_field(self):
- serializer = self.model_serializer_class(data={'title': None})
- self.assertEqual(serializer.is_valid(), True)
- serializer.save()
- self.assertIsNot(serializer.object.pk, None)
- self.assertEqual(serializer.object.title, '')
-
- def test_create_not_blank_field(self):
- """
- Test to ensure blank data in a field not marked as blank=True
- is considered invalid in a non-model serializer
- """
- serializer = self.not_blank_serializer_class(data=self.data)
- self.assertEqual(serializer.is_valid(), False)
-
- def test_create_model_not_blank_field(self):
- """
- Test to ensure blank data in a field not marked as blank=True
- is considered invalid in a model serializer
- """
- serializer = self.not_blank_model_serializer_class(data=self.data)
- self.assertEqual(serializer.is_valid(), False)
-
- def test_create_model_empty_field(self):
- serializer = self.model_serializer_class(data={})
- self.assertEqual(serializer.is_valid(), True)
-
- def test_create_model_null_field_save(self):
- """
- Regression test for #1330.
-
- https://github.com/tomchristie/django-rest-framework/pull/1330
- """
- serializer = self.model_serializer_class(data={'title': None})
- self.assertEqual(serializer.is_valid(), True)
-
- try:
- serializer.save()
- except Exception:
- self.fail('Exception raised on save() after validation passes')
-
-
-# Test for issue #460
-class SerializerPickleTests(TestCase):
- """
- Test pickleability of the output of Serializers
- """
- def test_pickle_simple_model_serializer_data(self):
- """
- Test simple serializer
- """
- pickle.dumps(PersonSerializer(Person(name="Methusela", age=969)).data)
-
- def test_pickle_inner_serializer(self):
- """
- Test pickling a serializer whose resulting .data (a SortedDictWithMetadata) will
- have unpickleable meta data--in order to make sure metadata doesn't get pulled into the pickle.
- See DictWithMetadata.__getstate__
- """
- class InnerPersonSerializer(serializers.ModelSerializer):
- class Meta:
- model = Person
- fields = ('name', 'age')
- pickle.dumps(InnerPersonSerializer(Person(name="Noah", age=950)).data, 0)
-
- def test_getstate_method_should_not_return_none(self):
- """
- Regression test for #645.
- """
- data = serializers.DictWithMetadata({1: 1})
- self.assertEqual(data.__getstate__(), serializers.SortedDict({1: 1}))
-
- def test_serializer_data_is_pickleable(self):
- """
- Another regression test for #645.
- """
- data = serializers.SortedDictWithMetadata({1: 1})
- repr(pickle.loads(pickle.dumps(data, 0)))
-
-
-# test for issue #725
-class SeveralChoicesModel(models.Model):
- color = models.CharField(
- max_length=10,
- choices=[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')],
- blank=False
- )
- drink = models.CharField(
- max_length=10,
- choices=[('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')],
- blank=False,
- default='beer'
- )
- os = models.CharField(
- max_length=10,
- choices=[('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')],
- blank=True
- )
- music_genre = models.CharField(
- max_length=10,
- choices=[('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')],
- blank=True,
- default='metal'
- )
-
-
-class SerializerChoiceFields(TestCase):
-
- def setUp(self):
- super(SerializerChoiceFields, self).setUp()
-
- class SeveralChoicesSerializer(serializers.ModelSerializer):
- class Meta:
- model = SeveralChoicesModel
- fields = ('color', 'drink', 'os', 'music_genre')
-
- self.several_choices_serializer = SeveralChoicesSerializer
-
- def test_choices_blank_false_not_default(self):
- serializer = self.several_choices_serializer()
- self.assertEqual(
- serializer.fields['color'].choices,
- [('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')]
- )
-
- def test_choices_blank_false_with_default(self):
- serializer = self.several_choices_serializer()
- self.assertEqual(
- serializer.fields['drink'].choices,
- [('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')]
- )
-
- def test_choices_blank_true_not_default(self):
- serializer = self.several_choices_serializer()
- self.assertEqual(
- serializer.fields['os'].choices,
- BLANK_CHOICE_DASH + [('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')]
- )
-
- def test_choices_blank_true_with_default(self):
- serializer = self.several_choices_serializer()
- self.assertEqual(
- serializer.fields['music_genre'].choices,
- BLANK_CHOICE_DASH + [('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')]
- )
-
-
-# Regression tests for #675
-class Ticket(models.Model):
- assigned = models.ForeignKey(
- Person, related_name='assigned_tickets')
- reviewer = models.ForeignKey(
- Person, blank=True, null=True, related_name='reviewed_tickets')
-
-
-class SerializerRelatedChoicesTest(TestCase):
-
- def setUp(self):
- super(SerializerRelatedChoicesTest, self).setUp()
-
- class RelatedChoicesSerializer(serializers.ModelSerializer):
- class Meta:
- model = Ticket
- fields = ('assigned', 'reviewer')
-
- self.related_fields_serializer = RelatedChoicesSerializer
-
- def test_empty_queryset_required(self):
- serializer = self.related_fields_serializer()
- self.assertEqual(serializer.fields['assigned'].queryset.count(), 0)
- self.assertEqual(
- [x for x in serializer.fields['assigned'].widget.choices],
- []
- )
-
- def test_empty_queryset_not_required(self):
- serializer = self.related_fields_serializer()
- self.assertEqual(serializer.fields['reviewer'].queryset.count(), 0)
- self.assertEqual(
- [x for x in serializer.fields['reviewer'].widget.choices],
- [('', '---------')]
- )
-
- def test_with_some_persons_required(self):
- Person.objects.create(name="Lionel Messi")
- Person.objects.create(name="Xavi Hernandez")
- serializer = self.related_fields_serializer()
- self.assertEqual(serializer.fields['assigned'].queryset.count(), 2)
- self.assertEqual(
- [x for x in serializer.fields['assigned'].widget.choices],
- [(1, 'Person object - 1'), (2, 'Person object - 2')]
- )
-
- def test_with_some_persons_not_required(self):
- Person.objects.create(name="Lionel Messi")
- Person.objects.create(name="Xavi Hernandez")
- serializer = self.related_fields_serializer()
- self.assertEqual(serializer.fields['reviewer'].queryset.count(), 2)
- self.assertEqual(
- [x for x in serializer.fields['reviewer'].widget.choices],
- [('', '---------'), (1, 'Person object - 1'), (2, 'Person object - 2')]
- )
+# def test_nested_traversal_with_none(self):
+# """
+# If a component of the dotted.source is None, return None for the field.
+# """
+# from tests.models import NullableForeignKeySource
+# instance = NullableForeignKeySource.objects.create(name='Source with null FK')
+
+# class NullableSourceSerializer(serializers.Serializer):
+# target_name = serializers.Field(source='target.name')
+
+# serializer = NullableSourceSerializer(instance=instance)
+
+# expected = {
+# 'target_name': None,
+# }
+
+# self.assertEqual(serializer.data, expected)
+
+
+# class SerializerMethodFieldTests(TestCase):
+# def setUp(self):
+
+# class BoopSerializer(serializers.Serializer):
+# beep = serializers.SerializerMethodField('get_beep')
+# boop = serializers.Field()
+# boop_count = serializers.SerializerMethodField('get_boop_count')
+
+# def get_beep(self, obj):
+# return 'hello!'
+
+# def get_boop_count(self, obj):
+# return len(obj.boop)
+
+# self.serializer_class = BoopSerializer
+
+# def test_serializer_method_field(self):
+
+# class MyModel(object):
+# boop = ['a', 'b', 'c']
+
+# source_data = MyModel()
+
+# serializer = self.serializer_class(source_data)
+
+# expected = {
+# 'beep': 'hello!',
+# 'boop': ['a', 'b', 'c'],
+# 'boop_count': 3,
+# }
+
+# self.assertEqual(serializer.data, expected)
+
+
+# # Test for issue #324
+# class BlankFieldTests(TestCase):
+# def setUp(self):
+
+# class BlankFieldModelSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = BlankFieldModel
+
+# class BlankFieldSerializer(serializers.Serializer):
+# title = serializers.CharField(required=False)
+
+# class NotBlankFieldModelSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = BasicModel
+
+# class NotBlankFieldSerializer(serializers.Serializer):
+# title = serializers.CharField()
+
+# self.model_serializer_class = BlankFieldModelSerializer
+# self.serializer_class = BlankFieldSerializer
+# self.not_blank_model_serializer_class = NotBlankFieldModelSerializer
+# self.not_blank_serializer_class = NotBlankFieldSerializer
+# self.data = {'title': ''}
+
+# def test_create_blank_field(self):
+# serializer = self.serializer_class(data=self.data)
+# self.assertEqual(serializer.is_valid(), True)
+
+# def test_create_model_blank_field(self):
+# serializer = self.model_serializer_class(data=self.data)
+# self.assertEqual(serializer.is_valid(), True)
+
+# def test_create_model_null_field(self):
+# serializer = self.model_serializer_class(data={'title': None})
+# self.assertEqual(serializer.is_valid(), True)
+# serializer.save()
+# self.assertIsNot(serializer.object.pk, None)
+# self.assertEqual(serializer.object.title, '')
+
+# def test_create_not_blank_field(self):
+# """
+# Test to ensure blank data in a field not marked as blank=True
+# is considered invalid in a non-model serializer
+# """
+# serializer = self.not_blank_serializer_class(data=self.data)
+# self.assertEqual(serializer.is_valid(), False)
+
+# def test_create_model_not_blank_field(self):
+# """
+# Test to ensure blank data in a field not marked as blank=True
+# is considered invalid in a model serializer
+# """
+# serializer = self.not_blank_model_serializer_class(data=self.data)
+# self.assertEqual(serializer.is_valid(), False)
+
+# def test_create_model_empty_field(self):
+# serializer = self.model_serializer_class(data={})
+# self.assertEqual(serializer.is_valid(), True)
+
+# def test_create_model_null_field_save(self):
+# """
+# Regression test for #1330.
+
+# https://github.com/tomchristie/django-rest-framework/pull/1330
+# """
+# serializer = self.model_serializer_class(data={'title': None})
+# self.assertEqual(serializer.is_valid(), True)
+
+# try:
+# serializer.save()
+# except Exception:
+# self.fail('Exception raised on save() after validation passes')
+
+
+# # Test for issue #460
+# class SerializerPickleTests(TestCase):
+# """
+# Test pickleability of the output of Serializers
+# """
+# def test_pickle_simple_model_serializer_data(self):
+# """
+# Test simple serializer
+# """
+# pickle.dumps(PersonSerializer(Person(name="Methusela", age=969)).data)
+
+# def test_pickle_inner_serializer(self):
+# """
+# Test pickling a serializer whose resulting .data (a SortedDictWithMetadata) will
+# have unpickleable meta data--in order to make sure metadata doesn't get pulled into the pickle.
+# See DictWithMetadata.__getstate__
+# """
+# class InnerPersonSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = Person
+# fields = ('name', 'age')
+# pickle.dumps(InnerPersonSerializer(Person(name="Noah", age=950)).data, 0)
+
+# def test_getstate_method_should_not_return_none(self):
+# """
+# Regression test for #645.
+# """
+# data = serializers.DictWithMetadata({1: 1})
+# self.assertEqual(data.__getstate__(), serializers.SortedDict({1: 1}))
+
+# def test_serializer_data_is_pickleable(self):
+# """
+# Another regression test for #645.
+# """
+# data = serializers.SortedDictWithMetadata({1: 1})
+# repr(pickle.loads(pickle.dumps(data, 0)))
+
+
+# # test for issue #725
+# class SeveralChoicesModel(models.Model):
+# color = models.CharField(
+# max_length=10,
+# choices=[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')],
+# blank=False
+# )
+# drink = models.CharField(
+# max_length=10,
+# choices=[('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')],
+# blank=False,
+# default='beer'
+# )
+# os = models.CharField(
+# max_length=10,
+# choices=[('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')],
+# blank=True
+# )
+# music_genre = models.CharField(
+# max_length=10,
+# choices=[('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')],
+# blank=True,
+# default='metal'
+# )
+
+
+# class SerializerChoiceFields(TestCase):
+
+# def setUp(self):
+# super(SerializerChoiceFields, self).setUp()
+
+# class SeveralChoicesSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = SeveralChoicesModel
+# fields = ('color', 'drink', 'os', 'music_genre')
+
+# self.several_choices_serializer = SeveralChoicesSerializer
+
+# def test_choices_blank_false_not_default(self):
+# serializer = self.several_choices_serializer()
+# self.assertEqual(
+# serializer.fields['color'].choices,
+# [('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')]
+# )
+
+# def test_choices_blank_false_with_default(self):
+# serializer = self.several_choices_serializer()
+# self.assertEqual(
+# serializer.fields['drink'].choices,
+# [('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')]
+# )
+
+# def test_choices_blank_true_not_default(self):
+# serializer = self.several_choices_serializer()
+# self.assertEqual(
+# serializer.fields['os'].choices,
+# BLANK_CHOICE_DASH + [('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')]
+# )
+
+# def test_choices_blank_true_with_default(self):
+# serializer = self.several_choices_serializer()
+# self.assertEqual(
+# serializer.fields['music_genre'].choices,
+# BLANK_CHOICE_DASH + [('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')]
+# )
+
+
+# # Regression tests for #675
+# class Ticket(models.Model):
+# assigned = models.ForeignKey(
+# Person, related_name='assigned_tickets')
+# reviewer = models.ForeignKey(
+# Person, blank=True, null=True, related_name='reviewed_tickets')
+
+
+# class SerializerRelatedChoicesTest(TestCase):
+
+# def setUp(self):
+# super(SerializerRelatedChoicesTest, self).setUp()
+
+# class RelatedChoicesSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = Ticket
+# fields = ('assigned', 'reviewer')
+
+# self.related_fields_serializer = RelatedChoicesSerializer
+
+# def test_empty_queryset_required(self):
+# serializer = self.related_fields_serializer()
+# self.assertEqual(serializer.fields['assigned'].queryset.count(), 0)
+# self.assertEqual(
+# [x for x in serializer.fields['assigned'].widget.choices],
+# []
+# )
+
+# def test_empty_queryset_not_required(self):
+# serializer = self.related_fields_serializer()
+# self.assertEqual(serializer.fields['reviewer'].queryset.count(), 0)
+# self.assertEqual(
+# [x for x in serializer.fields['reviewer'].widget.choices],
+# [('', '---------')]
+# )
+
+# def test_with_some_persons_required(self):
+# Person.objects.create(name="Lionel Messi")
+# Person.objects.create(name="Xavi Hernandez")
+# serializer = self.related_fields_serializer()
+# self.assertEqual(serializer.fields['assigned'].queryset.count(), 2)
+# self.assertEqual(
+# [x for x in serializer.fields['assigned'].widget.choices],
+# [(1, 'Person object - 1'), (2, 'Person object - 2')]
+# )
+
+# def test_with_some_persons_not_required(self):
+# Person.objects.create(name="Lionel Messi")
+# Person.objects.create(name="Xavi Hernandez")
+# serializer = self.related_fields_serializer()
+# self.assertEqual(serializer.fields['reviewer'].queryset.count(), 2)
+# self.assertEqual(
+# [x for x in serializer.fields['reviewer'].widget.choices],
+# [('', '---------'), (1, 'Person object - 1'), (2, 'Person object - 2')]
+# )
-class DepthTest(TestCase):
- def test_implicit_nesting(self):
-
- writer = Person.objects.create(name="django", age=1)
- post = BlogPost.objects.create(title="Test blog post", writer=writer)
- comment = BlogPostComment.objects.create(text="Test blog post comment", blog_post=post)
-
- class BlogPostCommentSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlogPostComment
- depth = 2
-
- serializer = BlogPostCommentSerializer(instance=comment)
- expected = {'id': 1, 'text': 'Test blog post comment', 'blog_post': {'id': 1, 'title': 'Test blog post',
- 'writer': {'id': 1, 'name': 'django', 'age': 1}}}
-
- self.assertEqual(serializer.data, expected)
-
- def test_explicit_nesting(self):
- writer = Person.objects.create(name="django", age=1)
- post = BlogPost.objects.create(title="Test blog post", writer=writer)
- comment = BlogPostComment.objects.create(text="Test blog post comment", blog_post=post)
-
- class PersonSerializer(serializers.ModelSerializer):
- class Meta:
- model = Person
-
- class BlogPostSerializer(serializers.ModelSerializer):
- writer = PersonSerializer()
-
- class Meta:
- model = BlogPost
-
- class BlogPostCommentSerializer(serializers.ModelSerializer):
- blog_post = BlogPostSerializer()
-
- class Meta:
- model = BlogPostComment
+# class DepthTest(TestCase):
+# def test_implicit_nesting(self):
+
+# writer = Person.objects.create(name="django", age=1)
+# post = BlogPost.objects.create(title="Test blog post", writer=writer)
+# comment = BlogPostComment.objects.create(text="Test blog post comment", blog_post=post)
+
+# class BlogPostCommentSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = BlogPostComment
+# depth = 2
+
+# serializer = BlogPostCommentSerializer(instance=comment)
+# expected = {'id': 1, 'text': 'Test blog post comment', 'blog_post': {'id': 1, 'title': 'Test blog post',
+# 'writer': {'id': 1, 'name': 'django', 'age': 1}}}
+
+# self.assertEqual(serializer.data, expected)
+
+# def test_explicit_nesting(self):
+# writer = Person.objects.create(name="django", age=1)
+# post = BlogPost.objects.create(title="Test blog post", writer=writer)
+# comment = BlogPostComment.objects.create(text="Test blog post comment", blog_post=post)
+
+# class PersonSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = Person
+
+# class BlogPostSerializer(serializers.ModelSerializer):
+# writer = PersonSerializer()
+
+# class Meta:
+# model = BlogPost
+
+# class BlogPostCommentSerializer(serializers.ModelSerializer):
+# blog_post = BlogPostSerializer()
+
+# class Meta:
+# model = BlogPostComment
- serializer = BlogPostCommentSerializer(instance=comment)
- expected = {'id': 1, 'text': 'Test blog post comment', 'blog_post': {'id': 1, 'title': 'Test blog post',
- 'writer': {'id': 1, 'name': 'django', 'age': 1}}}
+# serializer = BlogPostCommentSerializer(instance=comment)
+# expected = {'id': 1, 'text': 'Test blog post comment', 'blog_post': {'id': 1, 'title': 'Test blog post',
+# 'writer': {'id': 1, 'name': 'django', 'age': 1}}}
- self.assertEqual(serializer.data, expected)
+# self.assertEqual(serializer.data, expected)
-class NestedSerializerContextTests(TestCase):
+# class NestedSerializerContextTests(TestCase):
- def test_nested_serializer_context(self):
- """
- Regression for #497
-
- https://github.com/tomchristie/django-rest-framework/issues/497
- """
- class PhotoSerializer(serializers.ModelSerializer):
- class Meta:
- model = Photo
- fields = ("description", "callable")
+# def test_nested_serializer_context(self):
+# """
+# Regression for #497
+
+# https://github.com/tomchristie/django-rest-framework/issues/497
+# """
+# class PhotoSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = Photo
+# fields = ("description", "callable")
- callable = serializers.SerializerMethodField('_callable')
+# callable = serializers.SerializerMethodField('_callable')
- def _callable(self, instance):
- if 'context_item' not in self.context:
- raise RuntimeError("context isn't getting passed into 2nd level nested serializer")
- return "success"
-
- class AlbumSerializer(serializers.ModelSerializer):
- class Meta:
- model = Album
- fields = ("photo_set", "callable")
+# def _callable(self, instance):
+# if 'context_item' not in self.context:
+# raise RuntimeError("context isn't getting passed into 2nd level nested serializer")
+# return "success"
+
+# class AlbumSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = Album
+# fields = ("photo_set", "callable")
- photo_set = PhotoSerializer(source="photo_set", many=True)
- callable = serializers.SerializerMethodField("_callable")
+# photo_set = PhotoSerializer(source="photo_set", many=True)
+# callable = serializers.SerializerMethodField("_callable")
- def _callable(self, instance):
- if 'context_item' not in self.context:
- raise RuntimeError("context isn't getting passed into 1st level nested serializer")
- return "success"
+# def _callable(self, instance):
+# if 'context_item' not in self.context:
+# raise RuntimeError("context isn't getting passed into 1st level nested serializer")
+# return "success"
- class AlbumCollection(object):
- albums = None
+# class AlbumCollection(object):
+# albums = None
- class AlbumCollectionSerializer(serializers.Serializer):
- albums = AlbumSerializer(source="albums", many=True)
+# class AlbumCollectionSerializer(serializers.Serializer):
+# albums = AlbumSerializer(source="albums", many=True)
- album1 = Album.objects.create(title="album 1")
- album2 = Album.objects.create(title="album 2")
- Photo.objects.create(description="Bigfoot", album=album1)
- Photo.objects.create(description="Unicorn", album=album1)
- Photo.objects.create(description="Yeti", album=album2)
- Photo.objects.create(description="Sasquatch", album=album2)
- album_collection = AlbumCollection()
- album_collection.albums = [album1, album2]
+# album1 = Album.objects.create(title="album 1")
+# album2 = Album.objects.create(title="album 2")
+# Photo.objects.create(description="Bigfoot", album=album1)
+# Photo.objects.create(description="Unicorn", album=album1)
+# Photo.objects.create(description="Yeti", album=album2)
+# Photo.objects.create(description="Sasquatch", album=album2)
+# album_collection = AlbumCollection()
+# album_collection.albums = [album1, album2]
- # This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers
- AlbumCollectionSerializer(album_collection, context={'context_item': 'album context'}).data
+# # This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers
+# AlbumCollectionSerializer(album_collection, context={'context_item': 'album context'}).data
-class DeserializeListTestCase(TestCase):
+# class DeserializeListTestCase(TestCase):
- def setUp(self):
- self.data = {
- 'email': 'nobody@nowhere.com',
- 'content': 'This is some test content',
- 'created': datetime.datetime(2013, 3, 7),
- }
+# def setUp(self):
+# self.data = {
+# 'email': 'nobody@nowhere.com',
+# 'content': 'This is some test content',
+# 'created': datetime.datetime(2013, 3, 7),
+# }
- def test_no_errors(self):
- data = [self.data.copy() for x in range(0, 3)]
- serializer = CommentSerializer(data=data, many=True)
- self.assertTrue(serializer.is_valid())
- self.assertTrue(isinstance(serializer.object, list))
- self.assertTrue(
- all((isinstance(item, Comment) for item in serializer.object))
- )
+# def test_no_errors(self):
+# data = [self.data.copy() for x in range(0, 3)]
+# serializer = CommentSerializer(data=data, many=True)
+# self.assertTrue(serializer.is_valid())
+# self.assertTrue(isinstance(serializer.object, list))
+# self.assertTrue(
+# all((isinstance(item, Comment) for item in serializer.object))
+# )
- def test_errors_return_as_list(self):
- invalid_item = self.data.copy()
- invalid_item['email'] = ''
- data = [self.data.copy(), invalid_item, self.data.copy()]
+# def test_errors_return_as_list(self):
+# invalid_item = self.data.copy()
+# invalid_item['email'] = ''
+# data = [self.data.copy(), invalid_item, self.data.copy()]
- serializer = CommentSerializer(data=data, many=True)
- self.assertFalse(serializer.is_valid())
- expected = [{}, {'email': ['This field is required.']}, {}]
- self.assertEqual(serializer.errors, expected)
+# serializer = CommentSerializer(data=data, many=True)
+# self.assertFalse(serializer.is_valid())
+# expected = [{}, {'email': ['This field is required.']}, {}]
+# self.assertEqual(serializer.errors, expected)
-# Test for issue 747
+# # Test for issue 747
-class LazyStringModel(object):
- def __init__(self, lazystring):
- self.lazystring = lazystring
+# class LazyStringModel(object):
+# def __init__(self, lazystring):
+# self.lazystring = lazystring
-class LazyStringSerializer(serializers.Serializer):
- lazystring = serializers.Field()
-
- def restore_object(self, attrs, instance=None):
- if instance is not None:
- instance.lazystring = attrs.get('lazystring', instance.lazystring)
- return instance
- return LazyStringModel(**attrs)
-
-
-class LazyStringsTestCase(TestCase):
- def setUp(self):
- self.model = LazyStringModel(lazystring=_('lazystring'))
-
- def test_lazy_strings_are_translated(self):
- serializer = LazyStringSerializer(self.model)
- self.assertEqual(type(serializer.data['lazystring']),
- type('lazystring'))
-
-
-# Test for issue #467
-
-class FieldLabelTest(TestCase):
- def setUp(self):
- self.serializer_class = BasicModelSerializer
-
- def test_label_from_model(self):
- """
- Validates that label and help_text are correctly copied from the model class.
- """
- serializer = self.serializer_class()
- text_field = serializer.fields['text']
-
- self.assertEqual('Text comes here', text_field.label)
- self.assertEqual('Text description.', text_field.help_text)
-
- def test_field_ctor(self):
- """
- This is check that ctor supports both label and help_text.
- """
- self.assertEqual('Label', fields.Field(label='Label', help_text='Help').label)
- self.assertEqual('Help', fields.CharField(label='Label', help_text='Help').help_text)
- self.assertEqual('Label', relations.HyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help', many=True).label)
-
-
-# Test for issue #961
-
-class ManyFieldHelpTextTest(TestCase):
- def test_help_text_no_hold_down_control_msg(self):
- """
- Validate that help_text doesn't contain the 'Hold down "Control" ...'
- message that Django appends to choice fields.
- """
- rel_field = fields.Field(help_text=ManyToManyModel._meta.get_field('rel').help_text)
- self.assertEqual('Some help text.', rel_field.help_text)
-
-
-class AttributeMappingOnAutogeneratedRelatedFields(TestCase):
-
- def test_primary_key_related_field(self):
- serializer = ForeignKeySourceSerializer()
- self.assertEqual(serializer.fields['target'].help_text, 'Target')
- self.assertEqual(serializer.fields['target'].label, 'Target')
-
- def test_hyperlinked_related_field(self):
- serializer = HyperlinkedForeignKeySourceSerializer()
- self.assertEqual(serializer.fields['target'].help_text, 'Target')
- self.assertEqual(serializer.fields['target'].label, 'Target')
-
-
-@unittest.skipUnless(PIL is not None, 'PIL is not installed')
-class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
-
- def setUp(self):
-
- class AMOAFSerializer(serializers.ModelSerializer):
- class Meta:
- model = AMOAFModel
-
- self.serializer_class = AMOAFSerializer
- self.fields_attributes = {
- 'char_field': [
- ('max_length', 1024),
- ],
- 'comma_separated_integer_field': [
- ('max_length', 1024),
- ],
- 'decimal_field': [
- ('max_digits', 64),
- ('decimal_places', 32),
- ],
- 'email_field': [
- ('max_length', 1024),
- ],
- 'file_field': [
- ('max_length', 1024),
- ],
- 'image_field': [
- ('max_length', 1024),
- ],
- 'slug_field': [
- ('max_length', 1024),
- ],
- 'url_field': [
- ('max_length', 1024),
- ],
- 'nullable_char_field': [
- ('max_length', 1024),
- ('allow_none', True),
- ],
- }
-
- def field_test(self, field):
- serializer = self.serializer_class(data={})
- self.assertEqual(serializer.is_valid(), True)
-
- for attribute in self.fields_attributes[field]:
- self.assertEqual(
- getattr(serializer.fields[field], attribute[0]),
- attribute[1]
- )
-
- def test_char_field(self):
- self.field_test('char_field')
-
- def test_comma_separated_integer_field(self):
- self.field_test('comma_separated_integer_field')
-
- def test_decimal_field(self):
- self.field_test('decimal_field')
-
- def test_email_field(self):
- self.field_test('email_field')
-
- def test_file_field(self):
- self.field_test('file_field')
-
- def test_image_field(self):
- self.field_test('image_field')
-
- def test_slug_field(self):
- self.field_test('slug_field')
-
- def test_url_field(self):
- self.field_test('url_field')
-
- def test_nullable_char_field(self):
- self.field_test('nullable_char_field')
-
-
-@unittest.skipUnless(PIL is not None, 'PIL is not installed')
-class DefaultValuesOnAutogeneratedFieldsTests(TestCase):
-
- def setUp(self):
-
- class DVOAFSerializer(serializers.ModelSerializer):
- class Meta:
- model = DVOAFModel
-
- self.serializer_class = DVOAFSerializer
- self.fields_attributes = {
- 'positive_integer_field': [
- ('min_value', 0),
- ],
- 'positive_small_integer_field': [
- ('min_value', 0),
- ],
- 'email_field': [
- ('max_length', 75),
- ],
- 'file_field': [
- ('max_length', 100),
- ],
- 'image_field': [
- ('max_length', 100),
- ],
- 'slug_field': [
- ('max_length', 50),
- ],
- 'url_field': [
- ('max_length', 200),
- ],
- }
-
- def field_test(self, field):
- serializer = self.serializer_class(data={})
- self.assertEqual(serializer.is_valid(), True)
-
- for attribute in self.fields_attributes[field]:
- self.assertEqual(
- getattr(serializer.fields[field], attribute[0]),
- attribute[1]
- )
+# class LazyStringSerializer(serializers.Serializer):
+# lazystring = serializers.Field()
+
+# def restore_object(self, attrs, instance=None):
+# if instance is not None:
+# instance.lazystring = attrs.get('lazystring', instance.lazystring)
+# return instance
+# return LazyStringModel(**attrs)
+
+
+# class LazyStringsTestCase(TestCase):
+# def setUp(self):
+# self.model = LazyStringModel(lazystring=_('lazystring'))
+
+# def test_lazy_strings_are_translated(self):
+# serializer = LazyStringSerializer(self.model)
+# self.assertEqual(type(serializer.data['lazystring']),
+# type('lazystring'))
+
+
+# # Test for issue #467
+
+# class FieldLabelTest(TestCase):
+# def setUp(self):
+# self.serializer_class = BasicModelSerializer
+
+# def test_label_from_model(self):
+# """
+# Validates that label and help_text are correctly copied from the model class.
+# """
+# serializer = self.serializer_class()
+# text_field = serializer.fields['text']
+
+# self.assertEqual('Text comes here', text_field.label)
+# self.assertEqual('Text description.', text_field.help_text)
+
+# def test_field_ctor(self):
+# """
+# This is check that ctor supports both label and help_text.
+# """
+# self.assertEqual('Label', fields.Field(label='Label', help_text='Help').label)
+# self.assertEqual('Help', fields.CharField(label='Label', help_text='Help').help_text)
+# self.assertEqual('Label', relations.HyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help', many=True).label)
+
+
+# # Test for issue #961
+
+# class ManyFieldHelpTextTest(TestCase):
+# def test_help_text_no_hold_down_control_msg(self):
+# """
+# Validate that help_text doesn't contain the 'Hold down "Control" ...'
+# message that Django appends to choice fields.
+# """
+# rel_field = fields.Field(help_text=ManyToManyModel._meta.get_field('rel').help_text)
+# self.assertEqual('Some help text.', rel_field.help_text)
+
+
+# class AttributeMappingOnAutogeneratedRelatedFields(TestCase):
+
+# def test_primary_key_related_field(self):
+# serializer = ForeignKeySourceSerializer()
+# self.assertEqual(serializer.fields['target'].help_text, 'Target')
+# self.assertEqual(serializer.fields['target'].label, 'Target')
+
+# def test_hyperlinked_related_field(self):
+# serializer = HyperlinkedForeignKeySourceSerializer()
+# self.assertEqual(serializer.fields['target'].help_text, 'Target')
+# self.assertEqual(serializer.fields['target'].label, 'Target')
+
+
+# @unittest.skipUnless(PIL is not None, 'PIL is not installed')
+# class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
+
+# def setUp(self):
+
+# class AMOAFSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = AMOAFModel
+
+# self.serializer_class = AMOAFSerializer
+# self.fields_attributes = {
+# 'char_field': [
+# ('max_length', 1024),
+# ],
+# 'comma_separated_integer_field': [
+# ('max_length', 1024),
+# ],
+# 'decimal_field': [
+# ('max_digits', 64),
+# ('decimal_places', 32),
+# ],
+# 'email_field': [
+# ('max_length', 1024),
+# ],
+# 'file_field': [
+# ('max_length', 1024),
+# ],
+# 'image_field': [
+# ('max_length', 1024),
+# ],
+# 'slug_field': [
+# ('max_length', 1024),
+# ],
+# 'url_field': [
+# ('max_length', 1024),
+# ],
+# 'nullable_char_field': [
+# ('max_length', 1024),
+# ('allow_none', True),
+# ],
+# }
+
+# def field_test(self, field):
+# serializer = self.serializer_class(data={})
+# self.assertEqual(serializer.is_valid(), True)
+
+# for attribute in self.fields_attributes[field]:
+# self.assertEqual(
+# getattr(serializer.fields[field], attribute[0]),
+# attribute[1]
+# )
+
+# def test_char_field(self):
+# self.field_test('char_field')
+
+# def test_comma_separated_integer_field(self):
+# self.field_test('comma_separated_integer_field')
+
+# def test_decimal_field(self):
+# self.field_test('decimal_field')
+
+# def test_email_field(self):
+# self.field_test('email_field')
+
+# def test_file_field(self):
+# self.field_test('file_field')
+
+# def test_image_field(self):
+# self.field_test('image_field')
+
+# def test_slug_field(self):
+# self.field_test('slug_field')
+
+# def test_url_field(self):
+# self.field_test('url_field')
+
+# def test_nullable_char_field(self):
+# self.field_test('nullable_char_field')
+
+
+# @unittest.skipUnless(PIL is not None, 'PIL is not installed')
+# class DefaultValuesOnAutogeneratedFieldsTests(TestCase):
+
+# def setUp(self):
+
+# class DVOAFSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = DVOAFModel
+
+# self.serializer_class = DVOAFSerializer
+# self.fields_attributes = {
+# 'positive_integer_field': [
+# ('min_value', 0),
+# ],
+# 'positive_small_integer_field': [
+# ('min_value', 0),
+# ],
+# 'email_field': [
+# ('max_length', 75),
+# ],
+# 'file_field': [
+# ('max_length', 100),
+# ],
+# 'image_field': [
+# ('max_length', 100),
+# ],
+# 'slug_field': [
+# ('max_length', 50),
+# ],
+# 'url_field': [
+# ('max_length', 200),
+# ],
+# }
+
+# def field_test(self, field):
+# serializer = self.serializer_class(data={})
+# self.assertEqual(serializer.is_valid(), True)
+
+# for attribute in self.fields_attributes[field]:
+# self.assertEqual(
+# getattr(serializer.fields[field], attribute[0]),
+# attribute[1]
+# )
- def test_positive_integer_field(self):
- self.field_test('positive_integer_field')
+# def test_positive_integer_field(self):
+# self.field_test('positive_integer_field')
- def test_positive_small_integer_field(self):
- self.field_test('positive_small_integer_field')
+# def test_positive_small_integer_field(self):
+# self.field_test('positive_small_integer_field')
- def test_email_field(self):
- self.field_test('email_field')
+# def test_email_field(self):
+# self.field_test('email_field')
- def test_file_field(self):
- self.field_test('file_field')
+# def test_file_field(self):
+# self.field_test('file_field')
- def test_image_field(self):
- self.field_test('image_field')
+# def test_image_field(self):
+# self.field_test('image_field')
- def test_slug_field(self):
- self.field_test('slug_field')
+# def test_slug_field(self):
+# self.field_test('slug_field')
- def test_url_field(self):
- self.field_test('url_field')
+# def test_url_field(self):
+# self.field_test('url_field')
-class MetadataSerializer(serializers.Serializer):
- field1 = serializers.CharField(max_length=3, required=True)
- field2 = serializers.CharField(max_length=10, required=False)
+# class MetadataSerializer(serializers.Serializer):
+# field1 = serializers.CharField(max_length=3, required=True)
+# field2 = serializers.CharField(max_length=10, required=False)
-class MetadataSerializerTestCase(TestCase):
- def setUp(self):
- self.serializer = MetadataSerializer()
+# class MetadataSerializerTestCase(TestCase):
+# def setUp(self):
+# self.serializer = MetadataSerializer()
- def test_serializer_metadata(self):
- metadata = self.serializer.metadata()
- expected = {
- 'field1': {
- 'required': True,
- 'max_length': 3,
- 'type': 'string',
- 'read_only': False
- },
- 'field2': {
- 'required': False,
- 'max_length': 10,
- 'type': 'string',
- 'read_only': False
- }
- }
- self.assertEqual(expected, metadata)
+# def test_serializer_metadata(self):
+# metadata = self.serializer.metadata()
+# expected = {
+# 'field1': {
+# 'required': True,
+# 'max_length': 3,
+# 'type': 'string',
+# 'read_only': False
+# },
+# 'field2': {
+# 'required': False,
+# 'max_length': 10,
+# 'type': 'string',
+# 'read_only': False
+# }
+# }
+# self.assertEqual(expected, metadata)
-# Regression test for #840
+# # Regression test for #840
-class SimpleModel(models.Model):
- text = models.CharField(max_length=100)
+# class SimpleModel(models.Model):
+# text = models.CharField(max_length=100)
-
-class SimpleModelSerializer(serializers.ModelSerializer):
- text = serializers.CharField()
- other = serializers.CharField()
-
- class Meta:
- model = SimpleModel
-
- def validate_other(self, attrs, source):
- del attrs['other']
- return attrs
+
+# class SimpleModelSerializer(serializers.ModelSerializer):
+# text = serializers.CharField()
+# other = serializers.CharField()
+
+# class Meta:
+# model = SimpleModel
+
+# def validate_other(self, attrs, source):
+# del attrs['other']
+# return attrs
-class FieldValidationRemovingAttr(TestCase):
- def test_removing_non_model_field_in_validation(self):
- """
- Removing an attr during field valiation should ensure that it is not
- passed through when restoring the object.
+# class FieldValidationRemovingAttr(TestCase):
+# def test_removing_non_model_field_in_validation(self):
+# """
+# Removing an attr during field valiation should ensure that it is not
+# passed through when restoring the object.
- This allows additional non-model fields to be supported.
+# This allows additional non-model fields to be supported.
- Regression test for #840.
- """
- serializer = SimpleModelSerializer(data={'text': 'foo', 'other': 'bar'})
- self.assertTrue(serializer.is_valid())
- serializer.save()
- self.assertEqual(serializer.object.text, 'foo')
+# Regression test for #840.
+# """
+# serializer = SimpleModelSerializer(data={'text': 'foo', 'other': 'bar'})
+# self.assertTrue(serializer.is_valid())
+# serializer.save()
+# self.assertEqual(serializer.object.text, 'foo')
-# Regression test for #878
+# # Regression test for #878
-class SimpleTargetModel(models.Model):
- text = models.CharField(max_length=100)
+# class SimpleTargetModel(models.Model):
+# text = models.CharField(max_length=100)
-class SimplePKSourceModelSerializer(serializers.Serializer):
- targets = serializers.PrimaryKeyRelatedField(queryset=SimpleTargetModel.objects.all(), many=True)
- text = serializers.CharField()
+# class SimplePKSourceModelSerializer(serializers.Serializer):
+# targets = serializers.PrimaryKeyRelatedField(queryset=SimpleTargetModel.objects.all(), many=True)
+# text = serializers.CharField()
-class SimpleSlugSourceModelSerializer(serializers.Serializer):
- targets = serializers.SlugRelatedField(queryset=SimpleTargetModel.objects.all(), many=True, slug_field='pk')
- text = serializers.CharField()
+# class SimpleSlugSourceModelSerializer(serializers.Serializer):
+# targets = serializers.SlugRelatedField(queryset=SimpleTargetModel.objects.all(), many=True, slug_field='pk')
+# text = serializers.CharField()
-class SerializerSupportsManyRelationships(TestCase):
- def setUp(self):
- SimpleTargetModel.objects.create(text='foo')
- SimpleTargetModel.objects.create(text='bar')
+# class SerializerSupportsManyRelationships(TestCase):
+# def setUp(self):
+# SimpleTargetModel.objects.create(text='foo')
+# SimpleTargetModel.objects.create(text='bar')
- def test_serializer_supports_pk_many_relationships(self):
- """
- Regression test for #878.
+# def test_serializer_supports_pk_many_relationships(self):
+# """
+# Regression test for #878.
- Note that pk behavior has a different code path to usual cases,
- for performance reasons.
- """
- serializer = SimplePKSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]})
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]})
+# Note that pk behavior has a different code path to usual cases,
+# for performance reasons.
+# """
+# serializer = SimplePKSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]})
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]})
- def test_serializer_supports_slug_many_relationships(self):
- """
- Regression test for #878.
- """
- serializer = SimpleSlugSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]})
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]})
+# def test_serializer_supports_slug_many_relationships(self):
+# """
+# Regression test for #878.
+# """
+# serializer = SimpleSlugSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]})
+# self.assertTrue(serializer.is_valid())
+# self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]})
-class TransformMethodsSerializer(serializers.Serializer):
- a = serializers.CharField()
- b_renamed = serializers.CharField(source='b')
-
- def transform_a(self, obj, value):
- return value.lower()
-
- def transform_b_renamed(self, obj, value):
- if value is not None:
- return 'and ' + value
+# class TransformMethodsSerializer(serializers.Serializer):
+# a = serializers.CharField()
+# b_renamed = serializers.CharField(source='b')
+
+# def transform_a(self, obj, value):
+# return value.lower()
+
+# def transform_b_renamed(self, obj, value):
+# if value is not None:
+# return 'and ' + value
-class TestSerializerTransformMethods(TestCase):
- def setUp(self):
- self.s = TransformMethodsSerializer()
+# class TestSerializerTransformMethods(TestCase):
+# def setUp(self):
+# self.s = TransformMethodsSerializer()
- def test_transform_methods(self):
- self.assertEqual(
- self.s.to_native({'a': 'GREEN EGGS', 'b': 'HAM'}),
- {
- 'a': 'green eggs',
- 'b_renamed': 'and HAM',
- }
- )
+# def test_transform_methods(self):
+# self.assertEqual(
+# self.s.to_native({'a': 'GREEN EGGS', 'b': 'HAM'}),
+# {
+# 'a': 'green eggs',
+# 'b_renamed': 'and HAM',
+# }
+# )
- def test_missing_fields(self):
- self.assertEqual(
- self.s.to_native({'a': 'GREEN EGGS'}),
- {
- 'a': 'green eggs',
- 'b_renamed': None,
- }
- )
+# def test_missing_fields(self):
+# self.assertEqual(
+# self.s.to_native({'a': 'GREEN EGGS'}),
+# {
+# 'a': 'green eggs',
+# 'b_renamed': None,
+# }
+# )
-class DefaultTrueBooleanModel(models.Model):
- cat = models.BooleanField(default=True)
- dog = models.BooleanField(default=False)
+# class DefaultTrueBooleanModel(models.Model):
+# cat = models.BooleanField(default=True)
+# dog = models.BooleanField(default=False)
-class SerializerDefaultTrueBoolean(TestCase):
+# class SerializerDefaultTrueBoolean(TestCase):
- def setUp(self):
- super(SerializerDefaultTrueBoolean, self).setUp()
-
- class DefaultTrueBooleanSerializer(serializers.ModelSerializer):
- class Meta:
- model = DefaultTrueBooleanModel
- fields = ('cat', 'dog')
-
- self.default_true_boolean_serializer = DefaultTrueBooleanSerializer
-
- def test_enabled_as_false(self):
- serializer = self.default_true_boolean_serializer(data={'cat': False,
- 'dog': False})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.data['cat'], False)
- self.assertEqual(serializer.data['dog'], False)
+# def setUp(self):
+# super(SerializerDefaultTrueBoolean, self).setUp()
+
+# class DefaultTrueBooleanSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = DefaultTrueBooleanModel
+# fields = ('cat', 'dog')
+
+# self.default_true_boolean_serializer = DefaultTrueBooleanSerializer
+
+# def test_enabled_as_false(self):
+# serializer = self.default_true_boolean_serializer(data={'cat': False,
+# 'dog': False})
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.data['cat'], False)
+# self.assertEqual(serializer.data['dog'], False)
- def test_enabled_as_true(self):
- serializer = self.default_true_boolean_serializer(data={'cat': True,
- 'dog': True})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.data['cat'], True)
- self.assertEqual(serializer.data['dog'], True)
-
- def test_enabled_partial(self):
- serializer = self.default_true_boolean_serializer(data={'cat': False},
- partial=True)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.data['cat'], False)
- self.assertEqual(serializer.data['dog'], False)
-
-
-class BoolenFieldTypeTest(TestCase):
- '''
- Ensure the various Boolean based model fields are rendered as the proper
- field type
-
- '''
-
- def setUp(self):
- '''
- Setup an ActionItemSerializer for BooleanTesting
- '''
- data = {
- 'title': 'b' * 201,
- }
- self.serializer = ActionItemSerializer(data=data)
-
- def test_booleanfield_type(self):
- '''
- Test that BooleanField is infered from models.BooleanField
- '''
- bfield = self.serializer.get_fields()['done']
- self.assertEqual(type(bfield), fields.BooleanField)
-
- def test_nullbooleanfield_type(self):
- '''
- Test that BooleanField is infered from models.NullBooleanField
-
- https://groups.google.com/forum/#!topic/django-rest-framework/D9mXEftpuQ8
- '''
- bfield = self.serializer.get_fields()['started']
- self.assertEqual(type(bfield), fields.BooleanField)
+# def test_enabled_as_true(self):
+# serializer = self.default_true_boolean_serializer(data={'cat': True,
+# 'dog': True})
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.data['cat'], True)
+# self.assertEqual(serializer.data['dog'], True)
+
+# def test_enabled_partial(self):
+# serializer = self.default_true_boolean_serializer(data={'cat': False},
+# partial=True)
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.data['cat'], False)
+# self.assertEqual(serializer.data['dog'], False)
+
+
+# class BoolenFieldTypeTest(TestCase):
+# '''
+# Ensure the various Boolean based model fields are rendered as the proper
+# field type
+
+# '''
+
+# def setUp(self):
+# '''
+# Setup an ActionItemSerializer for BooleanTesting
+# '''
+# data = {
+# 'title': 'b' * 201,
+# }
+# self.serializer = ActionItemSerializer(data=data)
+
+# def test_booleanfield_type(self):
+# '''
+# Test that BooleanField is infered from models.BooleanField
+# '''
+# bfield = self.serializer.get_fields()['done']
+# self.assertEqual(type(bfield), fields.BooleanField)
+
+# def test_nullbooleanfield_type(self):
+# '''
+# Test that BooleanField is infered from models.NullBooleanField
+
+# https://groups.google.com/forum/#!topic/django-rest-framework/D9mXEftpuQ8
+# '''
+# bfield = self.serializer.get_fields()['started']
+# self.assertEqual(type(bfield), fields.BooleanField)
diff --git a/tests/test_serializer_bulk_update.py b/tests/test_serializer_bulk_update.py
index 67a8ed0d..3341ce59 100644
--- a/tests/test_serializer_bulk_update.py
+++ b/tests/test_serializer_bulk_update.py
@@ -1,278 +1,278 @@
-"""
-Tests to cover bulk create and update using serializers.
-"""
-from __future__ import unicode_literals
-from django.test import TestCase
-from rest_framework import serializers
-
-
-class BulkCreateSerializerTests(TestCase):
- """
- Creating multiple instances using serializers.
- """
-
- def setUp(self):
- class BookSerializer(serializers.Serializer):
- id = serializers.IntegerField()
- title = serializers.CharField(max_length=100)
- author = serializers.CharField(max_length=100)
-
- self.BookSerializer = BookSerializer
-
- def test_bulk_create_success(self):
- """
- Correct bulk update serialization should return the input data.
- """
-
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 1,
- 'title': 'If this is a man',
- 'author': 'Primo Levi'
- }, {
- 'id': 2,
- 'title': 'The wind-up bird chronicle',
- 'author': 'Haruki Murakami'
- }
- ]
-
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
-
- def test_bulk_create_errors(self):
- """
- Correct bulk update serialization should return the input data.
- """
-
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 1,
- 'title': 'If this is a man',
- 'author': 'Primo Levi'
- }, {
- 'id': 'foo',
- 'title': 'The wind-up bird chronicle',
- 'author': 'Haruki Murakami'
- }
- ]
- expected_errors = [
- {},
- {},
- {'id': ['Enter a whole number.']}
- ]
-
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_invalid_list_datatype(self):
- """
- Data containing list of incorrect data type should return errors.
- """
- data = ['foo', 'bar', 'baz']
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
-
- expected_errors = [
- {'non_field_errors': ['Invalid data']},
- {'non_field_errors': ['Invalid data']},
- {'non_field_errors': ['Invalid data']}
- ]
-
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_invalid_single_datatype(self):
- """
- Data containing a single incorrect data type should return errors.
- """
- data = 123
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
-
- expected_errors = {'non_field_errors': ['Expected a list of items.']}
-
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_invalid_single_object(self):
- """
- Data containing only a single object, instead of a list of objects
- should return errors.
- """
- data = {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
-
- expected_errors = {'non_field_errors': ['Expected a list of items.']}
-
- self.assertEqual(serializer.errors, expected_errors)
-
-
-class BulkUpdateSerializerTests(TestCase):
- """
- Updating multiple instances using serializers.
- """
-
- def setUp(self):
- class Book(object):
- """
- A data type that can be persisted to a mock storage backend
- with `.save()` and `.delete()`.
- """
- object_map = {}
-
- def __init__(self, id, title, author):
- self.id = id
- self.title = title
- self.author = author
-
- def save(self):
- Book.object_map[self.id] = self
-
- def delete(self):
- del Book.object_map[self.id]
-
- class BookSerializer(serializers.Serializer):
- id = serializers.IntegerField()
- title = serializers.CharField(max_length=100)
- author = serializers.CharField(max_length=100)
-
- def restore_object(self, attrs, instance=None):
- if instance:
- instance.id = attrs['id']
- instance.title = attrs['title']
- instance.author = attrs['author']
- return instance
- return Book(**attrs)
-
- self.Book = Book
- self.BookSerializer = BookSerializer
-
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 1,
- 'title': 'If this is a man',
- 'author': 'Primo Levi'
- }, {
- 'id': 2,
- 'title': 'The wind-up bird chronicle',
- 'author': 'Haruki Murakami'
- }
- ]
-
- for item in data:
- book = Book(item['id'], item['title'], item['author'])
- book.save()
-
- def books(self):
- """
- Return all the objects in the mock storage backend.
- """
- return self.Book.object_map.values()
-
- def test_bulk_update_success(self):
- """
- Correct bulk update serialization should return the input data.
- """
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 2,
- 'title': 'Kafka on the shore',
- 'author': 'Haruki Murakami'
- }
- ]
- serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.data, data)
- serializer.save()
- new_data = self.BookSerializer(self.books(), many=True).data
-
- self.assertEqual(data, new_data)
-
- def test_bulk_update_and_create(self):
- """
- Bulk update serialization may also include created items.
- """
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 3,
- 'title': 'Kafka on the shore',
- 'author': 'Haruki Murakami'
- }
- ]
- serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.data, data)
- serializer.save()
- new_data = self.BookSerializer(self.books(), many=True).data
- self.assertEqual(data, new_data)
-
- def test_bulk_update_invalid_create(self):
- """
- Bulk update serialization without allow_add_remove may not create items.
- """
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 3,
- 'title': 'Kafka on the shore',
- 'author': 'Haruki Murakami'
- }
- ]
- expected_errors = [
- {},
- {'non_field_errors': ['Cannot create a new item, only existing items may be updated.']}
- ]
- serializer = self.BookSerializer(self.books(), data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_bulk_update_error(self):
- """
- Incorrect bulk update serialization should return error data.
- """
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 'foo',
- 'title': 'Kafka on the shore',
- 'author': 'Haruki Murakami'
- }
- ]
- expected_errors = [
- {},
- {'id': ['Enter a whole number.']}
- ]
- serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
+# """
+# Tests to cover bulk create and update using serializers.
+# """
+# from __future__ import unicode_literals
+# from django.test import TestCase
+# from rest_framework import serializers
+
+
+# class BulkCreateSerializerTests(TestCase):
+# """
+# Creating multiple instances using serializers.
+# """
+
+# def setUp(self):
+# class BookSerializer(serializers.Serializer):
+# id = serializers.IntegerField()
+# title = serializers.CharField(max_length=100)
+# author = serializers.CharField(max_length=100)
+
+# self.BookSerializer = BookSerializer
+
+# def test_bulk_create_success(self):
+# """
+# Correct bulk update serialization should return the input data.
+# """
+
+# data = [
+# {
+# 'id': 0,
+# 'title': 'The electric kool-aid acid test',
+# 'author': 'Tom Wolfe'
+# }, {
+# 'id': 1,
+# 'title': 'If this is a man',
+# 'author': 'Primo Levi'
+# }, {
+# 'id': 2,
+# 'title': 'The wind-up bird chronicle',
+# 'author': 'Haruki Murakami'
+# }
+# ]
+
+# serializer = self.BookSerializer(data=data, many=True)
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.object, data)
+
+# def test_bulk_create_errors(self):
+# """
+# Correct bulk update serialization should return the input data.
+# """
+
+# data = [
+# {
+# 'id': 0,
+# 'title': 'The electric kool-aid acid test',
+# 'author': 'Tom Wolfe'
+# }, {
+# 'id': 1,
+# 'title': 'If this is a man',
+# 'author': 'Primo Levi'
+# }, {
+# 'id': 'foo',
+# 'title': 'The wind-up bird chronicle',
+# 'author': 'Haruki Murakami'
+# }
+# ]
+# expected_errors = [
+# {},
+# {},
+# {'id': ['Enter a whole number.']}
+# ]
+
+# serializer = self.BookSerializer(data=data, many=True)
+# self.assertEqual(serializer.is_valid(), False)
+# self.assertEqual(serializer.errors, expected_errors)
+
+# def test_invalid_list_datatype(self):
+# """
+# Data containing list of incorrect data type should return errors.
+# """
+# data = ['foo', 'bar', 'baz']
+# serializer = self.BookSerializer(data=data, many=True)
+# self.assertEqual(serializer.is_valid(), False)
+
+# expected_errors = [
+# {'non_field_errors': ['Invalid data']},
+# {'non_field_errors': ['Invalid data']},
+# {'non_field_errors': ['Invalid data']}
+# ]
+
+# self.assertEqual(serializer.errors, expected_errors)
+
+# def test_invalid_single_datatype(self):
+# """
+# Data containing a single incorrect data type should return errors.
+# """
+# data = 123
+# serializer = self.BookSerializer(data=data, many=True)
+# self.assertEqual(serializer.is_valid(), False)
+
+# expected_errors = {'non_field_errors': ['Expected a list of items.']}
+
+# self.assertEqual(serializer.errors, expected_errors)
+
+# def test_invalid_single_object(self):
+# """
+# Data containing only a single object, instead of a list of objects
+# should return errors.
+# """
+# data = {
+# 'id': 0,
+# 'title': 'The electric kool-aid acid test',
+# 'author': 'Tom Wolfe'
+# }
+# serializer = self.BookSerializer(data=data, many=True)
+# self.assertEqual(serializer.is_valid(), False)
+
+# expected_errors = {'non_field_errors': ['Expected a list of items.']}
+
+# self.assertEqual(serializer.errors, expected_errors)
+
+
+# class BulkUpdateSerializerTests(TestCase):
+# """
+# Updating multiple instances using serializers.
+# """
+
+# def setUp(self):
+# class Book(object):
+# """
+# A data type that can be persisted to a mock storage backend
+# with `.save()` and `.delete()`.
+# """
+# object_map = {}
+
+# def __init__(self, id, title, author):
+# self.id = id
+# self.title = title
+# self.author = author
+
+# def save(self):
+# Book.object_map[self.id] = self
+
+# def delete(self):
+# del Book.object_map[self.id]
+
+# class BookSerializer(serializers.Serializer):
+# id = serializers.IntegerField()
+# title = serializers.CharField(max_length=100)
+# author = serializers.CharField(max_length=100)
+
+# def restore_object(self, attrs, instance=None):
+# if instance:
+# instance.id = attrs['id']
+# instance.title = attrs['title']
+# instance.author = attrs['author']
+# return instance
+# return Book(**attrs)
+
+# self.Book = Book
+# self.BookSerializer = BookSerializer
+
+# data = [
+# {
+# 'id': 0,
+# 'title': 'The electric kool-aid acid test',
+# 'author': 'Tom Wolfe'
+# }, {
+# 'id': 1,
+# 'title': 'If this is a man',
+# 'author': 'Primo Levi'
+# }, {
+# 'id': 2,
+# 'title': 'The wind-up bird chronicle',
+# 'author': 'Haruki Murakami'
+# }
+# ]
+
+# for item in data:
+# book = Book(item['id'], item['title'], item['author'])
+# book.save()
+
+# def books(self):
+# """
+# Return all the objects in the mock storage backend.
+# """
+# return self.Book.object_map.values()
+
+# def test_bulk_update_success(self):
+# """
+# Correct bulk update serialization should return the input data.
+# """
+# data = [
+# {
+# 'id': 0,
+# 'title': 'The electric kool-aid acid test',
+# 'author': 'Tom Wolfe'
+# }, {
+# 'id': 2,
+# 'title': 'Kafka on the shore',
+# 'author': 'Haruki Murakami'
+# }
+# ]
+# serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.data, data)
+# serializer.save()
+# new_data = self.BookSerializer(self.books(), many=True).data
+
+# self.assertEqual(data, new_data)
+
+# def test_bulk_update_and_create(self):
+# """
+# Bulk update serialization may also include created items.
+# """
+# data = [
+# {
+# 'id': 0,
+# 'title': 'The electric kool-aid acid test',
+# 'author': 'Tom Wolfe'
+# }, {
+# 'id': 3,
+# 'title': 'Kafka on the shore',
+# 'author': 'Haruki Murakami'
+# }
+# ]
+# serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.data, data)
+# serializer.save()
+# new_data = self.BookSerializer(self.books(), many=True).data
+# self.assertEqual(data, new_data)
+
+# def test_bulk_update_invalid_create(self):
+# """
+# Bulk update serialization without allow_add_remove may not create items.
+# """
+# data = [
+# {
+# 'id': 0,
+# 'title': 'The electric kool-aid acid test',
+# 'author': 'Tom Wolfe'
+# }, {
+# 'id': 3,
+# 'title': 'Kafka on the shore',
+# 'author': 'Haruki Murakami'
+# }
+# ]
+# expected_errors = [
+# {},
+# {'non_field_errors': ['Cannot create a new item, only existing items may be updated.']}
+# ]
+# serializer = self.BookSerializer(self.books(), data=data, many=True)
+# self.assertEqual(serializer.is_valid(), False)
+# self.assertEqual(serializer.errors, expected_errors)
+
+# def test_bulk_update_error(self):
+# """
+# Incorrect bulk update serialization should return error data.
+# """
+# data = [
+# {
+# 'id': 0,
+# 'title': 'The electric kool-aid acid test',
+# 'author': 'Tom Wolfe'
+# }, {
+# 'id': 'foo',
+# 'title': 'Kafka on the shore',
+# 'author': 'Haruki Murakami'
+# }
+# ]
+# expected_errors = [
+# {},
+# {'id': ['Enter a whole number.']}
+# ]
+# serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
+# self.assertEqual(serializer.is_valid(), False)
+# self.assertEqual(serializer.errors, expected_errors)
diff --git a/tests/test_serializer_empty.py b/tests/test_serializer_empty.py
index 30cff361..d0006ad3 100644
--- a/tests/test_serializer_empty.py
+++ b/tests/test_serializer_empty.py
@@ -1,15 +1,15 @@
-from django.test import TestCase
-from rest_framework import serializers
+# from django.test import TestCase
+# from rest_framework import serializers
-class EmptySerializerTestCase(TestCase):
- def test_empty_serializer(self):
- class FooBarSerializer(serializers.Serializer):
- foo = serializers.IntegerField()
- bar = serializers.SerializerMethodField('get_bar')
+# class EmptySerializerTestCase(TestCase):
+# def test_empty_serializer(self):
+# class FooBarSerializer(serializers.Serializer):
+# foo = serializers.IntegerField()
+# bar = serializers.SerializerMethodField('get_bar')
- def get_bar(self, obj):
- return 'bar'
+# def get_bar(self, obj):
+# return 'bar'
- serializer = FooBarSerializer()
- self.assertEquals(serializer.data, {'foo': 0})
+# serializer = FooBarSerializer()
+# self.assertEquals(serializer.data, {'foo': 0})
diff --git a/tests/test_serializer_import.py b/tests/test_serializer_import.py
index 3b8ff4b3..d029c3c5 100644
--- a/tests/test_serializer_import.py
+++ b/tests/test_serializer_import.py
@@ -1,19 +1,19 @@
-from django.test import TestCase
+# from django.test import TestCase
-from rest_framework import serializers
-from tests.accounts.serializers import AccountSerializer
+# from rest_framework import serializers
+# from tests.accounts.serializers import AccountSerializer
-class ImportingModelSerializerTests(TestCase):
- """
- In some situations like, GH #1225, it is possible, especially in
- testing, to import a serializer who's related models have not yet
- been resolved by Django. `AccountSerializer` is an example of such
- a serializer (imported at the top of this file).
- """
- def test_import_model_serializer(self):
- """
- The serializer at the top of this file should have been
- imported successfully, and we should be able to instantiate it.
- """
- self.assertIsInstance(AccountSerializer(), serializers.ModelSerializer)
+# class ImportingModelSerializerTests(TestCase):
+# """
+# In some situations like, GH #1225, it is possible, especially in
+# testing, to import a serializer who's related models have not yet
+# been resolved by Django. `AccountSerializer` is an example of such
+# a serializer (imported at the top of this file).
+# """
+# def test_import_model_serializer(self):
+# """
+# The serializer at the top of this file should have been
+# imported successfully, and we should be able to instantiate it.
+# """
+# self.assertIsInstance(AccountSerializer(), serializers.ModelSerializer)
diff --git a/tests/test_serializer_nested.py b/tests/test_serializer_nested.py
index c09c24db..b0f64ca7 100644
--- a/tests/test_serializer_nested.py
+++ b/tests/test_serializer_nested.py
@@ -1,349 +1,349 @@
-"""
-Tests to cover nested serializers.
-
-Doesn't cover model serializers.
-"""
-from __future__ import unicode_literals
-from django.test import TestCase
-from rest_framework import serializers
-from . import models
-
-
-class WritableNestedSerializerBasicTests(TestCase):
- """
- Tests for deserializing nested entities.
- Basic tests that use serializers that simply restore to dicts.
- """
-
- def setUp(self):
- class TrackSerializer(serializers.Serializer):
- order = serializers.IntegerField()
- title = serializers.CharField(max_length=100)
- duration = serializers.IntegerField()
-
- class AlbumSerializer(serializers.Serializer):
- album_name = serializers.CharField(max_length=100)
- artist = serializers.CharField(max_length=100)
- tracks = TrackSerializer(many=True)
-
- self.AlbumSerializer = AlbumSerializer
-
- def test_nested_validation_success(self):
- """
- Correct nested serialization should return the input data.
- """
-
- data = {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 239}
- ]
- }
-
- serializer = self.AlbumSerializer(data=data)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
-
- def test_nested_validation_error(self):
- """
- Incorrect nested serialization should return appropriate error data.
- """
-
- data = {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 'foobar'}
- ]
- }
- expected_errors = {
- 'tracks': [
- {},
- {},
- {'duration': ['Enter a whole number.']}
- ]
- }
-
- serializer = self.AlbumSerializer(data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_many_nested_validation_error(self):
- """
- Incorrect nested serialization should return appropriate error data
- when multiple entities are being deserialized.
- """
-
- data = [
- {
- 'album_name': 'Russian Red',
- 'artist': 'I Love Your Glasses',
- 'tracks': [
- {'order': 1, 'title': 'Cigarettes', 'duration': 121},
- {'order': 2, 'title': 'No Past Land', 'duration': 198},
- {'order': 3, 'title': 'They Don\'t Believe', 'duration': 191}
- ]
- },
- {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 'foobar'}
- ]
- }
- ]
- expected_errors = [
- {},
- {
- 'tracks': [
- {},
- {},
- {'duration': ['Enter a whole number.']}
- ]
- }
- ]
-
- serializer = self.AlbumSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
-
-
-class WritableNestedSerializerObjectTests(TestCase):
- """
- Tests for deserializing nested entities.
- These tests use serializers that restore to concrete objects.
- """
-
- def setUp(self):
- # Couple of concrete objects that we're going to deserialize into
- class Track(object):
- def __init__(self, order, title, duration):
- self.order, self.title, self.duration = order, title, duration
-
- def __eq__(self, other):
- return (
- self.order == other.order and
- self.title == other.title and
- self.duration == other.duration
- )
-
- class Album(object):
- def __init__(self, album_name, artist, tracks):
- self.album_name, self.artist, self.tracks = album_name, artist, tracks
-
- def __eq__(self, other):
- return (
- self.album_name == other.album_name and
- self.artist == other.artist and
- self.tracks == other.tracks
- )
-
- # And their corresponding serializers
- class TrackSerializer(serializers.Serializer):
- order = serializers.IntegerField()
- title = serializers.CharField(max_length=100)
- duration = serializers.IntegerField()
-
- def restore_object(self, attrs, instance=None):
- return Track(attrs['order'], attrs['title'], attrs['duration'])
-
- class AlbumSerializer(serializers.Serializer):
- album_name = serializers.CharField(max_length=100)
- artist = serializers.CharField(max_length=100)
- tracks = TrackSerializer(many=True)
-
- def restore_object(self, attrs, instance=None):
- return Album(attrs['album_name'], attrs['artist'], attrs['tracks'])
-
- self.Album, self.Track = Album, Track
- self.AlbumSerializer = AlbumSerializer
-
- def test_nested_validation_success(self):
- """
- Correct nested serialization should return a restored object
- that corresponds to the input data.
- """
-
- data = {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 239}
- ]
- }
- expected_object = self.Album(
- album_name='Discovery',
- artist='Daft Punk',
- tracks=[
- self.Track(order=1, title='One More Time', duration=235),
- self.Track(order=2, title='Aerodynamic', duration=184),
- self.Track(order=3, title='Digital Love', duration=239),
- ]
- )
-
- serializer = self.AlbumSerializer(data=data)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected_object)
-
- def test_many_nested_validation_success(self):
- """
- Correct nested serialization should return multiple restored objects
- that corresponds to the input data when multiple objects are
- being deserialized.
- """
-
- data = [
- {
- 'album_name': 'Russian Red',
- 'artist': 'I Love Your Glasses',
- 'tracks': [
- {'order': 1, 'title': 'Cigarettes', 'duration': 121},
- {'order': 2, 'title': 'No Past Land', 'duration': 198},
- {'order': 3, 'title': 'They Don\'t Believe', 'duration': 191}
- ]
- },
- {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 239}
- ]
- }
- ]
- expected_object = [
- self.Album(
- album_name='Russian Red',
- artist='I Love Your Glasses',
- tracks=[
- self.Track(order=1, title='Cigarettes', duration=121),
- self.Track(order=2, title='No Past Land', duration=198),
- self.Track(order=3, title='They Don\'t Believe', duration=191),
- ]
- ),
- self.Album(
- album_name='Discovery',
- artist='Daft Punk',
- tracks=[
- self.Track(order=1, title='One More Time', duration=235),
- self.Track(order=2, title='Aerodynamic', duration=184),
- self.Track(order=3, title='Digital Love', duration=239),
- ]
- )
- ]
-
- serializer = self.AlbumSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected_object)
-
-
-class ForeignKeyNestedSerializerUpdateTests(TestCase):
- def setUp(self):
- class Artist(object):
- def __init__(self, name):
- self.name = name
-
- def __eq__(self, other):
- return self.name == other.name
-
- class Album(object):
- def __init__(self, name, artist):
- self.name, self.artist = name, artist
-
- def __eq__(self, other):
- return self.name == other.name and self.artist == other.artist
-
- class ArtistSerializer(serializers.Serializer):
- name = serializers.CharField()
-
- def restore_object(self, attrs, instance=None):
- if instance:
- instance.name = attrs['name']
- else:
- instance = Artist(attrs['name'])
- return instance
-
- class AlbumSerializer(serializers.Serializer):
- name = serializers.CharField()
- by = ArtistSerializer(source='artist')
-
- def restore_object(self, attrs, instance=None):
- if instance:
- instance.name = attrs['name']
- instance.artist = attrs['artist']
- else:
- instance = Album(attrs['name'], attrs['artist'])
- return instance
-
- self.Artist = Artist
- self.Album = Album
- self.AlbumSerializer = AlbumSerializer
-
- def test_create_via_foreign_key_with_source(self):
- """
- Check that we can both *create* and *update* into objects across
- ForeignKeys that have a `source` specified.
- Regression test for #1170
- """
- data = {
- 'name': 'Discovery',
- 'by': {'name': 'Daft Punk'},
- }
-
- expected = self.Album(artist=self.Artist('Daft Punk'), name='Discovery')
-
- # create
- serializer = self.AlbumSerializer(data=data)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected)
-
- # update
- original = self.Album(artist=self.Artist('The Bats'), name='Free All the Monsters')
- serializer = self.AlbumSerializer(instance=original, data=data)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected)
-
-
-class NestedModelSerializerUpdateTests(TestCase):
- def test_second_nested_level(self):
- john = models.Person.objects.create(name="john")
-
- post = john.blogpost_set.create(title="Test blog post")
- post.blogpostcomment_set.create(text="I hate this blog post")
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class BlogPostCommentSerializer(serializers.ModelSerializer):
- class Meta:
- model = models.BlogPostComment
-
- class BlogPostSerializer(serializers.ModelSerializer):
- comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set')
-
- class Meta:
- model = models.BlogPost
- fields = ('id', 'title', 'comments')
-
- class PersonSerializer(serializers.ModelSerializer):
- posts = BlogPostSerializer(many=True, source='blogpost_set')
-
- class Meta:
- model = models.Person
- fields = ('id', 'name', 'age', 'posts')
-
- serialize = PersonSerializer(instance=john)
- deserialize = PersonSerializer(data=serialize.data, instance=john)
- self.assertTrue(deserialize.is_valid())
-
- result = deserialize.object
- result.save()
- self.assertEqual(result.id, john.id)
+# """
+# Tests to cover nested serializers.
+
+# Doesn't cover model serializers.
+# """
+# from __future__ import unicode_literals
+# from django.test import TestCase
+# from rest_framework import serializers
+# from . import models
+
+
+# class WritableNestedSerializerBasicTests(TestCase):
+# """
+# Tests for deserializing nested entities.
+# Basic tests that use serializers that simply restore to dicts.
+# """
+
+# def setUp(self):
+# class TrackSerializer(serializers.Serializer):
+# order = serializers.IntegerField()
+# title = serializers.CharField(max_length=100)
+# duration = serializers.IntegerField()
+
+# class AlbumSerializer(serializers.Serializer):
+# album_name = serializers.CharField(max_length=100)
+# artist = serializers.CharField(max_length=100)
+# tracks = TrackSerializer(many=True)
+
+# self.AlbumSerializer = AlbumSerializer
+
+# def test_nested_validation_success(self):
+# """
+# Correct nested serialization should return the input data.
+# """
+
+# data = {
+# 'album_name': 'Discovery',
+# 'artist': 'Daft Punk',
+# 'tracks': [
+# {'order': 1, 'title': 'One More Time', 'duration': 235},
+# {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
+# {'order': 3, 'title': 'Digital Love', 'duration': 239}
+# ]
+# }
+
+# serializer = self.AlbumSerializer(data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.object, data)
+
+# def test_nested_validation_error(self):
+# """
+# Incorrect nested serialization should return appropriate error data.
+# """
+
+# data = {
+# 'album_name': 'Discovery',
+# 'artist': 'Daft Punk',
+# 'tracks': [
+# {'order': 1, 'title': 'One More Time', 'duration': 235},
+# {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
+# {'order': 3, 'title': 'Digital Love', 'duration': 'foobar'}
+# ]
+# }
+# expected_errors = {
+# 'tracks': [
+# {},
+# {},
+# {'duration': ['Enter a whole number.']}
+# ]
+# }
+
+# serializer = self.AlbumSerializer(data=data)
+# self.assertEqual(serializer.is_valid(), False)
+# self.assertEqual(serializer.errors, expected_errors)
+
+# def test_many_nested_validation_error(self):
+# """
+# Incorrect nested serialization should return appropriate error data
+# when multiple entities are being deserialized.
+# """
+
+# data = [
+# {
+# 'album_name': 'Russian Red',
+# 'artist': 'I Love Your Glasses',
+# 'tracks': [
+# {'order': 1, 'title': 'Cigarettes', 'duration': 121},
+# {'order': 2, 'title': 'No Past Land', 'duration': 198},
+# {'order': 3, 'title': 'They Don\'t Believe', 'duration': 191}
+# ]
+# },
+# {
+# 'album_name': 'Discovery',
+# 'artist': 'Daft Punk',
+# 'tracks': [
+# {'order': 1, 'title': 'One More Time', 'duration': 235},
+# {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
+# {'order': 3, 'title': 'Digital Love', 'duration': 'foobar'}
+# ]
+# }
+# ]
+# expected_errors = [
+# {},
+# {
+# 'tracks': [
+# {},
+# {},
+# {'duration': ['Enter a whole number.']}
+# ]
+# }
+# ]
+
+# serializer = self.AlbumSerializer(data=data, many=True)
+# self.assertEqual(serializer.is_valid(), False)
+# self.assertEqual(serializer.errors, expected_errors)
+
+
+# class WritableNestedSerializerObjectTests(TestCase):
+# """
+# Tests for deserializing nested entities.
+# These tests use serializers that restore to concrete objects.
+# """
+
+# def setUp(self):
+# # Couple of concrete objects that we're going to deserialize into
+# class Track(object):
+# def __init__(self, order, title, duration):
+# self.order, self.title, self.duration = order, title, duration
+
+# def __eq__(self, other):
+# return (
+# self.order == other.order and
+# self.title == other.title and
+# self.duration == other.duration
+# )
+
+# class Album(object):
+# def __init__(self, album_name, artist, tracks):
+# self.album_name, self.artist, self.tracks = album_name, artist, tracks
+
+# def __eq__(self, other):
+# return (
+# self.album_name == other.album_name and
+# self.artist == other.artist and
+# self.tracks == other.tracks
+# )
+
+# # And their corresponding serializers
+# class TrackSerializer(serializers.Serializer):
+# order = serializers.IntegerField()
+# title = serializers.CharField(max_length=100)
+# duration = serializers.IntegerField()
+
+# def restore_object(self, attrs, instance=None):
+# return Track(attrs['order'], attrs['title'], attrs['duration'])
+
+# class AlbumSerializer(serializers.Serializer):
+# album_name = serializers.CharField(max_length=100)
+# artist = serializers.CharField(max_length=100)
+# tracks = TrackSerializer(many=True)
+
+# def restore_object(self, attrs, instance=None):
+# return Album(attrs['album_name'], attrs['artist'], attrs['tracks'])
+
+# self.Album, self.Track = Album, Track
+# self.AlbumSerializer = AlbumSerializer
+
+# def test_nested_validation_success(self):
+# """
+# Correct nested serialization should return a restored object
+# that corresponds to the input data.
+# """
+
+# data = {
+# 'album_name': 'Discovery',
+# 'artist': 'Daft Punk',
+# 'tracks': [
+# {'order': 1, 'title': 'One More Time', 'duration': 235},
+# {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
+# {'order': 3, 'title': 'Digital Love', 'duration': 239}
+# ]
+# }
+# expected_object = self.Album(
+# album_name='Discovery',
+# artist='Daft Punk',
+# tracks=[
+# self.Track(order=1, title='One More Time', duration=235),
+# self.Track(order=2, title='Aerodynamic', duration=184),
+# self.Track(order=3, title='Digital Love', duration=239),
+# ]
+# )
+
+# serializer = self.AlbumSerializer(data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.object, expected_object)
+
+# def test_many_nested_validation_success(self):
+# """
+# Correct nested serialization should return multiple restored objects
+# that corresponds to the input data when multiple objects are
+# being deserialized.
+# """
+
+# data = [
+# {
+# 'album_name': 'Russian Red',
+# 'artist': 'I Love Your Glasses',
+# 'tracks': [
+# {'order': 1, 'title': 'Cigarettes', 'duration': 121},
+# {'order': 2, 'title': 'No Past Land', 'duration': 198},
+# {'order': 3, 'title': 'They Don\'t Believe', 'duration': 191}
+# ]
+# },
+# {
+# 'album_name': 'Discovery',
+# 'artist': 'Daft Punk',
+# 'tracks': [
+# {'order': 1, 'title': 'One More Time', 'duration': 235},
+# {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
+# {'order': 3, 'title': 'Digital Love', 'duration': 239}
+# ]
+# }
+# ]
+# expected_object = [
+# self.Album(
+# album_name='Russian Red',
+# artist='I Love Your Glasses',
+# tracks=[
+# self.Track(order=1, title='Cigarettes', duration=121),
+# self.Track(order=2, title='No Past Land', duration=198),
+# self.Track(order=3, title='They Don\'t Believe', duration=191),
+# ]
+# ),
+# self.Album(
+# album_name='Discovery',
+# artist='Daft Punk',
+# tracks=[
+# self.Track(order=1, title='One More Time', duration=235),
+# self.Track(order=2, title='Aerodynamic', duration=184),
+# self.Track(order=3, title='Digital Love', duration=239),
+# ]
+# )
+# ]
+
+# serializer = self.AlbumSerializer(data=data, many=True)
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.object, expected_object)
+
+
+# class ForeignKeyNestedSerializerUpdateTests(TestCase):
+# def setUp(self):
+# class Artist(object):
+# def __init__(self, name):
+# self.name = name
+
+# def __eq__(self, other):
+# return self.name == other.name
+
+# class Album(object):
+# def __init__(self, name, artist):
+# self.name, self.artist = name, artist
+
+# def __eq__(self, other):
+# return self.name == other.name and self.artist == other.artist
+
+# class ArtistSerializer(serializers.Serializer):
+# name = serializers.CharField()
+
+# def restore_object(self, attrs, instance=None):
+# if instance:
+# instance.name = attrs['name']
+# else:
+# instance = Artist(attrs['name'])
+# return instance
+
+# class AlbumSerializer(serializers.Serializer):
+# name = serializers.CharField()
+# by = ArtistSerializer(source='artist')
+
+# def restore_object(self, attrs, instance=None):
+# if instance:
+# instance.name = attrs['name']
+# instance.artist = attrs['artist']
+# else:
+# instance = Album(attrs['name'], attrs['artist'])
+# return instance
+
+# self.Artist = Artist
+# self.Album = Album
+# self.AlbumSerializer = AlbumSerializer
+
+# def test_create_via_foreign_key_with_source(self):
+# """
+# Check that we can both *create* and *update* into objects across
+# ForeignKeys that have a `source` specified.
+# Regression test for #1170
+# """
+# data = {
+# 'name': 'Discovery',
+# 'by': {'name': 'Daft Punk'},
+# }
+
+# expected = self.Album(artist=self.Artist('Daft Punk'), name='Discovery')
+
+# # create
+# serializer = self.AlbumSerializer(data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.object, expected)
+
+# # update
+# original = self.Album(artist=self.Artist('The Bats'), name='Free All the Monsters')
+# serializer = self.AlbumSerializer(instance=original, data=data)
+# self.assertEqual(serializer.is_valid(), True)
+# self.assertEqual(serializer.object, expected)
+
+
+# class NestedModelSerializerUpdateTests(TestCase):
+# def test_second_nested_level(self):
+# john = models.Person.objects.create(name="john")
+
+# post = john.blogpost_set.create(title="Test blog post")
+# post.blogpostcomment_set.create(text="I hate this blog post")
+# post.blogpostcomment_set.create(text="I love this blog post")
+
+# class BlogPostCommentSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = models.BlogPostComment
+
+# class BlogPostSerializer(serializers.ModelSerializer):
+# comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set')
+
+# class Meta:
+# model = models.BlogPost
+# fields = ('id', 'title', 'comments')
+
+# class PersonSerializer(serializers.ModelSerializer):
+# posts = BlogPostSerializer(many=True, source='blogpost_set')
+
+# class Meta:
+# model = models.Person
+# fields = ('id', 'name', 'age', 'posts')
+
+# serialize = PersonSerializer(instance=john)
+# deserialize = PersonSerializer(data=serialize.data, instance=john)
+# self.assertTrue(deserialize.is_valid())
+
+# result = deserialize.object
+# result.save()
+# self.assertEqual(result.id, john.id)
diff --git a/tests/test_serializers.py b/tests/test_serializers.py
index 31c41730..0a105e8e 100644
--- a/tests/test_serializers.py
+++ b/tests/test_serializers.py
@@ -1,31 +1,31 @@
-from django.test import TestCase
-from django.utils import six
-from rest_framework.serializers import _resolve_model
-from tests.models import BasicModel
+# from django.test import TestCase
+# from django.utils import six
+# from rest_framework.serializers import _resolve_model
+# from tests.models import BasicModel
-class ResolveModelTests(TestCase):
- """
- `_resolve_model` should return a Django model class given the
- provided argument is a Django model class itself, or a properly
- formatted string representation of one.
- """
- def test_resolve_django_model(self):
- resolved_model = _resolve_model(BasicModel)
- self.assertEqual(resolved_model, BasicModel)
+# class ResolveModelTests(TestCase):
+# """
+# `_resolve_model` should return a Django model class given the
+# provided argument is a Django model class itself, or a properly
+# formatted string representation of one.
+# """
+# def test_resolve_django_model(self):
+# resolved_model = _resolve_model(BasicModel)
+# self.assertEqual(resolved_model, BasicModel)
- def test_resolve_string_representation(self):
- resolved_model = _resolve_model('tests.BasicModel')
- self.assertEqual(resolved_model, BasicModel)
+# def test_resolve_string_representation(self):
+# resolved_model = _resolve_model('tests.BasicModel')
+# self.assertEqual(resolved_model, BasicModel)
- def test_resolve_unicode_representation(self):
- resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
- self.assertEqual(resolved_model, BasicModel)
+# def test_resolve_unicode_representation(self):
+# resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
+# self.assertEqual(resolved_model, BasicModel)
- def test_resolve_non_django_model(self):
- with self.assertRaises(ValueError):
- _resolve_model(TestCase)
+# def test_resolve_non_django_model(self):
+# with self.assertRaises(ValueError):
+# _resolve_model(TestCase)
- def test_resolve_improper_string_representation(self):
- with self.assertRaises(ValueError):
- _resolve_model('BasicModel')
+# def test_resolve_improper_string_representation(self):
+# with self.assertRaises(ValueError):
+# _resolve_model('BasicModel')
--
cgit v1.2.3
From d934824bff21e4a11226af61efba319be227f4f0 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 5 Sep 2014 16:29:46 +0100
Subject: Workin on
---
rest_framework/exceptions.py | 9 ++++-
rest_framework/fields.py | 26 +++++++++++--
rest_framework/generics.py | 30 +--------------
rest_framework/mixins.py | 83 +++++++++++++++++++++++------------------
rest_framework/serializers.py | 64 +++++++++++++++++++------------
tests/test_generics.py | 41 +++-----------------
tests/test_validation.py | 5 +--
tests/test_write_only_fields.py | 69 +++++++++++++++++-----------------
8 files changed, 159 insertions(+), 168 deletions(-)
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index ad52d172..852a08b1 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -15,7 +15,7 @@ class APIException(Exception):
Subclasses should provide `.status_code` and `.default_detail` properties.
"""
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
- default_detail = ''
+ default_detail = 'A server error occured'
def __init__(self, detail=None):
self.detail = detail or self.default_detail
@@ -29,6 +29,11 @@ class ParseError(APIException):
default_detail = 'Malformed request.'
+class ValidationError(APIException):
+ status_code = status.HTTP_400_BAD_REQUEST
+ default_detail = 'Invalid data in request.'
+
+
class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
default_detail = 'Incorrect authentication credentials.'
@@ -54,7 +59,7 @@ class MethodNotAllowed(APIException):
class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE
- default_detail = "Could not satisfy the request's Accept header"
+ default_detail = "Could not satisfy the request Accept header"
def __init__(self, detail=None, available_renderers=None):
self.detail = detail or self.default_detail
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 838aa3b0..d18551b3 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1,3 +1,4 @@
+from rest_framework.exceptions import ValidationError
from rest_framework.utils import html
import inspect
@@ -59,10 +60,6 @@ def set_value(dictionary, keys, value):
dictionary[keys[-1]] = value
-class ValidationError(Exception):
- pass
-
-
class SkipField(Exception):
pass
@@ -204,6 +201,22 @@ class Field(object):
msg = self._MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)
raise AssertionError(msg)
+ def __new__(cls, *args, **kwargs):
+ instance = super(Field, cls).__new__(cls)
+ instance._args = args
+ instance._kwargs = kwargs
+ return instance
+
+ def __repr__(self):
+ arg_string = ', '.join([repr(val) for val in self._args])
+ kwarg_string = ', '.join([
+ '%s=%s' % (key, repr(val)) for key, val in self._kwargs.items()
+ ])
+ if arg_string and kwarg_string:
+ arg_string += ', '
+ class_name = self.__class__.__name__
+ return "%s(%s%s)" % (class_name, arg_string, kwarg_string)
+
class BooleanField(Field):
MESSAGES = {
@@ -308,6 +321,11 @@ class IntegerField(Field):
'invalid_integer': 'A valid integer is required.'
}
+ def __init__(self, **kwargs):
+ self.max_value = kwargs.pop('max_value')
+ self.min_value = kwargs.pop('min_value')
+ super(CharField, self).__init__(**kwargs)
+
def to_native(self, data):
try:
data = int(str(data))
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 6705cbb2..c2c59154 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -27,7 +27,7 @@ def strict_positive_int(integer_string, cutoff=None):
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:
@@ -249,34 +249,6 @@ class GenericAPIView(views.APIView):
#
# The are not called by GenericAPIView directly,
# but are used by the mixin methods.
-
- def pre_save(self, obj):
- """
- Placeholder method for calling before saving an object.
-
- May be used to set attributes on the object that are implicit
- in either the request, or the url.
- """
- pass
-
- def post_save(self, obj, created=False):
- """
- Placeholder method for calling after saving an object.
- """
- pass
-
- def pre_delete(self, obj):
- """
- Placeholder method for calling before deleting an object.
- """
- pass
-
- def post_delete(self, obj):
- """
- Placeholder method for calling after deleting an object.
- """
- pass
-
def metadata(self, request):
"""
Return a dictionary of metadata about the view.
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 359740ce..14a6b44b 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -19,14 +19,10 @@ class CreateModelMixin(object):
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA)
-
- if serializer.is_valid():
- self.object = serializer.save()
- headers = self.get_success_headers(serializer.data)
- return Response(serializer.data, status=status.HTTP_201_CREATED,
- headers=headers)
-
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+ headers = self.get_success_headers(serializer.data)
+ return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def get_success_headers(self, data):
try:
@@ -40,15 +36,12 @@ class ListModelMixin(object):
List a queryset.
"""
def list(self, request, *args, **kwargs):
- self.object_list = self.filter_queryset(self.get_queryset())
-
- # Switch between paginated or standard style responses
- page = self.paginate_queryset(self.object_list)
+ instance = self.filter_queryset(self.get_queryset())
+ page = self.paginate_queryset(instance)
if page is not None:
serializer = self.get_pagination_serializer(page)
else:
- serializer = self.get_serializer(self.object_list, many=True)
-
+ serializer = self.get_serializer(instance, many=True)
return Response(serializer.data)
@@ -57,8 +50,8 @@ class RetrieveModelMixin(object):
Retrieve a model instance.
"""
def retrieve(self, request, *args, **kwargs):
- self.object = self.get_object()
- serializer = self.get_serializer(self.object)
+ instance = self.get_object()
+ serializer = self.get_serializer(instance)
return Response(serializer.data)
@@ -68,22 +61,52 @@ class UpdateModelMixin(object):
"""
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
- self.object = self.get_object_or_none()
+ instance = self.get_object()
+ serializer = self.get_serializer(instance, data=request.DATA, partial=partial)
+ serializer.is_valid(raise_exception=True)
+ serializer.save()
+ return Response(serializer.data)
+
+ def partial_update(self, request, *args, **kwargs):
+ kwargs['partial'] = True
+ return self.update(request, *args, **kwargs)
+
- serializer = self.get_serializer(self.object, data=request.DATA, partial=partial)
+class DestroyModelMixin(object):
+ """
+ Destroy a model instance.
+ """
+ def destroy(self, request, *args, **kwargs):
+ instance = self.get_object()
+ instance.delete()
+ return Response(status=status.HTTP_204_NO_CONTENT)
- if not serializer.is_valid():
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
- if self.object is None:
+# The AllowPUTAsCreateMixin was previously the default behaviour
+# for PUT requests. This has now been removed and must be *explictly*
+# included if it is the behavior that you want.
+# For more info see: ...
+
+class AllowPUTAsCreateMixin(object):
+ """
+ The following mixin class may be used in order to support PUT-as-create
+ behavior for incoming requests.
+ """
+ def update(self, request, *args, **kwargs):
+ partial = kwargs.pop('partial', False)
+ instance = self.get_object_or_none()
+ serializer = self.get_serializer(instance, data=request.DATA, partial=partial)
+ serializer.is_valid(raise_exception=True)
+
+ if instance is None:
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup_value = self.kwargs[lookup_url_kwarg]
extras = {self.lookup_field: lookup_value}
- self.object = serializer.save(extras=extras)
+ serializer.save(extras=extras)
return Response(serializer.data, status=status.HTTP_201_CREATED)
- self.object = serializer.save()
- return Response(serializer.data, status=status.HTTP_200_OK)
+ serializer.save()
+ return Response(serializer.data)
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
@@ -103,15 +126,3 @@ class UpdateModelMixin(object):
# PATCH requests where the object does not exist should still
# return a 404 response.
raise
-
-
-class DestroyModelMixin(object):
- """
- Destroy a model instance.
- """
- def destroy(self, request, *args, **kwargs):
- obj = self.get_object()
- self.pre_delete(obj)
- obj.delete()
- self.post_delete(obj)
- return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index c38d8968..49eb6ce9 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -13,7 +13,8 @@ response content is handled by parsers and renderers.
from django.db import models
from django.utils import six
from collections import namedtuple, OrderedDict
-from rest_framework.fields import empty, set_value, Field, SkipField, ValidationError
+from rest_framework.exceptions import ValidationError
+from rest_framework.fields import empty, set_value, Field, SkipField
from rest_framework.settings import api_settings
from rest_framework.utils import html
import copy
@@ -34,43 +35,53 @@ FieldResult = namedtuple('FieldResult', ['field', 'value', 'error'])
class BaseSerializer(Field):
+ """
+ The BaseSerializer class provides a minimal class which may be used
+ for writing custom serializer implementations.
+ """
+
def __init__(self, instance=None, data=None, **kwargs):
super(BaseSerializer, self).__init__(**kwargs)
self.instance = instance
self._initial_data = data
def to_native(self, data):
- raise NotImplementedError()
+ raise NotImplementedError('`to_native()` must be implemented.')
def to_primative(self, instance):
- raise NotImplementedError()
+ raise NotImplementedError('`to_primative()` must be implemented.')
- def update(self, instance):
- raise NotImplementedError()
+ def update(self, instance, attrs):
+ raise NotImplementedError('`update()` must be implemented.')
- def create(self):
- raise NotImplementedError()
+ def create(self, attrs):
+ raise NotImplementedError('`create()` must be implemented.')
def save(self, extras=None):
if extras is not None:
- self._validated_data.update(extras)
+ self.validated_data.update(extras)
if self.instance is not None:
- self.update(self.instance)
+ self.update(self.instance, self._validated_data)
else:
- self.instance = self.create()
+ self.instance = self.create(self._validated_data)
return self.instance
- def is_valid(self):
- try:
- self._validated_data = self.to_native(self._initial_data)
- except ValidationError as exc:
- self._validated_data = {}
- self._errors = exc.args[0]
- return False
- self._errors = {}
- return True
+ def is_valid(self, raise_exception=False):
+ if not hasattr(self, '_validated_data'):
+ try:
+ self._validated_data = self.to_native(self._initial_data)
+ except ValidationError as exc:
+ self._validated_data = {}
+ self._errors = exc.detail
+ else:
+ self._errors = {}
+
+ if self._errors and raise_exception:
+ raise ValidationError(self._errors)
+
+ return not bool(self._errors)
@property
def data(self):
@@ -184,14 +195,20 @@ class Serializer(BaseSerializer):
"""
Dict of native values <- Dict of primitive datatypes.
"""
+ if not isinstance(data, dict):
+ raise ValidationError({'non_field_errors': ['Invalid data']})
+
ret = {}
errors = {}
fields = [field for field in self.fields.values() if not field.read_only]
for field in fields:
+ validate_method = getattr(self, 'validate_' + field.field_name, None)
primitive_value = field.get_value(data)
try:
validated_value = field.validate(primitive_value)
+ if validate_method is not None:
+ validated_value = validate_method(validated_value)
except ValidationError as exc:
errors[field.field_name] = str(exc)
except SkipField:
@@ -202,6 +219,7 @@ class Serializer(BaseSerializer):
if errors:
raise ValidationError(errors)
+ # TODO: 'Non field errors'
return self.validate(ret)
def to_primative(self, instance):
@@ -340,12 +358,12 @@ class ModelSerializer(Serializer):
self.opts = self._options_class(self.Meta)
super(ModelSerializer, self).__init__(*args, **kwargs)
- def create(self):
+ def create(self, attrs):
ModelClass = self.opts.model
- return ModelClass.objects.create(**self.validated_data)
+ return ModelClass.objects.create(**attrs)
- def update(self, obj):
- for attr, value in self.validated_data.items():
+ def update(self, obj, attrs):
+ for attr, value in attrs.items():
setattr(obj, attr, value)
obj.save()
diff --git a/tests/test_generics.py b/tests/test_generics.py
index 55f361b2..1b00c351 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -360,18 +360,15 @@ class TestInstanceView(TestCase):
def test_put_to_deleted_instance(self):
"""
- PUT requests to RetrieveUpdateDestroyAPIView should create an object
- if it does not currently exist.
+ PUT requests to RetrieveUpdateDestroyAPIView should return 404 if
+ an object does not currently exist.
"""
self.objects.get(id=1).delete()
data = {'text': 'foobar'}
request = factory.put('/1', data, format='json')
- with self.assertNumQueries(2):
+ with self.assertNumQueries(1):
response = self.view(request, pk=1).render()
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
- updated = self.objects.get(id=1)
- self.assertEqual(updated.text, 'foobar')
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_put_to_filtered_out_instance(self):
"""
@@ -382,35 +379,7 @@ class TestInstanceView(TestCase):
filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk
request = factory.put('/{0}'.format(filtered_out_pk), data, format='json')
response = self.view(request, pk=filtered_out_pk).render()
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
-
- def test_put_as_create_on_id_based_url(self):
- """
- PUT requests to RetrieveUpdateDestroyAPIView should create an object
- at the requested url if it doesn't exist.
- """
- data = {'text': 'foobar'}
- # pk fields can not be created on demand, only the database can set the pk for a new object
- request = factory.put('/5', data, format='json')
- with self.assertNumQueries(2):
- response = self.view(request, pk=5).render()
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- new_obj = self.objects.get(pk=5)
- self.assertEqual(new_obj.text, 'foobar')
-
- def test_put_as_create_on_slug_based_url(self):
- """
- PUT requests to RetrieveUpdateDestroyAPIView should create an object
- at the requested url if possible, else return HTTP_403_FORBIDDEN error-response.
- """
- data = {'text': 'foobar'}
- request = factory.put('/test_slug', data, format='json')
- with self.assertNumQueries(2):
- response = self.slug_based_view(request, slug='test_slug').render()
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(response.data, {'slug': 'test_slug', 'text': 'foobar'})
- new_obj = SlugBasedModel.objects.get(slug='test_slug')
- self.assertEqual(new_obj.text, 'foobar')
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_patch_cannot_create_an_object(self):
"""
diff --git a/tests/test_validation.py b/tests/test_validation.py
index f62d9068..fcfc853d 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -48,11 +48,10 @@ class ShouldValidateModel(models.Model):
class ShouldValidateModelSerializer(serializers.ModelSerializer):
renamed = serializers.CharField(source='should_validate_field', required=False)
- def validate_renamed(self, attrs, source):
- value = attrs[source]
+ def validate_renamed(self, value):
if len(value) < 3:
raise serializers.ValidationError('Minimum 3 characters.')
- return attrs
+ return value
class Meta:
model = ShouldValidateModel
diff --git a/tests/test_write_only_fields.py b/tests/test_write_only_fields.py
index aabb18d6..367048ac 100644
--- a/tests/test_write_only_fields.py
+++ b/tests/test_write_only_fields.py
@@ -1,42 +1,41 @@
-from django.db import models
-from django.test import TestCase
-from rest_framework import serializers
+# from django.db import models
+# from django.test import TestCase
+# from rest_framework import serializers
-class ExampleModel(models.Model):
- email = models.EmailField(max_length=100)
- password = models.CharField(max_length=100)
+# class ExampleModel(models.Model):
+# email = models.EmailField(max_length=100)
+# password = models.CharField(max_length=100)
-class WriteOnlyFieldTests(TestCase):
- def test_write_only_fields(self):
- class ExampleSerializer(serializers.Serializer):
- email = serializers.EmailField()
- password = serializers.CharField(write_only=True)
+# class WriteOnlyFieldTests(TestCase):
+# def test_write_only_fields(self):
+# class ExampleSerializer(serializers.Serializer):
+# email = serializers.EmailField()
+# password = serializers.CharField(write_only=True)
- data = {
- 'email': 'foo@example.com',
- 'password': '123'
- }
- serializer = ExampleSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- self.assertEquals(serializer.object, data)
- self.assertEquals(serializer.data, {'email': 'foo@example.com'})
+# data = {
+# 'email': 'foo@example.com',
+# 'password': '123'
+# }
+# serializer = ExampleSerializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# self.assertEquals(serializer.validated_data, data)
+# self.assertEquals(serializer.data, {'email': 'foo@example.com'})
- def test_write_only_fields_meta(self):
- class ExampleSerializer(serializers.ModelSerializer):
- class Meta:
- model = ExampleModel
- fields = ('email', 'password')
- write_only_fields = ('password',)
+# def test_write_only_fields_meta(self):
+# class ExampleSerializer(serializers.ModelSerializer):
+# class Meta:
+# model = ExampleModel
+# fields = ('email', 'password')
+# write_only_fields = ('password',)
- data = {
- 'email': 'foo@example.com',
- 'password': '123'
- }
- serializer = ExampleSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- self.assertTrue(isinstance(serializer.object, ExampleModel))
- self.assertEquals(serializer.object.email, data['email'])
- self.assertEquals(serializer.object.password, data['password'])
- self.assertEquals(serializer.data, {'email': 'foo@example.com'})
+# data = {
+# 'email': 'foo@example.com',
+# 'password': '123'
+# }
+# serializer = ExampleSerializer(data=data)
+# self.assertTrue(serializer.is_valid())
+# self.assertTrue(isinstance(serializer.object, ExampleModel))
+# self.assertEquals(serializer.validated_data, data)
+# self.assertEquals(serializer.data, {'email': 'foo@example.com'})
--
cgit v1.2.3
From 21980b800d04a1d82a6003823abfdf4ab80ae979 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 8 Sep 2014 14:24:05 +0100
Subject: More test sorting
---
rest_framework/exceptions.py | 5 ---
rest_framework/fields.py | 85 ++++++++++++++++++++++++++++++++++++---
rest_framework/serializers.py | 25 +++++++-----
rest_framework/views.py | 9 ++++-
tests/put_as_create_workspace.txt | 33 +++++++++++++++
tests/test_permissions.py | 13 ------
tests/test_response.py | 12 +++---
tests/test_serializers.py | 50 +++++++++++------------
tests/test_validation.py | 13 ------
tests/test_write_only_fields.py | 60 ++++++++++++---------------
10 files changed, 190 insertions(+), 115 deletions(-)
create mode 100644 tests/put_as_create_workspace.txt
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 852a08b1..06b5e8a2 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -29,11 +29,6 @@ class ParseError(APIException):
default_detail = 'Malformed request.'
-class ValidationError(APIException):
- status_code = status.HTTP_400_BAD_REQUEST
- default_detail = 'Invalid data in request.'
-
-
class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
default_detail = 'Incorrect authentication credentials.'
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index d18551b3..250c0579 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1,4 +1,6 @@
-from rest_framework.exceptions import ValidationError
+from django.core import validators
+from django.core.exceptions import ValidationError
+from django.utils.encoding import is_protected_type
from rest_framework.utils import html
import inspect
@@ -33,9 +35,14 @@ def get_attribute(instance, attrs):
"""
Similar to Python's built in `getattr(instance, attr)`,
but takes a list of nested attributes, instead of a single attribute.
+
+ Also accepts either attribute lookup on objects or dictionary lookups.
"""
for attr in attrs:
- instance = getattr(instance, attr)
+ try:
+ instance = getattr(instance, attr)
+ except AttributeError:
+ return instance[attr]
return instance
@@ -80,9 +87,11 @@ class Field(object):
'not exist in the `MESSAGES` dictionary.'
)
+ default_validators = []
+
def __init__(self, read_only=False, write_only=False,
required=None, default=empty, initial=None, source=None,
- label=None, style=None, error_messages=None):
+ label=None, style=None, error_messages=None, validators=[]):
self._creation_counter = Field._creation_counter
Field._creation_counter += 1
@@ -104,6 +113,7 @@ class Field(object):
self.initial = initial
self.label = label
self.style = {} if style is None else style
+ self.validators = self.default_validators + validators
def bind(self, field_name, parent, root):
"""
@@ -176,8 +186,21 @@ class Field(object):
self.fail('required')
return self.get_default()
+ self.run_validators(data)
return self.to_native(data)
+ def run_validators(self, value):
+ if value in validators.EMPTY_VALUES:
+ return
+ errors = []
+ for validator in self.validators:
+ try:
+ validator(value)
+ except ValidationError as exc:
+ errors.extend(exc.messages)
+ if errors:
+ raise ValidationError(errors)
+
def to_native(self, data):
"""
Transform the *incoming* primative data into a native value.
@@ -322,9 +345,13 @@ class IntegerField(Field):
}
def __init__(self, **kwargs):
- self.max_value = kwargs.pop('max_value')
- self.min_value = kwargs.pop('min_value')
- super(CharField, self).__init__(**kwargs)
+ max_value = kwargs.pop('max_value', None)
+ min_value = kwargs.pop('min_value', None)
+ super(IntegerField, self).__init__(**kwargs)
+ if max_value is not None:
+ self.validators.append(validators.MaxValueValidator(max_value))
+ if min_value is not None:
+ self.validators.append(validators.MinValueValidator(min_value))
def to_native(self, data):
try:
@@ -392,3 +419,49 @@ class MethodField(Field):
attr = 'get_{field_name}'.format(field_name=self.field_name)
method = getattr(self.parent, attr)
return method(value)
+
+
+class ModelField(Field):
+ """
+ A generic field that can be used against an arbitrary model field.
+ """
+ def __init__(self, *args, **kwargs):
+ try:
+ self.model_field = kwargs.pop('model_field')
+ except KeyError:
+ raise ValueError("ModelField requires 'model_field' kwarg")
+
+ self.min_length = kwargs.pop('min_length',
+ getattr(self.model_field, 'min_length', None))
+ self.max_length = kwargs.pop('max_length',
+ getattr(self.model_field, 'max_length', None))
+ self.min_value = kwargs.pop('min_value',
+ getattr(self.model_field, 'min_value', None))
+ self.max_value = kwargs.pop('max_value',
+ getattr(self.model_field, 'max_value', None))
+
+ super(ModelField, self).__init__(*args, **kwargs)
+
+ if self.min_length is not None:
+ self.validators.append(validators.MinLengthValidator(self.min_length))
+ if self.max_length is not None:
+ self.validators.append(validators.MaxLengthValidator(self.max_length))
+ if self.min_value is not None:
+ self.validators.append(validators.MinValueValidator(self.min_value))
+ if self.max_value is not None:
+ self.validators.append(validators.MaxValueValidator(self.max_value))
+
+ def get_attribute(self, instance):
+ return get_attribute(instance, self.source_attrs[:-1])
+
+ def to_native(self, data):
+ rel = getattr(self.model_field, 'rel', None)
+ if rel is not None:
+ return rel.to._meta.get_field(rel.field_name).to_python(data)
+ return self.model_field.to_python(data)
+
+ def to_primative(self, obj):
+ value = self.model_field._get_val_from_obj(obj)
+ if is_protected_type(value):
+ return value
+ return self.model_field.value_to_string(obj)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 49eb6ce9..93226d32 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -10,10 +10,10 @@ python primitives.
2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
+from django.core.exceptions import ValidationError
from django.db import models
from django.utils import six
from collections import namedtuple, OrderedDict
-from rest_framework.exceptions import ValidationError
from rest_framework.fields import empty, set_value, Field, SkipField
from rest_framework.settings import api_settings
from rest_framework.utils import html
@@ -58,13 +58,14 @@ class BaseSerializer(Field):
raise NotImplementedError('`create()` must be implemented.')
def save(self, extras=None):
+ attrs = self.validated_data
if extras is not None:
- self.validated_data.update(extras)
+ attrs = dict(list(attrs.items()) + list(extras.items()))
if self.instance is not None:
- self.update(self.instance, self._validated_data)
+ self.update(self.instance, attrs)
else:
- self.instance = self.create(self._validated_data)
+ self.instance = self.create(attrs)
return self.instance
@@ -74,7 +75,7 @@ class BaseSerializer(Field):
self._validated_data = self.to_native(self._initial_data)
except ValidationError as exc:
self._validated_data = {}
- self._errors = exc.detail
+ self._errors = exc.message_dict
else:
self._errors = {}
@@ -210,7 +211,7 @@ class Serializer(BaseSerializer):
if validate_method is not None:
validated_value = validate_method(validated_value)
except ValidationError as exc:
- errors[field.field_name] = str(exc)
+ errors[field.field_name] = exc.messages
except SkipField:
pass
else:
@@ -219,8 +220,10 @@ class Serializer(BaseSerializer):
if errors:
raise ValidationError(errors)
- # TODO: 'Non field errors'
- return self.validate(ret)
+ try:
+ return self.validate(ret)
+ except ValidationError, exc:
+ raise ValidationError({'non_field_errors': exc.messages})
def to_primative(self, instance):
"""
@@ -539,6 +542,9 @@ class ModelSerializer(Serializer):
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
+ if model_field.validators is not None:
+ kwargs['validators'] = model_field.validators
+
# if model_field.help_text is not None:
# kwargs['help_text'] = model_field.help_text
@@ -577,8 +583,7 @@ class ModelSerializer(Serializer):
try:
return self.field_mapping[model_field.__class__](**kwargs)
except KeyError:
- # TODO: Change this to `return ModelField(model_field=model_field, **kwargs)`
- return CharField(**kwargs)
+ return ModelField(model_field=model_field, **kwargs)
class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 23df3443..079e9285 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -3,7 +3,7 @@ Provides an APIView class that is the base of all views in REST framework.
"""
from __future__ import unicode_literals
-from django.core.exceptions import PermissionDenied
+from django.core.exceptions import PermissionDenied, ValidationError
from django.http import Http404
from django.utils.datastructures import SortedDict
from django.views.decorators.csrf import csrf_exempt
@@ -51,7 +51,8 @@ def exception_handler(exc):
Returns the response that should be used for any given exception.
By default we handle the REST framework `APIException`, and also
- Django's builtin `Http404` and `PermissionDenied` exceptions.
+ Django's built-in `ValidationError`, `Http404` and `PermissionDenied`
+ exceptions.
Any unhandled exceptions may return `None`, which will cause a 500 error
to be raised.
@@ -68,6 +69,10 @@ def exception_handler(exc):
status=exc.status_code,
headers=headers)
+ elif isinstance(exc, ValidationError):
+ return Response(exc.message_dict,
+ status=status.HTTP_400_BAD_REQUEST)
+
elif isinstance(exc, Http404):
return Response({'detail': 'Not found'},
status=status.HTTP_404_NOT_FOUND)
diff --git a/tests/put_as_create_workspace.txt b/tests/put_as_create_workspace.txt
new file mode 100644
index 00000000..6bc5218e
--- /dev/null
+++ b/tests/put_as_create_workspace.txt
@@ -0,0 +1,33 @@
+# From test_validation...
+
+class TestPreSaveValidationExclusions(TestCase):
+ def test_pre_save_validation_exclusions(self):
+ """
+ Somewhat weird test case to ensure that we don't perform model
+ validation on read only fields.
+ """
+ obj = ValidationModel.objects.create(blank_validated_field='')
+ request = factory.put('/', {}, format='json')
+ view = UpdateValidationModel().as_view()
+ response = view(request, pk=obj.pk).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+
+# From test_permissions...
+
+class ModelPermissionsIntegrationTests(TestCase):
+ def setUp(...):
+ ...
+
+ def test_has_put_as_create_permissions(self):
+ # User only has update permissions - should be able to update an entity.
+ request = factory.put('/1', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.updateonly_credentials)
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ # But if PUTing to a new entity, permission should be denied.
+ request = factory.put('/2', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.updateonly_credentials)
+ response = instance_view(request, pk='2')
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
diff --git a/tests/test_permissions.py b/tests/test_permissions.py
index d5568c55..ac398f80 100644
--- a/tests/test_permissions.py
+++ b/tests/test_permissions.py
@@ -95,19 +95,6 @@ class ModelPermissionsIntegrationTests(TestCase):
response = instance_view(request, pk=1)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
- def test_has_put_as_create_permissions(self):
- # User only has update permissions - should be able to update an entity.
- request = factory.put('/1', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.updateonly_credentials)
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- # But if PUTing to a new entity, permission should be denied.
- request = factory.put('/2', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.updateonly_credentials)
- response = instance_view(request, pk='2')
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
# def test_options_permitted(self):
# request = factory.options(
# '/',
diff --git a/tests/test_response.py b/tests/test_response.py
index 004c565c..67419a71 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -225,8 +225,8 @@ class Issue467Tests(TestCase):
def test_form_has_label_and_help_text(self):
resp = self.client.get('/html_new_model')
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
- self.assertContains(resp, 'Text comes here')
- self.assertContains(resp, 'Text description.')
+ # self.assertContains(resp, 'Text comes here')
+ # self.assertContains(resp, 'Text description.')
class Issue807Tests(TestCase):
@@ -270,11 +270,11 @@ class Issue807Tests(TestCase):
)
resp = self.client.get('/html_new_model_viewset/' + param)
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
- self.assertContains(resp, 'Text comes here')
- self.assertContains(resp, 'Text description.')
+ # self.assertContains(resp, 'Text comes here')
+ # self.assertContains(resp, 'Text description.')
def test_form_has_label_and_help_text(self):
resp = self.client.get('/html_new_model')
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
- self.assertContains(resp, 'Text comes here')
- self.assertContains(resp, 'Text description.')
+ # self.assertContains(resp, 'Text comes here')
+ # self.assertContains(resp, 'Text description.')
diff --git a/tests/test_serializers.py b/tests/test_serializers.py
index 0a105e8e..31c41730 100644
--- a/tests/test_serializers.py
+++ b/tests/test_serializers.py
@@ -1,31 +1,31 @@
-# from django.test import TestCase
-# from django.utils import six
-# from rest_framework.serializers import _resolve_model
-# from tests.models import BasicModel
+from django.test import TestCase
+from django.utils import six
+from rest_framework.serializers import _resolve_model
+from tests.models import BasicModel
-# class ResolveModelTests(TestCase):
-# """
-# `_resolve_model` should return a Django model class given the
-# provided argument is a Django model class itself, or a properly
-# formatted string representation of one.
-# """
-# def test_resolve_django_model(self):
-# resolved_model = _resolve_model(BasicModel)
-# self.assertEqual(resolved_model, BasicModel)
+class ResolveModelTests(TestCase):
+ """
+ `_resolve_model` should return a Django model class given the
+ provided argument is a Django model class itself, or a properly
+ formatted string representation of one.
+ """
+ def test_resolve_django_model(self):
+ resolved_model = _resolve_model(BasicModel)
+ self.assertEqual(resolved_model, BasicModel)
-# def test_resolve_string_representation(self):
-# resolved_model = _resolve_model('tests.BasicModel')
-# self.assertEqual(resolved_model, BasicModel)
+ def test_resolve_string_representation(self):
+ resolved_model = _resolve_model('tests.BasicModel')
+ self.assertEqual(resolved_model, BasicModel)
-# def test_resolve_unicode_representation(self):
-# resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
-# self.assertEqual(resolved_model, BasicModel)
+ def test_resolve_unicode_representation(self):
+ resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
+ self.assertEqual(resolved_model, BasicModel)
-# def test_resolve_non_django_model(self):
-# with self.assertRaises(ValueError):
-# _resolve_model(TestCase)
+ def test_resolve_non_django_model(self):
+ with self.assertRaises(ValueError):
+ _resolve_model(TestCase)
-# def test_resolve_improper_string_representation(self):
-# with self.assertRaises(ValueError):
-# _resolve_model('BasicModel')
+ def test_resolve_improper_string_representation(self):
+ with self.assertRaises(ValueError):
+ _resolve_model('BasicModel')
diff --git a/tests/test_validation.py b/tests/test_validation.py
index fcfc853d..40005486 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -26,19 +26,6 @@ class UpdateValidationModel(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ValidationModelSerializer
-class TestPreSaveValidationExclusions(TestCase):
- def test_pre_save_validation_exclusions(self):
- """
- Somewhat weird test case to ensure that we don't perform model
- validation on read only fields.
- """
- obj = ValidationModel.objects.create(blank_validated_field='')
- request = factory.put('/', {}, format='json')
- view = UpdateValidationModel().as_view()
- response = view(request, pk=obj.pk).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-
# Regression for #653
class ShouldValidateModel(models.Model):
diff --git a/tests/test_write_only_fields.py b/tests/test_write_only_fields.py
index 367048ac..dd3bbd6e 100644
--- a/tests/test_write_only_fields.py
+++ b/tests/test_write_only_fields.py
@@ -1,41 +1,31 @@
-# from django.db import models
-# from django.test import TestCase
-# from rest_framework import serializers
+from django.test import TestCase
+from rest_framework import serializers
-# class ExampleModel(models.Model):
-# email = models.EmailField(max_length=100)
-# password = models.CharField(max_length=100)
+class WriteOnlyFieldTests(TestCase):
+ def setUp(self):
+ class ExampleSerializer(serializers.Serializer):
+ email = serializers.EmailField()
+ password = serializers.CharField(write_only=True)
+ def create(self, attrs):
+ return attrs
-# class WriteOnlyFieldTests(TestCase):
-# def test_write_only_fields(self):
-# class ExampleSerializer(serializers.Serializer):
-# email = serializers.EmailField()
-# password = serializers.CharField(write_only=True)
+ self.Serializer = ExampleSerializer
-# data = {
-# 'email': 'foo@example.com',
-# 'password': '123'
-# }
-# serializer = ExampleSerializer(data=data)
-# self.assertTrue(serializer.is_valid())
-# self.assertEquals(serializer.validated_data, data)
-# self.assertEquals(serializer.data, {'email': 'foo@example.com'})
+ def write_only_fields_are_present_on_input(self):
+ data = {
+ 'email': 'foo@example.com',
+ 'password': '123'
+ }
+ serializer = self.Serializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.validated_data, data)
-# def test_write_only_fields_meta(self):
-# class ExampleSerializer(serializers.ModelSerializer):
-# class Meta:
-# model = ExampleModel
-# fields = ('email', 'password')
-# write_only_fields = ('password',)
-
-# data = {
-# 'email': 'foo@example.com',
-# 'password': '123'
-# }
-# serializer = ExampleSerializer(data=data)
-# self.assertTrue(serializer.is_valid())
-# self.assertTrue(isinstance(serializer.object, ExampleModel))
-# self.assertEquals(serializer.validated_data, data)
-# self.assertEquals(serializer.data, {'email': 'foo@example.com'})
+ def write_only_fields_are_not_present_on_output(self):
+ instance = {
+ 'email': 'foo@example.com',
+ 'password': '123'
+ }
+ serializer = self.Serializer(instance)
+ self.assertEquals(serializer.data, {'email': 'foo@example.com'})
--
cgit v1.2.3
From b1c07670ca65084c5fef2bbb63d1f4163763014b Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 9 Sep 2014 17:46:28 +0100
Subject: Fleshing out serializer fields
---
rest_framework/fields.py | 591 +++++++++++++++++++++++-------
rest_framework/serializers.py | 380 +++++++++----------
rest_framework/utils/humanize_datetime.py | 47 +++
rest_framework/utils/modelinfo.py | 97 +++++
rest_framework/utils/representation.py | 72 ++++
tests/test_model_field_mappings.py | 160 ++++++++
tests/test_modelinfo.py | 31 ++
tests/test_relations.py | 12 +-
tests/test_serializer_empty.py | 2 +-
tests/test_serializers.py | 31 --
10 files changed, 1070 insertions(+), 353 deletions(-)
create mode 100644 rest_framework/utils/humanize_datetime.py
create mode 100644 rest_framework/utils/modelinfo.py
create mode 100644 rest_framework/utils/representation.py
create mode 100644 tests/test_model_field_mappings.py
create mode 100644 tests/test_modelinfo.py
delete mode 100644 tests/test_serializers.py
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 250c0579..043a44ed 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1,8 +1,18 @@
+from django.conf import settings
from django.core import validators
from django.core.exceptions import ValidationError
+from django.utils import timezone
+from django.utils.dateparse import parse_date, parse_datetime, parse_time
from django.utils.encoding import is_protected_type
-from rest_framework.utils import html
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import ISO_8601
+from rest_framework.compat import smart_text
+from rest_framework.settings import api_settings
+from rest_framework.utils import html, representation, humanize_datetime
+import datetime
+import decimal
import inspect
+import warnings
class empty:
@@ -71,22 +81,22 @@ class SkipField(Exception):
pass
+NOT_READ_ONLY_WRITE_ONLY = 'May not set both `read_only` and `write_only`'
+NOT_READ_ONLY_REQUIRED = 'May not set both `read_only` and `required`'
+NOT_READ_ONLY_DEFAULT = 'May not set both `read_only` and `default`'
+NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`'
+MISSING_ERROR_MESSAGE = (
+ 'ValidationError raised by `{class_name}`, but error key `{key}` does '
+ 'not exist in the `error_messages` dictionary.'
+)
+
+
class Field(object):
_creation_counter = 0
- MESSAGES = {
- 'required': 'This field is required.'
+ default_error_messages = {
+ 'required': _('This field is required.')
}
-
- _NOT_READ_ONLY_WRITE_ONLY = 'May not set both `read_only` and `write_only`'
- _NOT_READ_ONLY_REQUIRED = 'May not set both `read_only` and `required`'
- _NOT_READ_ONLY_DEFAULT = 'May not set both `read_only` and `default`'
- _NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`'
- _MISSING_ERROR_MESSAGE = (
- 'ValidationError raised by `{class_name}`, but error key `{key}` does '
- 'not exist in the `MESSAGES` dictionary.'
- )
-
default_validators = []
def __init__(self, read_only=False, write_only=False,
@@ -100,10 +110,10 @@ class Field(object):
required = default is empty and not read_only
# Some combinations of keyword arguments do not make sense.
- assert not (read_only and write_only), self._NOT_READ_ONLY_WRITE_ONLY
- assert not (read_only and required), self._NOT_READ_ONLY_REQUIRED
- assert not (read_only and default is not empty), self._NOT_READ_ONLY_DEFAULT
- assert not (required and default is not empty), self._NOT_REQUIRED_DEFAULT
+ assert not (read_only and write_only), NOT_READ_ONLY_WRITE_ONLY
+ assert not (read_only and required), NOT_READ_ONLY_REQUIRED
+ assert not (read_only and default is not empty), NOT_READ_ONLY_DEFAULT
+ assert not (required and default is not empty), NOT_REQUIRED_DEFAULT
self.read_only = read_only
self.write_only = write_only
@@ -113,7 +123,14 @@ class Field(object):
self.initial = initial
self.label = label
self.style = {} if style is None else style
- self.validators = self.default_validators + validators
+ self.validators = validators or self.default_validators[:]
+
+ # Collect default error message from self and parent classes
+ messages = {}
+ for cls in reversed(self.__class__.__mro__):
+ messages.update(getattr(cls, 'default_error_messages', {}))
+ messages.update(error_messages or {})
+ self.error_messages = messages
def bind(self, field_name, parent, root):
"""
@@ -186,12 +203,14 @@ class Field(object):
self.fail('required')
return self.get_default()
- self.run_validators(data)
- return self.to_native(data)
+ value = self.to_native(data)
+ self.run_validators(value)
+ return value
def run_validators(self, value):
if value in validators.EMPTY_VALUES:
return
+
errors = []
for validator in self.validators:
try:
@@ -218,33 +237,32 @@ class Field(object):
A helper method that simply raises a validation error.
"""
try:
- raise ValidationError(self.MESSAGES[key].format(**kwargs))
+ msg = self.error_messages[key]
except KeyError:
class_name = self.__class__.__name__
- msg = self._MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)
+ msg = MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)
raise AssertionError(msg)
+ raise ValidationError(msg.format(**kwargs))
def __new__(cls, *args, **kwargs):
+ """
+ When a field is instantiated, we store the arguments that were used,
+ so that we can present a helpful representation of the object.
+ """
instance = super(Field, cls).__new__(cls)
instance._args = args
instance._kwargs = kwargs
return instance
def __repr__(self):
- arg_string = ', '.join([repr(val) for val in self._args])
- kwarg_string = ', '.join([
- '%s=%s' % (key, repr(val)) for key, val in self._kwargs.items()
- ])
- if arg_string and kwarg_string:
- arg_string += ', '
- class_name = self.__class__.__name__
- return "%s(%s%s)" % (class_name, arg_string, kwarg_string)
+ return representation.field_repr(self)
+# Boolean types...
+
class BooleanField(Field):
- MESSAGES = {
- 'required': 'This field is required.',
- 'invalid_value': '`{input}` is not a valid boolean.'
+ default_error_messages = {
+ 'invalid': _('`{input}` is not a valid boolean.')
}
TRUE_VALUES = {'t', 'T', 'true', 'True', 'TRUE', '1', 1, True}
FALSE_VALUES = {'f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False}
@@ -261,13 +279,23 @@ class BooleanField(Field):
return True
elif data in self.FALSE_VALUES:
return False
- self.fail('invalid_value', input=data)
+ self.fail('invalid', input=data)
+
+ def to_primative(self, value):
+ if value is None:
+ return None
+ if value in self.TRUE_VALUES:
+ return True
+ elif value in self.FALSE_VALUES:
+ return False
+ return bool(value)
+# String types...
+
class CharField(Field):
- MESSAGES = {
- 'required': 'This field is required.',
- 'blank': 'This field may not be blank.'
+ default_error_messages = {
+ 'blank': _('This field may not be blank.')
}
def __init__(self, **kwargs):
@@ -281,19 +309,364 @@ class CharField(Field):
self.fail('blank')
return str(data)
+ def to_primative(self, value):
+ if value is None:
+ return None
+ return str(value)
-class ChoiceField(Field):
- MESSAGES = {
- 'required': 'This field is required.',
- 'invalid_choice': '`{input}` is not a valid choice.'
+
+class EmailField(CharField):
+ default_error_messages = {
+ 'invalid': _('Enter a valid email address.')
+ }
+ default_validators = [validators.validate_email]
+
+ def to_native(self, data):
+ ret = super(EmailField, self).to_native(data)
+ if ret is None:
+ return None
+ return ret.strip()
+
+ def to_primative(self, value):
+ ret = super(EmailField, self).to_primative(value)
+ if ret is None:
+ return None
+ return ret.strip()
+
+
+class RegexField(CharField):
+ def __init__(self, regex, **kwargs):
+ kwargs['validators'] = (
+ [validators.RegexValidator(regex)] +
+ kwargs.get('validators', [])
+ )
+ super(RegexField, self).__init__(**kwargs)
+
+
+class SlugField(CharField):
+ default_error_messages = {
+ 'invalid': _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.")
+ }
+ default_validators = [validators.validate_slug]
+
+
+class URLField(CharField):
+ default_error_messages = {
+ 'invalid': _("Enter a valid URL.")
+ }
+ default_validators = [validators.URLValidator()]
+
+
+# Number types...
+
+class IntegerField(Field):
+ default_error_messages = {
+ 'invalid': _('A valid integer is required.')
+ }
+
+ def __init__(self, **kwargs):
+ max_value = kwargs.pop('max_value', None)
+ min_value = kwargs.pop('min_value', None)
+ super(IntegerField, self).__init__(**kwargs)
+ if max_value is not None:
+ self.validators.append(validators.MaxValueValidator(max_value))
+ if min_value is not None:
+ self.validators.append(validators.MinValueValidator(min_value))
+ print self.__class__.__name__, self.validators
+
+ def to_native(self, data):
+ try:
+ data = int(str(data))
+ except (ValueError, TypeError):
+ self.fail('invalid')
+ return data
+
+ def to_primative(self, value):
+ if value is None:
+ return None
+ return int(value)
+
+
+class FloatField(Field):
+ default_error_messages = {
+ 'invalid': _("'%s' value must be a float."),
}
- coerce_to_type = str
def __init__(self, **kwargs):
- choices = kwargs.pop('choices')
+ max_value = kwargs.pop('max_value', None)
+ min_value = kwargs.pop('min_value', None)
+ super(FloatField, self).__init__(**kwargs)
+ if max_value is not None:
+ self.validators.append(validators.MaxValueValidator(max_value))
+ if min_value is not None:
+ self.validators.append(validators.MinValueValidator(min_value))
+
+ def to_primative(self, value):
+ if value is None:
+ return None
+ try:
+ return float(value)
+ except (TypeError, ValueError):
+ self.fail('invalid', value=value)
+
+ def to_native(self, value):
+ if value is None:
+ return None
+ return float(value)
+
+
+class DecimalField(Field):
+ default_error_messages = {
+ 'invalid': _('Enter a number.'),
+ 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
+ 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
+ 'max_digits': _('Ensure that there are no more than {max_digits} digits in total.'),
+ 'max_decimal_places': _('Ensure that there are no more than {max_decimal_places} decimal places.'),
+ 'max_whole_digits': _('Ensure that there are no more than {max_whole_digits} digits before the decimal point.')
+ }
+
+ def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, **kwargs):
+ self.max_value, self.min_value = max_value, min_value
+ self.max_digits, self.max_decimal_places = max_digits, decimal_places
+ super(DecimalField, self).__init__(**kwargs)
+ if max_value is not None:
+ self.validators.append(validators.MaxValueValidator(max_value))
+ if min_value is not None:
+ self.validators.append(validators.MinValueValidator(min_value))
+
+ def from_native(self, value):
+ """
+ Validates that the input is a decimal number. Returns a Decimal
+ instance. Returns None for empty values. Ensures that there are no more
+ than max_digits in the number, and no more than decimal_places digits
+ after the decimal point.
+ """
+ if value in validators.EMPTY_VALUES:
+ return None
+
+ value = smart_text(value).strip()
+ try:
+ value = decimal.Decimal(value)
+ except decimal.DecimalException:
+ self.fail('invalid')
+
+ # Check for NaN. It is the only value that isn't equal to itself,
+ # so we can use this to identify NaN values.
+ if value != value:
+ self.fail('invalid')
+
+ # Check for infinity and negative infinity.
+ if value in (decimal.Decimal('Inf'), decimal.Decimal('-Inf')):
+ self.fail('invalid')
+
+ sign, digittuple, exponent = value.as_tuple()
+ decimals = abs(exponent)
+ # digittuple doesn't include any leading zeros.
+ digits = len(digittuple)
+ if decimals > digits:
+ # We have leading zeros up to or past the decimal point. Count
+ # everything past the decimal point as a digit. We do not count
+ # 0 before the decimal point as a digit since that would mean
+ # we would not allow max_digits = decimal_places.
+ digits = decimals
+ whole_digits = digits - decimals
+
+ if self.max_digits is not None and digits > self.max_digits:
+ self.fail('max_digits', max_digits=self.max_digits)
+ if self.decimal_places is not None and decimals > self.decimal_places:
+ self.fail('max_decimal_places', max_decimal_places=self.max_decimal_places)
+ if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places):
+ self.fail('max_whole_digits', max_while_digits=self.max_digits - self.decimal_places)
+
+ return value
+
+
+# Date & time fields...
+
+class DateField(Field):
+ default_error_messages = {
+ 'invalid': _("Date has wrong format. Use one of these formats instead: %s"),
+ }
+ input_formats = api_settings.DATE_INPUT_FORMATS
+ format = api_settings.DATE_FORMAT
+
+ def __init__(self, input_formats=None, format=None, *args, **kwargs):
+ self.input_formats = input_formats if input_formats is not None else self.input_formats
+ self.format = format if format is not None else self.format
+ super(DateField, self).__init__(*args, **kwargs)
+
+ def from_native(self, value):
+ if value in validators.EMPTY_VALUES:
+ return None
+
+ if isinstance(value, datetime.datetime):
+ if timezone and settings.USE_TZ and timezone.is_aware(value):
+ # Convert aware datetimes to the default time zone
+ # before casting them to dates (#17742).
+ default_timezone = timezone.get_default_timezone()
+ value = timezone.make_naive(value, default_timezone)
+ return value.date()
+ if isinstance(value, datetime.date):
+ return value
+
+ for format in self.input_formats:
+ if format.lower() == ISO_8601:
+ try:
+ parsed = parse_date(value)
+ except (ValueError, TypeError):
+ pass
+ else:
+ if parsed is not None:
+ return parsed
+ else:
+ try:
+ parsed = datetime.datetime.strptime(value, format)
+ except (ValueError, TypeError):
+ pass
+ else:
+ return parsed.date()
+
+ humanized_format = humanize_datetime.date_formats(self.input_formats)
+ msg = self.error_messages['invalid'] % humanized_format
+ raise ValidationError(msg)
+
+ def to_primative(self, value):
+ if value is None or self.format is None:
+ return value
+
+ if isinstance(value, datetime.datetime):
+ value = value.date()
+
+ if self.format.lower() == ISO_8601:
+ return value.isoformat()
+ return value.strftime(self.format)
+
+
+class DateTimeField(Field):
+ default_error_messages = {
+ 'invalid': _("Datetime has wrong format. Use one of these formats instead: %s"),
+ }
+ input_formats = api_settings.DATETIME_INPUT_FORMATS
+ format = api_settings.DATETIME_FORMAT
+
+ def __init__(self, input_formats=None, format=None, *args, **kwargs):
+ self.input_formats = input_formats if input_formats is not None else self.input_formats
+ self.format = format if format is not None else self.format
+ super(DateTimeField, self).__init__(*args, **kwargs)
+
+ def from_native(self, value):
+ if value in validators.EMPTY_VALUES:
+ return None
+
+ if isinstance(value, datetime.datetime):
+ return value
+ if isinstance(value, datetime.date):
+ value = datetime.datetime(value.year, value.month, value.day)
+ if settings.USE_TZ:
+ # For backwards compatibility, interpret naive datetimes in
+ # local time. This won't work during DST change, but we can't
+ # do much about it, so we let the exceptions percolate up the
+ # call stack.
+ warnings.warn("DateTimeField received a naive datetime (%s)"
+ " while time zone support is active." % value,
+ RuntimeWarning)
+ default_timezone = timezone.get_default_timezone()
+ value = timezone.make_aware(value, default_timezone)
+ return value
+
+ for format in self.input_formats:
+ if format.lower() == ISO_8601:
+ try:
+ parsed = parse_datetime(value)
+ except (ValueError, TypeError):
+ pass
+ else:
+ if parsed is not None:
+ return parsed
+ else:
+ try:
+ parsed = datetime.datetime.strptime(value, format)
+ except (ValueError, TypeError):
+ pass
+ else:
+ return parsed
+
+ humanized_format = humanize_datetime.datetime_formats(self.input_formats)
+ msg = self.error_messages['invalid'] % humanized_format
+ raise ValidationError(msg)
+
+ def to_primative(self, value):
+ if value is None or self.format is None:
+ return value
+
+ if self.format.lower() == ISO_8601:
+ ret = value.isoformat()
+ if ret.endswith('+00:00'):
+ ret = ret[:-6] + 'Z'
+ return ret
+ return value.strftime(self.format)
- assert choices, '`choices` argument is required and may not be empty'
+class TimeField(Field):
+ default_error_messages = {
+ 'invalid': _("Time has wrong format. Use one of these formats instead: %s"),
+ }
+ input_formats = api_settings.TIME_INPUT_FORMATS
+ format = api_settings.TIME_FORMAT
+
+ def __init__(self, input_formats=None, format=None, *args, **kwargs):
+ self.input_formats = input_formats if input_formats is not None else self.input_formats
+ self.format = format if format is not None else self.format
+ super(TimeField, self).__init__(*args, **kwargs)
+
+ def from_native(self, value):
+ if value in validators.EMPTY_VALUES:
+ return None
+
+ if isinstance(value, datetime.time):
+ return value
+
+ for format in self.input_formats:
+ if format.lower() == ISO_8601:
+ try:
+ parsed = parse_time(value)
+ except (ValueError, TypeError):
+ pass
+ else:
+ if parsed is not None:
+ return parsed
+ else:
+ try:
+ parsed = datetime.datetime.strptime(value, format)
+ except (ValueError, TypeError):
+ pass
+ else:
+ return parsed.time()
+
+ humanized_format = humanize_datetime.time_formats(self.input_formats)
+ msg = self.error_messages['invalid'] % humanized_format
+ raise ValidationError(msg)
+
+ def to_primative(self, value):
+ if value is None or self.format is None:
+ return value
+
+ if isinstance(value, datetime.datetime):
+ value = value.time()
+
+ if self.format.lower() == ISO_8601:
+ return value.isoformat()
+ return value.strftime(self.format)
+
+
+# Choice types...
+
+class ChoiceField(Field):
+ default_error_messages = {
+ 'invalid_choice': _('`{input}` is not a valid choice.')
+ }
+
+ def __init__(self, choices, **kwargs):
# Allow either single or paired choices style:
# choices = [1, 2, 3]
# choices = [(1, 'First'), (2, 'Second'), (3, 'Third')]
@@ -321,12 +694,14 @@ class ChoiceField(Field):
except KeyError:
self.fail('invalid_choice', input=data)
+ def to_primative(self, value):
+ return value
+
class MultipleChoiceField(ChoiceField):
- MESSAGES = {
- 'required': 'This field is required.',
- 'invalid_choice': '`{input}` is not a valid choice.',
- 'not_a_list': 'Expected a list of items but got type `{input_type}`'
+ default_error_messages = {
+ 'invalid_choice': _('`{input}` is not a valid choice.'),
+ 'not_a_list': _('Expected a list of items but got type `{input_type}`')
}
def to_native(self, data):
@@ -337,72 +712,42 @@ class MultipleChoiceField(ChoiceField):
for item in data
])
-
-class IntegerField(Field):
- MESSAGES = {
- 'required': 'This field is required.',
- 'invalid_integer': 'A valid integer is required.'
- }
-
- def __init__(self, **kwargs):
- max_value = kwargs.pop('max_value', None)
- min_value = kwargs.pop('min_value', None)
- super(IntegerField, self).__init__(**kwargs)
- if max_value is not None:
- self.validators.append(validators.MaxValueValidator(max_value))
- if min_value is not None:
- self.validators.append(validators.MinValueValidator(min_value))
-
- def to_native(self, data):
- try:
- data = int(str(data))
- except (ValueError, TypeError):
- self.fail('invalid_integer')
- return data
-
def to_primative(self, value):
- if value is None:
- return None
- return int(value)
+ return value
-class EmailField(CharField):
+# File types...
+
+class FileField(Field):
pass # TODO
-class URLField(CharField):
+class ImageField(Field):
pass # TODO
-class RegexField(CharField):
- def __init__(self, **kwargs):
- self.regex = kwargs.pop('regex')
- super(CharField, self).__init__(**kwargs)
-
+# Advanced field types...
-class DateField(CharField):
- def __init__(self, **kwargs):
- self.input_formats = kwargs.pop('input_formats', None)
- super(DateField, self).__init__(**kwargs)
+class ReadOnlyField(Field):
+ """
+ A read-only field that simply returns the field value.
+ If the field is a method with no parameters, the method will be called
+ and it's return value used as the representation.
-class TimeField(CharField):
- def __init__(self, **kwargs):
- self.input_formats = kwargs.pop('input_formats', None)
- super(TimeField, self).__init__(**kwargs)
+ For example, the following would call `get_expiry_date()` on the object:
+ class ExampleSerializer(self):
+ expiry_date = ReadOnlyField(source='get_expiry_date')
+ """
-class DateTimeField(CharField):
def __init__(self, **kwargs):
- self.input_formats = kwargs.pop('input_formats', None)
- super(DateTimeField, self).__init__(**kwargs)
-
-
-class FileField(Field):
- pass # TODO
+ kwargs['read_only'] = True
+ super(ReadOnlyField, self).__init__(**kwargs)
+ def to_native(self, data):
+ raise NotImplemented('.to_native() not supported.')
-class ReadOnlyField(Field):
def to_primative(self, value):
if is_simple_callable(value):
return value()
@@ -410,11 +755,28 @@ class ReadOnlyField(Field):
class MethodField(Field):
+ """
+ A read-only field that get its representation from calling a method on the
+ parent serializer class. The method called will be of the form
+ "get_{field_name}", and should take a single argument, which is the
+ object being serialized.
+
+ For example:
+
+ class ExampleSerializer(self):
+ extra_info = MethodField()
+
+ def get_extra_info(self, obj):
+ return ... # Calculate some data to return.
+ """
def __init__(self, **kwargs):
kwargs['source'] = '*'
kwargs['read_only'] = True
super(MethodField, self).__init__(**kwargs)
+ def to_native(self, data):
+ raise NotImplemented('.to_native() not supported.')
+
def to_primative(self, value):
attr = 'get_{field_name}'.format(field_name=self.field_name)
method = getattr(self.parent, attr)
@@ -424,35 +786,14 @@ class MethodField(Field):
class ModelField(Field):
"""
A generic field that can be used against an arbitrary model field.
- """
- def __init__(self, *args, **kwargs):
- try:
- self.model_field = kwargs.pop('model_field')
- except KeyError:
- raise ValueError("ModelField requires 'model_field' kwarg")
-
- self.min_length = kwargs.pop('min_length',
- getattr(self.model_field, 'min_length', None))
- self.max_length = kwargs.pop('max_length',
- getattr(self.model_field, 'max_length', None))
- self.min_value = kwargs.pop('min_value',
- getattr(self.model_field, 'min_value', None))
- self.max_value = kwargs.pop('max_value',
- getattr(self.model_field, 'max_value', None))
-
- super(ModelField, self).__init__(*args, **kwargs)
-
- if self.min_length is not None:
- self.validators.append(validators.MinLengthValidator(self.min_length))
- if self.max_length is not None:
- self.validators.append(validators.MaxLengthValidator(self.max_length))
- if self.min_value is not None:
- self.validators.append(validators.MinValueValidator(self.min_value))
- if self.max_value is not None:
- self.validators.append(validators.MaxValueValidator(self.max_value))
- def get_attribute(self, instance):
- return get_attribute(instance, self.source_attrs[:-1])
+ This is used by `ModelSerializer` when dealing with custom model fields,
+ that do not have a serializer field to be mapped to.
+ """
+ def __init__(self, model_field, **kwargs):
+ self.model_field = model_field
+ kwargs['source'] = '*'
+ super(ModelField, self).__init__(**kwargs)
def to_native(self, data):
rel = getattr(self.model_field, 'rel', None)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 93226d32..8ca28387 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -10,15 +10,15 @@ python primitives.
2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
+from django.core import validators
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import six
from collections import namedtuple, OrderedDict
from rest_framework.fields import empty, set_value, Field, SkipField
from rest_framework.settings import api_settings
-from rest_framework.utils import html
+from rest_framework.utils import html, modelinfo, representation
import copy
-import inspect
# Note: We do the following so that users of the framework can use this style:
#
@@ -146,12 +146,10 @@ class SerializerMetaclass(type):
class Serializer(BaseSerializer):
def __new__(cls, *args, **kwargs):
- many = kwargs.pop('many', False)
- if many:
- class DynamicListSerializer(ListSerializer):
- child = cls()
- return DynamicListSerializer(*args, **kwargs)
- return super(Serializer, cls).__new__(cls)
+ if kwargs.pop('many', False):
+ kwargs['child'] = cls()
+ return ListSerializer(*args, **kwargs)
+ return super(Serializer, cls).__new__(cls, *args, **kwargs)
def __init__(self, *args, **kwargs):
self.context = kwargs.pop('context', {})
@@ -248,6 +246,9 @@ class Serializer(BaseSerializer):
error = errors.get(field.field_name)
yield FieldResult(field, value, error)
+ def __repr__(self):
+ return representation.serializer_repr(self, indent=1)
+
class ListSerializer(BaseSerializer):
child = None
@@ -299,26 +300,8 @@ class ListSerializer(BaseSerializer):
self.instance = self.create(self.validated_data)
return self.instance
-
-def _resolve_model(obj):
- """
- Resolve supplied `obj` to a Django model class.
-
- `obj` must be a Django model class itself, or a string
- representation of one. Useful in situtations like GH #1225 where
- Django may not have resolved a string-based reference to a model in
- another model's foreign key definition.
-
- String representations should have the format:
- 'appname.ModelName'
- """
- if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
- app_name, model_name = obj.split('.')
- return models.get_model(app_name, model_name)
- elif inspect.isclass(obj) and issubclass(obj, models.Model):
- return obj
- else:
- raise ValueError("{0} is not a Django model".format(obj))
+ def __repr__(self):
+ return representation.list_repr(self, indent=1)
class ModelSerializerOptions(object):
@@ -334,24 +317,25 @@ class ModelSerializerOptions(object):
class ModelSerializer(Serializer):
field_mapping = {
models.AutoField: IntegerField,
- # models.FloatField: FloatField,
+ models.BigIntegerField: IntegerField,
+ models.BooleanField: BooleanField,
+ models.CharField: CharField,
+ models.CommaSeparatedIntegerField: CharField,
+ models.DateField: DateField,
+ models.DateTimeField: DateTimeField,
+ models.DecimalField: DecimalField,
+ models.EmailField: EmailField,
+ models.FileField: FileField,
+ models.FloatField: FloatField,
models.IntegerField: IntegerField,
+ models.NullBooleanField: BooleanField,
models.PositiveIntegerField: IntegerField,
- models.SmallIntegerField: IntegerField,
models.PositiveSmallIntegerField: IntegerField,
- models.DateTimeField: DateTimeField,
- models.DateField: DateField,
+ models.SlugField: SlugField,
+ models.SmallIntegerField: IntegerField,
+ models.TextField: CharField,
models.TimeField: TimeField,
- # models.DecimalField: DecimalField,
- models.EmailField: EmailField,
- models.CharField: CharField,
models.URLField: URLField,
- # models.SlugField: SlugField,
- models.TextField: CharField,
- models.CommaSeparatedIntegerField: CharField,
- models.BooleanField: BooleanField,
- models.NullBooleanField: BooleanField,
- models.FileField: FileField,
# models.ImageField: ImageField,
}
@@ -392,85 +376,31 @@ class ModelSerializer(Serializer):
"""
Return all the fields that should be serialized for the model.
"""
- cls = self.opts.model
- opts = cls._meta.concrete_model._meta
+ info = modelinfo.get_field_info(self.opts.model)
ret = OrderedDict()
- nested = bool(self.opts.depth)
- # Deal with adding the primary key field
- pk_field = opts.pk
- while pk_field.rel and pk_field.rel.parent_link:
- # If model is a child via multitable inheritance, use parent's pk
- pk_field = pk_field.rel.to._meta.pk
-
- serializer_pk_field = self.get_pk_field(pk_field)
+ serializer_pk_field = self.get_pk_field(info.pk)
if serializer_pk_field:
- ret[pk_field.name] = serializer_pk_field
-
- # Deal with forward relationships
- forward_rels = [field for field in opts.fields if field.serialize]
- forward_rels += [field for field in opts.many_to_many if field.serialize]
-
- for model_field in forward_rels:
- has_through_model = False
-
- if model_field.rel:
- to_many = isinstance(model_field,
- models.fields.related.ManyToManyField)
- related_model = _resolve_model(model_field.rel.to)
-
- if to_many and not model_field.rel.through._meta.auto_created:
- has_through_model = True
+ ret[info.pk.name] = serializer_pk_field
- if model_field.rel and nested:
- field = self.get_nested_field(model_field, related_model, to_many)
- elif model_field.rel:
- field = self.get_related_field(model_field, related_model, to_many)
- else:
- field = self.get_field(model_field)
-
- if field:
- if has_through_model:
- field.read_only = True
+ # Regular fields
+ for field_name, field in info.fields.items():
+ ret[field_name] = self.get_field(field)
- ret[model_field.name] = field
-
- # Deal with reverse relationships
- if not self.opts.fields:
- reverse_rels = []
- else:
- # Reverse relationships are only included if they are explicitly
- # present in the `fields` option on the serializer
- reverse_rels = opts.get_all_related_objects()
- reverse_rels += opts.get_all_related_many_to_many_objects()
-
- for relation in reverse_rels:
- accessor_name = relation.get_accessor_name()
- if not self.opts.fields or accessor_name not in self.opts.fields:
- continue
- related_model = relation.model
- to_many = relation.field.rel.multiple
- has_through_model = False
- is_m2m = isinstance(relation.field,
- models.fields.related.ManyToManyField)
-
- if (
- is_m2m and
- hasattr(relation.field.rel, 'through') and
- not relation.field.rel.through._meta.auto_created
- ):
- has_through_model = True
-
- if nested:
- field = self.get_nested_field(None, related_model, to_many)
+ # Forward relations
+ for field_name, relation_info in info.forward_relations.items():
+ if self.opts.depth:
+ ret[field_name] = self.get_nested_field(*relation_info)
else:
- field = self.get_related_field(None, related_model, to_many)
+ ret[field_name] = self.get_related_field(*relation_info)
- if field:
- if has_through_model:
- field.read_only = True
-
- ret[accessor_name] = field
+ # Reverse relations
+ for accessor_name, relation_info in info.reverse_relations.items():
+ if accessor_name in self.opts.fields:
+ if self.opts.depth:
+ ret[field_name] = self.get_nested_field(*relation_info)
+ else:
+ ret[field_name] = self.get_related_field(*relation_info)
return ret
@@ -480,7 +410,7 @@ class ModelSerializer(Serializer):
"""
return self.get_field(model_field)
- def get_nested_field(self, model_field, related_model, to_many):
+ def get_nested_field(self, model_field, related_model, to_many, has_through_model):
"""
Creates a default instance of a nested relational field.
@@ -491,59 +421,148 @@ class ModelSerializer(Serializer):
model = related_model
depth = self.opts.depth - 1
- return NestedModelSerializer(many=to_many)
+ kwargs = {'read_only': True}
+ if to_many:
+ kwargs['many'] = True
+ return NestedModelSerializer(**kwargs)
- def get_related_field(self, model_field, related_model, to_many):
+ def get_related_field(self, model_field, related_model, to_many, has_through_model):
"""
Creates a default instance of a flat relational field.
Note that model_field will be `None` for reverse relationships.
"""
- # TODO: filter queryset using:
- # .using(db).complex_filter(self.rel.limit_choices_to)
+ kwargs = {
+ 'queryset': related_model._default_manager,
+ }
- kwargs = {}
- # 'queryset': related_model._default_manager,
- # 'many': to_many
- # }
+ if to_many:
+ kwargs['many'] = True
+
+ if has_through_model:
+ kwargs['read_only'] = True
+ kwargs.pop('queryset', None)
if model_field:
- kwargs['required'] = not(model_field.null or model_field.blank)
+ if model_field.null or model_field.blank:
+ kwargs['required'] = False
# if model_field.help_text is not None:
# kwargs['help_text'] = model_field.help_text
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
if not model_field.editable:
kwargs['read_only'] = True
- if model_field.verbose_name is not None:
- kwargs['label'] = model_field.verbose_name
+ kwargs.pop('queryset', None)
- return IntegerField(**kwargs)
- # TODO: return PrimaryKeyRelatedField(**kwargs)
+ return PrimaryKeyRelatedField(**kwargs)
def get_field(self, model_field):
"""
Creates a default instance of a basic non-relational field.
"""
kwargs = {}
+ validator_kwarg = model_field.validators
if model_field.null or model_field.blank:
kwargs['required'] = False
+ if model_field.verbose_name is not None:
+ kwargs['label'] = model_field.verbose_name
+
if isinstance(model_field, models.AutoField) or not model_field.editable:
kwargs['read_only'] = True
+ # Read only implies that the field is not required.
+ # We have a cleaner repr on the instance if we don't set it.
+ kwargs.pop('required', None)
if model_field.has_default():
kwargs['default'] = model_field.get_default()
-
- if issubclass(model_field.__class__, models.TextField):
- kwargs['widget'] = widgets.Textarea
-
- if model_field.verbose_name is not None:
- kwargs['label'] = model_field.verbose_name
-
- if model_field.validators is not None:
- kwargs['validators'] = model_field.validators
+ # Having a default implies that the field is not required.
+ # We have a cleaner repr on the instance if we don't set it.
+ kwargs.pop('required', None)
+
+ # Ensure that max_length is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ max_length = getattr(model_field, 'max_length', None)
+ if max_length is not None:
+ kwargs['max_length'] = max_length
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MaxLengthValidator)
+ ]
+
+ # Ensure that min_length is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ min_length = getattr(model_field, 'min_length', None)
+ if min_length is not None:
+ kwargs['min_length'] = min_length
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MinLengthValidator)
+ ]
+
+ # Ensure that max_value is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ max_value = next((
+ validator.limit_value for validator in validator_kwarg
+ if isinstance(validator, validators.MaxValueValidator)
+ ), None)
+ if max_value is not None:
+ kwargs['max_value'] = max_value
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MaxValueValidator)
+ ]
+
+ # Ensure that max_value is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ min_value = next((
+ validator.limit_value for validator in validator_kwarg
+ if isinstance(validator, validators.MinValueValidator)
+ ), None)
+ if min_value is not None:
+ kwargs['min_value'] = min_value
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MinValueValidator)
+ ]
+
+ # URLField does not need to include the URLValidator argument,
+ # as it is explicitly added in.
+ if isinstance(model_field, models.URLField):
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.URLValidator)
+ ]
+
+ # EmailField does not need to include the validate_email argument,
+ # as it is explicitly added in.
+ if isinstance(model_field, models.EmailField):
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if validator is not validators.validate_email
+ ]
+
+ # SlugField do not need to include the 'validate_slug' argument,
+ if isinstance(model_field, models.SlugField):
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if validator is not validators.validate_slug
+ ]
+
+ max_digits = getattr(model_field, 'max_digits', None)
+ if max_digits is not None:
+ kwargs['max_digits'] = max_digits
+
+ decimal_places = getattr(model_field, 'decimal_places', None)
+ if decimal_places is not None:
+ kwargs['decimal_places'] = decimal_places
+
+ if validator_kwarg:
+ kwargs['validators'] = validator_kwarg
+
+ # if issubclass(model_field.__class__, models.TextField):
+ # kwargs['widget'] = widgets.Textarea
# if model_field.help_text is not None:
# kwargs['help_text'] = model_field.help_text
@@ -555,31 +574,10 @@ class ModelSerializer(Serializer):
kwargs['empty'] = None
return ChoiceField(**kwargs)
- # put this below the ChoiceField because min_value isn't a valid initializer
- if issubclass(model_field.__class__, models.PositiveIntegerField) or \
- issubclass(model_field.__class__, models.PositiveSmallIntegerField):
- kwargs['min_value'] = 0
-
if model_field.null and \
issubclass(model_field.__class__, (models.CharField, models.TextField)):
kwargs['allow_none'] = True
- # attribute_dict = {
- # models.CharField: ['max_length'],
- # models.CommaSeparatedIntegerField: ['max_length'],
- # models.DecimalField: ['max_digits', 'decimal_places'],
- # models.EmailField: ['max_length'],
- # models.FileField: ['max_length'],
- # models.ImageField: ['max_length'],
- # models.SlugField: ['max_length'],
- # models.URLField: ['max_length'],
- # }
-
- # if model_field.__class__ in attribute_dict:
- # attributes = attribute_dict[model_field.__class__]
- # for attribute in attributes:
- # kwargs.update({attribute: getattr(model_field, attribute)})
-
try:
return self.field_mapping[model_field.__class__](**kwargs)
except KeyError:
@@ -594,28 +592,21 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
super(HyperlinkedModelSerializerOptions, self).__init__(meta)
self.view_name = getattr(meta, 'view_name', None)
self.lookup_field = getattr(meta, 'lookup_field', None)
- self.url_field_name = getattr(meta, 'url_field_name', api_settings.URL_FIELD_NAME)
class HyperlinkedModelSerializer(ModelSerializer):
_options_class = HyperlinkedModelSerializerOptions
- _default_view_name = '%(model_name)s-detail'
- _hyperlink_field_class = HyperlinkedRelatedField
- _hyperlink_identify_field_class = HyperlinkedIdentityField
def get_default_fields(self):
fields = super(HyperlinkedModelSerializer, self).get_default_fields()
if self.opts.view_name is None:
- self.opts.view_name = self._get_default_view_name(self.opts.model)
+ self.opts.view_name = self.get_default_view_name(self.opts.model)
- if self.opts.url_field_name not in fields:
- url_field = self._hyperlink_identify_field_class(
- view_name=self.opts.view_name,
- lookup_field=self.opts.lookup_field
- )
+ url_field_name = api_settings.URL_FIELD_NAME
+ if url_field_name not in fields:
ret = fields.__class__()
- ret[self.opts.url_field_name] = url_field
+ ret[url_field_name] = self.get_url_field()
ret.update(fields)
fields = ret
@@ -625,39 +616,48 @@ class HyperlinkedModelSerializer(ModelSerializer):
if self.opts.fields and model_field.name in self.opts.fields:
return self.get_field(model_field)
- def get_related_field(self, model_field, related_model, to_many):
+ def get_url_field(self):
+ kwargs = {
+ 'view_name': self.get_default_view_name(self.opts.model)
+ }
+ if self.opts.lookup_field:
+ kwargs['lookup_field'] = self.opts.lookup_field
+ return HyperlinkedIdentityField(**kwargs)
+
+ def get_related_field(self, model_field, related_model, to_many, has_through_model):
"""
Creates a default instance of a flat relational field.
"""
- # TODO: filter queryset using:
- # .using(db).complex_filter(self.rel.limit_choices_to)
- # kwargs = {
- # 'queryset': related_model._default_manager,
- # 'view_name': self._get_default_view_name(related_model),
- # 'many': to_many
- # }
- kwargs = {}
+ kwargs = {
+ 'queryset': related_model._default_manager,
+ 'view_name': self.get_default_view_name(related_model),
+ }
+
+ if to_many:
+ kwargs['many'] = True
+
+ if has_through_model:
+ kwargs['read_only'] = True
+ kwargs.pop('queryset', None)
if model_field:
- kwargs['required'] = not(model_field.null or model_field.blank)
+ if model_field.null or model_field.blank:
+ kwargs['required'] = False
# if model_field.help_text is not None:
# kwargs['help_text'] = model_field.help_text
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
+ if not model_field.editable:
+ kwargs['read_only'] = True
+ kwargs.pop('queryset', None)
- return IntegerField(**kwargs)
- # if self.opts.lookup_field:
- # kwargs['lookup_field'] = self.opts.lookup_field
-
- # return self._hyperlink_field_class(**kwargs)
+ return HyperlinkedRelatedField(**kwargs)
- def _get_default_view_name(self, model):
+ def get_default_view_name(self, model):
"""
- Return the view name to use if 'view_name' is not specified in 'Meta'
+ Return the view name to use for related models.
"""
- model_meta = model._meta
- format_kwargs = {
- 'app_label': model_meta.app_label,
- 'model_name': model_meta.object_name.lower()
+ return '%(model_name)s-detail' % {
+ 'app_label': model._meta.app_label,
+ 'model_name': model._meta.object_name.lower()
}
- return self._default_view_name % format_kwargs
diff --git a/rest_framework/utils/humanize_datetime.py b/rest_framework/utils/humanize_datetime.py
new file mode 100644
index 00000000..649f2abc
--- /dev/null
+++ b/rest_framework/utils/humanize_datetime.py
@@ -0,0 +1,47 @@
+"""
+Helper functions that convert strftime formats into more readable representations.
+"""
+from rest_framework import ISO_8601
+
+
+def datetime_formats(formats):
+ format = ', '.join(formats).replace(
+ ISO_8601,
+ 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'
+ )
+ return humanize_strptime(format)
+
+
+def date_formats(formats):
+ format = ', '.join(formats).replace(ISO_8601, 'YYYY[-MM[-DD]]')
+ return humanize_strptime(format)
+
+
+def time_formats(formats):
+ format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]')
+ return humanize_strptime(format)
+
+
+def humanize_strptime(format_string):
+ # Note that we're missing some of the locale specific mappings that
+ # don't really make sense.
+ mapping = {
+ "%Y": "YYYY",
+ "%y": "YY",
+ "%m": "MM",
+ "%b": "[Jan-Dec]",
+ "%B": "[January-December]",
+ "%d": "DD",
+ "%H": "hh",
+ "%I": "hh", # Requires '%p' to differentiate from '%H'.
+ "%M": "mm",
+ "%S": "ss",
+ "%f": "uuuuuu",
+ "%a": "[Mon-Sun]",
+ "%A": "[Monday-Sunday]",
+ "%p": "[AM|PM]",
+ "%z": "[+HHMM|-HHMM]"
+ }
+ for key, val in mapping.items():
+ format_string = format_string.replace(key, val)
+ return format_string
diff --git a/rest_framework/utils/modelinfo.py b/rest_framework/utils/modelinfo.py
new file mode 100644
index 00000000..c0513886
--- /dev/null
+++ b/rest_framework/utils/modelinfo.py
@@ -0,0 +1,97 @@
+"""
+Helper functions for returning the field information that is associated
+with a model class.
+"""
+from collections import namedtuple, OrderedDict
+from django.db import models
+from django.utils import six
+import inspect
+
+FieldInfo = namedtuple('FieldResult', ['pk', 'fields', 'forward_relations', 'reverse_relations'])
+RelationInfo = namedtuple('RelationInfo', ['field', 'related', 'to_many', 'has_through_model'])
+
+
+def _resolve_model(obj):
+ """
+ Resolve supplied `obj` to a Django model class.
+
+ `obj` must be a Django model class itself, or a string
+ representation of one. Useful in situtations like GH #1225 where
+ Django may not have resolved a string-based reference to a model in
+ another model's foreign key definition.
+
+ String representations should have the format:
+ 'appname.ModelName'
+ """
+ if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
+ app_name, model_name = obj.split('.')
+ return models.get_model(app_name, model_name)
+ elif inspect.isclass(obj) and issubclass(obj, models.Model):
+ return obj
+ raise ValueError("{0} is not a Django model".format(obj))
+
+
+def get_field_info(model):
+ """
+ Given a model class, returns a `FieldInfo` instance containing metadata
+ about the various field types on the model.
+ """
+ opts = model._meta.concrete_model._meta
+
+ # Deal with the primary key.
+ pk = opts.pk
+ while pk.rel and pk.rel.parent_link:
+ # If model is a child via multitable inheritance, use parent's pk.
+ pk = pk.rel.to._meta.pk
+
+ # Deal with regular fields.
+ fields = OrderedDict()
+ for field in [field for field in opts.fields if field.serialize and not field.rel]:
+ fields[field.name] = field
+
+ # Deal with forward relationships.
+ forward_relations = OrderedDict()
+ for field in [field for field in opts.fields if field.serialize and field.rel]:
+ forward_relations[field.name] = RelationInfo(
+ field=field,
+ related=_resolve_model(field.rel.to),
+ to_many=False,
+ has_through_model=False
+ )
+
+ # Deal with forward many-to-many relationships.
+ for field in [field for field in opts.many_to_many if field.serialize]:
+ forward_relations[field.name] = RelationInfo(
+ field=field,
+ related=_resolve_model(field.rel.to),
+ to_many=True,
+ has_through_model=(
+ not field.rel.through._meta.auto_created
+ )
+ )
+
+ # Deal with reverse relationships.
+ reverse_relations = OrderedDict()
+ for relation in opts.get_all_related_objects():
+ accessor_name = relation.get_accessor_name()
+ reverse_relations[accessor_name] = RelationInfo(
+ field=None,
+ related=relation.model,
+ to_many=relation.field.rel.multiple,
+ has_through_model=False
+ )
+
+ # Deal with reverse many-to-many relationships.
+ for relation in opts.get_all_related_many_to_many_objects():
+ accessor_name = relation.get_accessor_name()
+ reverse_relations[accessor_name] = RelationInfo(
+ field=None,
+ related=relation.model,
+ to_many=True,
+ has_through_model=(
+ hasattr(relation.field.rel, 'through') and
+ not relation.field.rel.through._meta.auto_created
+ )
+ )
+
+ return FieldInfo(pk, fields, forward_relations, reverse_relations)
diff --git a/rest_framework/utils/representation.py b/rest_framework/utils/representation.py
new file mode 100644
index 00000000..1de21597
--- /dev/null
+++ b/rest_framework/utils/representation.py
@@ -0,0 +1,72 @@
+"""
+Helper functions for creating user-friendly representations
+of serializer classes and serializer fields.
+"""
+import re
+
+
+def smart_repr(value):
+ value = repr(value)
+
+ # Representations like u'help text'
+ # should simply be presented as 'help text'
+ if value.startswith("u'") and value.endswith("'"):
+ return value[1:]
+
+ # Representations like
+ #
+ # Should be presented as
+ #
+ value = re.sub(' at 0x[0-9a-f]{8,10}>', '>', value)
+
+ return value
+
+
+def field_repr(field, force_many=False):
+ kwargs = field._kwargs
+ if force_many:
+ kwargs = kwargs.copy()
+ kwargs['many'] = True
+ kwargs.pop('child', None)
+
+ arg_string = ', '.join([smart_repr(val) for val in field._args])
+ kwarg_string = ', '.join([
+ '%s=%s' % (key, smart_repr(val))
+ for key, val in sorted(kwargs.items())
+ ])
+ if arg_string and kwarg_string:
+ arg_string += ', '
+
+ if force_many:
+ class_name = force_many.__class__.__name__
+ else:
+ class_name = field.__class__.__name__
+
+ return "%s(%s%s)" % (class_name, arg_string, kwarg_string)
+
+
+def serializer_repr(serializer, indent, force_many=None):
+ ret = field_repr(serializer, force_many) + ':'
+ indent_str = ' ' * indent
+
+ if force_many:
+ fields = force_many.fields
+ else:
+ fields = serializer.fields
+
+ for field_name, field in fields.items():
+ ret += '\n' + indent_str + field_name + ' = '
+ if hasattr(field, 'fields'):
+ ret += serializer_repr(field, indent + 1)
+ elif hasattr(field, 'child'):
+ ret += list_repr(field, indent + 1)
+ else:
+ ret += field_repr(field)
+ return ret
+
+
+def list_repr(serializer, indent):
+ child = serializer.child
+ if hasattr(child, 'fields'):
+ return serializer_repr(serializer, indent, force_many=child)
+ return field_repr(serializer)
diff --git a/tests/test_model_field_mappings.py b/tests/test_model_field_mappings.py
new file mode 100644
index 00000000..dc254da4
--- /dev/null
+++ b/tests/test_model_field_mappings.py
@@ -0,0 +1,160 @@
+"""
+The `ModelSerializer` and `HyperlinkedModelSerializer` classes are essentially
+shortcuts for automatically creating serializers based on a given model class.
+
+These tests deal with ensuring that we correctly map the model fields onto
+an appropriate set of serializer fields for each case.
+"""
+from django.db import models
+from django.test import TestCase
+from rest_framework import serializers
+
+
+# Models for testing regular field mapping
+
+class RegularFieldsModel(models.Model):
+ auto_field = models.AutoField(primary_key=True)
+ big_integer_field = models.BigIntegerField()
+ boolean_field = models.BooleanField()
+ char_field = models.CharField(max_length=100)
+ comma_seperated_integer_field = models.CommaSeparatedIntegerField(max_length=100)
+ date_field = models.DateField()
+ datetime_field = models.DateTimeField()
+ decimal_field = models.DecimalField(max_digits=3, decimal_places=1)
+ email_field = models.EmailField(max_length=100)
+ float_field = models.FloatField()
+ integer_field = models.IntegerField()
+ null_boolean_field = models.NullBooleanField()
+ positive_integer_field = models.PositiveIntegerField()
+ positive_small_integer_field = models.PositiveSmallIntegerField()
+ slug_field = models.SlugField(max_length=100)
+ small_integer_field = models.SmallIntegerField()
+ text_field = models.TextField()
+ time_field = models.TimeField()
+ url_field = models.URLField(max_length=100)
+
+
+REGULAR_FIELDS_REPR = """
+TestSerializer():
+ auto_field = IntegerField(label='auto field', read_only=True)
+ big_integer_field = IntegerField(label='big integer field')
+ boolean_field = BooleanField(default=False, label='boolean field')
+ char_field = CharField(label='char field', max_length=100)
+ comma_seperated_integer_field = CharField(label='comma seperated integer field', max_length=100, validators=[])
+ date_field = DateField(label='date field')
+ datetime_field = DateTimeField(label='datetime field')
+ decimal_field = DecimalField(decimal_places=1, label='decimal field', max_digits=3)
+ email_field = EmailField(label='email field', max_length=100)
+ float_field = FloatField(label='float field')
+ integer_field = IntegerField(label='integer field')
+ null_boolean_field = BooleanField(label='null boolean field', required=False)
+ positive_integer_field = IntegerField(label='positive integer field')
+ positive_small_integer_field = IntegerField(label='positive small integer field')
+ slug_field = SlugField(label='slug field', max_length=100)
+ small_integer_field = IntegerField(label='small integer field')
+ text_field = CharField(label='text field')
+ time_field = TimeField(label='time field')
+ url_field = URLField(label='url field', max_length=100)
+""".strip()
+
+
+# Model for testing relational field mapping
+
+class ForeignKeyTarget(models.Model):
+ char_field = models.CharField(max_length=100)
+
+
+class ManyToManyTarget(models.Model):
+ char_field = models.CharField(max_length=100)
+
+
+class OneToOneTarget(models.Model):
+ char_field = models.CharField(max_length=100)
+
+
+class RelationalModel(models.Model):
+ foreign_key = models.ForeignKey(ForeignKeyTarget)
+ many_to_many = models.ManyToManyField(ManyToManyTarget)
+ one_to_one = models.OneToOneField(OneToOneTarget)
+
+
+RELATIONAL_FLAT_REPR = """
+TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ foreign_key = PrimaryKeyRelatedField(label='foreign key', queryset=)
+ one_to_one = PrimaryKeyRelatedField(label='one to one', queryset=)
+ many_to_many = PrimaryKeyRelatedField(label='many to many', many=True, queryset=)
+""".strip()
+
+
+RELATIONAL_NESTED_REPR = """
+TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ foreign_key = NestedModelSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(label='name', max_length=100)
+ one_to_one = NestedModelSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(label='name', max_length=100)
+ many_to_many = NestedModelSerializer(many=True, read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(label='name', max_length=100)
+""".strip()
+
+
+HYPERLINKED_FLAT_REPR = """
+TestSerializer():
+ url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
+ foreign_key = HyperlinkedRelatedField(label='foreign key', queryset=, view_name='foreignkeytarget-detail')
+ one_to_one = HyperlinkedRelatedField(label='one to one', queryset=, view_name='onetoonetarget-detail')
+ many_to_many = HyperlinkedRelatedField(label='many to many', many=True, queryset=, view_name='manytomanytarget-detail')
+""".strip()
+
+
+HYPERLINKED_NESTED_REPR = """
+TestSerializer():
+ url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
+ foreign_key = NestedModelSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(label='name', max_length=100)
+ one_to_one = NestedModelSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(label='name', max_length=100)
+ many_to_many = NestedModelSerializer(many=True, read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(label='name', max_length=100)
+""".strip()
+
+
+class TestSerializerMappings(TestCase):
+ def test_regular_fields(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ self.assertEqual(repr(TestSerializer()), REGULAR_FIELDS_REPR)
+
+ def test_flat_relational_fields(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+ self.assertEqual(repr(TestSerializer()), RELATIONAL_FLAT_REPR)
+
+ def test_nested_relational_fields(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+ depth = 1
+ self.assertEqual(repr(TestSerializer()), RELATIONAL_NESTED_REPR)
+
+ def test_flat_hyperlinked_fields(self):
+ class TestSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = RelationalModel
+ self.assertEqual(repr(TestSerializer()), HYPERLINKED_FLAT_REPR)
+
+ def test_nested_hyperlinked_fields(self):
+ class TestSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = RelationalModel
+ depth = 1
+ self.assertEqual(repr(TestSerializer()), HYPERLINKED_NESTED_REPR)
diff --git a/tests/test_modelinfo.py b/tests/test_modelinfo.py
new file mode 100644
index 00000000..254a33c9
--- /dev/null
+++ b/tests/test_modelinfo.py
@@ -0,0 +1,31 @@
+from django.test import TestCase
+from django.utils import six
+from rest_framework.utils.modelinfo import _resolve_model
+from tests.models import BasicModel
+
+
+class ResolveModelTests(TestCase):
+ """
+ `_resolve_model` should return a Django model class given the
+ provided argument is a Django model class itself, or a properly
+ formatted string representation of one.
+ """
+ def test_resolve_django_model(self):
+ resolved_model = _resolve_model(BasicModel)
+ self.assertEqual(resolved_model, BasicModel)
+
+ def test_resolve_string_representation(self):
+ resolved_model = _resolve_model('tests.BasicModel')
+ self.assertEqual(resolved_model, BasicModel)
+
+ def test_resolve_unicode_representation(self):
+ resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
+ self.assertEqual(resolved_model, BasicModel)
+
+ def test_resolve_non_django_model(self):
+ with self.assertRaises(ValueError):
+ _resolve_model(TestCase)
+
+ def test_resolve_improper_string_representation(self):
+ with self.assertRaises(ValueError):
+ _resolve_model('BasicModel')
diff --git a/tests/test_relations.py b/tests/test_relations.py
index a30b12e6..b1bc66b6 100644
--- a/tests/test_relations.py
+++ b/tests/test_relations.py
@@ -22,18 +22,18 @@
# https://github.com/tomchristie/django-rest-framework/issues/446
# """
# field = serializers.PrimaryKeyRelatedField(queryset=NullModel.objects.all())
-# self.assertRaises(serializers.ValidationError, field.from_native, '')
-# self.assertRaises(serializers.ValidationError, field.from_native, [])
+# self.assertRaises(serializers.ValidationError, field.to_primative, '')
+# self.assertRaises(serializers.ValidationError, field.to_primative, [])
# def test_hyperlinked_related_field_with_empty_string(self):
# field = serializers.HyperlinkedRelatedField(queryset=NullModel.objects.all(), view_name='')
-# self.assertRaises(serializers.ValidationError, field.from_native, '')
-# self.assertRaises(serializers.ValidationError, field.from_native, [])
+# self.assertRaises(serializers.ValidationError, field.to_primative, '')
+# self.assertRaises(serializers.ValidationError, field.to_primative, [])
# def test_slug_related_field_with_empty_string(self):
# field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
-# self.assertRaises(serializers.ValidationError, field.from_native, '')
-# self.assertRaises(serializers.ValidationError, field.from_native, [])
+# self.assertRaises(serializers.ValidationError, field.to_primative, '')
+# self.assertRaises(serializers.ValidationError, field.to_primative, [])
# class TestManyRelatedMixin(TestCase):
diff --git a/tests/test_serializer_empty.py b/tests/test_serializer_empty.py
index d0006ad3..4e4a7b42 100644
--- a/tests/test_serializer_empty.py
+++ b/tests/test_serializer_empty.py
@@ -6,7 +6,7 @@
# def test_empty_serializer(self):
# class FooBarSerializer(serializers.Serializer):
# foo = serializers.IntegerField()
-# bar = serializers.SerializerMethodField('get_bar')
+# bar = serializers.MethodField()
# def get_bar(self, obj):
# return 'bar'
diff --git a/tests/test_serializers.py b/tests/test_serializers.py
deleted file mode 100644
index 31c41730..00000000
--- a/tests/test_serializers.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from django.test import TestCase
-from django.utils import six
-from rest_framework.serializers import _resolve_model
-from tests.models import BasicModel
-
-
-class ResolveModelTests(TestCase):
- """
- `_resolve_model` should return a Django model class given the
- provided argument is a Django model class itself, or a properly
- formatted string representation of one.
- """
- def test_resolve_django_model(self):
- resolved_model = _resolve_model(BasicModel)
- self.assertEqual(resolved_model, BasicModel)
-
- def test_resolve_string_representation(self):
- resolved_model = _resolve_model('tests.BasicModel')
- self.assertEqual(resolved_model, BasicModel)
-
- def test_resolve_unicode_representation(self):
- resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
- self.assertEqual(resolved_model, BasicModel)
-
- def test_resolve_non_django_model(self):
- with self.assertRaises(ValueError):
- _resolve_model(TestCase)
-
- def test_resolve_improper_string_representation(self):
- with self.assertRaises(ValueError):
- _resolve_model('BasicModel')
--
cgit v1.2.3
From 234369aefdf08d7d0161d851866990754c00d31f Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 10 Sep 2014 08:53:33 +0100
Subject: Tweaks
---
rest_framework/fields.py | 6 ++++--
rest_framework/serializers.py | 4 ++--
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 043a44ed..e2bd5700 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -190,7 +190,7 @@ class Field(object):
raise SkipField()
return self.default
- def validate(self, data=empty):
+ def validate_value(self, data=empty):
"""
Validate a simple representation and return the internal value.
@@ -506,6 +506,7 @@ class DateField(Field):
default_timezone = timezone.get_default_timezone()
value = timezone.make_naive(value, default_timezone)
return value.date()
+
if isinstance(value, datetime.date):
return value
@@ -560,6 +561,7 @@ class DateTimeField(Field):
if isinstance(value, datetime.datetime):
return value
+
if isinstance(value, datetime.date):
value = datetime.datetime(value.year, value.month, value.day)
if settings.USE_TZ:
@@ -675,7 +677,7 @@ class ChoiceField(Field):
for item in choices
]
if all(pairs):
- self.choices = {key: val for key, val in choices}
+ self.choices = {key: display_value for key, display_value in choices}
else:
self.choices = {item: item for item in choices}
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 8ca28387..0727b8cd 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -205,7 +205,7 @@ class Serializer(BaseSerializer):
validate_method = getattr(self, 'validate_' + field.field_name, None)
primitive_value = field.get_value(data)
try:
- validated_value = field.validate(primitive_value)
+ validated_value = field.validate_value(primitive_value)
if validate_method is not None:
validated_value = validate_method(validated_value)
except ValidationError as exc:
@@ -327,6 +327,7 @@ class ModelSerializer(Serializer):
models.EmailField: EmailField,
models.FileField: FileField,
models.FloatField: FloatField,
+ models.ImageField: ImageField,
models.IntegerField: IntegerField,
models.NullBooleanField: BooleanField,
models.PositiveIntegerField: IntegerField,
@@ -336,7 +337,6 @@ class ModelSerializer(Serializer):
models.TextField: CharField,
models.TimeField: TimeField,
models.URLField: URLField,
- # models.ImageField: ImageField,
}
_options_class = ModelSerializerOptions
--
cgit v1.2.3
From 01c8c0cad977fc0787dbfc78bd34f4fd37e613f4 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 10 Sep 2014 13:52:16 +0100
Subject: Added help_text argument to fields
---
rest_framework/compat.py | 23 ++++++------
rest_framework/fields.py | 5 ++-
rest_framework/serializers.py | 86 +++++++++++++++++++++----------------------
3 files changed, 56 insertions(+), 58 deletions(-)
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index fa0f0bfb..70b38df9 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -39,6 +39,17 @@ except ImportError:
django_filters = None
+if django.VERSION >= (1, 6):
+ def clean_manytomany_helptext(text):
+ return text
+else:
+ # Up to version 1.5 many to many fields automatically suffix
+ # the `help_text` attribute with hardcoded text.
+ def clean_manytomany_helptext(text):
+ if text.endswith(' Hold down "Control", or "Command" on a Mac, to select more than one.'):
+ text = text[:-69]
+ return text
+
# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
# Fixes (#1712). We keep the try/except for the test suite.
guardian = None
@@ -99,18 +110,8 @@ def get_concrete_model(model_cls):
return model_cls
-# View._allowed_methods only present from 1.5 onwards
-if django.VERSION >= (1, 5):
- from django.views.generic import View
-else:
- from django.views.generic import View as DjangoView
-
- class View(DjangoView):
- def _allowed_methods(self):
- return [m.upper() for m in self.http_method_names if hasattr(self, m)]
-
-
# PATCH method is not implemented by Django
+from django.views.generic import View
if 'patch' not in View.http_method_names:
View.http_method_names = View.http_method_names + ['patch']
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index e2bd5700..e71dce1d 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -101,7 +101,8 @@ class Field(object):
def __init__(self, read_only=False, write_only=False,
required=None, default=empty, initial=None, source=None,
- label=None, style=None, error_messages=None, validators=[]):
+ label=None, help_text=None, style=None,
+ error_messages=None, validators=[]):
self._creation_counter = Field._creation_counter
Field._creation_counter += 1
@@ -122,6 +123,7 @@ class Field(object):
self.source = source
self.initial = initial
self.label = label
+ self.help_text = help_text
self.style = {} if style is None else style
self.validators = validators or self.default_validators[:]
@@ -372,7 +374,6 @@ class IntegerField(Field):
self.validators.append(validators.MaxValueValidator(max_value))
if min_value is not None:
self.validators.append(validators.MinValueValidator(min_value))
- print self.__class__.__name__, self.validators
def to_native(self, data):
try:
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 0727b8cd..459f8a8c 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -15,6 +15,7 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.utils import six
from collections import namedtuple, OrderedDict
+from rest_framework.compat import clean_manytomany_helptext
from rest_framework.fields import empty, set_value, Field, SkipField
from rest_framework.settings import api_settings
from rest_framework.utils import html, modelinfo, representation
@@ -117,8 +118,9 @@ class SerializerMetaclass(type):
"""
This metaclass sets a dictionary named `base_fields` on the class.
- Any fields included as attributes on either the class or it's superclasses
- will be include in the `base_fields` dictionary.
+ Any instances of `Field` included as attributes on either the class
+ or on any of its superclasses will be include in the
+ `base_fields` dictionary.
"""
@classmethod
@@ -379,6 +381,10 @@ class ModelSerializer(Serializer):
info = modelinfo.get_field_info(self.opts.model)
ret = OrderedDict()
+ serializer_url_field = self.get_url_field()
+ if serializer_url_field:
+ ret[api_settings.URL_FIELD_NAME] = serializer_url_field
+
serializer_pk_field = self.get_pk_field(info.pk)
if serializer_pk_field:
ret[info.pk.name] = serializer_pk_field
@@ -404,6 +410,9 @@ class ModelSerializer(Serializer):
return ret
+ def get_url_field(self):
+ return None
+
def get_pk_field(self, model_field):
"""
Returns a default instance of the pk field.
@@ -446,13 +455,14 @@ class ModelSerializer(Serializer):
if model_field:
if model_field.null or model_field.blank:
kwargs['required'] = False
- # if model_field.help_text is not None:
- # kwargs['help_text'] = model_field.help_text
- if model_field.verbose_name is not None:
+ if model_field.verbose_name:
kwargs['label'] = model_field.verbose_name
if not model_field.editable:
kwargs['read_only'] = True
kwargs.pop('queryset', None)
+ help_text = clean_manytomany_helptext(model_field.help_text)
+ if help_text:
+ kwargs['help_text'] = help_text
return PrimaryKeyRelatedField(**kwargs)
@@ -469,6 +479,9 @@ class ModelSerializer(Serializer):
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
+ if model_field.help_text:
+ kwargs['help_text'] = model_field.help_text
+
if isinstance(model_field, models.AutoField) or not model_field.editable:
kwargs['read_only'] = True
# Read only implies that the field is not required.
@@ -481,6 +494,14 @@ class ModelSerializer(Serializer):
# We have a cleaner repr on the instance if we don't set it.
kwargs.pop('required', None)
+ if model_field.flatchoices:
+ # If this model field contains choices, then use a ChoiceField,
+ # rather than the standard serializer field for this type.
+ # Note that we return this prior to setting any validation type
+ # keyword arguments, as those are not valid initializers.
+ kwargs['choices'] = model_field.flatchoices
+ return ChoiceField(**kwargs)
+
# Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator.
max_length = getattr(model_field, 'max_length', None)
@@ -561,23 +582,6 @@ class ModelSerializer(Serializer):
if validator_kwarg:
kwargs['validators'] = validator_kwarg
- # if issubclass(model_field.__class__, models.TextField):
- # kwargs['widget'] = widgets.Textarea
-
- # if model_field.help_text is not None:
- # kwargs['help_text'] = model_field.help_text
-
- # TODO: TypedChoiceField?
- if model_field.flatchoices: # This ModelField contains choices
- kwargs['choices'] = model_field.flatchoices
- if model_field.null:
- kwargs['empty'] = None
- return ChoiceField(**kwargs)
-
- if model_field.null and \
- issubclass(model_field.__class__, (models.CharField, models.TextField)):
- kwargs['allow_none'] = True
-
try:
return self.field_mapping[model_field.__class__](**kwargs)
except KeyError:
@@ -597,33 +601,24 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
class HyperlinkedModelSerializer(ModelSerializer):
_options_class = HyperlinkedModelSerializerOptions
- def get_default_fields(self):
- fields = super(HyperlinkedModelSerializer, self).get_default_fields()
-
- if self.opts.view_name is None:
- self.opts.view_name = self.get_default_view_name(self.opts.model)
-
- url_field_name = api_settings.URL_FIELD_NAME
- if url_field_name not in fields:
- ret = fields.__class__()
- ret[url_field_name] = self.get_url_field()
- ret.update(fields)
- fields = ret
-
- return fields
-
- def get_pk_field(self, model_field):
- if self.opts.fields and model_field.name in self.opts.fields:
- return self.get_field(model_field)
-
def get_url_field(self):
+ if self.opts.view_name is not None:
+ view_name = self.opts.view_name
+ else:
+ view_name = self.get_default_view_name(self.opts.model)
+
kwargs = {
- 'view_name': self.get_default_view_name(self.opts.model)
+ 'view_name': view_name
}
if self.opts.lookup_field:
kwargs['lookup_field'] = self.opts.lookup_field
+
return HyperlinkedIdentityField(**kwargs)
+ def get_pk_field(self, model_field):
+ if self.opts.fields and model_field.name in self.opts.fields:
+ return self.get_field(model_field)
+
def get_related_field(self, model_field, related_model, to_many, has_through_model):
"""
Creates a default instance of a flat relational field.
@@ -643,13 +638,14 @@ class HyperlinkedModelSerializer(ModelSerializer):
if model_field:
if model_field.null or model_field.blank:
kwargs['required'] = False
- # if model_field.help_text is not None:
- # kwargs['help_text'] = model_field.help_text
- if model_field.verbose_name is not None:
+ if model_field.verbose_name:
kwargs['label'] = model_field.verbose_name
if not model_field.editable:
kwargs['read_only'] = True
kwargs.pop('queryset', None)
+ help_text = clean_manytomany_helptext(model_field.help_text)
+ if help_text:
+ kwargs['help_text'] = help_text
return HyperlinkedRelatedField(**kwargs)
--
cgit v1.2.3
From 80ba0473473501968154c5cc5dd5922e53d96a70 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 10 Sep 2014 16:57:22 +0100
Subject: Compat fixes
---
rest_framework/compat.py | 12 +++++++++++-
rest_framework/fields.py | 14 +++++++-------
rest_framework/serializers.py | 25 +++++++++++++------------
rest_framework/utils/modelinfo.py | 9 +++++----
4 files changed, 36 insertions(+), 24 deletions(-)
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 70b38df9..7c05bed9 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -110,8 +110,18 @@ def get_concrete_model(model_cls):
return model_cls
+# View._allowed_methods only present from 1.5 onwards
+if django.VERSION >= (1, 5):
+ from django.views.generic import View
+else:
+ from django.views.generic import View as DjangoView
+
+ class View(DjangoView):
+ def _allowed_methods(self):
+ return [m.upper() for m in self.http_method_names if hasattr(self, m)]
+
+
# PATCH method is not implemented by Django
-from django.views.generic import View
if 'patch' not in View.http_method_names:
View.http_method_names = View.http_method_names + ['patch']
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index e71dce1d..3ec28908 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -266,8 +266,8 @@ class BooleanField(Field):
default_error_messages = {
'invalid': _('`{input}` is not a valid boolean.')
}
- TRUE_VALUES = {'t', 'T', 'true', 'True', 'TRUE', '1', 1, True}
- FALSE_VALUES = {'f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False}
+ TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
+ FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
def get_value(self, dictionary):
if html.is_html_input(dictionary):
@@ -678,16 +678,16 @@ class ChoiceField(Field):
for item in choices
]
if all(pairs):
- self.choices = {key: display_value for key, display_value in choices}
+ self.choices = dict([(key, display_value) for key, display_value in choices])
else:
- self.choices = {item: item for item in choices}
+ self.choices = dict([(item, item) for item in choices])
# Map the string representation of choices to the underlying value.
# Allows us to deal with eg. integer choices while supporting either
# integer or string input, but still get the correct datatype out.
- self.choice_strings_to_values = {
- str(key): key for key in self.choices.keys()
- }
+ self.choice_strings_to_values = dict([
+ (str(key), key) for key in self.choices.keys()
+ ])
super(ChoiceField, self).__init__(**kwargs)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 459f8a8c..13e57939 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -14,7 +14,8 @@ from django.core import validators
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import six
-from collections import namedtuple, OrderedDict
+from django.utils.datastructures import SortedDict
+from collections import namedtuple
from rest_framework.compat import clean_manytomany_helptext
from rest_framework.fields import empty, set_value, Field, SkipField
from rest_framework.settings import api_settings
@@ -91,10 +92,10 @@ class BaseSerializer(Field):
if self.instance is not None:
self._data = self.to_primative(self.instance)
elif self._initial_data is not None:
- self._data = {
- field_name: field.get_value(self._initial_data)
+ self._data = dict([
+ (field_name, field.get_value(self._initial_data))
for field_name, field in self.fields.items()
- }
+ ])
else:
self._data = self.get_initial()
return self._data
@@ -137,7 +138,7 @@ class SerializerMetaclass(type):
if hasattr(base, 'base_fields'):
fields = list(base.base_fields.items()) + fields
- return OrderedDict(fields)
+ return SortedDict(fields)
def __new__(cls, name, bases, attrs):
attrs['base_fields'] = cls._get_fields(bases, attrs)
@@ -180,10 +181,10 @@ class Serializer(BaseSerializer):
field.bind(field_name, self, root)
def get_initial(self):
- return {
- field.field_name: field.get_initial()
+ return dict([
+ (field.field_name, field.get_initial())
for field in self.fields.values()
- }
+ ])
def get_value(self, dictionary):
# We override the default field access in order to support
@@ -222,14 +223,14 @@ class Serializer(BaseSerializer):
try:
return self.validate(ret)
- except ValidationError, exc:
+ except ValidationError as exc:
raise ValidationError({'non_field_errors': exc.messages})
def to_primative(self, instance):
"""
Object instance -> Dict of primitive datatypes.
"""
- ret = OrderedDict()
+ ret = SortedDict()
fields = [field for field in self.fields.values() if not field.write_only]
for field in fields:
@@ -368,7 +369,7 @@ class ModelSerializer(Serializer):
# If `fields` is set on the `Meta` class,
# then use only those fields, and in that order.
if self.opts.fields:
- fields = OrderedDict([
+ fields = SortedDict([
(key, fields[key]) for key in self.opts.fields
])
@@ -379,7 +380,7 @@ class ModelSerializer(Serializer):
Return all the fields that should be serialized for the model.
"""
info = modelinfo.get_field_info(self.opts.model)
- ret = OrderedDict()
+ ret = SortedDict()
serializer_url_field = self.get_url_field()
if serializer_url_field:
diff --git a/rest_framework/utils/modelinfo.py b/rest_framework/utils/modelinfo.py
index c0513886..a7a0346c 100644
--- a/rest_framework/utils/modelinfo.py
+++ b/rest_framework/utils/modelinfo.py
@@ -2,9 +2,10 @@
Helper functions for returning the field information that is associated
with a model class.
"""
-from collections import namedtuple, OrderedDict
+from collections import namedtuple
from django.db import models
from django.utils import six
+from django.utils.datastructures import SortedDict
import inspect
FieldInfo = namedtuple('FieldResult', ['pk', 'fields', 'forward_relations', 'reverse_relations'])
@@ -45,12 +46,12 @@ def get_field_info(model):
pk = pk.rel.to._meta.pk
# Deal with regular fields.
- fields = OrderedDict()
+ fields = SortedDict()
for field in [field for field in opts.fields if field.serialize and not field.rel]:
fields[field.name] = field
# Deal with forward relationships.
- forward_relations = OrderedDict()
+ forward_relations = SortedDict()
for field in [field for field in opts.fields if field.serialize and field.rel]:
forward_relations[field.name] = RelationInfo(
field=field,
@@ -71,7 +72,7 @@ def get_field_info(model):
)
# Deal with reverse relationships.
- reverse_relations = OrderedDict()
+ reverse_relations = SortedDict()
for relation in opts.get_all_related_objects():
accessor_name = relation.get_accessor_name()
reverse_relations[accessor_name] = RelationInfo(
--
cgit v1.2.3
From 54ccf7230d0fcdabe8c2457539e314893915a34b Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 11 Sep 2014 13:43:46 +0100
Subject: Improve memory address removal for serializer representations
---
rest_framework/utils/representation.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/utils/representation.py b/rest_framework/utils/representation.py
index 1de21597..e2a37497 100644
--- a/rest_framework/utils/representation.py
+++ b/rest_framework/utils/representation.py
@@ -17,7 +17,7 @@ def smart_repr(value):
#
# Should be presented as
#
- value = re.sub(' at 0x[0-9a-f]{8,10}>', '>', value)
+ value = re.sub(' at 0x[0-9a-f]{8,32}>', '>', value)
return value
--
cgit v1.2.3
From 3318f75a7166cbac76a40d0461ca7b3e4640d3a2 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 11 Sep 2014 13:50:53 +0100
Subject: Improve memory address removal for serializer representations
---
rest_framework/utils/representation.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/utils/representation.py b/rest_framework/utils/representation.py
index e2a37497..1a4d1a62 100644
--- a/rest_framework/utils/representation.py
+++ b/rest_framework/utils/representation.py
@@ -17,7 +17,7 @@ def smart_repr(value):
#
# Should be presented as
#
- value = re.sub(' at 0x[0-9a-f]{8,32}>', '>', value)
+ value = re.sub(' at 0x[0-9a-f]{4,32}>', '>', value)
return value
--
cgit v1.2.3
From ab40780dc2f341a271c2f489659dcd48eb47c07d Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 11 Sep 2014 20:22:32 +0100
Subject: Tidy up lookup_class
---
rest_framework/serializers.py | 21 +++++++++++----------
1 file changed, 11 insertions(+), 10 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 8fe999ae..4322f213 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -317,17 +317,17 @@ class ModelSerializerOptions(object):
self.depth = getattr(meta, 'depth', 0)
-def lookup_class(mapping, obj):
+def lookup_class(mapping, instance):
"""
Takes a dictionary with classes as keys, and an object.
Traverses the object's inheritance hierarchy in method
resolution order, and returns the first matching value
- from the dictionary or None.
+ from the dictionary or raises a KeyError if nothing matches.
"""
- return next(
- (mapping[cls] for cls in inspect.getmro(obj.__class__) if cls in mapping),
- None
- )
+ for cls in inspect.getmro(instance.__class__):
+ if cls in mapping:
+ return mapping[cls]
+ raise KeyError('Class %s not found in lookup.', cls.__name__)
class ModelSerializer(Serializer):
@@ -341,6 +341,7 @@ class ModelSerializer(Serializer):
models.DateTimeField: DateTimeField,
models.DecimalField: DecimalField,
models.EmailField: EmailField,
+ models.Field: ModelField,
models.FileField: FileField,
models.FloatField: FloatField,
models.ImageField: ImageField,
@@ -484,6 +485,7 @@ class ModelSerializer(Serializer):
"""
Creates a default instance of a basic non-relational field.
"""
+ serializer_cls = lookup_class(self.field_mapping, model_field)
kwargs = {}
validator_kwarg = model_field.validators
@@ -602,11 +604,10 @@ class ModelSerializer(Serializer):
if validator_kwarg:
kwargs['validators'] = validator_kwarg
- cls = lookup_class(self.field_mapping, model_field)
- if cls is None:
- cls = ModelField
+ if issubclass(serializer_cls, ModelField):
kwargs['model_field'] = model_field
- return cls(**kwargs)
+
+ return serializer_cls(**kwargs)
class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
--
cgit v1.2.3
From bf52d04f4c370d6917599d26c84b73124d5ef366 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 11 Sep 2014 20:37:27 +0100
Subject: Nice manager representations on serializer classes
---
rest_framework/utils/representation.py | 13 +++++++++++++
tests/test_model_field_mappings.py | 12 ++++++------
2 files changed, 19 insertions(+), 6 deletions(-)
diff --git a/rest_framework/utils/representation.py b/rest_framework/utils/representation.py
index 1a4d1a62..71db1886 100644
--- a/rest_framework/utils/representation.py
+++ b/rest_framework/utils/representation.py
@@ -2,10 +2,23 @@
Helper functions for creating user-friendly representations
of serializer classes and serializer fields.
"""
+from django.db import models
import re
+def manager_repr(value):
+ model = value.model
+ opts = model._meta
+ for _, name, manager in opts.concrete_managers + opts.abstract_managers:
+ if manager == value:
+ return '%s.%s.all()' % (model._meta.object_name, name)
+ return repr(value)
+
+
def smart_repr(value):
+ if isinstance(value, models.Manager):
+ return manager_repr(value)
+
value = repr(value)
# Representations like u'help text'
diff --git a/tests/test_model_field_mappings.py b/tests/test_model_field_mappings.py
index 57c97cb7..6e9aaba6 100644
--- a/tests/test_model_field_mappings.py
+++ b/tests/test_model_field_mappings.py
@@ -81,9 +81,9 @@ class RelationalModel(models.Model):
RELATIONAL_FLAT_REPR = """
TestSerializer():
id = IntegerField(label='ID', read_only=True)
- foreign_key = PrimaryKeyRelatedField(label='foreign key', queryset=)
- one_to_one = PrimaryKeyRelatedField(label='one to one', queryset=)
- many_to_many = PrimaryKeyRelatedField(label='many to many', many=True, queryset=)
+ foreign_key = PrimaryKeyRelatedField(label='foreign key', queryset=ForeignKeyTargetModel.objects.all())
+ one_to_one = PrimaryKeyRelatedField(label='one to one', queryset=OneToOneTargetModel.objects.all())
+ many_to_many = PrimaryKeyRelatedField(label='many to many', many=True, queryset=ManyToManyTargetModel.objects.all())
""".strip()
@@ -105,9 +105,9 @@ TestSerializer():
HYPERLINKED_FLAT_REPR = """
TestSerializer():
url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
- foreign_key = HyperlinkedRelatedField(label='foreign key', queryset=, view_name='foreignkeytargetmodel-detail')
- one_to_one = HyperlinkedRelatedField(label='one to one', queryset=, view_name='onetoonetargetmodel-detail')
- many_to_many = HyperlinkedRelatedField(label='many to many', many=True, queryset=, view_name='manytomanytargetmodel-detail')
+ foreign_key = HyperlinkedRelatedField(label='foreign key', queryset=ForeignKeyTargetModel.objects.all(), view_name='foreignkeytargetmodel-detail')
+ one_to_one = HyperlinkedRelatedField(label='one to one', queryset=OneToOneTargetModel.objects.all(), view_name='onetoonetargetmodel-detail')
+ many_to_many = HyperlinkedRelatedField(label='many to many', many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail')
""".strip()
--
cgit v1.2.3
From 19b8f779de82fa4737b37fb4359145af0b07a56c Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 11 Sep 2014 20:43:44 +0100
Subject: Throttles now use Retry-After header and no longer support the custom
style
---
docs/api-guide/throttling.md | 2 ++
rest_framework/views.py | 1 -
tests/test_throttling.py | 6 +-----
3 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md
index 832304f1..16a7457b 100644
--- a/docs/api-guide/throttling.md
+++ b/docs/api-guide/throttling.md
@@ -178,6 +178,8 @@ To create a custom throttle, override `BaseThrottle` and implement `.allow_reque
Optionally you may also override the `.wait()` method. If implemented, `.wait()` should return a recommended number of seconds to wait before attempting the next request, or `None`. The `.wait()` method will only be called if `.allow_request()` has previously returned `False`.
+If the `.wait()` method is implemented and the request is throttled, then a `Retry-After` header will be included in the response.
+
## Example
The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests.
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 3b7b1c16..cd394b2d 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -62,7 +62,6 @@ def exception_handler(exc):
if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None):
- headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
headers['Retry-After'] = '%d' % exc.wait
return Response({'detail': exc.detail},
diff --git a/tests/test_throttling.py b/tests/test_throttling.py
index 7b696f07..cc36a004 100644
--- a/tests/test_throttling.py
+++ b/tests/test_throttling.py
@@ -109,7 +109,7 @@ class ThrottlingTests(TestCase):
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
"""
- Ensure the response returns an X-Throttle field with status and next attributes
+ Ensure the response returns an Retry-After field with status and next attributes
set properly.
"""
request = self.factory.get('/')
@@ -117,10 +117,8 @@ class ThrottlingTests(TestCase):
self.set_throttle_timer(view, timer)
response = view.as_view()(request)
if expect is not None:
- self.assertEqual(response['X-Throttle-Wait-Seconds'], expect)
self.assertEqual(response['Retry-After'], expect)
else:
- self.assertFalse('X-Throttle-Wait-Seconds' in response)
self.assertFalse('Retry-After' in response)
def test_seconds_fields(self):
@@ -173,13 +171,11 @@ class ThrottlingTests(TestCase):
self.assertFalse(hasattr(MockView_NonTimeThrottling.throttle_classes[0], 'called'))
response = MockView_NonTimeThrottling.as_view()(request)
- self.assertFalse('X-Throttle-Wait-Seconds' in response)
self.assertFalse('Retry-After' in response)
self.assertTrue(MockView_NonTimeThrottling.throttle_classes[0].called)
response = MockView_NonTimeThrottling.as_view()(request)
- self.assertFalse('X-Throttle-Wait-Seconds' in response)
self.assertFalse('Retry-After' in response)
--
cgit v1.2.3
From 55650a743d579e0bc1643c8812428746b0271984 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 11 Sep 2014 20:49:10 +0100
Subject: no longer tightly coupled to private queryset API
---
rest_framework/generics.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index c2c59154..408b1246 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -3,6 +3,7 @@ Generic views that provide commonly needed behaviour.
"""
from __future__ import unicode_literals
+from django.db.models.query import QuerySet
from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator, InvalidPage
from django.http import Http404
@@ -214,7 +215,9 @@ class GenericAPIView(views.APIView):
% self.__class__.__name__
)
- return self.queryset._clone()
+ if isinstance(self.queryset, QuerySet):
+ return self.queryset.all()
+ return self.queryset
def get_object(self):
"""
--
cgit v1.2.3
From a7518719917c7ad8e699119b442cfeb568ba1dde Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 11 Sep 2014 20:50:26 +0100
Subject: no longer tightly coupled to private queryset API
---
rest_framework/generics.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 408b1246..338d56a6 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -215,9 +215,11 @@ class GenericAPIView(views.APIView):
% self.__class__.__name__
)
+ queryset = self.queryset
if isinstance(self.queryset, QuerySet):
- return self.queryset.all()
- return self.queryset
+ # Ensure queryset is re-evaluated on each request.
+ queryset = queryset.all()
+ return queryset
def get_object(self):
"""
--
cgit v1.2.3
From 040bfcc09c851bb3dadd60558c78a1f7937e9fbd Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 11 Sep 2014 21:48:54 +0100
Subject: NotImplemented stubs for Field, and DecimalField improvements
---
rest_framework/fields.py | 27 +++++++++++++++++++++------
rest_framework/pagination.py | 4 ++--
rest_framework/utils/encoders.py | 2 +-
tests/test_filters.py | 10 +++++-----
tests/test_pagination.py | 4 ++--
5 files changed, 31 insertions(+), 16 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 7496a629..20b8ffbf 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -229,13 +229,13 @@ class Field(object):
"""
Transform the *incoming* primative data into a native value.
"""
- return data
+ raise NotImplementedError('to_native() must be implemented.')
def to_primative(self, value):
"""
Transform the *outgoing* native value into primative data.
"""
- return value
+ raise NotImplementedError('to_primative() must be implemented.')
def fail(self, key, **kwargs):
"""
@@ -429,9 +429,10 @@ class DecimalField(Field):
'max_whole_digits': _('Ensure that there are no more than {max_whole_digits} digits before the decimal point.')
}
- def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, **kwargs):
- self.max_value, self.min_value = max_value, min_value
- self.max_digits, self.max_decimal_places = max_digits, decimal_places
+ def __init__(self, max_digits, decimal_places, coerce_to_string=True, max_value=None, min_value=None, **kwargs):
+ self.max_digits = max_digits
+ self.decimal_places = decimal_places
+ self.coerce_to_string = coerce_to_string
super(DecimalField, self).__init__(**kwargs)
if max_value is not None:
self.validators.append(validators.MaxValueValidator(max_value))
@@ -478,12 +479,26 @@ class DecimalField(Field):
if self.max_digits is not None and digits > self.max_digits:
self.fail('max_digits', max_digits=self.max_digits)
if self.decimal_places is not None and decimals > self.decimal_places:
- self.fail('max_decimal_places', max_decimal_places=self.max_decimal_places)
+ self.fail('max_decimal_places', max_decimal_places=self.decimal_places)
if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places):
self.fail('max_whole_digits', max_while_digits=self.max_digits - self.decimal_places)
return value
+ def to_primative(self, value):
+ if not self.coerce_to_string:
+ return value
+
+ if isinstance(value, decimal.Decimal):
+ context = decimal.getcontext().copy()
+ context.prec = self.max_digits
+ quantized = value.quantize(
+ decimal.Decimal('.1') ** self.decimal_places,
+ context=context
+ )
+ return '{0:f}'.format(quantized)
+ return '%.*f' % (self.max_decimal_places, value)
+
# Date & time fields...
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index 9cf31629..d82d2d3b 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -37,7 +37,7 @@ class PreviousPageField(serializers.Field):
return replace_query_param(url, self.page_field, page)
-class DefaultObjectSerializer(serializers.Field):
+class DefaultObjectSerializer(serializers.ReadOnlyField):
"""
If no object serializer is specified, then this serializer will be applied
as the default.
@@ -79,6 +79,6 @@ class PaginationSerializer(BasePaginationSerializer):
"""
A default implementation of a pagination serializer.
"""
- count = serializers.Field(source='paginator.count')
+ count = serializers.ReadOnlyField(source='paginator.count')
next = NextPageField(source='*')
previous = PreviousPageField(source='*')
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index 6a2f6126..7992b6b1 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -43,7 +43,7 @@ class JSONEncoder(json.JSONEncoder):
elif isinstance(o, datetime.timedelta):
return str(o.total_seconds())
elif isinstance(o, decimal.Decimal):
- return str(o)
+ return float(o)
elif isinstance(o, QuerySet):
return list(o)
elif hasattr(o, 'tolist'):
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 300e47e4..01668114 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -102,7 +102,7 @@ if django_filters:
class CommonFilteringTestCase(TestCase):
def _serialize_object(self, obj):
- return {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
+ return {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date}
def setUp(self):
"""
@@ -145,7 +145,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
request = factory.get('/', {'decimal': '%s' % search_decimal})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['decimal'] == search_decimal]
+ expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
self.assertEqual(response.data, expected_data)
# Tests that the date filter works.
@@ -168,7 +168,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
request = factory.get('/', {'decimal': '%s' % search_decimal})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['decimal'] == search_decimal]
+ expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
self.assertEqual(response.data, expected_data)
@unittest.skipUnless(django_filters, 'django-filter not installed')
@@ -201,7 +201,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
request = factory.get('/', {'decimal': '%s' % search_decimal})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['decimal'] < search_decimal]
+ expected_data = [f for f in self.data if Decimal(f['decimal']) < search_decimal]
self.assertEqual(response.data, expected_data)
# Tests that the date filter set with 'gt' in the filter class works.
@@ -230,7 +230,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
expected_data = [f for f in self.data if f['date'] > search_date and
- f['decimal'] < search_decimal]
+ Decimal(f['decimal']) < search_decimal]
self.assertEqual(response.data, expected_data)
@unittest.skipUnless(django_filters, 'django-filter not installed')
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 68983ba2..a7f8e691 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -135,7 +135,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
self.objects = FilterableItem.objects
self.data = [
- {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
+ {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date}
for obj in self.objects.all()
]
@@ -381,7 +381,7 @@ class TestMaxPaginateByParam(TestCase):
# Tests for context in pagination serializers
-class CustomField(serializers.Field):
+class CustomField(serializers.ReadOnlyField):
def to_native(self, value):
if 'view' not in self.context:
raise RuntimeError("context isn't getting passed into custom field")
--
cgit v1.2.3
From 1e53eb0aa2998385e26aa0a4d8542013bc7b575b Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 11 Sep 2014 21:57:32 +0100
Subject: DecimalFields should still be quantized even without coerce_to_string
---
rest_framework/fields.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 20b8ffbf..a56ea96b 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -486,9 +486,6 @@ class DecimalField(Field):
return value
def to_primative(self, value):
- if not self.coerce_to_string:
- return value
-
if isinstance(value, decimal.Decimal):
context = decimal.getcontext().copy()
context.prec = self.max_digits
@@ -496,7 +493,12 @@ class DecimalField(Field):
decimal.Decimal('.1') ** self.decimal_places,
context=context
)
+ if not self.coerce_to_string:
+ return quantized
return '{0:f}'.format(quantized)
+
+ if not self.coerce_to_string:
+ return value
return '%.*f' % (self.max_decimal_places, value)
--
cgit v1.2.3
From adcb64ab4198f35c61d5be68956d201685ed3538 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 09:12:56 +0100
Subject: MethodField -> SerializerMethodField
---
rest_framework/fields.py | 21 +++++++++------------
rest_framework/serializers.py | 3 ++-
rest_framework/utils/modelinfo.py | 3 ++-
tests/test_serializer_empty.py | 2 +-
4 files changed, 14 insertions(+), 15 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index a56ea96b..5f198767 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -768,16 +768,13 @@ class ReadOnlyField(Field):
kwargs['read_only'] = True
super(ReadOnlyField, self).__init__(**kwargs)
- def to_native(self, data):
- raise NotImplemented('.to_native() not supported.')
-
def to_primative(self, value):
if is_simple_callable(value):
return value()
return value
-class MethodField(Field):
+class SerializerMethodField(Field):
"""
A read-only field that get its representation from calling a method on the
parent serializer class. The method called will be of the form
@@ -787,22 +784,22 @@ class MethodField(Field):
For example:
class ExampleSerializer(self):
- extra_info = MethodField()
+ extra_info = SerializerMethodField()
def get_extra_info(self, obj):
return ... # Calculate some data to return.
"""
- def __init__(self, **kwargs):
+ def __init__(self, method_attr=None, **kwargs):
+ self.method_attr = method_attr
kwargs['source'] = '*'
kwargs['read_only'] = True
- super(MethodField, self).__init__(**kwargs)
-
- def to_native(self, data):
- raise NotImplemented('.to_native() not supported.')
+ super(SerializerMethodField, self).__init__(**kwargs)
def to_primative(self, value):
- attr = 'get_{field_name}'.format(field_name=self.field_name)
- method = getattr(self.parent, attr)
+ method_attr = self.method_attr
+ if method_attr is None:
+ method_attr = 'get_{field_name}'.format(field_name=self.field_name)
+ method = getattr(self.parent, method_attr)
return method(value)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 4322f213..388fe29f 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -598,7 +598,8 @@ class ModelSerializer(Serializer):
if isinstance(model_field, models.BooleanField):
# models.BooleanField has `blank=True`, but *is* actually
# required *unless* a default is provided.
- # Also note that <1.6 `default=False`, >=1.6 `default=None`.
+ # Also note that Django<1.6 uses `default=False` for
+ # models.BooleanField, but Django>=1.6 uses `default=None`.
kwargs.pop('required', None)
if validator_kwarg:
diff --git a/rest_framework/utils/modelinfo.py b/rest_framework/utils/modelinfo.py
index a7a0346c..960fa4d0 100644
--- a/rest_framework/utils/modelinfo.py
+++ b/rest_framework/utils/modelinfo.py
@@ -1,6 +1,7 @@
"""
Helper functions for returning the field information that is associated
-with a model class.
+with a model class. This includes returning all the forward and reverse
+relationships and their associated metadata.
"""
from collections import namedtuple
from django.db import models
diff --git a/tests/test_serializer_empty.py b/tests/test_serializer_empty.py
index 4e4a7b42..805ac7d4 100644
--- a/tests/test_serializer_empty.py
+++ b/tests/test_serializer_empty.py
@@ -6,7 +6,7 @@
# def test_empty_serializer(self):
# class FooBarSerializer(serializers.Serializer):
# foo = serializers.IntegerField()
-# bar = serializers.MethodField()
+# bar = serializers.SerializerMethodField()
# def get_bar(self, obj):
# return 'bar'
--
cgit v1.2.3
From 0d354e8f92c7daaf8dac3b80f0fd64f983f21e0b Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 09:49:35 +0100
Subject: to_internal_value() and to_representation()
---
rest_framework/fields.py | 86 ++++++++++++++++++++++---------------------
rest_framework/pagination.py | 4 +-
rest_framework/relations.py | 2 +-
rest_framework/serializers.py | 28 +++++++-------
tests/test_validation.py | 2 +-
5 files changed, 62 insertions(+), 60 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 5f198767..a96f9ba8 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -195,7 +195,7 @@ class Field(object):
raise SkipField()
return self.default
- def validate_value(self, data=empty):
+ def run_validation(self, data=empty):
"""
Validate a simple representation and return the internal value.
@@ -208,7 +208,7 @@ class Field(object):
self.fail('required')
return self.get_default()
- value = self.to_native(data)
+ value = self.to_internal_value(data)
self.run_validators(value)
return value
@@ -225,17 +225,17 @@ class Field(object):
if errors:
raise ValidationError(errors)
- def to_native(self, data):
+ def to_internal_value(self, data):
"""
Transform the *incoming* primative data into a native value.
"""
- raise NotImplementedError('to_native() must be implemented.')
+ raise NotImplementedError('to_internal_value() must be implemented.')
- def to_primative(self, value):
+ def to_representation(self, value):
"""
Transform the *outgoing* native value into primative data.
"""
- raise NotImplementedError('to_primative() must be implemented.')
+ raise NotImplementedError('to_representation() must be implemented.')
def fail(self, key, **kwargs):
"""
@@ -279,14 +279,14 @@ class BooleanField(Field):
return dictionary.get(self.field_name, False)
return dictionary.get(self.field_name, empty)
- def to_native(self, data):
+ def to_internal_value(self, data):
if data in self.TRUE_VALUES:
return True
elif data in self.FALSE_VALUES:
return False
self.fail('invalid', input=data)
- def to_primative(self, value):
+ def to_representation(self, value):
if value is None:
return None
if value in self.TRUE_VALUES:
@@ -309,12 +309,14 @@ class CharField(Field):
self.min_length = kwargs.pop('min_length', None)
super(CharField, self).__init__(**kwargs)
- def to_native(self, data):
+ def to_internal_value(self, data):
if data == '' and not self.allow_blank:
self.fail('blank')
+ if data is None:
+ return None
return str(data)
- def to_primative(self, value):
+ def to_representation(self, value):
if value is None:
return None
return str(value)
@@ -326,17 +328,17 @@ class EmailField(CharField):
}
default_validators = [validators.validate_email]
- def to_native(self, data):
- ret = super(EmailField, self).to_native(data)
- if ret is None:
+ def to_internal_value(self, data):
+ if data == '' and not self.allow_blank:
+ self.fail('blank')
+ if data is None:
return None
- return ret.strip()
+ return str(data).strip()
- def to_primative(self, value):
- ret = super(EmailField, self).to_primative(value)
- if ret is None:
+ def to_representation(self, value):
+ if value is None:
return None
- return ret.strip()
+ return str(value).strip()
class RegexField(CharField):
@@ -378,14 +380,14 @@ class IntegerField(Field):
if min_value is not None:
self.validators.append(validators.MinValueValidator(min_value))
- def to_native(self, data):
+ def to_internal_value(self, data):
try:
data = int(str(data))
except (ValueError, TypeError):
self.fail('invalid')
return data
- def to_primative(self, value):
+ def to_representation(self, value):
if value is None:
return None
return int(value)
@@ -405,7 +407,12 @@ class FloatField(Field):
if min_value is not None:
self.validators.append(validators.MinValueValidator(min_value))
- def to_primative(self, value):
+ def to_internal_value(self, value):
+ if value is None:
+ return None
+ return float(value)
+
+ def to_representation(self, value):
if value is None:
return None
try:
@@ -413,11 +420,6 @@ class FloatField(Field):
except (TypeError, ValueError):
self.fail('invalid', value=value)
- def to_native(self, value):
- if value is None:
- return None
- return float(value)
-
class DecimalField(Field):
default_error_messages = {
@@ -439,7 +441,7 @@ class DecimalField(Field):
if min_value is not None:
self.validators.append(validators.MinValueValidator(min_value))
- def from_native(self, value):
+ def to_internal_value(self, value):
"""
Validates that the input is a decimal number. Returns a Decimal
instance. Returns None for empty values. Ensures that there are no more
@@ -485,7 +487,7 @@ class DecimalField(Field):
return value
- def to_primative(self, value):
+ def to_representation(self, value):
if isinstance(value, decimal.Decimal):
context = decimal.getcontext().copy()
context.prec = self.max_digits
@@ -516,7 +518,7 @@ class DateField(Field):
self.format = format if format is not None else self.format
super(DateField, self).__init__(*args, **kwargs)
- def from_native(self, value):
+ def to_internal_value(self, value):
if value in validators.EMPTY_VALUES:
return None
@@ -552,7 +554,7 @@ class DateField(Field):
msg = self.error_messages['invalid'] % humanized_format
raise ValidationError(msg)
- def to_primative(self, value):
+ def to_representation(self, value):
if value is None or self.format is None:
return value
@@ -576,7 +578,7 @@ class DateTimeField(Field):
self.format = format if format is not None else self.format
super(DateTimeField, self).__init__(*args, **kwargs)
- def from_native(self, value):
+ def to_internal_value(self, value):
if value in validators.EMPTY_VALUES:
return None
@@ -618,7 +620,7 @@ class DateTimeField(Field):
msg = self.error_messages['invalid'] % humanized_format
raise ValidationError(msg)
- def to_primative(self, value):
+ def to_representation(self, value):
if value is None or self.format is None:
return value
@@ -670,7 +672,7 @@ class TimeField(Field):
msg = self.error_messages['invalid'] % humanized_format
raise ValidationError(msg)
- def to_primative(self, value):
+ def to_representation(self, value):
if value is None or self.format is None:
return value
@@ -711,13 +713,13 @@ class ChoiceField(Field):
super(ChoiceField, self).__init__(**kwargs)
- def to_native(self, data):
+ def to_internal_value(self, data):
try:
return self.choice_strings_to_values[str(data)]
except KeyError:
self.fail('invalid_choice', input=data)
- def to_primative(self, value):
+ def to_representation(self, value):
return value
@@ -727,15 +729,15 @@ class MultipleChoiceField(ChoiceField):
'not_a_list': _('Expected a list of items but got type `{input_type}`')
}
- def to_native(self, data):
+ def to_internal_value(self, data):
if not hasattr(data, '__iter__'):
self.fail('not_a_list', input_type=type(data).__name__)
return set([
- super(MultipleChoiceField, self).to_native(item)
+ super(MultipleChoiceField, self).to_internal_value(item)
for item in data
])
- def to_primative(self, value):
+ def to_representation(self, value):
return value
@@ -768,7 +770,7 @@ class ReadOnlyField(Field):
kwargs['read_only'] = True
super(ReadOnlyField, self).__init__(**kwargs)
- def to_primative(self, value):
+ def to_representation(self, value):
if is_simple_callable(value):
return value()
return value
@@ -795,7 +797,7 @@ class SerializerMethodField(Field):
kwargs['read_only'] = True
super(SerializerMethodField, self).__init__(**kwargs)
- def to_primative(self, value):
+ def to_representation(self, value):
method_attr = self.method_attr
if method_attr is None:
method_attr = 'get_{field_name}'.format(field_name=self.field_name)
@@ -815,13 +817,13 @@ class ModelField(Field):
kwargs['source'] = '*'
super(ModelField, self).__init__(**kwargs)
- def to_native(self, data):
+ def to_internal_value(self, data):
rel = getattr(self.model_field, 'rel', None)
if rel is not None:
return rel.to._meta.get_field(rel.field_name).to_python(data)
return self.model_field.to_python(data)
- def to_primative(self, obj):
+ def to_representation(self, obj):
value = self.model_field._get_val_from_obj(obj)
if is_protected_type(value):
return value
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index d82d2d3b..c5a9270a 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -13,7 +13,7 @@ class NextPageField(serializers.Field):
"""
page_field = 'page'
- def to_primative(self, value):
+ def to_representation(self, value):
if not value.has_next():
return None
page = value.next_page_number()
@@ -28,7 +28,7 @@ class PreviousPageField(serializers.Field):
"""
page_field = 'page'
- def to_primative(self, value):
+ def to_representation(self, value):
if not value.has_previous():
return None
page = value.previous_page_number()
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 661a1249..30a252db 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -110,7 +110,7 @@ class HyperlinkedIdentityField(RelatedField):
def get_attribute(self, instance):
return instance
- def to_primative(self, value):
+ def to_representation(self, value):
request = self.context.get('request', None)
format = self.context.get('format', None)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 388fe29f..502b1e19 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -47,11 +47,11 @@ class BaseSerializer(Field):
self.instance = instance
self._initial_data = data
- def to_native(self, data):
- raise NotImplementedError('`to_native()` must be implemented.')
+ def to_internal_value(self, data):
+ raise NotImplementedError('`to_internal_value()` must be implemented.')
- def to_primative(self, instance):
- raise NotImplementedError('`to_primative()` must be implemented.')
+ def to_representation(self, instance):
+ raise NotImplementedError('`to_representation()` must be implemented.')
def update(self, instance, attrs):
raise NotImplementedError('`update()` must be implemented.')
@@ -74,7 +74,7 @@ class BaseSerializer(Field):
def is_valid(self, raise_exception=False):
if not hasattr(self, '_validated_data'):
try:
- self._validated_data = self.to_native(self._initial_data)
+ self._validated_data = self.to_internal_value(self._initial_data)
except ValidationError as exc:
self._validated_data = {}
self._errors = exc.message_dict
@@ -90,7 +90,7 @@ class BaseSerializer(Field):
def data(self):
if not hasattr(self, '_data'):
if self.instance is not None:
- self._data = self.to_primative(self.instance)
+ self._data = self.to_representation(self.instance)
elif self._initial_data is not None:
self._data = dict([
(field_name, field.get_value(self._initial_data))
@@ -193,7 +193,7 @@ class Serializer(BaseSerializer):
return html.parse_html_dict(dictionary, prefix=self.field_name)
return dictionary.get(self.field_name, empty)
- def to_native(self, data):
+ def to_internal_value(self, data):
"""
Dict of native values <- Dict of primitive datatypes.
"""
@@ -208,7 +208,7 @@ class Serializer(BaseSerializer):
validate_method = getattr(self, 'validate_' + field.field_name, None)
primitive_value = field.get_value(data)
try:
- validated_value = field.validate_value(primitive_value)
+ validated_value = field.run_validation(primitive_value)
if validate_method is not None:
validated_value = validate_method(validated_value)
except ValidationError as exc:
@@ -226,7 +226,7 @@ class Serializer(BaseSerializer):
except ValidationError as exc:
raise ValidationError({'non_field_errors': exc.messages})
- def to_primative(self, instance):
+ def to_representation(self, instance):
"""
Object instance -> Dict of primitive datatypes.
"""
@@ -235,7 +235,7 @@ class Serializer(BaseSerializer):
for field in fields:
native_value = field.get_attribute(instance)
- ret[field.field_name] = field.to_primative(native_value)
+ ret[field.field_name] = field.to_representation(native_value)
return ret
@@ -279,20 +279,20 @@ class ListSerializer(BaseSerializer):
return html.parse_html_list(dictionary, prefix=self.field_name)
return dictionary.get(self.field_name, empty)
- def to_native(self, data):
+ def to_internal_value(self, data):
"""
List of dicts of native values <- List of dicts of primitive datatypes.
"""
if html.is_html_input(data):
data = html.parse_html_list(data)
- return [self.child.validate(item) for item in data]
+ return [self.child.run_validation(item) for item in data]
- def to_primative(self, data):
+ def to_representation(self, data):
"""
List of object instances -> List of dicts of primitive datatypes.
"""
- return [self.child.to_primative(item) for item in data]
+ return [self.child.to_representation(item) for item in data]
def create(self, attrs_list):
return [self.child.create(attrs) for attrs in attrs_list]
diff --git a/tests/test_validation.py b/tests/test_validation.py
index c4506e7e..7543d849 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -158,7 +158,7 @@ class TestChoiceFieldChoicesValidate(TestCase):
f = serializers.ChoiceField(choices=self.CHOICES)
value = self.CHOICES[0][0]
try:
- f.to_native(value)
+ f.to_internal_value(value)
except ValidationError:
self.fail("Value %s does not validate" % str(value))
--
cgit v1.2.3
From 6db3356c4d1aa4f9a042b0ec67d47238abc16dd7 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 10:21:35 +0100
Subject: NON_FIELD_ERRORS_KEY setting
---
rest_framework/serializers.py | 8 ++++++--
rest_framework/settings.py | 1 +
rest_framework/views.py | 8 +++++++-
3 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 502b1e19..0c2aedfa 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -198,7 +198,9 @@ class Serializer(BaseSerializer):
Dict of native values <- Dict of primitive datatypes.
"""
if not isinstance(data, dict):
- raise ValidationError({'non_field_errors': ['Invalid data']})
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: ['Invalid data']
+ })
ret = {}
errors = {}
@@ -224,7 +226,9 @@ class Serializer(BaseSerializer):
try:
return self.validate(ret)
except ValidationError as exc:
- raise ValidationError({'non_field_errors': exc.messages})
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: exc.messages
+ })
def to_representation(self, instance):
"""
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index bbe7a56a..f48643b5 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -77,6 +77,7 @@ DEFAULTS = {
# Exception handling
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
+ 'NON_FIELD_ERRORS_KEY': 'non_field_errors',
# Testing
'TEST_REQUEST_RENDERER_CLASSES': (
diff --git a/rest_framework/views.py b/rest_framework/views.py
index cd394b2d..9f08a4ad 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -3,7 +3,7 @@ Provides an APIView class that is the base of all views in REST framework.
"""
from __future__ import unicode_literals
-from django.core.exceptions import PermissionDenied, ValidationError
+from django.core.exceptions import PermissionDenied, ValidationError, NON_FIELD_ERRORS
from django.http import Http404
from django.utils.datastructures import SortedDict
from django.views.decorators.csrf import csrf_exempt
@@ -69,6 +69,12 @@ def exception_handler(exc):
headers=headers)
elif isinstance(exc, ValidationError):
+ # ValidationErrors may include the non-field key named '__all__'.
+ # When returning a response we map this to a key name that can be
+ # modified in settings.
+ if NON_FIELD_ERRORS in exc.message_dict:
+ errors = exc.message_dict.pop(NON_FIELD_ERRORS)
+ exc.message_dict[api_settings.NON_FIELD_ERRORS_KEY] = errors
return Response(exc.message_dict,
status=status.HTTP_400_BAD_REQUEST)
--
cgit v1.2.3
From 250755def707e1397876614fa0c08130d9fcc449 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 10:59:51 +0100
Subject: Clean up relational fields queryset usage
---
rest_framework/fields.py | 15 ++++------
rest_framework/generics.py | 2 +-
rest_framework/relations.py | 73 ++++++++++++++++++++++++---------------------
tests/test_generics.py | 4 ++-
4 files changed, 49 insertions(+), 45 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index a96f9ba8..4f06d186 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -508,7 +508,7 @@ class DecimalField(Field):
class DateField(Field):
default_error_messages = {
- 'invalid': _("Date has wrong format. Use one of these formats instead: %s"),
+ 'invalid': _('Date has wrong format. Use one of these formats instead: {format}'),
}
input_formats = api_settings.DATE_INPUT_FORMATS
format = api_settings.DATE_FORMAT
@@ -551,8 +551,7 @@ class DateField(Field):
return parsed.date()
humanized_format = humanize_datetime.date_formats(self.input_formats)
- msg = self.error_messages['invalid'] % humanized_format
- raise ValidationError(msg)
+ self.fail('invalid', format=humanized_format)
def to_representation(self, value):
if value is None or self.format is None:
@@ -568,7 +567,7 @@ class DateField(Field):
class DateTimeField(Field):
default_error_messages = {
- 'invalid': _("Datetime has wrong format. Use one of these formats instead: %s"),
+ 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'),
}
input_formats = api_settings.DATETIME_INPUT_FORMATS
format = api_settings.DATETIME_FORMAT
@@ -617,8 +616,7 @@ class DateTimeField(Field):
return parsed
humanized_format = humanize_datetime.datetime_formats(self.input_formats)
- msg = self.error_messages['invalid'] % humanized_format
- raise ValidationError(msg)
+ self.fail('invalid', format=humanized_format)
def to_representation(self, value):
if value is None or self.format is None:
@@ -634,7 +632,7 @@ class DateTimeField(Field):
class TimeField(Field):
default_error_messages = {
- 'invalid': _("Time has wrong format. Use one of these formats instead: %s"),
+ 'invalid': _('Time has wrong format. Use one of these formats instead: {format}'),
}
input_formats = api_settings.TIME_INPUT_FORMATS
format = api_settings.TIME_FORMAT
@@ -669,8 +667,7 @@ class TimeField(Field):
return parsed.time()
humanized_format = humanize_datetime.time_formats(self.input_formats)
- msg = self.error_messages['invalid'] % humanized_format
- raise ValidationError(msg)
+ self.fail('invalid', format=humanized_format)
def to_representation(self, value):
if value is None or self.format is None:
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 338d56a6..eb6b64ef 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -216,7 +216,7 @@ class GenericAPIView(views.APIView):
)
queryset = self.queryset
- if isinstance(self.queryset, QuerySet):
+ if isinstance(queryset, QuerySet):
# Ensure queryset is re-evaluated on each request.
queryset = queryset.all()
return queryset
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 30a252db..e23a4152 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -2,28 +2,35 @@ from rest_framework.fields import Field
from rest_framework.reverse import reverse
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
+from django.db.models.query import QuerySet
from rest_framework.compat import urlparse
-def get_default_queryset(serializer_class, field_name):
- manager = getattr(serializer_class.opts.model, field_name)
- if hasattr(manager, 'related'):
- # Forward relationships
- return manager.related.model._default_manager.all()
- # Reverse relationships
- return manager.field.rel.to._default_manager.all()
-
-
class RelatedField(Field):
def __init__(self, **kwargs):
self.queryset = kwargs.pop('queryset', None)
self.many = kwargs.pop('many', False)
+ assert self.queryset is not None or kwargs.get('read_only', False), (
+ 'Relational field must provide a `queryset` argument, '
+ 'or set read_only=`True`.'
+ )
super(RelatedField, self).__init__(**kwargs)
- def bind(self, field_name, parent, root):
- super(RelatedField, self).bind(field_name, parent, root)
- if self.queryset is None and not self.read_only:
- self.queryset = get_default_queryset(parent, self.source)
+ def get_queryset(self):
+ queryset = self.queryset
+ if isinstance(queryset, QuerySet):
+ # Ensure queryset is re-evaluated whenever used.
+ queryset = queryset.all()
+ return queryset
+
+
+class StringRelatedField(Field):
+ def __init__(self, **kwargs):
+ kwargs['read_only'] = True
+ super(StringRelatedField, self).__init__(**kwargs)
+
+ def to_representation(self, value):
+ return str(value)
class PrimaryKeyRelatedField(RelatedField):
@@ -33,9 +40,9 @@ class PrimaryKeyRelatedField(RelatedField):
'incorrect_type': 'Incorrect type. Expected pk value, received {data_type}.',
}
- def from_native(self, data):
+ def to_internal_value(self, data):
try:
- return self.queryset.get(pk=data)
+ return self.get_queryset().get(pk=data)
except ObjectDoesNotExist:
self.fail('does_not_exist', pk_value=data)
except (TypeError, ValueError):
@@ -68,9 +75,9 @@ class HyperlinkedRelatedField(RelatedField):
"""
lookup_value = view_kwargs[self.lookup_url_kwarg]
lookup_kwargs = {self.lookup_field: lookup_value}
- return self.queryset.get(**lookup_kwargs)
+ return self.get_queryset().get(**lookup_kwargs)
- def from_native(self, value):
+ def to_internal_value(self, value):
try:
http_prefix = value.startswith(('http:', 'https:'))
except AttributeError:
@@ -102,13 +109,26 @@ class HyperlinkedIdentityField(RelatedField):
def __init__(self, **kwargs):
kwargs['read_only'] = True
+ kwargs['source'] = '*'
self.view_name = kwargs.pop('view_name')
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
super(HyperlinkedIdentityField, self).__init__(**kwargs)
- def get_attribute(self, instance):
- return instance
+ 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.
+ """
+ # Unsaved objects will not yet have a valid URL.
+ if obj.pk is None:
+ return None
+
+ lookup_value = getattr(obj, self.lookup_field)
+ kwargs = {self.lookup_url_kwarg: lookup_value}
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
def to_representation(self, value):
request = self.context.get('request', None)
@@ -144,21 +164,6 @@ class HyperlinkedIdentityField(RelatedField):
)
raise Exception(msg % self.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.
- """
- # Unsaved objects will not yet have a valid URL.
- if obj.pk is None:
- return None
-
- lookup_value = getattr(obj, self.lookup_field)
- kwargs = {self.lookup_url_kwarg: lookup_value}
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
-
class SlugRelatedField(RelatedField):
def __init__(self, **kwargs):
diff --git a/tests/test_generics.py b/tests/test_generics.py
index 17bfca2f..51004edf 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -547,7 +547,9 @@ class ClassA(models.Model):
class ClassASerializer(serializers.ModelSerializer):
- childs = serializers.PrimaryKeyRelatedField(many=True, source='childs')
+ childs = serializers.PrimaryKeyRelatedField(
+ many=True, queryset=ClassB.objects.all()
+ )
class Meta:
model = ClassA
--
cgit v1.2.3
From 5e39e159ee6aee90755709cfecec7d22c7ea3049 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 11:38:22 +0100
Subject: UNICODE_JSON and COMPACT_JSON settings
---
rest_framework/fields.py | 20 ++++++++++----------
rest_framework/parsers.py | 2 +-
rest_framework/renderers.py | 33 ++++++++++++---------------------
rest_framework/settings.py | 3 +++
tests/test_renderers.py | 29 ++++++++++++++++-------------
tests/test_validation.py | 2 +-
6 files changed, 43 insertions(+), 46 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 4f06d186..9d96cf5c 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -137,6 +137,16 @@ class Field(object):
messages.update(error_messages or {})
self.error_messages = messages
+ def __new__(cls, *args, **kwargs):
+ """
+ When a field is instantiated, we store the arguments that were used,
+ so that we can present a helpful representation of the object.
+ """
+ instance = super(Field, cls).__new__(cls)
+ instance._args = args
+ instance._kwargs = kwargs
+ return instance
+
def bind(self, field_name, parent, root):
"""
Setup the context for the field instance.
@@ -249,16 +259,6 @@ class Field(object):
raise AssertionError(msg)
raise ValidationError(msg.format(**kwargs))
- def __new__(cls, *args, **kwargs):
- """
- When a field is instantiated, we store the arguments that were used,
- so that we can present a helpful representation of the object.
- """
- instance = super(Field, cls).__new__(cls)
- instance._args = args
- instance._kwargs = kwargs
- return instance
-
def __repr__(self):
return representation.field_repr(self)
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index c287908d..fa02ecf1 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -48,7 +48,7 @@ class JSONParser(BaseParser):
"""
media_type = 'application/json'
- renderer_class = renderers.UnicodeJSONRenderer
+ renderer_class = renderers.JSONRenderer
def parse(self, stream, media_type=None, parser_context=None):
"""
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index dfc5a39f..3bf03e62 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -26,6 +26,10 @@ from rest_framework.utils.breadcrumbs import get_breadcrumbs
from rest_framework import exceptions, status, VERSION
+def zero_as_none(value):
+ return None if value == 0 else value
+
+
class BaseRenderer(object):
"""
All renderers should extend this class, setting the `media_type`
@@ -44,13 +48,13 @@ class BaseRenderer(object):
class JSONRenderer(BaseRenderer):
"""
Renderer which serializes to JSON.
- Applies JSON's backslash-u character escaping for non-ascii characters.
"""
media_type = 'application/json'
format = 'json'
encoder_class = encoders.JSONEncoder
- ensure_ascii = True
+ ensure_ascii = not api_settings.UNICODE_JSON
+ compact = api_settings.COMPACT_JSON
# We don't set a charset because JSON is a binary encoding,
# that can be encoded as utf-8, utf-16 or utf-32.
@@ -62,9 +66,10 @@ class JSONRenderer(BaseRenderer):
if accepted_media_type:
# If the media type looks like 'application/json; indent=4',
# then pretty print the result.
+ # Note that we coerce `indent=0` into `indent=None`.
base_media_type, params = parse_header(accepted_media_type.encode('ascii'))
try:
- return max(min(int(params['indent']), 8), 0)
+ return zero_as_none(max(min(int(params['indent']), 8), 0))
except (KeyError, ValueError, TypeError):
pass
@@ -81,10 +86,12 @@ class JSONRenderer(BaseRenderer):
renderer_context = renderer_context or {}
indent = self.get_indent(accepted_media_type, renderer_context)
+ separators = (',', ':') if (indent is None and self.compact) else (', ', ': ')
ret = json.dumps(
data, cls=self.encoder_class,
- indent=indent, ensure_ascii=self.ensure_ascii
+ indent=indent, ensure_ascii=self.ensure_ascii,
+ separators=separators
)
# On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
@@ -96,14 +103,6 @@ class JSONRenderer(BaseRenderer):
return ret
-class UnicodeJSONRenderer(JSONRenderer):
- ensure_ascii = False
- """
- Renderer which serializes to JSON.
- Does *not* apply JSON's character escaping for non-ascii characters.
- """
-
-
class JSONPRenderer(JSONRenderer):
"""
Renderer which serializes to json,
@@ -196,7 +195,7 @@ class YAMLRenderer(BaseRenderer):
format = 'yaml'
encoder = encoders.SafeDumper
charset = 'utf-8'
- ensure_ascii = True
+ ensure_ascii = False
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
@@ -210,14 +209,6 @@ class YAMLRenderer(BaseRenderer):
return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder, allow_unicode=not self.ensure_ascii)
-class UnicodeYAMLRenderer(YAMLRenderer):
- """
- Renderer which serializes to YAML.
- Does *not* apply character escaping for non-ascii characters.
- """
- ensure_ascii = False
-
-
class TemplateHTMLRenderer(BaseRenderer):
"""
An HTML renderer for use with templates.
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index f48643b5..e55610bb 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -112,6 +112,9 @@ DEFAULTS = {
),
'TIME_FORMAT': None,
+ # Encoding
+ 'UNICODE_JSON': True,
+ 'COMPACT_JSON': True
}
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
index 91244e26..a8fd5f46 100644
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -13,7 +13,7 @@ from rest_framework.compat import yaml, etree, StringIO
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
- XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer, UnicodeYAMLRenderer
+ XMLRenderer, JSONPRenderer, BrowsableAPIRenderer
from rest_framework.parsers import YAMLParser, XMLParser
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
@@ -32,7 +32,7 @@ RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii')
expected_results = [
- ((elem for elem in [1, 2, 3]), JSONRenderer, b'[1, 2, 3]') # Generator
+ ((elem for elem in [1, 2, 3]), JSONRenderer, b'[1,2,3]') # Generator
]
@@ -270,7 +270,7 @@ class RendererEndToEndTests(TestCase):
self.assertNotContains(resp, '>text/html; charset=utf-8<')
-_flat_repr = '{"foo": ["bar", "baz"]}'
+_flat_repr = '{"foo":["bar","baz"]}'
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
@@ -373,22 +373,29 @@ class JSONRendererTests(TestCase):
content = renderer.render(obj, 'application/json; indent=2')
self.assertEqual(strip_trailing_whitespace(content.decode('utf-8')), _indented_repr)
- def test_check_ascii(self):
+
+class UnicodeJSONRendererTests(TestCase):
+ """
+ Tests specific for the Unicode JSON Renderer
+ """
+ def test_proper_encoding(self):
obj = {'countries': ['United Kingdom', 'France', 'España']}
renderer = JSONRenderer()
content = renderer.render(obj, 'application/json')
- self.assertEqual(content, '{"countries": ["United Kingdom", "France", "Espa\\u00f1a"]}'.encode('utf-8'))
+ self.assertEqual(content, '{"countries":["United Kingdom","France","España"]}'.encode('utf-8'))
-class UnicodeJSONRendererTests(TestCase):
+class AsciiJSONRendererTests(TestCase):
"""
Tests specific for the Unicode JSON Renderer
"""
def test_proper_encoding(self):
+ class AsciiJSONRenderer(JSONRenderer):
+ ensure_ascii = True
obj = {'countries': ['United Kingdom', 'France', 'España']}
- renderer = UnicodeJSONRenderer()
+ renderer = AsciiJSONRenderer()
content = renderer.render(obj, 'application/json')
- self.assertEqual(content, '{"countries": ["United Kingdom", "France", "España"]}'.encode('utf-8'))
+ self.assertEqual(content, '{"countries":["United Kingdom","France","Espa\\u00f1a"]}'.encode('utf-8'))
class JSONPRendererTests(TestCase):
@@ -487,13 +494,9 @@ if yaml:
def assertYAMLContains(self, content, string):
self.assertTrue(string in content, '%r not in %r' % (string, content))
- class UnicodeYAMLRendererTests(TestCase):
- """
- Tests specific for the Unicode YAML Renderer
- """
def test_proper_encoding(self):
obj = {'countries': ['United Kingdom', 'France', 'España']}
- renderer = UnicodeYAMLRenderer()
+ renderer = YAMLRenderer()
content = renderer.render(obj, 'application/yaml')
self.assertEqual(content.strip(), 'countries: [United Kingdom, France, España]'.encode('utf-8'))
diff --git a/tests/test_validation.py b/tests/test_validation.py
index 7543d849..ce39714d 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -131,7 +131,7 @@ class TestMaxValueValidatorValidation(TestCase):
request = factory.patch('/{0}'.format(obj.pk), {'number_value': 101}, format='json')
view = UpdateMaxValueValidationModel().as_view()
response = view(request, pk=obj.pk).render()
- self.assertEqual(response.content, b'{"number_value": ["Ensure this value is less than or equal to 100."]}')
+ self.assertEqual(response.content, b'{"number_value":["Ensure this value is less than or equal to 100."]}')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
--
cgit v1.2.3
From 22af49bf8ffc73afc9b638f1b9cd2e909c6c89a8 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 11:50:20 +0100
Subject: Tidy up JSONEncoder
---
rest_framework/utils/encoders.py | 67 ++++++++++++++++++++--------------------
1 file changed, 34 insertions(+), 33 deletions(-)
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index 7992b6b1..174b08b8 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -7,7 +7,6 @@ from django.db.models.query import QuerySet
from django.utils.datastructures import SortedDict
from django.utils.functional import Promise
from rest_framework.compat import force_text
-# from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
import datetime
import decimal
import types
@@ -17,45 +16,47 @@ import json
class JSONEncoder(json.JSONEncoder):
"""
JSONEncoder subclass that knows how to encode date/time/timedelta,
- decimal types, and generators.
+ decimal types, generators and other basic python objects.
"""
- def default(self, o):
+ def default(self, obj):
# For Date Time string spec, see ECMA 262
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
- if isinstance(o, Promise):
- return force_text(o)
- elif isinstance(o, datetime.datetime):
- r = o.isoformat()
- if o.microsecond:
- r = r[:23] + r[26:]
- if r.endswith('+00:00'):
- r = r[:-6] + 'Z'
- return r
- elif isinstance(o, datetime.date):
- return o.isoformat()
- elif isinstance(o, datetime.time):
- if timezone and timezone.is_aware(o):
+ if isinstance(obj, Promise):
+ return force_text(obj)
+ elif isinstance(obj, datetime.datetime):
+ representation = obj.isoformat()
+ if obj.microsecond:
+ representation = representation[:23] + representation[26:]
+ if representation.endswith('+00:00'):
+ representation = representation[:-6] + 'Z'
+ return representation
+ elif isinstance(obj, datetime.date):
+ return obj.isoformat()
+ elif isinstance(obj, datetime.time):
+ if timezone and timezone.is_aware(obj):
raise ValueError("JSON can't represent timezone-aware times.")
- r = o.isoformat()
- if o.microsecond:
- r = r[:12]
- return r
- elif isinstance(o, datetime.timedelta):
- return str(o.total_seconds())
- elif isinstance(o, decimal.Decimal):
- return float(o)
- elif isinstance(o, QuerySet):
- return list(o)
- elif hasattr(o, 'tolist'):
- return o.tolist()
- elif hasattr(o, '__getitem__'):
+ representation = obj.isoformat()
+ if obj.microsecond:
+ representation = representation[:12]
+ return representation
+ elif isinstance(obj, datetime.timedelta):
+ return str(obj.total_seconds())
+ elif isinstance(obj, decimal.Decimal):
+ # Serializers will coerce decimals to strings by default.
+ return float(obj)
+ elif isinstance(obj, QuerySet):
+ return list(obj)
+ elif hasattr(obj, 'tolist'):
+ # Numpy arrays and array scalars.
+ return obj.tolist()
+ elif hasattr(obj, '__getitem__'):
try:
- return dict(o)
+ return dict(obj)
except:
pass
- elif hasattr(o, '__iter__'):
- return [i for i in o]
- return super(JSONEncoder, self).default(o)
+ elif hasattr(obj, '__iter__'):
+ return [item for item in obj]
+ return super(JSONEncoder, self).default(obj)
try:
--
cgit v1.2.3
From 79715f01f8c34fdd55c2291b6b21d09fa3a8153e Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 12:10:22 +0100
Subject: Coerce dates etc to ISO_8601 in seralizer, by default.
---
rest_framework/fields.py | 24 +++++++++++++-----------
rest_framework/settings.py | 21 ++++++++-------------
tests/test_filters.py | 17 +++++++++--------
tests/test_pagination.py | 2 +-
4 files changed, 31 insertions(+), 33 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 9d96cf5c..e1855ff7 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -431,10 +431,12 @@ class DecimalField(Field):
'max_whole_digits': _('Ensure that there are no more than {max_whole_digits} digits before the decimal point.')
}
- def __init__(self, max_digits, decimal_places, coerce_to_string=True, max_value=None, min_value=None, **kwargs):
+ coerce_to_string = api_settings.COERCE_DECIMAL_TO_STRING
+
+ def __init__(self, max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None, **kwargs):
self.max_digits = max_digits
self.decimal_places = decimal_places
- self.coerce_to_string = coerce_to_string
+ self.coerce_to_string = coerce_to_string if (coerce_to_string is not None) else self.coerce_to_string
super(DecimalField, self).__init__(**kwargs)
if max_value is not None:
self.validators.append(validators.MaxValueValidator(max_value))
@@ -510,12 +512,12 @@ class DateField(Field):
default_error_messages = {
'invalid': _('Date has wrong format. Use one of these formats instead: {format}'),
}
- input_formats = api_settings.DATE_INPUT_FORMATS
format = api_settings.DATE_FORMAT
+ input_formats = api_settings.DATE_INPUT_FORMATS
- def __init__(self, input_formats=None, format=None, *args, **kwargs):
- self.input_formats = input_formats if input_formats is not None else self.input_formats
+ def __init__(self, format=None, input_formats=None, *args, **kwargs):
self.format = format if format is not None else self.format
+ self.input_formats = input_formats if input_formats is not None else self.input_formats
super(DateField, self).__init__(*args, **kwargs)
def to_internal_value(self, value):
@@ -569,12 +571,12 @@ class DateTimeField(Field):
default_error_messages = {
'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'),
}
- input_formats = api_settings.DATETIME_INPUT_FORMATS
format = api_settings.DATETIME_FORMAT
+ input_formats = api_settings.DATETIME_INPUT_FORMATS
- def __init__(self, input_formats=None, format=None, *args, **kwargs):
- self.input_formats = input_formats if input_formats is not None else self.input_formats
+ def __init__(self, format=None, input_formats=None, *args, **kwargs):
self.format = format if format is not None else self.format
+ self.input_formats = input_formats if input_formats is not None else self.input_formats
super(DateTimeField, self).__init__(*args, **kwargs)
def to_internal_value(self, value):
@@ -634,12 +636,12 @@ class TimeField(Field):
default_error_messages = {
'invalid': _('Time has wrong format. Use one of these formats instead: {format}'),
}
- input_formats = api_settings.TIME_INPUT_FORMATS
format = api_settings.TIME_FORMAT
+ input_formats = api_settings.TIME_INPUT_FORMATS
- def __init__(self, input_formats=None, format=None, *args, **kwargs):
- self.input_formats = input_formats if input_formats is not None else self.input_formats
+ def __init__(self, format=None, input_formats=None, *args, **kwargs):
self.format = format if format is not None else self.format
+ self.input_formats = input_formats if input_formats is not None else self.input_formats
super(TimeField, self).__init__(*args, **kwargs)
def from_native(self, value):
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index e55610bb..421e146c 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -97,24 +97,19 @@ DEFAULTS = {
'URL_FIELD_NAME': 'url',
# Input and output formats
- 'DATE_INPUT_FORMATS': (
- ISO_8601,
- ),
- 'DATE_FORMAT': None,
+ 'DATE_FORMAT': ISO_8601,
+ 'DATE_INPUT_FORMATS': (ISO_8601,),
- 'DATETIME_INPUT_FORMATS': (
- ISO_8601,
- ),
- 'DATETIME_FORMAT': None,
+ 'DATETIME_FORMAT': ISO_8601,
+ 'DATETIME_INPUT_FORMATS': (ISO_8601,),
- 'TIME_INPUT_FORMATS': (
- ISO_8601,
- ),
- 'TIME_FORMAT': None,
+ 'TIME_FORMAT': ISO_8601,
+ 'TIME_INPUT_FORMATS': (ISO_8601,),
# Encoding
'UNICODE_JSON': True,
- 'COMPACT_JSON': True
+ 'COMPACT_JSON': True,
+ 'COERCE_DECIMAL_TO_STRING': True
}
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 01668114..dc84dcbd 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -2,10 +2,11 @@ from __future__ import unicode_literals
import datetime
from decimal import Decimal
from django.db import models
+from django.conf.urls import patterns, url
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.utils import unittest
-from django.conf.urls import patterns, url
+from django.utils.dateparse import parse_date
from rest_framework import generics, serializers, status, filters
from rest_framework.compat import django_filters
from rest_framework.test import APIRequestFactory
@@ -102,7 +103,7 @@ if django_filters:
class CommonFilteringTestCase(TestCase):
def _serialize_object(self, obj):
- return {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date}
+ return {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()}
def setUp(self):
"""
@@ -153,7 +154,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-09-22'
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['date'] == search_date]
+ expected_data = [f for f in self.data if parse_date(f['date']) == search_date]
self.assertEqual(response.data, expected_data)
@unittest.skipUnless(django_filters, 'django-filter not installed')
@@ -209,7 +210,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-10-02'
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['date'] > search_date]
+ expected_data = [f for f in self.data if parse_date(f['date']) > search_date]
self.assertEqual(response.data, expected_data)
# Tests that the text filter set with 'icontains' in the filter class works.
@@ -229,7 +230,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase):
})
response = view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['date'] > search_date and
+ expected_data = [f for f in self.data if parse_date(f['date']) > search_date and
Decimal(f['decimal']) < search_decimal]
self.assertEqual(response.data, expected_data)
@@ -481,9 +482,9 @@ class DjangoFilterOrderingTests(TestCase):
self.assertEqual(
response.data,
[
- {'id': 3, 'date': datetime.date(2014, 10, 8), 'text': 'cde'},
- {'id': 2, 'date': datetime.date(2013, 10, 8), 'text': 'bcd'},
- {'id': 1, 'date': datetime.date(2012, 10, 8), 'text': 'abc'}
+ {'id': 3, 'date': '2014-10-08', 'text': 'cde'},
+ {'id': 2, 'date': '2013-10-08', 'text': 'bcd'},
+ {'id': 1, 'date': '2012-10-08', 'text': 'abc'}
]
)
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index a7f8e691..1fd9cf9c 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -135,7 +135,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
self.objects = FilterableItem.objects
self.data = [
- {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date}
+ {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()}
for obj in self.objects.all()
]
--
cgit v1.2.3
From f95e7fae38968f58e742b93842bda9110a61b9f7 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 12:40:53 +0100
Subject: Updating docs
---
docs/api-guide/renderers.md | 29 +++++----------------------
docs/api-guide/settings.md | 48 ++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 50 insertions(+), 27 deletions(-)
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index 7a3429bf..b1adf31f 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -74,37 +74,18 @@ If your API includes views that can serve both regular webpages and API response
Renders the request data into `JSON`, using utf-8 encoding.
-Note that non-ascii characters will be rendered using JSON's `\uXXXX` character escape. For example:
+Note that the default style is to include unicode characters, and render the response using a compact style with no uneccessary whitespace:
- {"unicode black star": "\u2605"}
+ {"unicode black star":"★","value":999}
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
{
- "unicode black star": "\u2605"
+ "unicode black star": "★",
+ "value": 999
}
-**.media_type**: `application/json`
-
-**.format**: `'.json'`
-
-**.charset**: `None`
-
-## UnicodeJSONRenderer
-
-Renders the request data into `JSON`, using utf-8 encoding.
-
-Note that non-ascii characters will not be character escaped. For example:
-
- {"unicode black star": "★"}
-
-The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
-
- {
- "unicode black star": "★"
- }
-
-Both the `JSONRenderer` and `UnicodeJSONRenderer` styles conform to [RFC 4627][rfc4627], and are syntactically valid JSON.
+The default JSON encoding style can be altered using the `UNICODE_JSON` and `COMPACT_JSON` settings keys.
**.media_type**: `application/json`
diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md
index 27a09163..6a855c92 100644
--- a/docs/api-guide/settings.md
+++ b/docs/api-guide/settings.md
@@ -265,7 +265,7 @@ A format string that should be used by default for rendering the output of `Date
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
-Default: `None`
+Default: `'iso-8601'`
#### DATETIME_INPUT_FORMATS
@@ -281,7 +281,7 @@ A format string that should be used by default for rendering the output of `Date
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
-Default: `None`
+Default: `'iso-8601'`
#### DATE_INPUT_FORMATS
@@ -297,7 +297,7 @@ A format string that should be used by default for rendering the output of `Time
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
-Default: `None`
+Default: `'iso-8601'`
#### TIME_INPUT_FORMATS
@@ -309,6 +309,46 @@ Default: `['iso-8601']`
---
+## Encodings
+
+#### UNICODE_JSON
+
+When set to `True`, JSON responses will allow unicode characters in responses. For example:
+
+ {"unicode black star":"★"}
+
+When set to `False`, JSON responses will escape non-ascii characters, like so:
+
+ {"unicode black star":"\u2605"}
+
+Both styles conform to [RFC 4627][rfc4627], and are syntactically valid JSON. The unicode style is prefered as being more user-friendly when inspecting API responses.
+
+Default: `True`
+
+#### COMPACT_JSON
+
+When set to `True`, JSON responses will return compact representations, with no spacing after `':'` and `','` characters. For example:
+
+ {"is_admin":false,"email":"jane@example"}
+
+When set to `False`, JSON responses will return slightly more verbose representations, like so:
+
+ {"is_admin": false, "email": "jane@example"}
+
+The default style is to return minified responses, in line with [Heroku's API design guidelines][heroku-minified-json].
+
+Default: `True`
+
+#### COERCE_DECIMAL_TO_STRING
+
+When returning decimal objects in API representations that do not support a native decimal type, it is normally best to return the value as a string. This avoids the loss of precision that occurs with binary floating point implementations.
+
+When set to `True`, the serializer `DecimalField` class will return strings instead of `Decimal` objects. When set to `False`, serializers will return `Decimal` objects, which the default JSON encoder will return as floats.
+
+Default: `True`
+
+---
+
## View names and descriptions
**The following settings are used to generate the view names and descriptions, as used in responses to `OPTIONS` requests, and as used in the browsable API.**
@@ -378,4 +418,6 @@ An integer of 0 or more, that may be used to specify the number of application p
Default: `None`
[cite]: http://www.python.org/dev/peps/pep-0020/
+[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
+[heroku-minified-json]: https://github.com/interagent/http-api-design#keep-json-minified-in-all-responses
[strftime]: http://docs.python.org/2/library/time.html#time.strftime
--
cgit v1.2.3
From b73a205cc021983d9a508b447f30e144a1ce4129 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 17:03:42 +0100
Subject: Tests for relational fields (not including many=True)
---
rest_framework/relations.py | 143 +++++++++++++++++++++++++++++---------------
tests/test_relations.py | 139 ++++++++++++++++++++++++++++++++++++++++++
tests/utils.py | 53 ++++++++++++++++
3 files changed, 286 insertions(+), 49 deletions(-)
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index e23a4152..75ec89a8 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -1,19 +1,24 @@
+from rest_framework.compat import smart_text, urlparse
from rest_framework.fields import Field
from rest_framework.reverse import reverse
-from django.core.exceptions import ObjectDoesNotExist
+from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
from django.db.models.query import QuerySet
-from rest_framework.compat import urlparse
+from django.utils.translation import ugettext_lazy as _
class RelatedField(Field):
def __init__(self, **kwargs):
self.queryset = kwargs.pop('queryset', None)
self.many = kwargs.pop('many', False)
- assert self.queryset is not None or kwargs.get('read_only', False), (
+ assert self.queryset is not None or kwargs.get('read_only', None), (
'Relational field must provide a `queryset` argument, '
'or set read_only=`True`.'
)
+ assert not (self.queryset is not None and kwargs.get('read_only', None)), (
+ 'Relational fields should not provide a `queryset` argument, '
+ 'when setting read_only=`True`.'
+ )
super(RelatedField, self).__init__(**kwargs)
def get_queryset(self):
@@ -25,6 +30,11 @@ class RelatedField(Field):
class StringRelatedField(Field):
+ """
+ A read only field that represents its targets using their
+ plain string representation.
+ """
+
def __init__(self, **kwargs):
kwargs['read_only'] = True
super(StringRelatedField, self).__init__(**kwargs)
@@ -34,10 +44,10 @@ class StringRelatedField(Field):
class PrimaryKeyRelatedField(RelatedField):
- MESSAGES = {
+ default_error_messages = {
'required': 'This field is required.',
'does_not_exist': "Invalid pk '{pk_value}' - object does not exist.",
- 'incorrect_type': 'Incorrect type. Expected pk value, received {data_type}.',
+ 'incorrect_type': 'Incorrect type. Expected pk value, received {data_type}.',
}
def to_internal_value(self, data):
@@ -48,22 +58,33 @@ class PrimaryKeyRelatedField(RelatedField):
except (TypeError, ValueError):
self.fail('incorrect_type', data_type=type(data).__name__)
+ def to_representation(self, value):
+ return value.pk
+
class HyperlinkedRelatedField(RelatedField):
lookup_field = 'pk'
- MESSAGES = {
+ default_error_messages = {
'required': 'This field is required.',
'no_match': 'Invalid hyperlink - No URL match',
'incorrect_match': 'Invalid hyperlink - Incorrect URL match.',
- 'does_not_exist': "Invalid hyperlink - Object does not exist.",
- 'incorrect_type': 'Incorrect type. Expected URL string, received {data_type}.',
+ 'does_not_exist': 'Invalid hyperlink - Object does not exist.',
+ 'incorrect_type': 'Incorrect type. Expected URL string, received {data_type}.',
}
- def __init__(self, **kwargs):
- self.view_name = kwargs.pop('view_name')
+ def __init__(self, view_name, **kwargs):
+ self.view_name = view_name
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
+ self.format = kwargs.pop('format', None)
+
+ # We include these simply for dependancy injection in tests.
+ # We can't add them as class attributes or they would expect an
+ # implict `self` argument to be passed.
+ self.reverse = reverse
+ self.resolve = resolve
+
super(HyperlinkedRelatedField, self).__init__(**kwargs)
def get_object(self, view_name, view_args, view_kwargs):
@@ -77,21 +98,36 @@ class HyperlinkedRelatedField(RelatedField):
lookup_kwargs = {self.lookup_field: lookup_value}
return self.get_queryset().get(**lookup_kwargs)
- def to_internal_value(self, value):
+ 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.
+ """
+ # Unsaved objects will not yet have a valid URL.
+ if obj.pk is None:
+ return None
+
+ lookup_value = getattr(obj, self.lookup_field)
+ kwargs = {self.lookup_url_kwarg: lookup_value}
+ return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
+
+ def to_internal_value(self, data):
try:
- http_prefix = value.startswith(('http:', 'https:'))
+ http_prefix = data.startswith(('http:', 'https:'))
except AttributeError:
- self.fail('incorrect_type', data_type=type(value).__name__)
+ self.fail('incorrect_type', data_type=type(data).__name__)
if http_prefix:
# If needed convert absolute URLs to relative path
- value = urlparse.urlparse(value).path
+ data = urlparse.urlparse(data).path
prefix = get_script_prefix()
- if value.startswith(prefix):
- value = '/' + value[len(prefix):]
+ if data.startswith(prefix):
+ data = '/' + data[len(prefix):]
try:
- match = resolve(value)
+ match = self.resolve(data)
except Exception:
self.fail('no_match')
@@ -103,41 +139,14 @@ class HyperlinkedRelatedField(RelatedField):
except (ObjectDoesNotExist, TypeError, ValueError):
self.fail('does_not_exist')
-
-class HyperlinkedIdentityField(RelatedField):
- lookup_field = 'pk'
-
- def __init__(self, **kwargs):
- kwargs['read_only'] = True
- kwargs['source'] = '*'
- self.view_name = kwargs.pop('view_name')
- self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
- self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
- super(HyperlinkedIdentityField, self).__init__(**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.
- """
- # Unsaved objects will not yet have a valid URL.
- if obj.pk is None:
- return None
-
- lookup_value = getattr(obj, self.lookup_field)
- kwargs = {self.lookup_url_kwarg: lookup_value}
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
-
def to_representation(self, value):
request = self.context.get('request', None)
format = self.context.get('format', None)
assert request is not None, (
- "`HyperlinkedIdentityField` requires the request in the serializer"
+ "`%s` requires the request in the serializer"
" context. Add `context={'request': request}` when instantiating "
- "the serializer."
+ "the serializer." % self.__class__.__name__
)
# By default use whatever format is given for the current context
@@ -162,9 +171,45 @@ class HyperlinkedIdentityField(RelatedField):
'model in your API, or incorrectly configured the '
'`lookup_field` attribute on this field.'
)
- raise Exception(msg % self.view_name)
+ raise ImproperlyConfigured(msg % self.view_name)
+
+
+class HyperlinkedIdentityField(HyperlinkedRelatedField):
+ """
+ A read-only field that represents the identity URL for an object, itself.
+
+ This is in contrast to `HyperlinkedRelatedField` which represents the
+ URL of relationships to other objects.
+ """
+
+ def __init__(self, view_name, **kwargs):
+ kwargs['read_only'] = True
+ kwargs['source'] = '*'
+ super(HyperlinkedIdentityField, self).__init__(view_name, **kwargs)
class SlugRelatedField(RelatedField):
- def __init__(self, **kwargs):
- self.slug_field = kwargs.pop('slug_field', None)
+ """
+ A read-write field the represents the target of the relationship
+ by a unique 'slug' attribute.
+ """
+
+ default_error_messages = {
+ 'does_not_exist': _("Object with {slug_name}={value} does not exist."),
+ 'invalid': _('Invalid value.'),
+ }
+
+ def __init__(self, slug_field, **kwargs):
+ self.slug_field = slug_field
+ super(SlugRelatedField, self).__init__(**kwargs)
+
+ def to_internal_value(self, data):
+ try:
+ return self.get_queryset().get(**{self.slug_field: data})
+ except ObjectDoesNotExist:
+ self.fail('does_not_exist', slug_name=self.slug_field, value=smart_text(data))
+ except (TypeError, ValueError):
+ self.fail('invalid')
+
+ def to_representation(self, obj):
+ return getattr(obj, self.slug_field)
diff --git a/tests/test_relations.py b/tests/test_relations.py
index b1bc66b6..a3672117 100644
--- a/tests/test_relations.py
+++ b/tests/test_relations.py
@@ -1,3 +1,142 @@
+from .utils import mock_reverse, fail_reverse, BadType, MockObject, MockQueryset
+from django.core.exceptions import ImproperlyConfigured, ValidationError
+from rest_framework import serializers
+from rest_framework.test import APISimpleTestCase
+import pytest
+
+
+class TestStringRelatedField(APISimpleTestCase):
+ def setUp(self):
+ self.instance = MockObject(pk=1, name='foo')
+ self.field = serializers.StringRelatedField()
+
+ def test_string_related_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == ''
+
+
+class TestPrimaryKeyRelatedField(APISimpleTestCase):
+ def setUp(self):
+ self.queryset = MockQueryset([
+ MockObject(pk=1, name='foo'),
+ MockObject(pk=2, name='bar'),
+ MockObject(pk=3, name='baz')
+ ])
+ self.instance = self.queryset.items[2]
+ self.field = serializers.PrimaryKeyRelatedField(queryset=self.queryset)
+
+ def test_pk_related_lookup_exists(self):
+ instance = self.field.to_internal_value(self.instance.pk)
+ assert instance is self.instance
+
+ def test_pk_related_lookup_does_not_exist(self):
+ with pytest.raises(ValidationError) as excinfo:
+ self.field.to_internal_value(4)
+ msg = excinfo.value.message
+ assert msg == "Invalid pk '4' - object does not exist."
+
+ def test_pk_related_lookup_invalid_type(self):
+ with pytest.raises(ValidationError) as excinfo:
+ self.field.to_internal_value(BadType())
+ msg = excinfo.value.message
+ assert msg == 'Incorrect type. Expected pk value, received BadType.'
+
+ def test_pk_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == self.instance.pk
+
+
+class TestHyperlinkedIdentityField(APISimpleTestCase):
+ def setUp(self):
+ self.instance = MockObject(pk=1, name='foo')
+ self.field = serializers.HyperlinkedIdentityField(view_name='example')
+ self.field.reverse = mock_reverse
+ self.field.context = {'request': True}
+
+ def test_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == 'http://example.org/example/1/'
+
+ def test_representation_unsaved_object(self):
+ representation = self.field.to_representation(MockObject(pk=None))
+ assert representation is None
+
+ def test_representation_with_format(self):
+ self.field.context['format'] = 'xml'
+ representation = self.field.to_representation(self.instance)
+ assert representation == 'http://example.org/example/1.xml/'
+
+ def test_improperly_configured(self):
+ """
+ If a matching view cannot be reversed with the given instance,
+ the the user has misconfigured something, as the URL conf and the
+ hyperlinked field do not match.
+ """
+ self.field.reverse = fail_reverse
+ with pytest.raises(ImproperlyConfigured):
+ self.field.to_representation(self.instance)
+
+
+class TestHyperlinkedIdentityFieldWithFormat(APISimpleTestCase):
+ """
+ Tests for a hyperlinked identity field that has a `format` set,
+ which enforces that alternate formats are never linked too.
+
+ Eg. If your API includes some endpoints that accept both `.xml` and `.json`,
+ but other endpoints that only accept `.json`, we allow for hyperlinked
+ relationships that enforce only a single suffix type.
+ """
+
+ def setUp(self):
+ self.instance = MockObject(pk=1, name='foo')
+ self.field = serializers.HyperlinkedIdentityField(view_name='example', format='json')
+ self.field.reverse = mock_reverse
+ self.field.context = {'request': True}
+
+ def test_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == 'http://example.org/example/1/'
+
+ def test_representation_with_format(self):
+ self.field.context['format'] = 'xml'
+ representation = self.field.to_representation(self.instance)
+ assert representation == 'http://example.org/example/1.json/'
+
+
+class TestSlugRelatedField(APISimpleTestCase):
+ def setUp(self):
+ self.queryset = MockQueryset([
+ MockObject(pk=1, name='foo'),
+ MockObject(pk=2, name='bar'),
+ MockObject(pk=3, name='baz')
+ ])
+ self.instance = self.queryset.items[2]
+ self.field = serializers.SlugRelatedField(
+ slug_field='name', queryset=self.queryset
+ )
+
+ def test_slug_related_lookup_exists(self):
+ instance = self.field.to_internal_value(self.instance.name)
+ assert instance is self.instance
+
+ def test_slug_related_lookup_does_not_exist(self):
+ with pytest.raises(ValidationError) as excinfo:
+ self.field.to_internal_value('doesnotexist')
+ msg = excinfo.value.message
+ assert msg == 'Object with name=doesnotexist does not exist.'
+
+ def test_slug_related_lookup_invalid_type(self):
+ with pytest.raises(ValidationError) as excinfo:
+ self.field.to_internal_value(BadType())
+ msg = excinfo.value.message
+ assert msg == 'Invalid value.'
+
+ def test_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == self.instance.name
+
+# Older tests, for review...
+
# """
# General tests for relational fields.
# """
diff --git a/tests/utils.py b/tests/utils.py
index 28be81bd..5e902ba9 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,4 +1,6 @@
from contextlib import contextmanager
+from django.core.exceptions import ObjectDoesNotExist
+from django.core.urlresolvers import NoReverseMatch
from django.utils import six
from rest_framework.settings import api_settings
@@ -23,3 +25,54 @@ def temporary_setting(setting, value, module=None):
if module is not None:
six.moves.reload_module(module)
+
+
+class MockObject(object):
+ def __init__(self, **kwargs):
+ self._kwargs = kwargs
+ for key, val in kwargs.items():
+ setattr(self, key, val)
+
+ def __str__(self):
+ kwargs_str = ', '.join([
+ '%s=%s' % (key, value)
+ for key, value in sorted(self._kwargs.items())
+ ])
+ return '' % kwargs_str
+
+
+class MockQueryset(object):
+ def __init__(self, iterable):
+ self.items = iterable
+
+ def get(self, **lookup):
+ for item in self.items:
+ if all([
+ getattr(item, key, None) == value
+ for key, value in lookup.items()
+ ]):
+ return item
+ raise ObjectDoesNotExist()
+
+
+class BadType(object):
+ """
+ When used as a lookup with a `MockQueryset`, these objects
+ will raise a `TypeError`, as occurs in Django when making
+ queryset lookups with an incorrect type for the lookup value.
+ """
+ def __eq__(self):
+ raise TypeError()
+
+
+def mock_reverse(view_name, args=None, kwargs=None, request=None, format=None):
+ args = args or []
+ kwargs = kwargs or {}
+ value = (args + list(kwargs.values()) + ['-'])[0]
+ prefix = 'http://example.org' if request else ''
+ suffix = ('.' + format) if (format is not None) else ''
+ return '%s/%s/%s%s/' % (prefix, view_name, value, suffix)
+
+
+def fail_reverse(view_name, args=None, kwargs=None, request=None, format=None):
+ raise NoReverseMatch()
--
cgit v1.2.3
From 0ac52e0808288892717c017e57c57aa8ad81e6d3 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 17:06:37 +0100
Subject: Use Resolver404 instead of base Exception
---
rest_framework/relations.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 75ec89a8..46fe55ef 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -2,7 +2,7 @@ from rest_framework.compat import smart_text, urlparse
from rest_framework.fields import Field
from rest_framework.reverse import reverse
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
-from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
+from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch, Resolver404
from django.db.models.query import QuerySet
from django.utils.translation import ugettext_lazy as _
@@ -128,7 +128,7 @@ class HyperlinkedRelatedField(RelatedField):
try:
match = self.resolve(data)
- except Exception:
+ except Resolver404:
self.fail('no_match')
if match.view_name != self.view_name:
--
cgit v1.2.3
From 1f75ffc7912725e771a67bf3e6c3c6362dc3199a Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 19:50:30 +0100
Subject: Access validation messages in a way thats compatible with 1.4, 1.5
---
tests/test_relations.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/tests/test_relations.py b/tests/test_relations.py
index a3672117..c29618ce 100644
--- a/tests/test_relations.py
+++ b/tests/test_relations.py
@@ -32,13 +32,13 @@ class TestPrimaryKeyRelatedField(APISimpleTestCase):
def test_pk_related_lookup_does_not_exist(self):
with pytest.raises(ValidationError) as excinfo:
self.field.to_internal_value(4)
- msg = excinfo.value.message
+ msg = excinfo.value.messages[0]
assert msg == "Invalid pk '4' - object does not exist."
def test_pk_related_lookup_invalid_type(self):
with pytest.raises(ValidationError) as excinfo:
self.field.to_internal_value(BadType())
- msg = excinfo.value.message
+ msg = excinfo.value.messages[0]
assert msg == 'Incorrect type. Expected pk value, received BadType.'
def test_pk_representation(self):
@@ -122,13 +122,13 @@ class TestSlugRelatedField(APISimpleTestCase):
def test_slug_related_lookup_does_not_exist(self):
with pytest.raises(ValidationError) as excinfo:
self.field.to_internal_value('doesnotexist')
- msg = excinfo.value.message
+ msg = excinfo.value.messages[0]
assert msg == 'Object with name=doesnotexist does not exist.'
def test_slug_related_lookup_invalid_type(self):
with pytest.raises(ValidationError) as excinfo:
self.field.to_internal_value(BadType())
- msg = excinfo.value.message
+ msg = excinfo.value.messages[0]
assert msg == 'Invalid value.'
def test_representation(self):
--
cgit v1.2.3
From e6c88a423361b084ba171af7a74a183bd557e73e Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 19:54:27 +0100
Subject: Drop usage of validatiors.EMPTY_VALUES
---
rest_framework/fields.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index e1855ff7..33ab0682 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -223,7 +223,7 @@ class Field(object):
return value
def run_validators(self, value):
- if value in validators.EMPTY_VALUES:
+ if value in (None, '', [], (), {}):
return
errors = []
@@ -450,7 +450,7 @@ class DecimalField(Field):
than max_digits in the number, and no more than decimal_places digits
after the decimal point.
"""
- if value in validators.EMPTY_VALUES:
+ if value in (None, ''):
return None
value = smart_text(value).strip()
@@ -521,7 +521,7 @@ class DateField(Field):
super(DateField, self).__init__(*args, **kwargs)
def to_internal_value(self, value):
- if value in validators.EMPTY_VALUES:
+ if value in (None, ''):
return None
if isinstance(value, datetime.datetime):
@@ -580,7 +580,7 @@ class DateTimeField(Field):
super(DateTimeField, self).__init__(*args, **kwargs)
def to_internal_value(self, value):
- if value in validators.EMPTY_VALUES:
+ if value in (None, ''):
return None
if isinstance(value, datetime.datetime):
@@ -645,7 +645,7 @@ class TimeField(Field):
super(TimeField, self).__init__(*args, **kwargs)
def from_native(self, value):
- if value in validators.EMPTY_VALUES:
+ if value in (None, ''):
return None
if isinstance(value, datetime.time):
--
cgit v1.2.3
From 4fceceae7d1abf4d841677daee2636273ff09de8 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 20:09:08 +0100
Subject: DecimalField documentation
---
docs/api-guide/fields.md | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index bfbff2ad..f0778318 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -274,7 +274,27 @@ Corresponds to `django.db.models.fields.FloatField`.
## DecimalField
-A decimal representation.
+A decimal representation, represented in Python by a Decimal instance.
+
+Has two required arguments:
+
+- `max_digits` The maximum number of digits allowed in the number. Note that this number must be greater than or equal to decimal_places.
+
+- `decimal_places` The number of decimal places to store with the number.
+
+For example, to validate numbers up to 999 with a resolution of 2 decimal places, you would use:
+
+ serializers.DecimalField(max_digits=5, decimal_places=2)
+
+And to validate numbers up to anything lesss than one billion with a resolution of 10 decimal places:
+
+ serializers.DecimalField(max_digits=19, decimal_places=10)
+
+This field also takes an optional argument, `coerce_to_string`. If set to `True` the representation will be output as a string. If set to `False` the representation will be left as a `Decimal` instance and the final representation will be determined by the renderer.
+
+If unset, this will default to the same value as the `COERCE_DECIMAL_TO_STRING` setting, which is `True` unless set otherwise.
+
+**Signature:** `DecimalField(max_digits, decimal_places, coerce_to_string=None)`
Corresponds to `django.db.models.fields.DecimalField`.
--
cgit v1.2.3
From afb28a44ad1737cd6fcd6da50ba9552f38293368 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 12 Sep 2014 21:32:20 +0100
Subject: Dealing with reverse relationships
---
rest_framework/serializers.py | 8 ++++----
tests/test_model_field_mappings.py | 27 ++++++++++++++++++++++++---
2 files changed, 28 insertions(+), 7 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 0c2aedfa..ecb2829b 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -157,7 +157,7 @@ class Serializer(BaseSerializer):
def __init__(self, *args, **kwargs):
self.context = kwargs.pop('context', {})
kwargs.pop('partial', None)
- kwargs.pop('many', False)
+ kwargs.pop('many', None)
super(Serializer, self).__init__(*args, **kwargs)
@@ -423,9 +423,9 @@ class ModelSerializer(Serializer):
for accessor_name, relation_info in info.reverse_relations.items():
if accessor_name in self.opts.fields:
if self.opts.depth:
- ret[field_name] = self.get_nested_field(*relation_info)
+ ret[accessor_name] = self.get_nested_field(*relation_info)
else:
- ret[field_name] = self.get_related_field(*relation_info)
+ ret[accessor_name] = self.get_related_field(*relation_info)
return ret
@@ -444,7 +444,7 @@ class ModelSerializer(Serializer):
Note that model_field will be `None` for reverse relationships.
"""
- class NestedModelSerializer(ModelSerializer):
+ class NestedModelSerializer(ModelSerializer): # Not right!
class Meta:
model = related_model
depth = self.opts.depth - 1
diff --git a/tests/test_model_field_mappings.py b/tests/test_model_field_mappings.py
index 6e9aaba6..d5750f9e 100644
--- a/tests/test_model_field_mappings.py
+++ b/tests/test_model_field_mappings.py
@@ -73,9 +73,9 @@ class OneToOneTargetModel(models.Model):
class RelationalModel(models.Model):
- foreign_key = models.ForeignKey(ForeignKeyTargetModel)
- many_to_many = models.ManyToManyField(ManyToManyTargetModel)
- one_to_one = models.OneToOneField(OneToOneTargetModel)
+ foreign_key = models.ForeignKey(ForeignKeyTargetModel, related_name='reverse_foreign_key')
+ many_to_many = models.ManyToManyField(ManyToManyTargetModel, related_name='reverse_many_to_many')
+ one_to_one = models.OneToOneField(OneToOneTargetModel, related_name='reverse_one_to_one')
RELATIONAL_FLAT_REPR = """
@@ -160,3 +160,24 @@ class TestSerializerMappings(TestCase):
model = RelationalModel
depth = 1
self.assertEqual(repr(TestSerializer()), HYPERLINKED_NESTED_REPR)
+
+ # def test_flat_reverse_foreign_key(self):
+ # class TestSerializer(serializers.ModelSerializer):
+ # class Meta:
+ # model = ForeignKeyTargetModel
+ # fields = ('id', 'name', 'reverse_foreign_key')
+ # print repr(TestSerializer())
+
+ # def test_flat_reverse_one_to_one(self):
+ # class TestSerializer(serializers.ModelSerializer):
+ # class Meta:
+ # model = OneToOneTargetModel
+ # fields = ('id', 'name', 'reverse_one_to_one')
+ # print repr(TestSerializer())
+
+ # def test_flat_reverse_many_to_many(self):
+ # class TestSerializer(serializers.ModelSerializer):
+ # class Meta:
+ # model = ManyToManyTargetModel
+ # fields = ('id', 'name', 'reverse_many_to_many')
+ # print repr(TestSerializer())
\ No newline at end of file
--
cgit v1.2.3
From 40dc588a372375608701f7e521dea6d860a49eb2 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 15 Sep 2014 09:50:51 +0100
Subject: Drop label from serializer fields when not needed
---
rest_framework/fields.py | 6 ++-
rest_framework/serializers.py | 56 ++++++++++++---------
rest_framework/utils/model_meta.py | 99 ++++++++++++++++++++++++++++++++++++++
rest_framework/utils/modelinfo.py | 99 --------------------------------------
tests/test_model_field_mappings.py | 64 ++++++++++++------------
5 files changed, 169 insertions(+), 155 deletions(-)
create mode 100644 rest_framework/utils/model_meta.py
delete mode 100644 rest_framework/utils/modelinfo.py
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 33ab0682..1818e705 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -80,6 +80,10 @@ def set_value(dictionary, keys, value):
dictionary[keys[-1]] = value
+def field_name_to_label(field_name):
+ return field_name.replace('_', ' ').capitalize()
+
+
class SkipField(Exception):
pass
@@ -158,7 +162,7 @@ class Field(object):
# `self.label` should deafult to being based on the field name.
if self.label is None:
- self.label = self.field_name.replace('_', ' ').capitalize()
+ self.label = field_name_to_label(self.field_name)
# self.source should default to being the same as the field name.
if self.source is None:
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index ecb2829b..ba8d475f 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -15,11 +15,12 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.utils import six
from django.utils.datastructures import SortedDict
+from django.utils.text import capfirst
from collections import namedtuple
from rest_framework.compat import clean_manytomany_helptext
from rest_framework.fields import empty, set_value, Field, SkipField
from rest_framework.settings import api_settings
-from rest_framework.utils import html, modelinfo, representation
+from rest_framework.utils import html, model_meta, representation
import copy
# Note: We do the following so that users of the framework can use this style:
@@ -334,6 +335,14 @@ def lookup_class(mapping, instance):
raise KeyError('Class %s not found in lookup.', cls.__name__)
+def needs_label(model_field, field_name):
+ """
+ Returns `True` if the label based on the model's verbose name
+ is not equal to the default label it would have based on it's field name.
+ """
+ return capfirst(model_field.verbose_name) != field_name_to_label(field_name)
+
+
class ModelSerializer(Serializer):
field_mapping = {
models.AutoField: IntegerField,
@@ -397,54 +406,55 @@ class ModelSerializer(Serializer):
"""
Return all the fields that should be serialized for the model.
"""
- info = modelinfo.get_field_info(self.opts.model)
+ info = model_meta.get_field_info(self.opts.model)
ret = SortedDict()
serializer_url_field = self.get_url_field()
if serializer_url_field:
ret[api_settings.URL_FIELD_NAME] = serializer_url_field
- serializer_pk_field = self.get_pk_field(info.pk)
+ field_name = info.pk.name
+ serializer_pk_field = self.get_pk_field(field_name, info.pk)
if serializer_pk_field:
- ret[info.pk.name] = serializer_pk_field
+ ret[field_name] = serializer_pk_field
# Regular fields
for field_name, field in info.fields.items():
- ret[field_name] = self.get_field(field)
+ ret[field_name] = self.get_field(field_name, field)
# Forward relations
for field_name, relation_info in info.forward_relations.items():
if self.opts.depth:
- ret[field_name] = self.get_nested_field(*relation_info)
+ ret[field_name] = self.get_nested_field(field_name, *relation_info)
else:
- ret[field_name] = self.get_related_field(*relation_info)
+ ret[field_name] = self.get_related_field(field_name, *relation_info)
# Reverse relations
for accessor_name, relation_info in info.reverse_relations.items():
if accessor_name in self.opts.fields:
if self.opts.depth:
- ret[accessor_name] = self.get_nested_field(*relation_info)
+ ret[accessor_name] = self.get_nested_field(accessor_name, *relation_info)
else:
- ret[accessor_name] = self.get_related_field(*relation_info)
+ ret[accessor_name] = self.get_related_field(accessor_name, *relation_info)
return ret
def get_url_field(self):
return None
- def get_pk_field(self, model_field):
+ def get_pk_field(self, field_name, model_field):
"""
Returns a default instance of the pk field.
"""
- return self.get_field(model_field)
+ return self.get_field(field_name, model_field)
- def get_nested_field(self, model_field, related_model, to_many, has_through_model):
+ def get_nested_field(self, field_name, model_field, related_model, to_many, has_through_model):
"""
Creates a default instance of a nested relational field.
Note that model_field will be `None` for reverse relationships.
"""
- class NestedModelSerializer(ModelSerializer): # Not right!
+ class NestedModelSerializer(ModelSerializer):
class Meta:
model = related_model
depth = self.opts.depth - 1
@@ -454,7 +464,7 @@ class ModelSerializer(Serializer):
kwargs['many'] = True
return NestedModelSerializer(**kwargs)
- def get_related_field(self, model_field, related_model, to_many, has_through_model):
+ def get_related_field(self, field_name, model_field, related_model, to_many, has_through_model):
"""
Creates a default instance of a flat relational field.
@@ -474,8 +484,8 @@ class ModelSerializer(Serializer):
if model_field:
if model_field.null or model_field.blank:
kwargs['required'] = False
- if model_field.verbose_name:
- kwargs['label'] = model_field.verbose_name
+ if model_field.verbose_name and needs_label(model_field, field_name):
+ kwargs['label'] = capfirst(model_field.verbose_name)
if not model_field.editable:
kwargs['read_only'] = True
kwargs.pop('queryset', None)
@@ -485,7 +495,7 @@ class ModelSerializer(Serializer):
return PrimaryKeyRelatedField(**kwargs)
- def get_field(self, model_field):
+ def get_field(self, field_name, model_field):
"""
Creates a default instance of a basic non-relational field.
"""
@@ -496,8 +506,8 @@ class ModelSerializer(Serializer):
if model_field.null or model_field.blank:
kwargs['required'] = False
- if model_field.verbose_name is not None:
- kwargs['label'] = model_field.verbose_name
+ if model_field.verbose_name and needs_label(model_field, field_name):
+ kwargs['label'] = capfirst(model_field.verbose_name)
if model_field.help_text:
kwargs['help_text'] = model_field.help_text
@@ -642,11 +652,11 @@ class HyperlinkedModelSerializer(ModelSerializer):
return HyperlinkedIdentityField(**kwargs)
- def get_pk_field(self, model_field):
+ def get_pk_field(self, field_name, model_field):
if self.opts.fields and model_field.name in self.opts.fields:
return self.get_field(model_field)
- def get_related_field(self, model_field, related_model, to_many, has_through_model):
+ def get_related_field(self, field_name, model_field, related_model, to_many, has_through_model):
"""
Creates a default instance of a flat relational field.
"""
@@ -665,8 +675,8 @@ class HyperlinkedModelSerializer(ModelSerializer):
if model_field:
if model_field.null or model_field.blank:
kwargs['required'] = False
- if model_field.verbose_name:
- kwargs['label'] = model_field.verbose_name
+ if model_field.verbose_name and needs_label(model_field, field_name):
+ kwargs['label'] = capfirst(model_field.verbose_name)
if not model_field.editable:
kwargs['read_only'] = True
kwargs.pop('queryset', None)
diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py
new file mode 100644
index 00000000..960fa4d0
--- /dev/null
+++ b/rest_framework/utils/model_meta.py
@@ -0,0 +1,99 @@
+"""
+Helper functions for returning the field information that is associated
+with a model class. This includes returning all the forward and reverse
+relationships and their associated metadata.
+"""
+from collections import namedtuple
+from django.db import models
+from django.utils import six
+from django.utils.datastructures import SortedDict
+import inspect
+
+FieldInfo = namedtuple('FieldResult', ['pk', 'fields', 'forward_relations', 'reverse_relations'])
+RelationInfo = namedtuple('RelationInfo', ['field', 'related', 'to_many', 'has_through_model'])
+
+
+def _resolve_model(obj):
+ """
+ Resolve supplied `obj` to a Django model class.
+
+ `obj` must be a Django model class itself, or a string
+ representation of one. Useful in situtations like GH #1225 where
+ Django may not have resolved a string-based reference to a model in
+ another model's foreign key definition.
+
+ String representations should have the format:
+ 'appname.ModelName'
+ """
+ if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
+ app_name, model_name = obj.split('.')
+ return models.get_model(app_name, model_name)
+ elif inspect.isclass(obj) and issubclass(obj, models.Model):
+ return obj
+ raise ValueError("{0} is not a Django model".format(obj))
+
+
+def get_field_info(model):
+ """
+ Given a model class, returns a `FieldInfo` instance containing metadata
+ about the various field types on the model.
+ """
+ opts = model._meta.concrete_model._meta
+
+ # Deal with the primary key.
+ pk = opts.pk
+ while pk.rel and pk.rel.parent_link:
+ # If model is a child via multitable inheritance, use parent's pk.
+ pk = pk.rel.to._meta.pk
+
+ # Deal with regular fields.
+ fields = SortedDict()
+ for field in [field for field in opts.fields if field.serialize and not field.rel]:
+ fields[field.name] = field
+
+ # Deal with forward relationships.
+ forward_relations = SortedDict()
+ for field in [field for field in opts.fields if field.serialize and field.rel]:
+ forward_relations[field.name] = RelationInfo(
+ field=field,
+ related=_resolve_model(field.rel.to),
+ to_many=False,
+ has_through_model=False
+ )
+
+ # Deal with forward many-to-many relationships.
+ for field in [field for field in opts.many_to_many if field.serialize]:
+ forward_relations[field.name] = RelationInfo(
+ field=field,
+ related=_resolve_model(field.rel.to),
+ to_many=True,
+ has_through_model=(
+ not field.rel.through._meta.auto_created
+ )
+ )
+
+ # Deal with reverse relationships.
+ reverse_relations = SortedDict()
+ for relation in opts.get_all_related_objects():
+ accessor_name = relation.get_accessor_name()
+ reverse_relations[accessor_name] = RelationInfo(
+ field=None,
+ related=relation.model,
+ to_many=relation.field.rel.multiple,
+ has_through_model=False
+ )
+
+ # Deal with reverse many-to-many relationships.
+ for relation in opts.get_all_related_many_to_many_objects():
+ accessor_name = relation.get_accessor_name()
+ reverse_relations[accessor_name] = RelationInfo(
+ field=None,
+ related=relation.model,
+ to_many=True,
+ has_through_model=(
+ hasattr(relation.field.rel, 'through') and
+ not relation.field.rel.through._meta.auto_created
+ )
+ )
+
+ return FieldInfo(pk, fields, forward_relations, reverse_relations)
diff --git a/rest_framework/utils/modelinfo.py b/rest_framework/utils/modelinfo.py
deleted file mode 100644
index 960fa4d0..00000000
--- a/rest_framework/utils/modelinfo.py
+++ /dev/null
@@ -1,99 +0,0 @@
-"""
-Helper functions for returning the field information that is associated
-with a model class. This includes returning all the forward and reverse
-relationships and their associated metadata.
-"""
-from collections import namedtuple
-from django.db import models
-from django.utils import six
-from django.utils.datastructures import SortedDict
-import inspect
-
-FieldInfo = namedtuple('FieldResult', ['pk', 'fields', 'forward_relations', 'reverse_relations'])
-RelationInfo = namedtuple('RelationInfo', ['field', 'related', 'to_many', 'has_through_model'])
-
-
-def _resolve_model(obj):
- """
- Resolve supplied `obj` to a Django model class.
-
- `obj` must be a Django model class itself, or a string
- representation of one. Useful in situtations like GH #1225 where
- Django may not have resolved a string-based reference to a model in
- another model's foreign key definition.
-
- String representations should have the format:
- 'appname.ModelName'
- """
- if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
- app_name, model_name = obj.split('.')
- return models.get_model(app_name, model_name)
- elif inspect.isclass(obj) and issubclass(obj, models.Model):
- return obj
- raise ValueError("{0} is not a Django model".format(obj))
-
-
-def get_field_info(model):
- """
- Given a model class, returns a `FieldInfo` instance containing metadata
- about the various field types on the model.
- """
- opts = model._meta.concrete_model._meta
-
- # Deal with the primary key.
- pk = opts.pk
- while pk.rel and pk.rel.parent_link:
- # If model is a child via multitable inheritance, use parent's pk.
- pk = pk.rel.to._meta.pk
-
- # Deal with regular fields.
- fields = SortedDict()
- for field in [field for field in opts.fields if field.serialize and not field.rel]:
- fields[field.name] = field
-
- # Deal with forward relationships.
- forward_relations = SortedDict()
- for field in [field for field in opts.fields if field.serialize and field.rel]:
- forward_relations[field.name] = RelationInfo(
- field=field,
- related=_resolve_model(field.rel.to),
- to_many=False,
- has_through_model=False
- )
-
- # Deal with forward many-to-many relationships.
- for field in [field for field in opts.many_to_many if field.serialize]:
- forward_relations[field.name] = RelationInfo(
- field=field,
- related=_resolve_model(field.rel.to),
- to_many=True,
- has_through_model=(
- not field.rel.through._meta.auto_created
- )
- )
-
- # Deal with reverse relationships.
- reverse_relations = SortedDict()
- for relation in opts.get_all_related_objects():
- accessor_name = relation.get_accessor_name()
- reverse_relations[accessor_name] = RelationInfo(
- field=None,
- related=relation.model,
- to_many=relation.field.rel.multiple,
- has_through_model=False
- )
-
- # Deal with reverse many-to-many relationships.
- for relation in opts.get_all_related_many_to_many_objects():
- accessor_name = relation.get_accessor_name()
- reverse_relations[accessor_name] = RelationInfo(
- field=None,
- related=relation.model,
- to_many=True,
- has_through_model=(
- hasattr(relation.field.rel, 'through') and
- not relation.field.rel.through._meta.auto_created
- )
- )
-
- return FieldInfo(pk, fields, forward_relations, reverse_relations)
diff --git a/tests/test_model_field_mappings.py b/tests/test_model_field_mappings.py
index d5750f9e..b04ad5f2 100644
--- a/tests/test_model_field_mappings.py
+++ b/tests/test_model_field_mappings.py
@@ -36,25 +36,25 @@ class RegularFieldsModel(models.Model):
REGULAR_FIELDS_REPR = """
TestSerializer():
- auto_field = IntegerField(label='auto field', read_only=True)
- big_integer_field = IntegerField(label='big integer field')
- boolean_field = BooleanField(default=False, label='boolean field')
- char_field = CharField(label='char field', max_length=100)
- comma_seperated_integer_field = CharField(label='comma seperated integer field', max_length=100, validators=[])
- date_field = DateField(label='date field')
- datetime_field = DateTimeField(label='datetime field')
- decimal_field = DecimalField(decimal_places=1, label='decimal field', max_digits=3)
- email_field = EmailField(label='email field', max_length=100)
- float_field = FloatField(label='float field')
- integer_field = IntegerField(label='integer field')
- null_boolean_field = BooleanField(label='null boolean field', required=False)
- positive_integer_field = IntegerField(label='positive integer field')
- positive_small_integer_field = IntegerField(label='positive small integer field')
- slug_field = SlugField(label='slug field', max_length=100)
- small_integer_field = IntegerField(label='small integer field')
- text_field = CharField(label='text field')
- time_field = TimeField(label='time field')
- url_field = URLField(label='url field', max_length=100)
+ auto_field = IntegerField(read_only=True)
+ big_integer_field = IntegerField()
+ boolean_field = BooleanField(default=False)
+ char_field = CharField(max_length=100)
+ comma_seperated_integer_field = CharField(max_length=100, validators=[])
+ date_field = DateField()
+ datetime_field = DateTimeField()
+ decimal_field = DecimalField(decimal_places=1, max_digits=3)
+ email_field = EmailField(max_length=100)
+ float_field = FloatField()
+ integer_field = IntegerField()
+ null_boolean_field = BooleanField(required=False)
+ positive_integer_field = IntegerField()
+ positive_small_integer_field = IntegerField()
+ slug_field = SlugField(max_length=100)
+ small_integer_field = IntegerField()
+ text_field = CharField()
+ time_field = TimeField()
+ url_field = URLField(max_length=100)
""".strip()
@@ -81,9 +81,9 @@ class RelationalModel(models.Model):
RELATIONAL_FLAT_REPR = """
TestSerializer():
id = IntegerField(label='ID', read_only=True)
- foreign_key = PrimaryKeyRelatedField(label='foreign key', queryset=ForeignKeyTargetModel.objects.all())
- one_to_one = PrimaryKeyRelatedField(label='one to one', queryset=OneToOneTargetModel.objects.all())
- many_to_many = PrimaryKeyRelatedField(label='many to many', many=True, queryset=ManyToManyTargetModel.objects.all())
+ foreign_key = PrimaryKeyRelatedField(queryset=ForeignKeyTargetModel.objects.all())
+ one_to_one = PrimaryKeyRelatedField(queryset=OneToOneTargetModel.objects.all())
+ many_to_many = PrimaryKeyRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all())
""".strip()
@@ -92,22 +92,22 @@ TestSerializer():
id = IntegerField(label='ID', read_only=True)
foreign_key = NestedModelSerializer(read_only=True):
id = IntegerField(label='ID', read_only=True)
- name = CharField(label='name', max_length=100)
+ name = CharField(max_length=100)
one_to_one = NestedModelSerializer(read_only=True):
id = IntegerField(label='ID', read_only=True)
- name = CharField(label='name', max_length=100)
+ name = CharField(max_length=100)
many_to_many = NestedModelSerializer(many=True, read_only=True):
id = IntegerField(label='ID', read_only=True)
- name = CharField(label='name', max_length=100)
+ name = CharField(max_length=100)
""".strip()
HYPERLINKED_FLAT_REPR = """
TestSerializer():
url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
- foreign_key = HyperlinkedRelatedField(label='foreign key', queryset=ForeignKeyTargetModel.objects.all(), view_name='foreignkeytargetmodel-detail')
- one_to_one = HyperlinkedRelatedField(label='one to one', queryset=OneToOneTargetModel.objects.all(), view_name='onetoonetargetmodel-detail')
- many_to_many = HyperlinkedRelatedField(label='many to many', many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail')
+ foreign_key = HyperlinkedRelatedField(queryset=ForeignKeyTargetModel.objects.all(), view_name='foreignkeytargetmodel-detail')
+ one_to_one = HyperlinkedRelatedField(queryset=OneToOneTargetModel.objects.all(), view_name='onetoonetargetmodel-detail')
+ many_to_many = HyperlinkedRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail')
""".strip()
@@ -116,13 +116,13 @@ TestSerializer():
url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
foreign_key = NestedModelSerializer(read_only=True):
id = IntegerField(label='ID', read_only=True)
- name = CharField(label='name', max_length=100)
+ name = CharField(max_length=100)
one_to_one = NestedModelSerializer(read_only=True):
id = IntegerField(label='ID', read_only=True)
- name = CharField(label='name', max_length=100)
+ name = CharField(max_length=100)
many_to_many = NestedModelSerializer(many=True, read_only=True):
id = IntegerField(label='ID', read_only=True)
- name = CharField(label='name', max_length=100)
+ name = CharField(max_length=100)
""".strip()
@@ -180,4 +180,4 @@ class TestSerializerMappings(TestCase):
# class Meta:
# model = ManyToManyTargetModel
# fields = ('id', 'name', 'reverse_many_to_many')
- # print repr(TestSerializer())
\ No newline at end of file
+ # print repr(TestSerializer())
--
cgit v1.2.3
From 3cf7ed10d23300104a4613bd6cf3333fd05556ab Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 15 Sep 2014 13:10:58 +0100
Subject: Fix erronous import
---
tests/test_modelinfo.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_modelinfo.py b/tests/test_modelinfo.py
index 254a33c9..04b67f04 100644
--- a/tests/test_modelinfo.py
+++ b/tests/test_modelinfo.py
@@ -1,6 +1,6 @@
from django.test import TestCase
from django.utils import six
-from rest_framework.utils.modelinfo import _resolve_model
+from rest_framework.utils.model_meta import _resolve_model
from tests.models import BasicModel
--
cgit v1.2.3
From c1e2a9cba6b4b69281ad47b64132bf5ac5691875 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 15 Sep 2014 13:38:28 +0100
Subject: Clean up field mapping tests
---
tests/test_model_field_mappings.py | 166 ++++++++++++++++++-------------------
1 file changed, 83 insertions(+), 83 deletions(-)
diff --git a/tests/test_model_field_mappings.py b/tests/test_model_field_mappings.py
index b04ad5f2..8d59e985 100644
--- a/tests/test_model_field_mappings.py
+++ b/tests/test_model_field_mappings.py
@@ -10,6 +10,10 @@ from django.test import TestCase
from rest_framework import serializers
+def dedent(blocktext):
+ return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]])
+
+
# Models for testing regular field mapping
class RegularFieldsModel(models.Model):
@@ -34,28 +38,36 @@ class RegularFieldsModel(models.Model):
url_field = models.URLField(max_length=100)
-REGULAR_FIELDS_REPR = """
-TestSerializer():
- auto_field = IntegerField(read_only=True)
- big_integer_field = IntegerField()
- boolean_field = BooleanField(default=False)
- char_field = CharField(max_length=100)
- comma_seperated_integer_field = CharField(max_length=100, validators=[])
- date_field = DateField()
- datetime_field = DateTimeField()
- decimal_field = DecimalField(decimal_places=1, max_digits=3)
- email_field = EmailField(max_length=100)
- float_field = FloatField()
- integer_field = IntegerField()
- null_boolean_field = BooleanField(required=False)
- positive_integer_field = IntegerField()
- positive_small_integer_field = IntegerField()
- slug_field = SlugField(max_length=100)
- small_integer_field = IntegerField()
- text_field = CharField()
- time_field = TimeField()
- url_field = URLField(max_length=100)
-""".strip()
+class TestRegularFieldMappings(TestCase):
+ def test_regular_fields(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+
+ expected = dedent("""
+ TestSerializer():
+ auto_field = IntegerField(read_only=True)
+ big_integer_field = IntegerField()
+ boolean_field = BooleanField(default=False)
+ char_field = CharField(max_length=100)
+ comma_seperated_integer_field = CharField(max_length=100, validators=[])
+ date_field = DateField()
+ datetime_field = DateTimeField()
+ decimal_field = DecimalField(decimal_places=1, max_digits=3)
+ email_field = EmailField(max_length=100)
+ float_field = FloatField()
+ integer_field = IntegerField()
+ null_boolean_field = BooleanField(required=False)
+ positive_integer_field = IntegerField()
+ positive_small_integer_field = IntegerField()
+ slug_field = SlugField(max_length=100)
+ small_integer_field = IntegerField()
+ text_field = CharField()
+ time_field = TimeField()
+ url_field = URLField(max_length=100)
+ """)
+
+ self.assertEqual(repr(TestSerializer()), expected)
# Model for testing relational field mapping
@@ -78,88 +90,76 @@ class RelationalModel(models.Model):
one_to_one = models.OneToOneField(OneToOneTargetModel, related_name='reverse_one_to_one')
-RELATIONAL_FLAT_REPR = """
-TestSerializer():
- id = IntegerField(label='ID', read_only=True)
- foreign_key = PrimaryKeyRelatedField(queryset=ForeignKeyTargetModel.objects.all())
- one_to_one = PrimaryKeyRelatedField(queryset=OneToOneTargetModel.objects.all())
- many_to_many = PrimaryKeyRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all())
-""".strip()
-
-
-RELATIONAL_NESTED_REPR = """
-TestSerializer():
- id = IntegerField(label='ID', read_only=True)
- foreign_key = NestedModelSerializer(read_only=True):
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
- one_to_one = NestedModelSerializer(read_only=True):
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
- many_to_many = NestedModelSerializer(many=True, read_only=True):
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
-""".strip()
-
-
-HYPERLINKED_FLAT_REPR = """
-TestSerializer():
- url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
- foreign_key = HyperlinkedRelatedField(queryset=ForeignKeyTargetModel.objects.all(), view_name='foreignkeytargetmodel-detail')
- one_to_one = HyperlinkedRelatedField(queryset=OneToOneTargetModel.objects.all(), view_name='onetoonetargetmodel-detail')
- many_to_many = HyperlinkedRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail')
-""".strip()
-
-
-HYPERLINKED_NESTED_REPR = """
-TestSerializer():
- url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
- foreign_key = NestedModelSerializer(read_only=True):
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
- one_to_one = NestedModelSerializer(read_only=True):
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
- many_to_many = NestedModelSerializer(many=True, read_only=True):
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
-""".strip()
-
-
-class TestSerializerMappings(TestCase):
- maxDiff = 10000
-
- def test_regular_fields(self):
- class TestSerializer(serializers.ModelSerializer):
- class Meta:
- model = RegularFieldsModel
- self.assertEqual(repr(TestSerializer()), REGULAR_FIELDS_REPR)
-
+class TestRelationalFieldMappings(TestCase):
def test_flat_relational_fields(self):
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = RelationalModel
- self.assertEqual(repr(TestSerializer()), RELATIONAL_FLAT_REPR)
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ foreign_key = PrimaryKeyRelatedField(queryset=ForeignKeyTargetModel.objects.all())
+ one_to_one = PrimaryKeyRelatedField(queryset=OneToOneTargetModel.objects.all())
+ many_to_many = PrimaryKeyRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all())
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
def test_nested_relational_fields(self):
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = RelationalModel
depth = 1
- self.assertEqual(repr(TestSerializer()), RELATIONAL_NESTED_REPR)
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ foreign_key = NestedModelSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ one_to_one = NestedModelSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ many_to_many = NestedModelSerializer(many=True, read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
def test_flat_hyperlinked_fields(self):
class TestSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = RelationalModel
- self.assertEqual(repr(TestSerializer()), HYPERLINKED_FLAT_REPR)
+
+ expected = dedent("""
+ TestSerializer():
+ url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
+ foreign_key = HyperlinkedRelatedField(queryset=ForeignKeyTargetModel.objects.all(), view_name='foreignkeytargetmodel-detail')
+ one_to_one = HyperlinkedRelatedField(queryset=OneToOneTargetModel.objects.all(), view_name='onetoonetargetmodel-detail')
+ many_to_many = HyperlinkedRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail')
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
def test_nested_hyperlinked_fields(self):
class TestSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = RelationalModel
depth = 1
- self.assertEqual(repr(TestSerializer()), HYPERLINKED_NESTED_REPR)
+
+ expected = dedent("""
+ TestSerializer():
+ url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
+ foreign_key = NestedModelSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ one_to_one = NestedModelSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ many_to_many = NestedModelSerializer(many=True, read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
# def test_flat_reverse_foreign_key(self):
# class TestSerializer(serializers.ModelSerializer):
--
cgit v1.2.3
From 0c15b97b331f7937cbc5d85f660c07b3c92917a0 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 15 Sep 2014 13:48:03 +0100
Subject: Tests for reverse relationships
---
tests/test_model_field_mappings.py | 63 +++++++++++++++++++++++++-------------
1 file changed, 42 insertions(+), 21 deletions(-)
diff --git a/tests/test_model_field_mappings.py b/tests/test_model_field_mappings.py
index 8d59e985..a32345b3 100644
--- a/tests/test_model_field_mappings.py
+++ b/tests/test_model_field_mappings.py
@@ -158,26 +158,47 @@ class TestRelationalFieldMappings(TestCase):
many_to_many = NestedModelSerializer(many=True, read_only=True):
id = IntegerField(label='ID', read_only=True)
name = CharField(max_length=100)
- """)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_flat_reverse_foreign_key(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ForeignKeyTargetModel
+ fields = ('id', 'name', 'reverse_foreign_key')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_foreign_key = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
+ """)
self.assertEqual(repr(TestSerializer()), expected)
- # def test_flat_reverse_foreign_key(self):
- # class TestSerializer(serializers.ModelSerializer):
- # class Meta:
- # model = ForeignKeyTargetModel
- # fields = ('id', 'name', 'reverse_foreign_key')
- # print repr(TestSerializer())
-
- # def test_flat_reverse_one_to_one(self):
- # class TestSerializer(serializers.ModelSerializer):
- # class Meta:
- # model = OneToOneTargetModel
- # fields = ('id', 'name', 'reverse_one_to_one')
- # print repr(TestSerializer())
-
- # def test_flat_reverse_many_to_many(self):
- # class TestSerializer(serializers.ModelSerializer):
- # class Meta:
- # model = ManyToManyTargetModel
- # fields = ('id', 'name', 'reverse_many_to_many')
- # print repr(TestSerializer())
+ def test_flat_reverse_one_to_one(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = OneToOneTargetModel
+ fields = ('id', 'name', 'reverse_one_to_one')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_one_to_one = PrimaryKeyRelatedField(queryset=RelationalModel.objects.all())
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_flat_reverse_many_to_many(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ManyToManyTargetModel
+ fields = ('id', 'name', 'reverse_many_to_many')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_many_to_many = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
--
cgit v1.2.3
From d196608d5af912057baba79ab13d05d876368ad2 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 15 Sep 2014 13:55:09 +0100
Subject: Fix nested model serializer base class
---
rest_framework/serializers.py | 7 ++++++-
tests/test_model_field_mappings.py | 6 +++---
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index ba8d475f..40d76897 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -368,6 +368,7 @@ class ModelSerializer(Serializer):
models.TimeField: TimeField,
models.URLField: URLField,
}
+ nested_class = None # We fill this in at the end of this module.
_options_class = ModelSerializerOptions
@@ -454,7 +455,7 @@ class ModelSerializer(Serializer):
Note that model_field will be `None` for reverse relationships.
"""
- class NestedModelSerializer(ModelSerializer):
+ class NestedModelSerializer(self.nested_class):
class Meta:
model = related_model
depth = self.opts.depth - 1
@@ -694,3 +695,7 @@ class HyperlinkedModelSerializer(ModelSerializer):
'app_label': model._meta.app_label,
'model_name': model._meta.object_name.lower()
}
+
+
+ModelSerializer.nested_class = ModelSerializer
+HyperlinkedModelSerializer.nested_class = HyperlinkedModelSerializer
diff --git a/tests/test_model_field_mappings.py b/tests/test_model_field_mappings.py
index a32345b3..d946172e 100644
--- a/tests/test_model_field_mappings.py
+++ b/tests/test_model_field_mappings.py
@@ -150,13 +150,13 @@ class TestRelationalFieldMappings(TestCase):
TestSerializer():
url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
foreign_key = NestedModelSerializer(read_only=True):
- id = IntegerField(label='ID', read_only=True)
+ url = HyperlinkedIdentityField(view_name='foreignkeytargetmodel-detail')
name = CharField(max_length=100)
one_to_one = NestedModelSerializer(read_only=True):
- id = IntegerField(label='ID', read_only=True)
+ url = HyperlinkedIdentityField(view_name='onetoonetargetmodel-detail')
name = CharField(max_length=100)
many_to_many = NestedModelSerializer(many=True, read_only=True):
- id = IntegerField(label='ID', read_only=True)
+ url = HyperlinkedIdentityField(view_name='manytomanytargetmodel-detail')
name = CharField(max_length=100)
""")
self.assertEqual(repr(TestSerializer()), expected)
--
cgit v1.2.3
From 4ddc661b01410d0073d463ae729f152cabf2ef4d Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 15 Sep 2014 14:05:58 +0100
Subject: Tests for through relationships
---
tests/test_model_field_mappings.py | 33 +++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/tests/test_model_field_mappings.py b/tests/test_model_field_mappings.py
index d946172e..957bf661 100644
--- a/tests/test_model_field_mappings.py
+++ b/tests/test_model_field_mappings.py
@@ -84,10 +84,21 @@ class OneToOneTargetModel(models.Model):
name = models.CharField(max_length=100)
+class ThroughTargetModel(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class Supplementary(models.Model):
+ extra = models.IntegerField()
+ forwards = models.ForeignKey('ThroughTargetModel')
+ backwards = models.ForeignKey('RelationalModel')
+
+
class RelationalModel(models.Model):
foreign_key = models.ForeignKey(ForeignKeyTargetModel, related_name='reverse_foreign_key')
many_to_many = models.ManyToManyField(ManyToManyTargetModel, related_name='reverse_many_to_many')
one_to_one = models.OneToOneField(OneToOneTargetModel, related_name='reverse_one_to_one')
+ through = models.ManyToManyField(ThroughTargetModel, through=Supplementary, related_name='reverse_through')
class TestRelationalFieldMappings(TestCase):
@@ -102,6 +113,7 @@ class TestRelationalFieldMappings(TestCase):
foreign_key = PrimaryKeyRelatedField(queryset=ForeignKeyTargetModel.objects.all())
one_to_one = PrimaryKeyRelatedField(queryset=OneToOneTargetModel.objects.all())
many_to_many = PrimaryKeyRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all())
+ through = PrimaryKeyRelatedField(many=True, read_only=True)
""")
self.assertEqual(repr(TestSerializer()), expected)
@@ -123,6 +135,9 @@ class TestRelationalFieldMappings(TestCase):
many_to_many = NestedModelSerializer(many=True, read_only=True):
id = IntegerField(label='ID', read_only=True)
name = CharField(max_length=100)
+ through = NestedModelSerializer(many=True, read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
""")
self.assertEqual(repr(TestSerializer()), expected)
@@ -137,6 +152,7 @@ class TestRelationalFieldMappings(TestCase):
foreign_key = HyperlinkedRelatedField(queryset=ForeignKeyTargetModel.objects.all(), view_name='foreignkeytargetmodel-detail')
one_to_one = HyperlinkedRelatedField(queryset=OneToOneTargetModel.objects.all(), view_name='onetoonetargetmodel-detail')
many_to_many = HyperlinkedRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail')
+ through = HyperlinkedRelatedField(many=True, read_only=True, view_name='throughtargetmodel-detail')
""")
self.assertEqual(repr(TestSerializer()), expected)
@@ -158,6 +174,9 @@ class TestRelationalFieldMappings(TestCase):
many_to_many = NestedModelSerializer(many=True, read_only=True):
url = HyperlinkedIdentityField(view_name='manytomanytargetmodel-detail')
name = CharField(max_length=100)
+ through = NestedModelSerializer(many=True, read_only=True):
+ url = HyperlinkedIdentityField(view_name='throughtargetmodel-detail')
+ name = CharField(max_length=100)
""")
self.assertEqual(repr(TestSerializer()), expected)
@@ -202,3 +221,17 @@ class TestRelationalFieldMappings(TestCase):
reverse_many_to_many = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
""")
self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_flat_reverse_through(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ThroughTargetModel
+ fields = ('id', 'name', 'reverse_through')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_through = PrimaryKeyRelatedField(many=True, read_only=True)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
--
cgit v1.2.3
From c0155fd9dc654dc5932effd46a00f66495ce700b Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 17 Sep 2014 14:11:53 +0100
Subject: Update comments
---
rest_framework/serializers.py | 2 ++
tests/test_model_field_mappings.py | 4 ++--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 40d76897..1fea1380 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -410,10 +410,12 @@ class ModelSerializer(Serializer):
info = model_meta.get_field_info(self.opts.model)
ret = SortedDict()
+ # URL field
serializer_url_field = self.get_url_field()
if serializer_url_field:
ret[api_settings.URL_FIELD_NAME] = serializer_url_field
+ # Primary key field
field_name = info.pk.name
serializer_pk_field = self.get_pk_field(field_name, info.pk)
if serializer_pk_field:
diff --git a/tests/test_model_field_mappings.py b/tests/test_model_field_mappings.py
index 957bf661..bae63e5a 100644
--- a/tests/test_model_field_mappings.py
+++ b/tests/test_model_field_mappings.py
@@ -14,7 +14,7 @@ def dedent(blocktext):
return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]])
-# Models for testing regular field mapping
+# Testing regular field mappings
class RegularFieldsModel(models.Model):
auto_field = models.AutoField(primary_key=True)
@@ -70,7 +70,7 @@ class TestRegularFieldMappings(TestCase):
self.assertEqual(repr(TestSerializer()), expected)
-# Model for testing relational field mapping
+# Testing relational field mappings
class ForeignKeyTargetModel(models.Model):
name = models.CharField(max_length=100)
--
cgit v1.2.3
From 5b7e4af0d657a575cb15eea85a63a7100c636085 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 18 Sep 2014 11:20:56 +0100
Subject: get_base_field() refactor
---
rest_framework/fields.py | 6 +-
rest_framework/relations.py | 9 +-
rest_framework/serializers.py | 464 ++++++++--------------------------
rest_framework/utils/field_mapping.py | 215 ++++++++++++++++
rest_framework/utils/model_meta.py | 46 +++-
tests/models.py | 7 -
tests/test_model_field_mappings.py | 16 +-
tests/test_response.py | 9 +-
tests/test_routers.py | 7 +-
9 files changed, 384 insertions(+), 395 deletions(-)
create mode 100644 rest_framework/utils/field_mapping.py
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 1818e705..0c78b3fb 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -80,10 +80,6 @@ def set_value(dictionary, keys, value):
dictionary[keys[-1]] = value
-def field_name_to_label(field_name):
- return field_name.replace('_', ' ').capitalize()
-
-
class SkipField(Exception):
pass
@@ -162,7 +158,7 @@ class Field(object):
# `self.label` should deafult to being based on the field name.
if self.label is None:
- self.label = field_name_to_label(self.field_name)
+ self.label = field_name.replace('_', ' ').capitalize()
# self.source should default to being the same as the field name.
if self.source is None:
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 46fe55ef..9f44ab63 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -73,7 +73,8 @@ class HyperlinkedRelatedField(RelatedField):
'incorrect_type': 'Incorrect type. Expected URL string, received {data_type}.',
}
- def __init__(self, view_name, **kwargs):
+ def __init__(self, view_name=None, **kwargs):
+ assert view_name is not None, 'The `view_name` argument is required.'
self.view_name = view_name
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
@@ -182,7 +183,8 @@ class HyperlinkedIdentityField(HyperlinkedRelatedField):
URL of relationships to other objects.
"""
- def __init__(self, view_name, **kwargs):
+ def __init__(self, view_name=None, **kwargs):
+ assert view_name is not None, 'The `view_name` argument is required.'
kwargs['read_only'] = True
kwargs['source'] = '*'
super(HyperlinkedIdentityField, self).__init__(view_name, **kwargs)
@@ -199,7 +201,8 @@ class SlugRelatedField(RelatedField):
'invalid': _('Invalid value.'),
}
- def __init__(self, slug_field, **kwargs):
+ def __init__(self, slug_field=None, **kwargs):
+ assert slug_field is not None, 'The `slug_field` argument is required.'
self.slug_field = slug_field
super(SlugRelatedField, self).__init__(**kwargs)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 1fea1380..99dcc349 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -10,17 +10,19 @@ python primitives.
2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
-from django.core import validators
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import six
from django.utils.datastructures import SortedDict
-from django.utils.text import capfirst
from collections import namedtuple
-from rest_framework.compat import clean_manytomany_helptext
from rest_framework.fields import empty, set_value, Field, SkipField
from rest_framework.settings import api_settings
from rest_framework.utils import html, model_meta, representation
+from rest_framework.utils.field_mapping import (
+ get_url_kwargs, get_field_kwargs,
+ get_relation_kwargs, get_nested_relation_kwargs,
+ lookup_class
+)
import copy
# Note: We do the following so that users of the framework can use this style:
@@ -126,7 +128,7 @@ class SerializerMetaclass(type):
"""
@classmethod
- def _get_fields(cls, bases, attrs):
+ def _get_declared_fields(cls, bases, attrs):
fields = [(field_name, attrs.pop(field_name))
for field_name, obj in list(attrs.items())
if isinstance(obj, Field)]
@@ -136,25 +138,18 @@ class SerializerMetaclass(type):
# fields. Note that we loop over the bases in *reverse*. This is necessary
# in order to maintain the correct order of fields.
for base in bases[::-1]:
- if hasattr(base, 'base_fields'):
- fields = list(base.base_fields.items()) + fields
+ if hasattr(base, '_declared_fields'):
+ fields = list(base._declared_fields.items()) + fields
return SortedDict(fields)
def __new__(cls, name, bases, attrs):
- attrs['base_fields'] = cls._get_fields(bases, attrs)
+ attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs)
return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
@six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer):
-
- def __new__(cls, *args, **kwargs):
- if kwargs.pop('many', False):
- kwargs['child'] = cls()
- return ListSerializer(*args, **kwargs)
- return super(Serializer, cls).__new__(cls, *args, **kwargs)
-
def __init__(self, *args, **kwargs):
self.context = kwargs.pop('context', {})
kwargs.pop('partial', None)
@@ -165,14 +160,22 @@ class Serializer(BaseSerializer):
# Every new serializer is created with a clone of the field instances.
# This allows users to dynamically modify the fields on a serializer
# instance without affecting every other serializer class.
- self.fields = self.get_fields()
+ self.fields = self._get_base_fields()
# Setup all the child fields, to provide them with the current context.
for field_name, field in self.fields.items():
field.bind(field_name, self, self)
- def get_fields(self):
- return copy.deepcopy(self.base_fields)
+ def __new__(cls, *args, **kwargs):
+ # We override this method in order to automagically create
+ # `ListSerializer` classes instead when `many=True` is set.
+ if kwargs.pop('many', False):
+ kwargs['child'] = cls()
+ return ListSerializer(*args, **kwargs)
+ return super(Serializer, cls).__new__(cls, *args, **kwargs)
+
+ def _get_base_fields(self):
+ return copy.deepcopy(self._declared_fields)
def bind(self, field_name, parent, root):
# If the serializer is used as a field then when it becomes bound
@@ -312,39 +315,8 @@ class ListSerializer(BaseSerializer):
return representation.list_repr(self, indent=1)
-class ModelSerializerOptions(object):
- """
- Meta class options for ModelSerializer
- """
- def __init__(self, meta):
- self.model = getattr(meta, 'model')
- self.fields = getattr(meta, 'fields', ())
- self.depth = getattr(meta, 'depth', 0)
-
-
-def lookup_class(mapping, instance):
- """
- Takes a dictionary with classes as keys, and an object.
- Traverses the object's inheritance hierarchy in method
- resolution order, and returns the first matching value
- from the dictionary or raises a KeyError if nothing matches.
- """
- for cls in inspect.getmro(instance.__class__):
- if cls in mapping:
- return mapping[cls]
- raise KeyError('Class %s not found in lookup.', cls.__name__)
-
-
-def needs_label(model_field, field_name):
- """
- Returns `True` if the label based on the model's verbose name
- is not equal to the default label it would have based on it's field name.
- """
- return capfirst(model_field.verbose_name) != field_name_to_label(field_name)
-
-
class ModelSerializer(Serializer):
- field_mapping = {
+ _field_mapping = {
models.AutoField: IntegerField,
models.BigIntegerField: IntegerField,
models.BooleanField: BooleanField,
@@ -368,16 +340,10 @@ class ModelSerializer(Serializer):
models.TimeField: TimeField,
models.URLField: URLField,
}
- nested_class = None # We fill this in at the end of this module.
-
- _options_class = ModelSerializerOptions
-
- def __init__(self, *args, **kwargs):
- self.opts = self._options_class(self.Meta)
- super(ModelSerializer, self).__init__(*args, **kwargs)
+ _related_class = PrimaryKeyRelatedField
def create(self, attrs):
- ModelClass = self.opts.model
+ ModelClass = self.Meta.model
return ModelClass.objects.create(**attrs)
def update(self, obj, attrs):
@@ -385,319 +351,97 @@ class ModelSerializer(Serializer):
setattr(obj, attr, value)
obj.save()
- def get_fields(self):
- # Get the explicitly declared fields.
- fields = copy.deepcopy(self.base_fields)
+ def _get_base_fields(self):
+ declared_fields = copy.deepcopy(self._declared_fields)
- # Add in the default fields.
- for key, val in self.get_default_fields().items():
- if key not in fields:
- fields[key] = val
-
- # If `fields` is set on the `Meta` class,
- # then use only those fields, and in that order.
- if self.opts.fields:
- fields = SortedDict([
- (key, fields[key]) for key in self.opts.fields
- ])
-
- return fields
-
- def get_default_fields(self):
- """
- Return all the fields that should be serialized for the model.
- """
- info = model_meta.get_field_info(self.opts.model)
ret = SortedDict()
+ model = getattr(self.Meta, 'model')
+ fields = getattr(self.Meta, 'fields', None)
+ depth = getattr(self.Meta, 'depth', 0)
+
+ # Retrieve metadata about fields & relationships on the model class.
+ info = model_meta.get_field_info(model)
+
+ # Use the default set of fields if none is supplied explicitly.
+ if fields is None:
+ fields = self._get_default_field_names(declared_fields, info)
+
+ for field_name in fields:
+ if field_name in declared_fields:
+ # Field is explicitly declared on the class, use that.
+ ret[field_name] = declared_fields[field_name]
+ continue
+
+ elif field_name == api_settings.URL_FIELD_NAME:
+ # Create the URL field.
+ field_cls = HyperlinkedIdentityField
+ kwargs = get_url_kwargs(model)
+
+ elif field_name in info.fields_and_pk:
+ # Create regular model fields.
+ model_field = info.fields_and_pk[field_name]
+ field_cls = lookup_class(self._field_mapping, model_field)
+ kwargs = get_field_kwargs(field_name, model_field)
+ if 'choices' in kwargs:
+ # Fields with choices get coerced into `ChoiceField`
+ # instead of using their regular typed field.
+ field_cls = ChoiceField
+ if not issubclass(field_cls, ModelField):
+ # `model_field` is only valid for the fallback case of
+ # `ModelField`, which is used when no other typed field
+ # matched to the model field.
+ kwargs.pop('model_field', None)
+
+ elif field_name in info.relations:
+ # Create forward and reverse relationships.
+ relation_info = info.relations[field_name]
+ if depth:
+ field_cls = self._get_nested_class(depth, relation_info)
+ kwargs = get_nested_relation_kwargs(relation_info)
+ else:
+ field_cls = self._related_class
+ kwargs = get_relation_kwargs(field_name, relation_info)
+ # `view_name` is only valid for hyperlinked relationships.
+ if not issubclass(field_cls, HyperlinkedRelatedField):
+ kwargs.pop('view_name', None)
- # URL field
- serializer_url_field = self.get_url_field()
- if serializer_url_field:
- ret[api_settings.URL_FIELD_NAME] = serializer_url_field
-
- # Primary key field
- field_name = info.pk.name
- serializer_pk_field = self.get_pk_field(field_name, info.pk)
- if serializer_pk_field:
- ret[field_name] = serializer_pk_field
-
- # Regular fields
- for field_name, field in info.fields.items():
- ret[field_name] = self.get_field(field_name, field)
-
- # Forward relations
- for field_name, relation_info in info.forward_relations.items():
- if self.opts.depth:
- ret[field_name] = self.get_nested_field(field_name, *relation_info)
else:
- ret[field_name] = self.get_related_field(field_name, *relation_info)
+ assert False, 'Field name `%s` is not valid.' % field_name
- # Reverse relations
- for accessor_name, relation_info in info.reverse_relations.items():
- if accessor_name in self.opts.fields:
- if self.opts.depth:
- ret[accessor_name] = self.get_nested_field(accessor_name, *relation_info)
- else:
- ret[accessor_name] = self.get_related_field(accessor_name, *relation_info)
+ ret[field_name] = field_cls(**kwargs)
return ret
- def get_url_field(self):
- return None
-
- def get_pk_field(self, field_name, model_field):
- """
- Returns a default instance of the pk field.
- """
- return self.get_field(field_name, model_field)
-
- def get_nested_field(self, field_name, model_field, related_model, to_many, has_through_model):
- """
- Creates a default instance of a nested relational field.
+ def _get_default_field_names(self, declared_fields, model_info):
+ return (
+ [model_info.pk.name] +
+ list(declared_fields.keys()) +
+ list(model_info.fields.keys()) +
+ list(model_info.forward_relations.keys())
+ )
- Note that model_field will be `None` for reverse relationships.
- """
- class NestedModelSerializer(self.nested_class):
+ def _get_nested_class(self, nested_depth, relation_info):
+ class NestedSerializer(ModelSerializer):
class Meta:
- model = related_model
- depth = self.opts.depth - 1
-
- kwargs = {'read_only': True}
- if to_many:
- kwargs['many'] = True
- return NestedModelSerializer(**kwargs)
-
- def get_related_field(self, field_name, model_field, related_model, to_many, has_through_model):
- """
- Creates a default instance of a flat relational field.
-
- Note that model_field will be `None` for reverse relationships.
- """
- kwargs = {
- 'queryset': related_model._default_manager,
- }
-
- if to_many:
- kwargs['many'] = True
-
- if has_through_model:
- kwargs['read_only'] = True
- kwargs.pop('queryset', None)
-
- if model_field:
- if model_field.null or model_field.blank:
- kwargs['required'] = False
- if model_field.verbose_name and needs_label(model_field, field_name):
- kwargs['label'] = capfirst(model_field.verbose_name)
- if not model_field.editable:
- kwargs['read_only'] = True
- kwargs.pop('queryset', None)
- help_text = clean_manytomany_helptext(model_field.help_text)
- if help_text:
- kwargs['help_text'] = help_text
-
- return PrimaryKeyRelatedField(**kwargs)
-
- def get_field(self, field_name, model_field):
- """
- Creates a default instance of a basic non-relational field.
- """
- serializer_cls = lookup_class(self.field_mapping, model_field)
- kwargs = {}
- validator_kwarg = model_field.validators
-
- if model_field.null or model_field.blank:
- kwargs['required'] = False
-
- if model_field.verbose_name and needs_label(model_field, field_name):
- kwargs['label'] = capfirst(model_field.verbose_name)
-
- if model_field.help_text:
- kwargs['help_text'] = model_field.help_text
-
- if isinstance(model_field, models.AutoField) or not model_field.editable:
- kwargs['read_only'] = True
- # Read only implies that the field is not required.
- # We have a cleaner repr on the instance if we don't set it.
- kwargs.pop('required', None)
-
- if model_field.has_default():
- kwargs['default'] = model_field.get_default()
- # Having a default implies that the field is not required.
- # We have a cleaner repr on the instance if we don't set it.
- kwargs.pop('required', None)
-
- if model_field.flatchoices:
- # If this model field contains choices, then use a ChoiceField,
- # rather than the standard serializer field for this type.
- # Note that we return this prior to setting any validation type
- # keyword arguments, as those are not valid initializers.
- kwargs['choices'] = model_field.flatchoices
- return ChoiceField(**kwargs)
-
- # Ensure that max_length is passed explicitly as a keyword arg,
- # rather than as a validator.
- max_length = getattr(model_field, 'max_length', None)
- if max_length is not None:
- kwargs['max_length'] = max_length
- validator_kwarg = [
- validator for validator in validator_kwarg
- if not isinstance(validator, validators.MaxLengthValidator)
- ]
-
- # Ensure that min_length is passed explicitly as a keyword arg,
- # rather than as a validator.
- min_length = getattr(model_field, 'min_length', None)
- if min_length is not None:
- kwargs['min_length'] = min_length
- validator_kwarg = [
- validator for validator in validator_kwarg
- if not isinstance(validator, validators.MinLengthValidator)
- ]
-
- # Ensure that max_value is passed explicitly as a keyword arg,
- # rather than as a validator.
- max_value = next((
- validator.limit_value for validator in validator_kwarg
- if isinstance(validator, validators.MaxValueValidator)
- ), None)
- if max_value is not None:
- kwargs['max_value'] = max_value
- validator_kwarg = [
- validator for validator in validator_kwarg
- if not isinstance(validator, validators.MaxValueValidator)
- ]
-
- # Ensure that max_value is passed explicitly as a keyword arg,
- # rather than as a validator.
- min_value = next((
- validator.limit_value for validator in validator_kwarg
- if isinstance(validator, validators.MinValueValidator)
- ), None)
- if min_value is not None:
- kwargs['min_value'] = min_value
- validator_kwarg = [
- validator for validator in validator_kwarg
- if not isinstance(validator, validators.MinValueValidator)
- ]
-
- # URLField does not need to include the URLValidator argument,
- # as it is explicitly added in.
- if isinstance(model_field, models.URLField):
- validator_kwarg = [
- validator for validator in validator_kwarg
- if not isinstance(validator, validators.URLValidator)
- ]
-
- # EmailField does not need to include the validate_email argument,
- # as it is explicitly added in.
- if isinstance(model_field, models.EmailField):
- validator_kwarg = [
- validator for validator in validator_kwarg
- if validator is not validators.validate_email
- ]
-
- # SlugField do not need to include the 'validate_slug' argument,
- if isinstance(model_field, models.SlugField):
- validator_kwarg = [
- validator for validator in validator_kwarg
- if validator is not validators.validate_slug
- ]
-
- max_digits = getattr(model_field, 'max_digits', None)
- if max_digits is not None:
- kwargs['max_digits'] = max_digits
-
- decimal_places = getattr(model_field, 'decimal_places', None)
- if decimal_places is not None:
- kwargs['decimal_places'] = decimal_places
-
- if isinstance(model_field, models.BooleanField):
- # models.BooleanField has `blank=True`, but *is* actually
- # required *unless* a default is provided.
- # Also note that Django<1.6 uses `default=False` for
- # models.BooleanField, but Django>=1.6 uses `default=None`.
- kwargs.pop('required', None)
-
- if validator_kwarg:
- kwargs['validators'] = validator_kwarg
-
- if issubclass(serializer_cls, ModelField):
- kwargs['model_field'] = model_field
-
- return serializer_cls(**kwargs)
-
-
-class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
- """
- Options for HyperlinkedModelSerializer
- """
- def __init__(self, meta):
- super(HyperlinkedModelSerializerOptions, self).__init__(meta)
- self.view_name = getattr(meta, 'view_name', None)
- self.lookup_field = getattr(meta, 'lookup_field', None)
+ model = relation_info.related
+ depth = nested_depth
+ return NestedSerializer
class HyperlinkedModelSerializer(ModelSerializer):
- _options_class = HyperlinkedModelSerializerOptions
-
- def get_url_field(self):
- if self.opts.view_name is not None:
- view_name = self.opts.view_name
- else:
- view_name = self.get_default_view_name(self.opts.model)
-
- kwargs = {
- 'view_name': view_name
- }
- if self.opts.lookup_field:
- kwargs['lookup_field'] = self.opts.lookup_field
-
- return HyperlinkedIdentityField(**kwargs)
-
- def get_pk_field(self, field_name, model_field):
- if self.opts.fields and model_field.name in self.opts.fields:
- return self.get_field(model_field)
-
- def get_related_field(self, field_name, model_field, related_model, to_many, has_through_model):
- """
- Creates a default instance of a flat relational field.
- """
- kwargs = {
- 'queryset': related_model._default_manager,
- 'view_name': self.get_default_view_name(related_model),
- }
-
- if to_many:
- kwargs['many'] = True
-
- if has_through_model:
- kwargs['read_only'] = True
- kwargs.pop('queryset', None)
-
- if model_field:
- if model_field.null or model_field.blank:
- kwargs['required'] = False
- if model_field.verbose_name and needs_label(model_field, field_name):
- kwargs['label'] = capfirst(model_field.verbose_name)
- if not model_field.editable:
- kwargs['read_only'] = True
- kwargs.pop('queryset', None)
- help_text = clean_manytomany_helptext(model_field.help_text)
- if help_text:
- kwargs['help_text'] = help_text
-
- return HyperlinkedRelatedField(**kwargs)
-
- def get_default_view_name(self, model):
- """
- Return the view name to use for related models.
- """
- return '%(model_name)s-detail' % {
- 'app_label': model._meta.app_label,
- 'model_name': model._meta.object_name.lower()
- }
-
-
-ModelSerializer.nested_class = ModelSerializer
-HyperlinkedModelSerializer.nested_class = HyperlinkedModelSerializer
+ _related_class = HyperlinkedRelatedField
+
+ def _get_default_field_names(self, declared_fields, model_info):
+ return (
+ [api_settings.URL_FIELD_NAME] +
+ list(declared_fields.keys()) +
+ list(model_info.fields.keys()) +
+ list(model_info.forward_relations.keys())
+ )
+
+ def _get_nested_class(self, nested_depth, relation_info):
+ class NestedSerializer(HyperlinkedModelSerializer):
+ class Meta:
+ model = relation_info.related
+ depth = nested_depth
+ return NestedSerializer
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
new file mode 100644
index 00000000..be72e444
--- /dev/null
+++ b/rest_framework/utils/field_mapping.py
@@ -0,0 +1,215 @@
+"""
+Helper functions for mapping model fields to a dictionary of default
+keyword arguments that should be used for their equivelent serializer fields.
+"""
+from django.core import validators
+from django.db import models
+from django.utils.text import capfirst
+from rest_framework.compat import clean_manytomany_helptext
+import inspect
+
+
+def lookup_class(mapping, instance):
+ """
+ Takes a dictionary with classes as keys, and an object.
+ Traverses the object's inheritance hierarchy in method
+ resolution order, and returns the first matching value
+ from the dictionary or raises a KeyError if nothing matches.
+ """
+ for cls in inspect.getmro(instance.__class__):
+ if cls in mapping:
+ return mapping[cls]
+ raise KeyError('Class %s not found in lookup.', cls.__name__)
+
+
+def needs_label(model_field, field_name):
+ """
+ Returns `True` if the label based on the model's verbose name
+ is not equal to the default label it would have based on it's field name.
+ """
+ default_label = field_name.replace('_', ' ').capitalize()
+ return capfirst(model_field.verbose_name) != default_label
+
+
+def get_detail_view_name(model):
+ """
+ Given a model class, return the view name to use for URL relationships
+ that refer to instances of the model.
+ """
+ return '%(model_name)s-detail' % {
+ 'app_label': model._meta.app_label,
+ 'model_name': model._meta.object_name.lower()
+ }
+
+
+def get_field_kwargs(field_name, model_field):
+ """
+ Creates a default instance of a basic non-relational field.
+ """
+ kwargs = {}
+ validator_kwarg = model_field.validators
+
+ if model_field.null or model_field.blank:
+ kwargs['required'] = False
+
+ if model_field.verbose_name and needs_label(model_field, field_name):
+ kwargs['label'] = capfirst(model_field.verbose_name)
+
+ if model_field.help_text:
+ kwargs['help_text'] = model_field.help_text
+
+ if isinstance(model_field, models.AutoField) or not model_field.editable:
+ kwargs['read_only'] = True
+ # Read only implies that the field is not required.
+ # We have a cleaner repr on the instance if we don't set it.
+ kwargs.pop('required', None)
+
+ if model_field.has_default():
+ kwargs['default'] = model_field.get_default()
+ # Having a default implies that the field is not required.
+ # We have a cleaner repr on the instance if we don't set it.
+ kwargs.pop('required', None)
+
+ if model_field.flatchoices:
+ # If this model field contains choices, then return now,
+ # any further keyword arguments are not valid.
+ kwargs['choices'] = model_field.flatchoices
+ return kwargs
+
+ # Ensure that max_length is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ max_length = getattr(model_field, 'max_length', None)
+ if max_length is not None:
+ kwargs['max_length'] = max_length
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MaxLengthValidator)
+ ]
+
+ # Ensure that min_length is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ min_length = getattr(model_field, 'min_length', None)
+ if min_length is not None:
+ kwargs['min_length'] = min_length
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MinLengthValidator)
+ ]
+
+ # Ensure that max_value is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ max_value = next((
+ validator.limit_value for validator in validator_kwarg
+ if isinstance(validator, validators.MaxValueValidator)
+ ), None)
+ if max_value is not None:
+ kwargs['max_value'] = max_value
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MaxValueValidator)
+ ]
+
+ # Ensure that max_value is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ min_value = next((
+ validator.limit_value for validator in validator_kwarg
+ if isinstance(validator, validators.MinValueValidator)
+ ), None)
+ if min_value is not None:
+ kwargs['min_value'] = min_value
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MinValueValidator)
+ ]
+
+ # URLField does not need to include the URLValidator argument,
+ # as it is explicitly added in.
+ if isinstance(model_field, models.URLField):
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.URLValidator)
+ ]
+
+ # EmailField does not need to include the validate_email argument,
+ # as it is explicitly added in.
+ if isinstance(model_field, models.EmailField):
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if validator is not validators.validate_email
+ ]
+
+ # SlugField do not need to include the 'validate_slug' argument,
+ if isinstance(model_field, models.SlugField):
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if validator is not validators.validate_slug
+ ]
+
+ max_digits = getattr(model_field, 'max_digits', None)
+ if max_digits is not None:
+ kwargs['max_digits'] = max_digits
+
+ decimal_places = getattr(model_field, 'decimal_places', None)
+ if decimal_places is not None:
+ kwargs['decimal_places'] = decimal_places
+
+ if isinstance(model_field, models.BooleanField):
+ # models.BooleanField has `blank=True`, but *is* actually
+ # required *unless* a default is provided.
+ # Also note that Django<1.6 uses `default=False` for
+ # models.BooleanField, but Django>=1.6 uses `default=None`.
+ kwargs.pop('required', None)
+
+ if validator_kwarg:
+ kwargs['validators'] = validator_kwarg
+
+ # The following will only be used by ModelField classes.
+ # Gets removed for everything else.
+ kwargs['model_field'] = model_field
+
+ return kwargs
+
+
+def get_relation_kwargs(field_name, relation_info):
+ """
+ Creates a default instance of a flat relational field.
+ """
+ model_field, related_model, to_many, has_through_model = relation_info
+ kwargs = {
+ 'queryset': related_model._default_manager,
+ 'view_name': get_detail_view_name(related_model)
+ }
+
+ if to_many:
+ kwargs['many'] = True
+
+ if has_through_model:
+ kwargs['read_only'] = True
+ kwargs.pop('queryset', None)
+
+ if model_field:
+ if model_field.null or model_field.blank:
+ kwargs['required'] = False
+ if model_field.verbose_name and needs_label(model_field, field_name):
+ kwargs['label'] = capfirst(model_field.verbose_name)
+ if not model_field.editable:
+ kwargs['read_only'] = True
+ kwargs.pop('queryset', None)
+ help_text = clean_manytomany_helptext(model_field.help_text)
+ if help_text:
+ kwargs['help_text'] = help_text
+
+ return kwargs
+
+
+def get_nested_relation_kwargs(relation_info):
+ kwargs = {'read_only': True}
+ if relation_info.to_many:
+ kwargs['many'] = True
+ return kwargs
+
+
+def get_url_kwargs(model_field):
+ return {
+ 'view_name': get_detail_view_name(model_field)
+ }
diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py
index 960fa4d0..b6c41174 100644
--- a/rest_framework/utils/model_meta.py
+++ b/rest_framework/utils/model_meta.py
@@ -1,7 +1,9 @@
"""
-Helper functions for returning the field information that is associated
+Helper function for returning the field information that is associated
with a model class. This includes returning all the forward and reverse
relationships and their associated metadata.
+
+Usage: `get_field_info(model)` returns a `FieldInfo` instance.
"""
from collections import namedtuple
from django.db import models
@@ -9,8 +11,22 @@ from django.utils import six
from django.utils.datastructures import SortedDict
import inspect
-FieldInfo = namedtuple('FieldResult', ['pk', 'fields', 'forward_relations', 'reverse_relations'])
-RelationInfo = namedtuple('RelationInfo', ['field', 'related', 'to_many', 'has_through_model'])
+
+FieldInfo = namedtuple('FieldResult', [
+ 'pk', # Model field instance
+ 'fields', # Dict of field name -> model field instance
+ 'forward_relations', # Dict of field name -> RelationInfo
+ 'reverse_relations', # Dict of field name -> RelationInfo
+ 'fields_and_pk', # Shortcut for 'pk' + 'fields'
+ 'relations' # Shortcut for 'forward_relations' + 'reverse_relations'
+])
+
+RelationInfo = namedtuple('RelationInfo', [
+ 'model_field',
+ 'related',
+ 'to_many',
+ 'has_through_model'
+])
def _resolve_model(obj):
@@ -55,7 +71,7 @@ def get_field_info(model):
forward_relations = SortedDict()
for field in [field for field in opts.fields if field.serialize and field.rel]:
forward_relations[field.name] = RelationInfo(
- field=field,
+ model_field=field,
related=_resolve_model(field.rel.to),
to_many=False,
has_through_model=False
@@ -64,7 +80,7 @@ def get_field_info(model):
# Deal with forward many-to-many relationships.
for field in [field for field in opts.many_to_many if field.serialize]:
forward_relations[field.name] = RelationInfo(
- field=field,
+ model_field=field,
related=_resolve_model(field.rel.to),
to_many=True,
has_through_model=(
@@ -77,7 +93,7 @@ def get_field_info(model):
for relation in opts.get_all_related_objects():
accessor_name = relation.get_accessor_name()
reverse_relations[accessor_name] = RelationInfo(
- field=None,
+ model_field=None,
related=relation.model,
to_many=relation.field.rel.multiple,
has_through_model=False
@@ -87,7 +103,7 @@ def get_field_info(model):
for relation in opts.get_all_related_many_to_many_objects():
accessor_name = relation.get_accessor_name()
reverse_relations[accessor_name] = RelationInfo(
- field=None,
+ model_field=None,
related=relation.model,
to_many=True,
has_through_model=(
@@ -96,4 +112,18 @@ def get_field_info(model):
)
)
- return FieldInfo(pk, fields, forward_relations, reverse_relations)
+ # Shortcut that merges both regular fields and the pk,
+ # for simplifying regular field lookup.
+ fields_and_pk = SortedDict()
+ fields_and_pk['pk'] = pk
+ fields_and_pk[pk.name] = pk
+ fields_and_pk.update(fields)
+
+ # Shortcut that merges both forward and reverse relationships
+
+ relations = SortedDict(
+ list(forward_relations.items()) +
+ list(reverse_relations.items())
+ )
+
+ return FieldInfo(pk, fields, forward_relations, reverse_relations, fields_and_pk, relations)
diff --git a/tests/models.py b/tests/models.py
index fe064b46..06ec5a22 100644
--- a/tests/models.py
+++ b/tests/models.py
@@ -1,7 +1,6 @@
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
-from rest_framework import serializers
def foobar():
@@ -178,9 +177,3 @@ class NullableOneToOneSource(RESTFrameworkModel):
name = models.CharField(max_length=100)
target = models.OneToOneField(OneToOneTarget, null=True, blank=True,
related_name='nullable_source')
-
-
-# Serializer used to test BasicModel
-class BasicModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = BasicModel
diff --git a/tests/test_model_field_mappings.py b/tests/test_model_field_mappings.py
index bae63e5a..6daa574e 100644
--- a/tests/test_model_field_mappings.py
+++ b/tests/test_model_field_mappings.py
@@ -126,16 +126,16 @@ class TestRelationalFieldMappings(TestCase):
expected = dedent("""
TestSerializer():
id = IntegerField(label='ID', read_only=True)
- foreign_key = NestedModelSerializer(read_only=True):
+ foreign_key = NestedSerializer(read_only=True):
id = IntegerField(label='ID', read_only=True)
name = CharField(max_length=100)
- one_to_one = NestedModelSerializer(read_only=True):
+ one_to_one = NestedSerializer(read_only=True):
id = IntegerField(label='ID', read_only=True)
name = CharField(max_length=100)
- many_to_many = NestedModelSerializer(many=True, read_only=True):
+ many_to_many = NestedSerializer(many=True, read_only=True):
id = IntegerField(label='ID', read_only=True)
name = CharField(max_length=100)
- through = NestedModelSerializer(many=True, read_only=True):
+ through = NestedSerializer(many=True, read_only=True):
id = IntegerField(label='ID', read_only=True)
name = CharField(max_length=100)
""")
@@ -165,16 +165,16 @@ class TestRelationalFieldMappings(TestCase):
expected = dedent("""
TestSerializer():
url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
- foreign_key = NestedModelSerializer(read_only=True):
+ foreign_key = NestedSerializer(read_only=True):
url = HyperlinkedIdentityField(view_name='foreignkeytargetmodel-detail')
name = CharField(max_length=100)
- one_to_one = NestedModelSerializer(read_only=True):
+ one_to_one = NestedSerializer(read_only=True):
url = HyperlinkedIdentityField(view_name='onetoonetargetmodel-detail')
name = CharField(max_length=100)
- many_to_many = NestedModelSerializer(many=True, read_only=True):
+ many_to_many = NestedSerializer(many=True, read_only=True):
url = HyperlinkedIdentityField(view_name='manytomanytargetmodel-detail')
name = CharField(max_length=100)
- through = NestedModelSerializer(many=True, read_only=True):
+ through = NestedSerializer(many=True, read_only=True):
url = HyperlinkedIdentityField(view_name='throughtargetmodel-detail')
name = CharField(max_length=100)
""")
diff --git a/tests/test_response.py b/tests/test_response.py
index 67419a71..84c39c1a 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -2,11 +2,12 @@ from __future__ import unicode_literals
from django.conf.urls import patterns, url, include
from django.test import TestCase
from django.utils import six
-from tests.models import BasicModel, BasicModelSerializer
+from tests.models import BasicModel
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import generics
from rest_framework import routers
+from rest_framework import serializers
from rest_framework import status
from rest_framework.renderers import (
BaseRenderer,
@@ -17,6 +18,12 @@ from rest_framework import viewsets
from rest_framework.settings import api_settings
+# Serializer used to test BasicModel
+class BasicModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BasicModel
+
+
class MockPickleRenderer(BaseRenderer):
media_type = 'application/pickle'
diff --git a/tests/test_routers.py b/tests/test_routers.py
index b076f134..c2d595f7 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -76,9 +76,10 @@ class TestCustomLookupFields(TestCase):
def setUp(self):
class NoteSerializer(serializers.HyperlinkedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='routertestmodel-detail', lookup_field='uuid')
+
class Meta:
model = RouterTestModel
- lookup_field = 'uuid'
fields = ('url', 'uuid', 'text')
class NoteViewSet(viewsets.ModelViewSet):
@@ -86,8 +87,6 @@ class TestCustomLookupFields(TestCase):
serializer_class = NoteSerializer
lookup_field = 'uuid'
- RouterTestModel.objects.create(uuid='123', text='foo bar')
-
self.router = SimpleRouter()
self.router.register(r'notes', NoteViewSet)
@@ -98,6 +97,8 @@ class TestCustomLookupFields(TestCase):
url(r'^', include(self.router.urls)),
)
+ RouterTestModel.objects.create(uuid='123', text='foo bar')
+
def test_custom_lookup_field_route(self):
detail_route = self.router.urls[-1]
detail_url_pattern = detail_route.regex.pattern
--
cgit v1.2.3
From 87734be5f41de921ac32ad1f6664db243aab6d07 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 18 Sep 2014 12:17:21 +0100
Subject: Configuration correctness tests on ModelSerializer
---
rest_framework/serializers.py | 30 +++-
tests/test_model_field_mappings.py | 237 ---------------------------
tests/test_model_serializer.py | 322 +++++++++++++++++++++++++++++++++++++
3 files changed, 349 insertions(+), 240 deletions(-)
delete mode 100644 tests/test_model_field_mappings.py
create mode 100644 tests/test_model_serializer.py
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 99dcc349..9f3e53fd 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -10,7 +10,7 @@ python primitives.
2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
-from django.core.exceptions import ValidationError
+from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.db import models
from django.utils import six
from django.utils.datastructures import SortedDict
@@ -358,6 +358,7 @@ class ModelSerializer(Serializer):
model = getattr(self.Meta, 'model')
fields = getattr(self.Meta, 'fields', None)
depth = getattr(self.Meta, 'depth', 0)
+ extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
# Retrieve metadata about fields & relationships on the model class.
info = model_meta.get_field_info(model)
@@ -405,9 +406,32 @@ class ModelSerializer(Serializer):
if not issubclass(field_cls, HyperlinkedRelatedField):
kwargs.pop('view_name', None)
- else:
- assert False, 'Field name `%s` is not valid.' % field_name
+ elif hasattr(model, field_name):
+ # Create a read only field for model methods and properties.
+ field_cls = ReadOnlyField
+ kwargs = {}
+ else:
+ raise ImproperlyConfigured(
+ 'Field name `%s` is not valid for model `%s`.' %
+ (field_name, model.__class__.__name__)
+ )
+
+ # Check that any fields declared on the class are
+ # also explicity included in `Meta.fields`.
+ missing_fields = set(declared_fields.keys()) - set(fields)
+ if missing_fields:
+ missing_field = list(missing_fields)[0]
+ raise ImproperlyConfigured(
+ 'Field `%s` has been declared on serializer `%s`, but '
+ 'is missing from `Meta.fields`.' %
+ (missing_field, self.__class__.__name__)
+ )
+
+ # Populate any kwargs defined in `Meta.extra_kwargs`
+ kwargs.update(extra_kwargs.get(field_name, {}))
+
+ # Create the serializer field.
ret[field_name] = field_cls(**kwargs)
return ret
diff --git a/tests/test_model_field_mappings.py b/tests/test_model_field_mappings.py
deleted file mode 100644
index 6daa574e..00000000
--- a/tests/test_model_field_mappings.py
+++ /dev/null
@@ -1,237 +0,0 @@
-"""
-The `ModelSerializer` and `HyperlinkedModelSerializer` classes are essentially
-shortcuts for automatically creating serializers based on a given model class.
-
-These tests deal with ensuring that we correctly map the model fields onto
-an appropriate set of serializer fields for each case.
-"""
-from django.db import models
-from django.test import TestCase
-from rest_framework import serializers
-
-
-def dedent(blocktext):
- return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]])
-
-
-# Testing regular field mappings
-
-class RegularFieldsModel(models.Model):
- auto_field = models.AutoField(primary_key=True)
- big_integer_field = models.BigIntegerField()
- boolean_field = models.BooleanField(default=False)
- char_field = models.CharField(max_length=100)
- comma_seperated_integer_field = models.CommaSeparatedIntegerField(max_length=100)
- date_field = models.DateField()
- datetime_field = models.DateTimeField()
- decimal_field = models.DecimalField(max_digits=3, decimal_places=1)
- email_field = models.EmailField(max_length=100)
- float_field = models.FloatField()
- integer_field = models.IntegerField()
- null_boolean_field = models.NullBooleanField()
- positive_integer_field = models.PositiveIntegerField()
- positive_small_integer_field = models.PositiveSmallIntegerField()
- slug_field = models.SlugField(max_length=100)
- small_integer_field = models.SmallIntegerField()
- text_field = models.TextField()
- time_field = models.TimeField()
- url_field = models.URLField(max_length=100)
-
-
-class TestRegularFieldMappings(TestCase):
- def test_regular_fields(self):
- class TestSerializer(serializers.ModelSerializer):
- class Meta:
- model = RegularFieldsModel
-
- expected = dedent("""
- TestSerializer():
- auto_field = IntegerField(read_only=True)
- big_integer_field = IntegerField()
- boolean_field = BooleanField(default=False)
- char_field = CharField(max_length=100)
- comma_seperated_integer_field = CharField(max_length=100, validators=[])
- date_field = DateField()
- datetime_field = DateTimeField()
- decimal_field = DecimalField(decimal_places=1, max_digits=3)
- email_field = EmailField(max_length=100)
- float_field = FloatField()
- integer_field = IntegerField()
- null_boolean_field = BooleanField(required=False)
- positive_integer_field = IntegerField()
- positive_small_integer_field = IntegerField()
- slug_field = SlugField(max_length=100)
- small_integer_field = IntegerField()
- text_field = CharField()
- time_field = TimeField()
- url_field = URLField(max_length=100)
- """)
-
- self.assertEqual(repr(TestSerializer()), expected)
-
-
-# Testing relational field mappings
-
-class ForeignKeyTargetModel(models.Model):
- name = models.CharField(max_length=100)
-
-
-class ManyToManyTargetModel(models.Model):
- name = models.CharField(max_length=100)
-
-
-class OneToOneTargetModel(models.Model):
- name = models.CharField(max_length=100)
-
-
-class ThroughTargetModel(models.Model):
- name = models.CharField(max_length=100)
-
-
-class Supplementary(models.Model):
- extra = models.IntegerField()
- forwards = models.ForeignKey('ThroughTargetModel')
- backwards = models.ForeignKey('RelationalModel')
-
-
-class RelationalModel(models.Model):
- foreign_key = models.ForeignKey(ForeignKeyTargetModel, related_name='reverse_foreign_key')
- many_to_many = models.ManyToManyField(ManyToManyTargetModel, related_name='reverse_many_to_many')
- one_to_one = models.OneToOneField(OneToOneTargetModel, related_name='reverse_one_to_one')
- through = models.ManyToManyField(ThroughTargetModel, through=Supplementary, related_name='reverse_through')
-
-
-class TestRelationalFieldMappings(TestCase):
- def test_flat_relational_fields(self):
- class TestSerializer(serializers.ModelSerializer):
- class Meta:
- model = RelationalModel
-
- expected = dedent("""
- TestSerializer():
- id = IntegerField(label='ID', read_only=True)
- foreign_key = PrimaryKeyRelatedField(queryset=ForeignKeyTargetModel.objects.all())
- one_to_one = PrimaryKeyRelatedField(queryset=OneToOneTargetModel.objects.all())
- many_to_many = PrimaryKeyRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all())
- through = PrimaryKeyRelatedField(many=True, read_only=True)
- """)
- self.assertEqual(repr(TestSerializer()), expected)
-
- def test_nested_relational_fields(self):
- class TestSerializer(serializers.ModelSerializer):
- class Meta:
- model = RelationalModel
- depth = 1
-
- expected = dedent("""
- TestSerializer():
- id = IntegerField(label='ID', read_only=True)
- foreign_key = NestedSerializer(read_only=True):
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
- one_to_one = NestedSerializer(read_only=True):
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
- many_to_many = NestedSerializer(many=True, read_only=True):
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
- through = NestedSerializer(many=True, read_only=True):
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
- """)
- self.assertEqual(repr(TestSerializer()), expected)
-
- def test_flat_hyperlinked_fields(self):
- class TestSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = RelationalModel
-
- expected = dedent("""
- TestSerializer():
- url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
- foreign_key = HyperlinkedRelatedField(queryset=ForeignKeyTargetModel.objects.all(), view_name='foreignkeytargetmodel-detail')
- one_to_one = HyperlinkedRelatedField(queryset=OneToOneTargetModel.objects.all(), view_name='onetoonetargetmodel-detail')
- many_to_many = HyperlinkedRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail')
- through = HyperlinkedRelatedField(many=True, read_only=True, view_name='throughtargetmodel-detail')
- """)
- self.assertEqual(repr(TestSerializer()), expected)
-
- def test_nested_hyperlinked_fields(self):
- class TestSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = RelationalModel
- depth = 1
-
- expected = dedent("""
- TestSerializer():
- url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
- foreign_key = NestedSerializer(read_only=True):
- url = HyperlinkedIdentityField(view_name='foreignkeytargetmodel-detail')
- name = CharField(max_length=100)
- one_to_one = NestedSerializer(read_only=True):
- url = HyperlinkedIdentityField(view_name='onetoonetargetmodel-detail')
- name = CharField(max_length=100)
- many_to_many = NestedSerializer(many=True, read_only=True):
- url = HyperlinkedIdentityField(view_name='manytomanytargetmodel-detail')
- name = CharField(max_length=100)
- through = NestedSerializer(many=True, read_only=True):
- url = HyperlinkedIdentityField(view_name='throughtargetmodel-detail')
- name = CharField(max_length=100)
- """)
- self.assertEqual(repr(TestSerializer()), expected)
-
- def test_flat_reverse_foreign_key(self):
- class TestSerializer(serializers.ModelSerializer):
- class Meta:
- model = ForeignKeyTargetModel
- fields = ('id', 'name', 'reverse_foreign_key')
-
- expected = dedent("""
- TestSerializer():
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
- reverse_foreign_key = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
- """)
- self.assertEqual(repr(TestSerializer()), expected)
-
- def test_flat_reverse_one_to_one(self):
- class TestSerializer(serializers.ModelSerializer):
- class Meta:
- model = OneToOneTargetModel
- fields = ('id', 'name', 'reverse_one_to_one')
-
- expected = dedent("""
- TestSerializer():
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
- reverse_one_to_one = PrimaryKeyRelatedField(queryset=RelationalModel.objects.all())
- """)
- self.assertEqual(repr(TestSerializer()), expected)
-
- def test_flat_reverse_many_to_many(self):
- class TestSerializer(serializers.ModelSerializer):
- class Meta:
- model = ManyToManyTargetModel
- fields = ('id', 'name', 'reverse_many_to_many')
-
- expected = dedent("""
- TestSerializer():
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
- reverse_many_to_many = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
- """)
- self.assertEqual(repr(TestSerializer()), expected)
-
- def test_flat_reverse_through(self):
- class TestSerializer(serializers.ModelSerializer):
- class Meta:
- model = ThroughTargetModel
- fields = ('id', 'name', 'reverse_through')
-
- expected = dedent("""
- TestSerializer():
- id = IntegerField(label='ID', read_only=True)
- name = CharField(max_length=100)
- reverse_through = PrimaryKeyRelatedField(many=True, read_only=True)
- """)
- self.assertEqual(repr(TestSerializer()), expected)
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
new file mode 100644
index 00000000..63e36452
--- /dev/null
+++ b/tests/test_model_serializer.py
@@ -0,0 +1,322 @@
+"""
+The `ModelSerializer` and `HyperlinkedModelSerializer` classes are essentially
+shortcuts for automatically creating serializers based on a given model class.
+
+These tests deal with ensuring that we correctly map the model fields onto
+an appropriate set of serializer fields for each case.
+"""
+from django.core.exceptions import ImproperlyConfigured
+from django.db import models
+from django.test import TestCase
+from rest_framework import serializers
+
+
+def dedent(blocktext):
+ return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]])
+
+
+# Testing regular field mappings
+
+class RegularFieldsModel(models.Model):
+ auto_field = models.AutoField(primary_key=True)
+ big_integer_field = models.BigIntegerField()
+ boolean_field = models.BooleanField(default=False)
+ char_field = models.CharField(max_length=100)
+ comma_seperated_integer_field = models.CommaSeparatedIntegerField(max_length=100)
+ date_field = models.DateField()
+ datetime_field = models.DateTimeField()
+ decimal_field = models.DecimalField(max_digits=3, decimal_places=1)
+ email_field = models.EmailField(max_length=100)
+ float_field = models.FloatField()
+ integer_field = models.IntegerField()
+ null_boolean_field = models.NullBooleanField()
+ positive_integer_field = models.PositiveIntegerField()
+ positive_small_integer_field = models.PositiveSmallIntegerField()
+ slug_field = models.SlugField(max_length=100)
+ small_integer_field = models.SmallIntegerField()
+ text_field = models.TextField()
+ time_field = models.TimeField()
+ url_field = models.URLField(max_length=100)
+
+ def method(self):
+ return 'method'
+
+
+class TestRegularFieldMappings(TestCase):
+ def test_regular_fields(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+
+ expected = dedent("""
+ TestSerializer():
+ auto_field = IntegerField(read_only=True)
+ big_integer_field = IntegerField()
+ boolean_field = BooleanField(default=False)
+ char_field = CharField(max_length=100)
+ comma_seperated_integer_field = CharField(max_length=100, validators=[])
+ date_field = DateField()
+ datetime_field = DateTimeField()
+ decimal_field = DecimalField(decimal_places=1, max_digits=3)
+ email_field = EmailField(max_length=100)
+ float_field = FloatField()
+ integer_field = IntegerField()
+ null_boolean_field = BooleanField(required=False)
+ positive_integer_field = IntegerField()
+ positive_small_integer_field = IntegerField()
+ slug_field = SlugField(max_length=100)
+ small_integer_field = IntegerField()
+ text_field = CharField()
+ time_field = TimeField()
+ url_field = URLField(max_length=100)
+ """)
+
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_method_field(self):
+ """
+ Properties and methods on the model should be allowed as `Meta.fields`
+ values, and should map to `ReadOnlyField`.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('auto_field', 'method')
+
+ expected = dedent("""
+ TestSerializer():
+ auto_field = IntegerField(read_only=True)
+ method = ReadOnlyField()
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_pk_fields(self):
+ """
+ Both `pk` and the actual primary key name are valid in `Meta.fields`.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('pk', 'auto_field')
+
+ expected = dedent("""
+ TestSerializer():
+ pk = IntegerField(label='Auto field', read_only=True)
+ auto_field = IntegerField(read_only=True)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_extra_field_kwargs(self):
+ """
+ Ensure `extra_kwargs` are passed to generated fields.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('pk', 'char_field')
+ extra_kwargs = {'char_field': {'default': 'extra'}}
+
+ expected = dedent("""
+ TestSerializer():
+ pk = IntegerField(label='Auto field', read_only=True)
+ char_field = CharField(default='extra', max_length=100)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_invalid_field(self):
+ """
+ Field names that do not map to a model field or relationship should
+ raise a configuration errror.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('auto_field', 'invalid')
+
+ with self.assertRaises(ImproperlyConfigured) as excinfo:
+ TestSerializer()
+ expected = 'Field name `invalid` is not valid for model `ModelBase`.'
+ assert str(excinfo.exception) == expected
+
+ def test_missing_field(self):
+ class TestSerializer(serializers.ModelSerializer):
+ missing = serializers.ReadOnlyField()
+
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('auto_field',)
+
+ with self.assertRaises(ImproperlyConfigured) as excinfo:
+ TestSerializer()
+ expected = (
+ 'Field `missing` has been declared on serializer '
+ '`TestSerializer`, but is missing from `Meta.fields`.'
+ )
+ assert str(excinfo.exception) == expected
+
+
+# Testing relational field mappings
+
+class ForeignKeyTargetModel(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ManyToManyTargetModel(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class OneToOneTargetModel(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ThroughTargetModel(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class Supplementary(models.Model):
+ extra = models.IntegerField()
+ forwards = models.ForeignKey('ThroughTargetModel')
+ backwards = models.ForeignKey('RelationalModel')
+
+
+class RelationalModel(models.Model):
+ foreign_key = models.ForeignKey(ForeignKeyTargetModel, related_name='reverse_foreign_key')
+ many_to_many = models.ManyToManyField(ManyToManyTargetModel, related_name='reverse_many_to_many')
+ one_to_one = models.OneToOneField(OneToOneTargetModel, related_name='reverse_one_to_one')
+ through = models.ManyToManyField(ThroughTargetModel, through=Supplementary, related_name='reverse_through')
+
+
+class TestRelationalFieldMappings(TestCase):
+ def test_flat_relational_fields(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ foreign_key = PrimaryKeyRelatedField(queryset=ForeignKeyTargetModel.objects.all())
+ one_to_one = PrimaryKeyRelatedField(queryset=OneToOneTargetModel.objects.all())
+ many_to_many = PrimaryKeyRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all())
+ through = PrimaryKeyRelatedField(many=True, read_only=True)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_nested_relational_fields(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+ depth = 1
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ foreign_key = NestedSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ one_to_one = NestedSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ many_to_many = NestedSerializer(many=True, read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ through = NestedSerializer(many=True, read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_flat_hyperlinked_fields(self):
+ class TestSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = RelationalModel
+
+ expected = dedent("""
+ TestSerializer():
+ url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
+ foreign_key = HyperlinkedRelatedField(queryset=ForeignKeyTargetModel.objects.all(), view_name='foreignkeytargetmodel-detail')
+ one_to_one = HyperlinkedRelatedField(queryset=OneToOneTargetModel.objects.all(), view_name='onetoonetargetmodel-detail')
+ many_to_many = HyperlinkedRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail')
+ through = HyperlinkedRelatedField(many=True, read_only=True, view_name='throughtargetmodel-detail')
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_nested_hyperlinked_fields(self):
+ class TestSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = RelationalModel
+ depth = 1
+
+ expected = dedent("""
+ TestSerializer():
+ url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
+ foreign_key = NestedSerializer(read_only=True):
+ url = HyperlinkedIdentityField(view_name='foreignkeytargetmodel-detail')
+ name = CharField(max_length=100)
+ one_to_one = NestedSerializer(read_only=True):
+ url = HyperlinkedIdentityField(view_name='onetoonetargetmodel-detail')
+ name = CharField(max_length=100)
+ many_to_many = NestedSerializer(many=True, read_only=True):
+ url = HyperlinkedIdentityField(view_name='manytomanytargetmodel-detail')
+ name = CharField(max_length=100)
+ through = NestedSerializer(many=True, read_only=True):
+ url = HyperlinkedIdentityField(view_name='throughtargetmodel-detail')
+ name = CharField(max_length=100)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_flat_reverse_foreign_key(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ForeignKeyTargetModel
+ fields = ('id', 'name', 'reverse_foreign_key')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_foreign_key = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_flat_reverse_one_to_one(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = OneToOneTargetModel
+ fields = ('id', 'name', 'reverse_one_to_one')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_one_to_one = PrimaryKeyRelatedField(queryset=RelationalModel.objects.all())
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_flat_reverse_many_to_many(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ManyToManyTargetModel
+ fields = ('id', 'name', 'reverse_many_to_many')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_many_to_many = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_flat_reverse_through(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ThroughTargetModel
+ fields = ('id', 'name', 'reverse_through')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_through = PrimaryKeyRelatedField(many=True, read_only=True)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
--
cgit v1.2.3
From 3bc628edc01a6bd4feeceaefa45168549b79a31a Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 18 Sep 2014 13:07:38 +0100
Subject: Test for custom fields
---
tests/test_model_serializer.py | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index 63e36452..3ee91126 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -17,6 +17,10 @@ def dedent(blocktext):
# Testing regular field mappings
+class CustomField(models.Field):
+ pass
+
+
class RegularFieldsModel(models.Model):
auto_field = models.AutoField(primary_key=True)
big_integer_field = models.BigIntegerField()
@@ -37,6 +41,7 @@ class RegularFieldsModel(models.Model):
text_field = models.TextField()
time_field = models.TimeField()
url_field = models.URLField(max_length=100)
+ custom_field = CustomField()
def method(self):
return 'method'
@@ -44,6 +49,9 @@ class RegularFieldsModel(models.Model):
class TestRegularFieldMappings(TestCase):
def test_regular_fields(self):
+ """
+ Model fields should map to their equivelent serializer fields.
+ """
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = RegularFieldsModel
@@ -69,8 +77,8 @@ class TestRegularFieldMappings(TestCase):
text_field = CharField()
time_field = TimeField()
url_field = URLField(max_length=100)
+ custom_field = ModelField(model_field=)
""")
-
self.assertEqual(repr(TestSerializer()), expected)
def test_method_field(self):
@@ -139,6 +147,10 @@ class TestRegularFieldMappings(TestCase):
assert str(excinfo.exception) == expected
def test_missing_field(self):
+ """
+ Fields that have been declared on the serializer class must be included
+ in the `Meta.fields` if it exists.
+ """
class TestSerializer(serializers.ModelSerializer):
missing = serializers.ReadOnlyField()
--
cgit v1.2.3
From 9fdb2280d11db126771686d626aa8a0247b8a46c Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 18 Sep 2014 14:23:00 +0100
Subject: First pass on ManyRelation
---
rest_framework/relations.py | 42 +++++++++++++++++++++++++-
rest_framework/utils/representation.py | 2 ++
tests/test_model_serializer.py | 54 +++++++++++++++++++++++++++++-----
3 files changed, 89 insertions(+), 9 deletions(-)
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 9f44ab63..474d3e75 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -10,7 +10,6 @@ from django.utils.translation import ugettext_lazy as _
class RelatedField(Field):
def __init__(self, **kwargs):
self.queryset = kwargs.pop('queryset', None)
- self.many = kwargs.pop('many', False)
assert self.queryset is not None or kwargs.get('read_only', None), (
'Relational field must provide a `queryset` argument, '
'or set read_only=`True`.'
@@ -21,6 +20,13 @@ class RelatedField(Field):
)
super(RelatedField, self).__init__(**kwargs)
+ def __new__(cls, *args, **kwargs):
+ # We override this method in order to automagically create
+ # `ManyRelation` classes instead when `many=True` is set.
+ if kwargs.pop('many', False):
+ return ManyRelation(child_relation=cls(*args, **kwargs))
+ return super(RelatedField, cls).__new__(cls, *args, **kwargs)
+
def get_queryset(self):
queryset = self.queryset
if isinstance(queryset, QuerySet):
@@ -216,3 +222,37 @@ class SlugRelatedField(RelatedField):
def to_representation(self, obj):
return getattr(obj, self.slug_field)
+
+
+class ManyRelation(Field):
+ """
+ Relationships with `many=True` transparently get coerced into instead being
+ a ManyRelation with a child relationship.
+
+ The `ManyRelation` class is responsible for handling iterating through
+ the values and passing each one to the child relationship.
+
+ You shouldn't need to be using this class directly yourself.
+ """
+
+ def __init__(self, child_relation=None, *args, **kwargs):
+ self.child_relation = child_relation
+ assert child_relation is not None, '`child_relation` is a required argument.'
+ super(ManyRelation, self).__init__(*args, **kwargs)
+
+ def bind(self, field_name, parent, root):
+ # ManyRelation needs to provide the current context to the child relation.
+ super(ManyRelation, self).bind(field_name, parent, root)
+ self.child_relation.bind(field_name, parent, root)
+
+ def to_internal_value(self, data):
+ return [
+ self.child_relation.to_internal_value(item)
+ for item in data
+ ]
+
+ def to_representation(self, obj):
+ return [
+ self.child_relation.to_representation(value)
+ for value in obj.all()
+ ]
diff --git a/rest_framework/utils/representation.py b/rest_framework/utils/representation.py
index 71db1886..e64fdd22 100644
--- a/rest_framework/utils/representation.py
+++ b/rest_framework/utils/representation.py
@@ -73,6 +73,8 @@ def serializer_repr(serializer, indent, force_many=None):
ret += serializer_repr(field, indent + 1)
elif hasattr(field, 'child'):
ret += list_repr(field, indent + 1)
+ elif hasattr(field, 'child_relation'):
+ ret += field_repr(field.child_relation, force_many=field.child_relation)
else:
ret += field_repr(field)
return ret
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index 3ee91126..b3dae713 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -199,7 +199,7 @@ class RelationalModel(models.Model):
class TestRelationalFieldMappings(TestCase):
- def test_flat_relational_fields(self):
+ def test_pk_relations(self):
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = RelationalModel
@@ -214,7 +214,7 @@ class TestRelationalFieldMappings(TestCase):
""")
self.assertEqual(repr(TestSerializer()), expected)
- def test_nested_relational_fields(self):
+ def test_nested_relations(self):
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = RelationalModel
@@ -238,7 +238,7 @@ class TestRelationalFieldMappings(TestCase):
""")
self.assertEqual(repr(TestSerializer()), expected)
- def test_flat_hyperlinked_fields(self):
+ def test_hyperlinked_relations(self):
class TestSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = RelationalModel
@@ -253,7 +253,7 @@ class TestRelationalFieldMappings(TestCase):
""")
self.assertEqual(repr(TestSerializer()), expected)
- def test_nested_hyperlinked_fields(self):
+ def test_nested_hyperlinked_relations(self):
class TestSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = RelationalModel
@@ -277,7 +277,7 @@ class TestRelationalFieldMappings(TestCase):
""")
self.assertEqual(repr(TestSerializer()), expected)
- def test_flat_reverse_foreign_key(self):
+ def test_pk_reverse_foreign_key(self):
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = ForeignKeyTargetModel
@@ -291,7 +291,7 @@ class TestRelationalFieldMappings(TestCase):
""")
self.assertEqual(repr(TestSerializer()), expected)
- def test_flat_reverse_one_to_one(self):
+ def test_pk_reverse_one_to_one(self):
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = OneToOneTargetModel
@@ -305,7 +305,7 @@ class TestRelationalFieldMappings(TestCase):
""")
self.assertEqual(repr(TestSerializer()), expected)
- def test_flat_reverse_many_to_many(self):
+ def test_pk_reverse_many_to_many(self):
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = ManyToManyTargetModel
@@ -319,7 +319,7 @@ class TestRelationalFieldMappings(TestCase):
""")
self.assertEqual(repr(TestSerializer()), expected)
- def test_flat_reverse_through(self):
+ def test_pk_reverse_through(self):
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = ThroughTargetModel
@@ -332,3 +332,41 @@ class TestRelationalFieldMappings(TestCase):
reverse_through = PrimaryKeyRelatedField(many=True, read_only=True)
""")
self.assertEqual(repr(TestSerializer()), expected)
+
+
+class TestIntegration(TestCase):
+ def setUp(self):
+ self.foreign_key_target = ForeignKeyTargetModel.objects.create(
+ name='foreign_key'
+ )
+ self.one_to_one_target = OneToOneTargetModel.objects.create(
+ name='one_to_one'
+ )
+ self.many_to_many_targets = [
+ ManyToManyTargetModel.objects.create(
+ name='many_to_many (%d)' % idx
+ ) for idx in range(3)
+ ]
+ self.instance = RelationalModel.objects.create(
+ foreign_key=self.foreign_key_target,
+ one_to_one=self.one_to_one_target,
+ )
+ self.instance.many_to_many = self.many_to_many_targets
+ self.instance.save()
+
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+
+ self.serializer_cls = TestSerializer
+
+ def test_pk_relationship_representations(self):
+ serializer = self.serializer_cls(self.instance)
+ expected = {
+ 'id': self.instance.pk,
+ 'foreign_key': self.foreign_key_target.pk,
+ 'one_to_one': self.one_to_one_target.pk,
+ 'many_to_many': [item.pk for item in self.many_to_many_targets],
+ 'through': []
+ }
+ self.assertEqual(serializer.data, expected)
--
cgit v1.2.3
From 106362b437f45e04faaea759df57a66a8a2d7cfd Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 18 Sep 2014 14:58:08 +0100
Subject: ModelSerializer.create() to handle many to many by default
---
rest_framework/relations.py | 5 ++++-
rest_framework/serializers.py | 20 ++++++++++++++++++-
tests/test_model_serializer.py | 45 +++++++++++++++++++++++++++++++++++++++++-
3 files changed, 67 insertions(+), 3 deletions(-)
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 474d3e75..5aa1f8bd 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -24,7 +24,10 @@ class RelatedField(Field):
# We override this method in order to automagically create
# `ManyRelation` classes instead when `many=True` is set.
if kwargs.pop('many', False):
- return ManyRelation(child_relation=cls(*args, **kwargs))
+ return ManyRelation(
+ child_relation=cls(*args, **kwargs),
+ read_only=kwargs.get('read_only', False)
+ )
return super(RelatedField, cls).__new__(cls, *args, **kwargs)
def get_queryset(self):
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 9f3e53fd..03e20df8 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -344,7 +344,25 @@ class ModelSerializer(Serializer):
def create(self, attrs):
ModelClass = self.Meta.model
- return ModelClass.objects.create(**attrs)
+
+ # Remove many-to-many relationships from attrs.
+ # They are not valid arguments to the default `.create()` method,
+ # as they require that the instance has already been saved.
+ info = model_meta.get_field_info(ModelClass)
+ many_to_many = {}
+ for key, relation_info in info.relations.items():
+ if relation_info.to_many and (key in attrs):
+ many_to_many[key] = attrs.pop(key)
+
+ instance = ModelClass.objects.create(**attrs)
+
+ # Save many to many relationships after the instance is created.
+ if many_to_many:
+ for key, value in many_to_many.items():
+ setattr(instance, key, value)
+ instance.save()
+
+ return instance
def update(self, obj, attrs):
for attr, value in attrs.items():
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index b3dae713..6f207e02 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -360,7 +360,7 @@ class TestIntegration(TestCase):
self.serializer_cls = TestSerializer
- def test_pk_relationship_representations(self):
+ def test_pk_retrival(self):
serializer = self.serializer_cls(self.instance)
expected = {
'id': self.instance.pk,
@@ -370,3 +370,46 @@ class TestIntegration(TestCase):
'through': []
}
self.assertEqual(serializer.data, expected)
+
+ def test_pk_create(self):
+ new_foreign_key = ForeignKeyTargetModel.objects.create(
+ name='foreign_key'
+ )
+ new_one_to_one = OneToOneTargetModel.objects.create(
+ name='one_to_one'
+ )
+ new_many_to_many = [
+ ManyToManyTargetModel.objects.create(
+ name='new many_to_many (%d)' % idx
+ ) for idx in range(3)
+ ]
+ data = {
+ 'foreign_key': new_foreign_key.pk,
+ 'one_to_one': new_one_to_one.pk,
+ 'many_to_many': [item.pk for item in new_many_to_many],
+ }
+
+ # Serializer should validate okay.
+ serializer = self.serializer_cls(data=data)
+ assert serializer.is_valid()
+
+ # Creating the instance, relationship attributes should be set.
+ instance = serializer.save()
+ assert instance.foreign_key.pk == new_foreign_key.pk
+ assert instance.one_to_one.pk == new_one_to_one.pk
+ assert [
+ item.pk for item in instance.many_to_many.all()
+ ] == [
+ item.pk for item in new_many_to_many
+ ]
+ assert list(instance.through.all()) == []
+
+ # Representation should be correct.
+ expected = {
+ 'id': instance.pk,
+ 'foreign_key': new_foreign_key.pk,
+ 'one_to_one': new_one_to_one.pk,
+ 'many_to_many': [item.pk for item in new_many_to_many],
+ 'through': []
+ }
+ self.assertEqual(serializer.data, expected)
--
cgit v1.2.3
From f90049316a3ecca6c92e10b57bfa5becbceff386 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 18 Sep 2014 15:47:27 +0100
Subject: Added a model update integration test
---
rest_framework/serializers.py | 13 ++++++-------
tests/test_model_serializer.py | 43 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 49 insertions(+), 7 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 03e20df8..d2740fc2 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -350,17 +350,16 @@ class ModelSerializer(Serializer):
# as they require that the instance has already been saved.
info = model_meta.get_field_info(ModelClass)
many_to_many = {}
- for key, relation_info in info.relations.items():
- if relation_info.to_many and (key in attrs):
- many_to_many[key] = attrs.pop(key)
+ for field_name, relation_info in info.relations.items():
+ if relation_info.to_many and (field_name in attrs):
+ many_to_many[field_name] = attrs.pop(field_name)
instance = ModelClass.objects.create(**attrs)
- # Save many to many relationships after the instance is created.
+ # Save many-to-many relationships after the instance is created.
if many_to_many:
- for key, value in many_to_many.items():
- setattr(instance, key, value)
- instance.save()
+ for field_name, value in many_to_many.items():
+ setattr(instance, field_name, value)
return instance
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index 6f207e02..e0120c31 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -413,3 +413,46 @@ class TestIntegration(TestCase):
'through': []
}
self.assertEqual(serializer.data, expected)
+
+ def test_pk_update(self):
+ new_foreign_key = ForeignKeyTargetModel.objects.create(
+ name='foreign_key'
+ )
+ new_one_to_one = OneToOneTargetModel.objects.create(
+ name='one_to_one'
+ )
+ new_many_to_many = [
+ ManyToManyTargetModel.objects.create(
+ name='new many_to_many (%d)' % idx
+ ) for idx in range(3)
+ ]
+ data = {
+ 'foreign_key': new_foreign_key.pk,
+ 'one_to_one': new_one_to_one.pk,
+ 'many_to_many': [item.pk for item in new_many_to_many],
+ }
+
+ # Serializer should validate okay.
+ serializer = self.serializer_cls(self.instance, data=data)
+ assert serializer.is_valid()
+
+ # Creating the instance, relationship attributes should be set.
+ instance = serializer.save()
+ assert instance.foreign_key.pk == new_foreign_key.pk
+ assert instance.one_to_one.pk == new_one_to_one.pk
+ assert [
+ item.pk for item in instance.many_to_many.all()
+ ] == [
+ item.pk for item in new_many_to_many
+ ]
+ assert list(instance.through.all()) == []
+
+ # Representation should be correct.
+ expected = {
+ 'id': self.instance.pk,
+ 'foreign_key': new_foreign_key.pk,
+ 'one_to_one': new_one_to_one.pk,
+ 'many_to_many': [item.pk for item in new_many_to_many],
+ 'through': []
+ }
+ self.assertEqual(serializer.data, expected)
--
cgit v1.2.3
From 12ccb0fe8a7dfbefa3687d07ff59a456fbda9ee6 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Sep 2014 09:09:26 +0100
Subject: Test tweaking
---
tests/test_model_serializer.py | 28 ++++++++++++++++++++--------
1 file changed, 20 insertions(+), 8 deletions(-)
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index e0120c31..ec922b6d 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -18,10 +18,16 @@ def dedent(blocktext):
# Testing regular field mappings
class CustomField(models.Field):
+ """
+ A custom model field simply for testing purposes.
+ """
pass
class RegularFieldsModel(models.Model):
+ """
+ A model class for testing regular flat fields.
+ """
auto_field = models.AutoField(primary_key=True)
big_integer_field = models.BigIntegerField()
boolean_field = models.BooleanField(default=False)
@@ -121,12 +127,12 @@ class TestRegularFieldMappings(TestCase):
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = RegularFieldsModel
- fields = ('pk', 'char_field')
+ fields = ('auto_field', 'char_field')
extra_kwargs = {'char_field': {'default': 'extra'}}
expected = dedent("""
TestSerializer():
- pk = IntegerField(label='Auto field', read_only=True)
+ auto_field = IntegerField(read_only=True)
char_field = CharField(default='extra', max_length=100)
""")
self.assertEqual(repr(TestSerializer()), expected)
@@ -354,14 +360,12 @@ class TestIntegration(TestCase):
self.instance.many_to_many = self.many_to_many_targets
self.instance.save()
+ def test_pk_retrival(self):
class TestSerializer(serializers.ModelSerializer):
class Meta:
model = RelationalModel
- self.serializer_cls = TestSerializer
-
- def test_pk_retrival(self):
- serializer = self.serializer_cls(self.instance)
+ serializer = TestSerializer(self.instance)
expected = {
'id': self.instance.pk,
'foreign_key': self.foreign_key_target.pk,
@@ -372,6 +376,10 @@ class TestIntegration(TestCase):
self.assertEqual(serializer.data, expected)
def test_pk_create(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+
new_foreign_key = ForeignKeyTargetModel.objects.create(
name='foreign_key'
)
@@ -390,7 +398,7 @@ class TestIntegration(TestCase):
}
# Serializer should validate okay.
- serializer = self.serializer_cls(data=data)
+ serializer = TestSerializer(data=data)
assert serializer.is_valid()
# Creating the instance, relationship attributes should be set.
@@ -415,6 +423,10 @@ class TestIntegration(TestCase):
self.assertEqual(serializer.data, expected)
def test_pk_update(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+
new_foreign_key = ForeignKeyTargetModel.objects.create(
name='foreign_key'
)
@@ -433,7 +445,7 @@ class TestIntegration(TestCase):
}
# Serializer should validate okay.
- serializer = self.serializer_cls(self.instance, data=data)
+ serializer = TestSerializer(self.instance, data=data)
assert serializer.is_valid()
# Creating the instance, relationship attributes should be set.
--
cgit v1.2.3
From 6d73b5969a9d415a6d43175e77bc10ba76151e97 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Sep 2014 14:02:17 +0100
Subject: Initial release notes
---
docs/topics/3.0-announcement.md | 239 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 239 insertions(+)
create mode 100644 docs/topics/3.0-announcement.md
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
new file mode 100644
index 00000000..029d9896
--- /dev/null
+++ b/docs/topics/3.0-announcement.md
@@ -0,0 +1,239 @@
+**THIS DOCUMENT IS CURRENTLY A WORK IN PROGRESS**
+
+See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-framework/pull/1800) for more details.
+
+# REST framework 3.0
+
+**Note incremental nature, discuss upgrading.**
+
+## Motivation
+
+**TODO**
+
+---
+
+## Request objects
+
+#### The `request.data` property.
+
+**TODO**
+
+#### The parser API.
+
+**TODO**
+
+## Serializers
+
+#### Single-step object creation.
+
+**TODO**: Drop `.restore_object()`, use `.create()` and `.update()` which should save the instance.
+
+**TODO**: Drop`.object`, use `.validated_data` or get the instance with `.save()`.
+
+#### Always use `fields`, not `exclude`.
+
+The `exclude` option is no longer available. You should use the more explicit `fields` option instead.
+
+#### The `extra_kwargs` option.
+
+The `read_only_fields` and `write_only_fields` options have been removed and replaced with a more generic `extra_kwargs`.
+
+ class MySerializer(serializer.ModelSerializer):
+ class Meta:
+ model = MyModel
+ fields = ('id', 'email', 'notes', 'is_admin')
+ extra_kwargs = {
+ 'is_admin': {'read_only': True}
+ }
+
+Alternatively, specify the field explicitly on the serializer class:
+
+ class MySerializer(serializer.ModelSerializer):
+ is_admin = serializers.BooleanField(read_only=True)
+
+ class Meta:
+ model = MyModel
+ fields = ('id', 'email', 'notes', 'is_admin')
+
+#### Changes to `HyperlinkedModelSerializer`.
+
+The `view_name` and `lookup_field` options have been removed. They are no longer required, as you can use the `extra_kwargs` argument instead:
+
+ class MySerializer(serializer.HyperlinkedModelSerializer):
+ class Meta:
+ model = MyModel
+ fields = ('url', 'email', 'notes', 'is_admin')
+ extra_kwargs = {
+ 'url': {'lookup_field': 'uuid'}
+ }
+
+Alternatively, specify the field explicitly on the serializer class:
+
+ class MySerializer(serializer.HyperlinkedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='mymodel-detail',
+ lookup_field='uuid'
+ )
+
+ class Meta:
+ model = MyModel
+ fields = ('url', 'email', 'notes', 'is_admin')
+
+#### Fields for model methods and properties.
+
+You can now specify field names in the `fields` option that refer to model methods or properties. For example, suppose you have the following model:
+
+ class Invitation(models.Model):
+ created = models.DateTimeField()
+ to_email = models.EmailField()
+ message = models.CharField(max_length=1000)
+
+ def expiry_date(self):
+ return self.created + datetime.timedelta(days=30)
+
+You can include `expiry_date` as a field option on a `ModelSerializer` class.
+
+ class InvitationSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Invitation
+ fields = ('to_email', 'message', 'expiry_date')
+
+These fields will be mapped to `serializers.ReadOnlyField()` instances.
+
+ >>> serializer = InvitationSerializer()
+ >>> print repr(serializer)
+ InvitationSerializer():
+ to_email = EmailField(max_length=75)
+ message = CharField(max_length=1000)
+ expiry_date = ReadOnlyField()
+
+
+## Serializer fields
+
+#### The `Field` and `ReadOnly` field classes.
+
+**TODO**
+
+#### Coercing output types.
+
+**TODO**
+
+#### The `ListSerializer` class.
+
+**TODO**
+
+#### The `MultipleChoiceField` class.
+
+**TODO**
+
+#### Changes to the custom field API.
+
+**TODO** `to_representation`, `to_internal_value`.
+
+#### Explicit `querysets` required on relational fields.
+
+**TODO**
+
+#### Optional argument to `SerializerMethodField`.
+
+**TODO**
+
+## Generic views
+
+#### Simplification of view logic.
+
+**TODO**
+
+#### Removal of pre/post save hooks.
+
+The following method hooks no longer exist on the new, simplified, generic views: `pre_save`, `post_save`, `pre_delete`, `post_delete`.
+
+If you do need custom behavior, you might choose to instead override the `.save()` method on your serializer class. For example:
+
+ def save(self, *args, **kwargs):
+ instance = super(MySerializer).save(*args, **kwarg)
+ send_email(instance.to_email, instance.message)
+ return instance
+
+Alternatively write your view logic exlpicitly, or tie your pre/post save behavior into the model class or model manager.
+
+#### Removal of view attributes.
+
+The `.object` and `.object_list` attributes are no longer set on the view instance. Treating views as mutable object instances that store state during the processing of the view tends to be poor design, and can lead to obscure flow logic.
+
+I would personally recommend that developers treat view instances as immutable objects in their application code.
+
+#### PUT as create.
+
+**TODO**
+
+## API style
+
+There are some improvements in the default style we use in our API responses.
+
+#### Unicode JSON by default.
+
+Unicode JSON is now the default. The `UnicodeJSONRenderer` class no longer exists, and the `UNICODE_JSON` setting has been added. To revert this behavior use the new setting:
+
+ REST_FRAMEWORK = {
+ 'UNICODE_JSON': False
+ }
+
+#### Compact JSON by default.
+
+We now output compact JSON in responses by default. For example, we return:
+
+ {"email":"amy@example.com","is_admin":true}
+
+Instead of the following:
+
+ {"email": "amy@example.com", "is_admin": true}
+
+The `COMPACT_JSON` setting has been added, and can be used to revert this behavior if needed:
+
+ REST_FRAMEWORK = {
+ 'COMPACT_JSON': False
+ }
+
+#### Throttle headers using `Retry-After`.
+
+The custom `X-Throttle-Wait-Second` header has now been dropped in favor of the standard `Retry-After` header. You can revert this behavior if needed by writing a custom exception handler for your application.
+
+#### Date and time objects as ISO-8859-1 strings in serializer data.
+
+Date and Time objects are now coerced to strings by default in the serializer output. Previously they were returned as `Date`, `Time` and `DateTime` objects, and later coerced to strings by the renderer.
+
+You can modify this behavior globally by settings the existing `DATE_FORMAT`, `DATETIME_FORMAT` and `TIME_FORMAT` settings keys. Setting these values to `None` instead of their default value of `'iso-8859-1'` will result in native objects being returned in serializer data.
+
+ REST_FRAMEWORK = {
+ # Return native `Date` and `Time` objects in `serializer.data`
+ 'DATETIME_FORMAT': None
+ 'DATE_FORMAT': None
+ 'TIME_FORMAT': None
+ }
+
+You can also modify serializer fields individually, using the `date_format`, `time_format` and `datetime_format` arguments:
+
+ # Return `DateTime` instances in `serializer.data`, not strings.
+ created = serializers.DateTimeField(format=None)
+
+#### Decimals as strings in serializer data.
+
+Decimals are now coerced to strings by default in the serializer output. Previously they were returned as `Decimal` objects, and later coerced to strings by the renderer.
+
+You can modify this behavior globally by using the `COERCE_DECIMAL_TO_STRING` settings key.
+
+ REST_FRAMEWORK = {
+ 'COERCE_DECIMAL_TO_STRING': False
+ }
+
+Or modify it on an individual serializer field, using the `corece_to_string` keyword argument.
+
+ # Return `Decimal` instances in `serializer.data`, not strings.
+ amount = serializers.DecimalField(
+ max_digits=10,
+ decimal_places=2,
+ coerce_to_string=False
+ )
+
+The default JSON renderer will return float objects for uncoerced `Decimal` instances. This allows you to easily switch between string or float representations for decimals depending on your API design needs.
--
cgit v1.2.3
From c0150e619ca02a69d87c335a70c47644e9b2e509 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Sep 2014 14:59:59 +0100
Subject: Add BaseSerializer heading
---
docs/topics/3.0-announcement.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 029d9896..cd883cdd 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -30,6 +30,10 @@ See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-fr
**TODO**: Drop`.object`, use `.validated_data` or get the instance with `.save()`.
+#### The `BaseSerializer` class.
+
+**TODO**
+
#### Always use `fields`, not `exclude`.
The `exclude` option is no longer available. You should use the more explicit `fields` option instead.
--
cgit v1.2.3
From b361c54c5c198583e5085cf49ef44291ec09d2e8 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Sep 2014 15:46:32 +0100
Subject: Test rejigging
---
tests/test_breadcrumbs.py | 100 -------------------------------
tests/test_model_serializer.py | 5 ++
tests/test_modelinfo.py | 31 ----------
tests/test_templatetags.py | 35 +++++++++++
tests/test_urlizer.py | 37 ------------
tests/test_utils.py | 132 +++++++++++++++++++++++++++++++++++++++++
6 files changed, 172 insertions(+), 168 deletions(-)
delete mode 100644 tests/test_breadcrumbs.py
delete mode 100644 tests/test_modelinfo.py
delete mode 100644 tests/test_urlizer.py
create mode 100644 tests/test_utils.py
diff --git a/tests/test_breadcrumbs.py b/tests/test_breadcrumbs.py
deleted file mode 100644
index 780fd5c4..00000000
--- a/tests/test_breadcrumbs.py
+++ /dev/null
@@ -1,100 +0,0 @@
-from __future__ import unicode_literals
-from django.conf.urls import patterns, url
-from django.test import TestCase
-from rest_framework.utils.breadcrumbs import get_breadcrumbs
-from rest_framework.views import APIView
-
-
-class Root(APIView):
- pass
-
-
-class ResourceRoot(APIView):
- pass
-
-
-class ResourceInstance(APIView):
- pass
-
-
-class NestedResourceRoot(APIView):
- pass
-
-
-class NestedResourceInstance(APIView):
- pass
-
-urlpatterns = patterns(
- '',
- url(r'^$', Root.as_view()),
- url(r'^resource/$', ResourceRoot.as_view()),
- url(r'^resource/(?P[0-9]+)$', ResourceInstance.as_view()),
- url(r'^resource/(?P[0-9]+)/$', NestedResourceRoot.as_view()),
- url(r'^resource/(?P[0-9]+)/(?P[A-Za-z]+)$', NestedResourceInstance.as_view()),
-)
-
-
-class BreadcrumbTests(TestCase):
- """Tests the breadcrumb functionality used by the HTML renderer."""
-
- urls = 'tests.test_breadcrumbs'
-
- def test_root_breadcrumbs(self):
- url = '/'
- self.assertEqual(
- get_breadcrumbs(url),
- [('Root', '/')]
- )
-
- def test_resource_root_breadcrumbs(self):
- url = '/resource/'
- self.assertEqual(
- get_breadcrumbs(url),
- [
- ('Root', '/'),
- ('Resource Root', '/resource/')
- ]
- )
-
- def test_resource_instance_breadcrumbs(self):
- url = '/resource/123'
- self.assertEqual(
- get_breadcrumbs(url),
- [
- ('Root', '/'),
- ('Resource Root', '/resource/'),
- ('Resource Instance', '/resource/123')
- ]
- )
-
- def test_nested_resource_breadcrumbs(self):
- url = '/resource/123/'
- self.assertEqual(
- get_breadcrumbs(url),
- [
- ('Root', '/'),
- ('Resource Root', '/resource/'),
- ('Resource Instance', '/resource/123'),
- ('Nested Resource Root', '/resource/123/')
- ]
- )
-
- def test_nested_resource_instance_breadcrumbs(self):
- url = '/resource/123/abc'
- self.assertEqual(
- get_breadcrumbs(url),
- [
- ('Root', '/'),
- ('Resource Root', '/resource/'),
- ('Resource Instance', '/resource/123'),
- ('Nested Resource Root', '/resource/123/'),
- ('Nested Resource Instance', '/resource/123/abc')
- ]
- )
-
- def test_broken_url_breadcrumbs_handled_gracefully(self):
- url = '/foobar'
- self.assertEqual(
- get_breadcrumbs(url),
- [('Root', '/')]
- )
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index ec922b6d..d518dd58 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -24,6 +24,9 @@ class CustomField(models.Field):
pass
+COLOR_CHOICES = (('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))
+
+
class RegularFieldsModel(models.Model):
"""
A model class for testing regular flat fields.
@@ -32,6 +35,7 @@ class RegularFieldsModel(models.Model):
big_integer_field = models.BigIntegerField()
boolean_field = models.BooleanField(default=False)
char_field = models.CharField(max_length=100)
+ choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES)
comma_seperated_integer_field = models.CommaSeparatedIntegerField(max_length=100)
date_field = models.DateField()
datetime_field = models.DateTimeField()
@@ -68,6 +72,7 @@ class TestRegularFieldMappings(TestCase):
big_integer_field = IntegerField()
boolean_field = BooleanField(default=False)
char_field = CharField(max_length=100)
+ choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')])
comma_seperated_integer_field = CharField(max_length=100, validators=[])
date_field = DateField()
datetime_field = DateTimeField()
diff --git a/tests/test_modelinfo.py b/tests/test_modelinfo.py
deleted file mode 100644
index 04b67f04..00000000
--- a/tests/test_modelinfo.py
+++ /dev/null
@@ -1,31 +0,0 @@
-from django.test import TestCase
-from django.utils import six
-from rest_framework.utils.model_meta import _resolve_model
-from tests.models import BasicModel
-
-
-class ResolveModelTests(TestCase):
- """
- `_resolve_model` should return a Django model class given the
- provided argument is a Django model class itself, or a properly
- formatted string representation of one.
- """
- def test_resolve_django_model(self):
- resolved_model = _resolve_model(BasicModel)
- self.assertEqual(resolved_model, BasicModel)
-
- def test_resolve_string_representation(self):
- resolved_model = _resolve_model('tests.BasicModel')
- self.assertEqual(resolved_model, BasicModel)
-
- def test_resolve_unicode_representation(self):
- resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
- self.assertEqual(resolved_model, BasicModel)
-
- def test_resolve_non_django_model(self):
- with self.assertRaises(ValueError):
- _resolve_model(TestCase)
-
- def test_resolve_improper_string_representation(self):
- with self.assertRaises(ValueError):
- _resolve_model('BasicModel')
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py
index 75ee0eaa..b04a937e 100644
--- a/tests/test_templatetags.py
+++ b/tests/test_templatetags.py
@@ -4,6 +4,7 @@ from django.test import TestCase
from rest_framework.test import APIRequestFactory
from rest_framework.templatetags.rest_framework import add_query_param, urlize_quoted_links
+
factory = APIRequestFactory()
@@ -49,3 +50,37 @@ class Issue1386Tests(TestCase):
# example from issue #1386, this shouldn't raise an exception
urlize_quoted_links("asdf:[/p]zxcv.com")
+
+
+class URLizerTests(TestCase):
+ """
+ Test if both JSON and YAML URLs are transformed into links well
+ """
+ def _urlize_dict_check(self, data):
+ """
+ For all items in dict test assert that the value is urlized key
+ """
+ for original, urlized in data.items():
+ assert urlize_quoted_links(original, nofollow=False) == urlized
+
+ def test_json_with_url(self):
+ """
+ Test if JSON URLs are transformed into links well
+ """
+ data = {}
+ data['"url": "http://api/users/1/", '] = \
+ '"url": "http://api/users/1/", '
+ data['"foo_set": [\n "http://api/foos/1/"\n], '] = \
+ '"foo_set": [\n "http://api/foos/1/"\n], '
+ self._urlize_dict_check(data)
+
+ def test_yaml_with_url(self):
+ """
+ Test if YAML URLs are transformed into links well
+ """
+ data = {}
+ data['''{users: 'http://api/users/'}'''] = \
+ '''{users: 'http://api/users/'}'''
+ data['''foo_set: ['http://api/foos/1/']'''] = \
+ '''foo_set: ['http://api/foos/1/']'''
+ self._urlize_dict_check(data)
diff --git a/tests/test_urlizer.py b/tests/test_urlizer.py
deleted file mode 100644
index a77aa22a..00000000
--- a/tests/test_urlizer.py
+++ /dev/null
@@ -1,37 +0,0 @@
-from __future__ import unicode_literals
-from django.test import TestCase
-from rest_framework.templatetags.rest_framework import urlize_quoted_links
-
-
-class URLizerTests(TestCase):
- """
- Test if both JSON and YAML URLs are transformed into links well
- """
- def _urlize_dict_check(self, data):
- """
- For all items in dict test assert that the value is urlized key
- """
- for original, urlized in data.items():
- assert urlize_quoted_links(original, nofollow=False) == urlized
-
- def test_json_with_url(self):
- """
- Test if JSON URLs are transformed into links well
- """
- data = {}
- data['"url": "http://api/users/1/", '] = \
- '"url": "http://api/users/1/", '
- data['"foo_set": [\n "http://api/foos/1/"\n], '] = \
- '"foo_set": [\n "http://api/foos/1/"\n], '
- self._urlize_dict_check(data)
-
- def test_yaml_with_url(self):
- """
- Test if YAML URLs are transformed into links well
- """
- data = {}
- data['''{users: 'http://api/users/'}'''] = \
- '''{users: 'http://api/users/'}'''
- data['''foo_set: ['http://api/foos/1/']'''] = \
- '''foo_set: ['http://api/foos/1/']'''
- self._urlize_dict_check(data)
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 00000000..96c5f997
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,132 @@
+from __future__ import unicode_literals
+from django.conf.urls import patterns, url
+from django.test import TestCase
+from django.utils import six
+from rest_framework.utils.model_meta import _resolve_model
+from rest_framework.utils.breadcrumbs import get_breadcrumbs
+from rest_framework.views import APIView
+from tests.models import BasicModel
+
+
+class Root(APIView):
+ pass
+
+
+class ResourceRoot(APIView):
+ pass
+
+
+class ResourceInstance(APIView):
+ pass
+
+
+class NestedResourceRoot(APIView):
+ pass
+
+
+class NestedResourceInstance(APIView):
+ pass
+
+
+urlpatterns = patterns(
+ '',
+ url(r'^$', Root.as_view()),
+ url(r'^resource/$', ResourceRoot.as_view()),
+ url(r'^resource/(?P[0-9]+)$', ResourceInstance.as_view()),
+ url(r'^resource/(?P[0-9]+)/$', NestedResourceRoot.as_view()),
+ url(r'^resource/(?P[0-9]+)/(?P[A-Za-z]+)$', NestedResourceInstance.as_view()),
+)
+
+
+class BreadcrumbTests(TestCase):
+ """
+ Tests the breadcrumb functionality used by the HTML renderer.
+ """
+ urls = 'tests.test_utils'
+
+ def test_root_breadcrumbs(self):
+ url = '/'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [('Root', '/')]
+ )
+
+ def test_resource_root_breadcrumbs(self):
+ url = '/resource/'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/')
+ ]
+ )
+
+ def test_resource_instance_breadcrumbs(self):
+ url = '/resource/123'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/'),
+ ('Resource Instance', '/resource/123')
+ ]
+ )
+
+ def test_nested_resource_breadcrumbs(self):
+ url = '/resource/123/'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/'),
+ ('Resource Instance', '/resource/123'),
+ ('Nested Resource Root', '/resource/123/')
+ ]
+ )
+
+ def test_nested_resource_instance_breadcrumbs(self):
+ url = '/resource/123/abc'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/'),
+ ('Resource Instance', '/resource/123'),
+ ('Nested Resource Root', '/resource/123/'),
+ ('Nested Resource Instance', '/resource/123/abc')
+ ]
+ )
+
+ def test_broken_url_breadcrumbs_handled_gracefully(self):
+ url = '/foobar'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [('Root', '/')]
+ )
+
+
+class ResolveModelTests(TestCase):
+ """
+ `_resolve_model` should return a Django model class given the
+ provided argument is a Django model class itself, or a properly
+ formatted string representation of one.
+ """
+ def test_resolve_django_model(self):
+ resolved_model = _resolve_model(BasicModel)
+ self.assertEqual(resolved_model, BasicModel)
+
+ def test_resolve_string_representation(self):
+ resolved_model = _resolve_model('tests.BasicModel')
+ self.assertEqual(resolved_model, BasicModel)
+
+ def test_resolve_unicode_representation(self):
+ resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
+ self.assertEqual(resolved_model, BasicModel)
+
+ def test_resolve_non_django_model(self):
+ with self.assertRaises(ValueError):
+ _resolve_model(TestCase)
+
+ def test_resolve_improper_string_representation(self):
+ with self.assertRaises(ValueError):
+ _resolve_model('BasicModel')
--
cgit v1.2.3
From cf72b9a8b755652cec4ad19a27488e3a79c2e401 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 19 Sep 2014 16:43:13 +0100
Subject: Moar tests
---
rest_framework/serializers.py | 2 ++
tests/test_model_serializer.py | 33 +++++++++++++++++++++++++++++++++
2 files changed, 35 insertions(+)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index d2740fc2..d9f9c8cb 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -24,6 +24,7 @@ from rest_framework.utils.field_mapping import (
lookup_class
)
import copy
+import inspect
# Note: We do the following so that users of the framework can use this style:
#
@@ -268,6 +269,7 @@ class ListSerializer(BaseSerializer):
def __init__(self, *args, **kwargs):
self.child = kwargs.pop('child', copy.deepcopy(self.child))
assert self.child is not None, '`child` is a required argument.'
+ assert not inspect.isclass(self.child), '`child` has not been instantiated.'
self.context = kwargs.pop('context', {})
kwargs.pop('partial', None)
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index d518dd58..d9f9efbe 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -473,3 +473,36 @@ class TestIntegration(TestCase):
'through': []
}
self.assertEqual(serializer.data, expected)
+
+
+# Tests for bulk create using `ListSerializer`.
+
+class BulkCreateModel(models.Model):
+ name = models.CharField(max_length=10)
+
+
+class TestBulkCreate(TestCase):
+ def test_bulk_create(self):
+ class BasicModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BulkCreateModel
+ fields = ('name',)
+
+ class BulkCreateSerializer(serializers.ListSerializer):
+ child = BasicModelSerializer()
+
+ data = [{'name': 'a'}, {'name': 'b'}, {'name': 'c'}]
+ serializer = BulkCreateSerializer(data=data)
+ assert serializer.is_valid()
+
+ # Objects are returned by save().
+ instances = serializer.save()
+ assert len(instances) == 3
+ assert [item.name for item in instances] == ['a', 'b', 'c']
+
+ # Objects have been created in the database.
+ assert BulkCreateModel.objects.count() == 3
+ assert list(BulkCreateModel.objects.values_list('name', flat=True)) == ['a', 'b', 'c']
+
+ # Serializer returns correct data.
+ assert serializer.data == data
--
cgit v1.2.3
From af46fd6b00f1d7f018049c19094af58acb1415fb Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 22 Sep 2014 12:25:57 +0100
Subject: Field tests and associated cleanup
---
rest_framework/fields.py | 64 +++++----
tests/test_fields.py | 334 +++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 365 insertions(+), 33 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 0c78b3fb..db75ddf9 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -12,7 +12,6 @@ from rest_framework.utils import html, representation, humanize_datetime
import datetime
import decimal
import inspect
-import warnings
class empty:
@@ -395,7 +394,7 @@ class IntegerField(Field):
class FloatField(Field):
default_error_messages = {
- 'invalid': _("'%s' value must be a float."),
+ 'invalid': _("A valid number is required."),
}
def __init__(self, **kwargs):
@@ -410,20 +409,20 @@ class FloatField(Field):
def to_internal_value(self, value):
if value is None:
return None
- return float(value)
+ try:
+ return float(value)
+ except (TypeError, ValueError):
+ self.fail('invalid')
def to_representation(self, value):
if value is None:
return None
- try:
- return float(value)
- except (TypeError, ValueError):
- self.fail('invalid', value=value)
+ return float(value)
class DecimalField(Field):
default_error_messages = {
- 'invalid': _('Enter a number.'),
+ 'invalid': _('A valid number is required.'),
'max_value': _('Ensure this value is less than or equal to {max_value}.'),
'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
'max_digits': _('Ensure that there are no more than {max_digits} digits in total.'),
@@ -485,7 +484,7 @@ class DecimalField(Field):
if self.decimal_places is not None and decimals > self.decimal_places:
self.fail('max_decimal_places', max_decimal_places=self.decimal_places)
if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places):
- self.fail('max_whole_digits', max_while_digits=self.max_digits - self.decimal_places)
+ self.fail('max_whole_digits', max_whole_digits=self.max_digits - self.decimal_places)
return value
@@ -511,6 +510,7 @@ class DecimalField(Field):
class DateField(Field):
default_error_messages = {
'invalid': _('Date has wrong format. Use one of these formats instead: {format}'),
+ 'datetime': _('Expected a date but got a datetime.'),
}
format = api_settings.DATE_FORMAT
input_formats = api_settings.DATE_INPUT_FORMATS
@@ -525,12 +525,7 @@ class DateField(Field):
return None
if isinstance(value, datetime.datetime):
- if timezone and settings.USE_TZ and timezone.is_aware(value):
- # Convert aware datetimes to the default time zone
- # before casting them to dates (#17742).
- default_timezone = timezone.get_default_timezone()
- value = timezone.make_naive(value, default_timezone)
- return value.date()
+ self.fail('datetime')
if isinstance(value, datetime.date):
return value
@@ -570,35 +565,38 @@ class DateField(Field):
class DateTimeField(Field):
default_error_messages = {
'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'),
+ 'date': _('Expected a datetime but got a date.'),
}
format = api_settings.DATETIME_FORMAT
input_formats = api_settings.DATETIME_INPUT_FORMATS
+ default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None
- def __init__(self, format=None, input_formats=None, *args, **kwargs):
+ def __init__(self, format=None, input_formats=None, default_timezone=None, *args, **kwargs):
self.format = format if format is not None else self.format
self.input_formats = input_formats if input_formats is not None else self.input_formats
+ self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone
super(DateTimeField, self).__init__(*args, **kwargs)
+ def enforce_timezone(self, value):
+ """
+ When `self.default_timezone` is `None`, always return naive datetimes.
+ When `self.default_timezone` is not `None`, always return aware datetimes.
+ """
+ if (self.default_timezone is not None) and not timezone.is_aware(value):
+ return timezone.make_aware(value, self.default_timezone)
+ elif (self.default_timezone is None) and timezone.is_aware(value):
+ return timezone.make_naive(value, timezone.UTC())
+ return value
+
def to_internal_value(self, value):
if value in (None, ''):
return None
- if isinstance(value, datetime.datetime):
- return value
+ if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
+ self.fail('date')
- if isinstance(value, datetime.date):
- value = datetime.datetime(value.year, value.month, value.day)
- if settings.USE_TZ:
- # For backwards compatibility, interpret naive datetimes in
- # local time. This won't work during DST change, but we can't
- # do much about it, so we let the exceptions percolate up the
- # call stack.
- warnings.warn("DateTimeField received a naive datetime (%s)"
- " while time zone support is active." % value,
- RuntimeWarning)
- default_timezone = timezone.get_default_timezone()
- value = timezone.make_aware(value, default_timezone)
- return value
+ if isinstance(value, datetime.datetime):
+ return self.enforce_timezone(value)
for format in self.input_formats:
if format.lower() == ISO_8601:
@@ -608,14 +606,14 @@ class DateTimeField(Field):
pass
else:
if parsed is not None:
- return parsed
+ return self.enforce_timezone(parsed)
else:
try:
parsed = datetime.datetime.strptime(value, format)
except (ValueError, TypeError):
pass
else:
- return parsed
+ return self.enforce_timezone(parsed)
humanized_format = humanize_datetime.datetime_formats(self.input_formats)
self.fail('invalid', format=humanized_format)
diff --git a/tests/test_fields.py b/tests/test_fields.py
index a92fafbc..6ec18041 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1,3 +1,337 @@
+from decimal import Decimal
+from django.utils import timezone
+from rest_framework import fields
+import datetime
+import pytest
+
+
+class ValidAndInvalidValues:
+ """
+ Base class for testing valid and invalid field values.
+ """
+ def test_valid_values(self):
+ """
+ Ensure that valid values return the expected validated data.
+ """
+ for input_value, expected_output in self.valid_mappings.items():
+ assert self.field.run_validation(input_value) == expected_output
+
+ def test_invalid_values(self):
+ """
+ Ensure that invalid values raise the expected validation error.
+ """
+ for input_value, expected_failure in self.invalid_mappings.items():
+ with pytest.raises(fields.ValidationError) as exc_info:
+ self.field.run_validation(input_value)
+ assert exc_info.value.messages == expected_failure
+
+
+class TestCharField(ValidAndInvalidValues):
+ valid_mappings = {
+ 1: '1',
+ 'abc': 'abc'
+ }
+ invalid_mappings = {
+ '': ['This field may not be blank.']
+ }
+ field = fields.CharField()
+
+
+class TestBooleanField(ValidAndInvalidValues):
+ valid_mappings = {
+ 'true': True,
+ 'false': False,
+ '1': True,
+ '0': False,
+ 1: True,
+ 0: False,
+ True: True,
+ False: False,
+ }
+ invalid_mappings = {
+ 'foo': ['`foo` is not a valid boolean.']
+ }
+ field = fields.BooleanField()
+
+
+# Number types...
+
+class TestIntegerField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `IntegerField`.
+ """
+ valid_mappings = {
+ '1': 1,
+ '0': 0,
+ 1: 1,
+ 0: 0,
+ 1.0: 1,
+ 0.0: 0
+ }
+ invalid_mappings = {
+ 'abc': ['A valid integer is required.']
+ }
+ field = fields.IntegerField()
+
+
+class TestMinMaxIntegerField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `IntegerField` with min and max limits.
+ """
+ valid_mappings = {
+ '1': 1,
+ '3': 3,
+ 1: 1,
+ 3: 3,
+ }
+ invalid_mappings = {
+ 0: ['Ensure this value is greater than or equal to 1.'],
+ 4: ['Ensure this value is less than or equal to 3.'],
+ '0': ['Ensure this value is greater than or equal to 1.'],
+ '4': ['Ensure this value is less than or equal to 3.'],
+ }
+ field = fields.IntegerField(min_value=1, max_value=3)
+
+
+class TestFloatField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `FloatField`.
+ """
+ valid_mappings = {
+ '1': 1.0,
+ '0': 0.0,
+ 1: 1.0,
+ 0: 0.0,
+ 1.0: 1.0,
+ 0.0: 0.0,
+ }
+ invalid_mappings = {
+ 'abc': ["A valid number is required."]
+ }
+ field = fields.FloatField()
+
+
+class TestMinMaxFloatField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `FloatField` with min and max limits.
+ """
+ valid_mappings = {
+ '1': 1,
+ '3': 3,
+ 1: 1,
+ 3: 3,
+ 1.0: 1.0,
+ 3.0: 3.0,
+ }
+ invalid_mappings = {
+ 0.9: ['Ensure this value is greater than or equal to 1.'],
+ 3.1: ['Ensure this value is less than or equal to 3.'],
+ '0.0': ['Ensure this value is greater than or equal to 1.'],
+ '3.1': ['Ensure this value is less than or equal to 3.'],
+ }
+ field = fields.FloatField(min_value=1, max_value=3)
+
+
+class TestDecimalField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `DecimalField`.
+ """
+ valid_mappings = {
+ '12.3': Decimal('12.3'),
+ '0.1': Decimal('0.1'),
+ 10: Decimal('10'),
+ 0: Decimal('0'),
+ 12.3: Decimal('12.3'),
+ 0.1: Decimal('0.1'),
+ }
+ invalid_mappings = {
+ 'abc': ["A valid number is required."],
+ Decimal('Nan'): ["A valid number is required."],
+ Decimal('Inf'): ["A valid number is required."],
+ '12.345': ["Ensure that there are no more than 3 digits in total."],
+ '0.01': ["Ensure that there are no more than 1 decimal places."],
+ 123: ["Ensure that there are no more than 2 digits before the decimal point."]
+ }
+ field = fields.DecimalField(max_digits=3, decimal_places=1)
+
+
+class TestMinMaxDecimalField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `DecimalField` with min and max limits.
+ """
+ valid_mappings = {
+ '10.0': 10.0,
+ '20.0': 20.0,
+ }
+ invalid_mappings = {
+ '9.9': ['Ensure this value is greater than or equal to 10.'],
+ '20.1': ['Ensure this value is less than or equal to 20.'],
+ }
+ field = fields.DecimalField(
+ max_digits=3, decimal_places=1,
+ min_value=10, max_value=20
+ )
+
+
+# Date & time fields...
+
+class TestDateField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `DateField`.
+ """
+ valid_mappings = {
+ '2001-01-01': datetime.date(2001, 1, 1),
+ datetime.date(2001, 1, 1): datetime.date(2001, 1, 1),
+ }
+ invalid_mappings = {
+ 'abc': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
+ '2001-99-99': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
+ datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
+ }
+ field = fields.DateField()
+
+
+class TestCustomInputFormatDateField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `DateField` with a cutom input format.
+ """
+ valid_mappings = {
+ '1 Jan 2001': datetime.date(2001, 1, 1),
+ }
+ invalid_mappings = {
+ '2001-01-01': ['Date has wrong format. Use one of these formats instead: DD [Jan-Dec] YYYY']
+ }
+ field = fields.DateField(input_formats=['%d %b %Y'])
+
+
+class TestDateTimeField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `DateTimeField`.
+ """
+ valid_mappings = {
+ '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ '2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ '2001-01-01T14:00+0100': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ }
+ invalid_mappings = {
+ 'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
+ '2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
+ datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
+ }
+ field = fields.DateTimeField(default_timezone=timezone.UTC())
+
+
+class TestCustomInputFormatDateTimeField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `DateTimeField` with a cutom input format.
+ """
+ valid_mappings = {
+ '1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()),
+ }
+ invalid_mappings = {
+ '2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY']
+ }
+ field = fields.DateTimeField(default_timezone=timezone.UTC(), input_formats=['%I:%M%p, %d %b %Y'])
+
+
+class TestNaiveDateTimeField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `DateTimeField` with naive datetimes.
+ """
+ valid_mappings = {
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00),
+ '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
+ }
+ invalid_mappings = {}
+ field = fields.DateTimeField(default_timezone=None)
+
+
+# Choice types...
+
+class TestChoiceField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `ChoiceField`.
+ """
+ valid_mappings = {
+ 'poor': 'poor',
+ 'medium': 'medium',
+ 'good': 'good',
+ }
+ invalid_mappings = {
+ 'awful': ['`awful` is not a valid choice.']
+ }
+ field = fields.ChoiceField(
+ choices=[
+ ('poor', 'Poor quality'),
+ ('medium', 'Medium quality'),
+ ('good', 'Good quality'),
+ ]
+ )
+
+
+class TestChoiceFieldWithType(ValidAndInvalidValues):
+ """
+ Valid and invalid values for a `Choice` field that uses an integer type,
+ instead of a char type.
+ """
+ valid_mappings = {
+ '1': 1,
+ 3: 3,
+ }
+ invalid_mappings = {
+ 5: ['`5` is not a valid choice.'],
+ 'abc': ['`abc` is not a valid choice.']
+ }
+ field = fields.ChoiceField(
+ choices=[
+ (1, 'Poor quality'),
+ (2, 'Medium quality'),
+ (3, 'Good quality'),
+ ]
+ )
+
+
+class TestChoiceFieldWithListChoices(ValidAndInvalidValues):
+ """
+ Valid and invalid values for a `Choice` field that uses a flat list for the
+ choices, rather than a list of pairs of (`value`, `description`).
+ """
+ valid_mappings = {
+ 'poor': 'poor',
+ 'medium': 'medium',
+ 'good': 'good',
+ }
+ invalid_mappings = {
+ 'awful': ['`awful` is not a valid choice.']
+ }
+ field = fields.ChoiceField(choices=('poor', 'medium', 'good'))
+
+
+class TestMultipleChoiceField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `MultipleChoiceField`.
+ """
+ valid_mappings = {
+ (): set(),
+ ('aircon',): set(['aircon']),
+ ('aircon', 'manual'): set(['aircon', 'manual']),
+ }
+ invalid_mappings = {
+ 'abc': ['Expected a list of items but got type `str`'],
+ ('aircon', 'incorrect'): ['`incorrect` is not a valid choice.']
+ }
+ field = fields.MultipleChoiceField(
+ choices=[
+ ('aircon', 'AirCon'),
+ ('manual', 'Manual drive'),
+ ('diesel', 'Diesel'),
+ ]
+ )
+
+
# """
# General serializer field tests.
# """
--
cgit v1.2.3
From afb3f8ab0ad6c33b147292e9777ba0ddf3871d14 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 22 Sep 2014 13:26:47 +0100
Subject: Tests and tweaks for text fields
---
rest_framework/fields.py | 32 ++++++++++++-----
tests/test_fields.py | 93 ++++++++++++++++++++++++++++++++++++++++++------
2 files changed, 107 insertions(+), 18 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index db75ddf9..35bd5c4b 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -12,6 +12,7 @@ from rest_framework.utils import html, representation, humanize_datetime
import datetime
import decimal
import inspect
+import re
class empty:
@@ -325,7 +326,11 @@ class EmailField(CharField):
default_error_messages = {
'invalid': _('Enter a valid email address.')
}
- default_validators = [validators.validate_email]
+
+ def __init__(self, **kwargs):
+ super(EmailField, self).__init__(**kwargs)
+ validator = validators.EmailValidator(message=self.error_messages['invalid'])
+ self.validators = [validator] + self.validators
def to_internal_value(self, data):
if data == '' and not self.allow_blank:
@@ -341,26 +346,37 @@ class EmailField(CharField):
class RegexField(CharField):
+ default_error_messages = {
+ 'invalid': _('This value does not match the required pattern.')
+ }
+
def __init__(self, regex, **kwargs):
- kwargs['validators'] = (
- [validators.RegexValidator(regex)] +
- kwargs.get('validators', [])
- )
super(RegexField, self).__init__(**kwargs)
+ validator = validators.RegexValidator(regex, message=self.error_messages['invalid'])
+ self.validators = [validator] + self.validators
class SlugField(CharField):
default_error_messages = {
'invalid': _("Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.")
}
- default_validators = [validators.validate_slug]
+
+ def __init__(self, **kwargs):
+ super(SlugField, self).__init__(**kwargs)
+ slug_regex = re.compile(r'^[-a-zA-Z0-9_]+$')
+ validator = validators.RegexValidator(slug_regex, message=self.error_messages['invalid'])
+ self.validators = [validator] + self.validators
class URLField(CharField):
default_error_messages = {
'invalid': _("Enter a valid URL.")
}
- default_validators = [validators.URLValidator()]
+
+ def __init__(self, **kwargs):
+ super(URLField, self).__init__(**kwargs)
+ validator = validators.URLValidator(message=self.error_messages['invalid'])
+ self.validators = [validator] + self.validators
# Number types...
@@ -642,7 +658,7 @@ class TimeField(Field):
self.input_formats = input_formats if input_formats is not None else self.input_formats
super(TimeField, self).__init__(*args, **kwargs)
- def from_native(self, value):
+ def to_internal_value(self, value):
if value in (None, ''):
return None
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 6ec18041..ae7f1919 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -26,16 +26,7 @@ class ValidAndInvalidValues:
assert exc_info.value.messages == expected_failure
-class TestCharField(ValidAndInvalidValues):
- valid_mappings = {
- 1: '1',
- 'abc': 'abc'
- }
- invalid_mappings = {
- '': ['This field may not be blank.']
- }
- field = fields.CharField()
-
+# Boolean types...
class TestBooleanField(ValidAndInvalidValues):
valid_mappings = {
@@ -54,6 +45,60 @@ class TestBooleanField(ValidAndInvalidValues):
field = fields.BooleanField()
+# String types...
+
+class TestCharField(ValidAndInvalidValues):
+ valid_mappings = {
+ 1: '1',
+ 'abc': 'abc'
+ }
+ invalid_mappings = {
+ '': ['This field may not be blank.']
+ }
+ field = fields.CharField()
+
+
+class TestEmailField(ValidAndInvalidValues):
+ valid_mappings = {
+ 'example@example.com': 'example@example.com',
+ ' example@example.com ': 'example@example.com',
+ }
+ invalid_mappings = {
+ 'example.com': ['Enter a valid email address.']
+ }
+ field = fields.EmailField()
+
+
+class TestRegexField(ValidAndInvalidValues):
+ valid_mappings = {
+ 'a9': 'a9',
+ }
+ invalid_mappings = {
+ 'A9': ["This value does not match the required pattern."]
+ }
+ field = fields.RegexField(regex='[a-z][0-9]')
+
+
+class TestSlugField(ValidAndInvalidValues):
+ valid_mappings = {
+ 'slug-99': 'slug-99',
+ }
+ invalid_mappings = {
+ 'slug 99': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]
+ }
+ field = fields.SlugField()
+
+
+class TestURLField(ValidAndInvalidValues):
+ valid_mappings = {
+ 'http://example.com': 'http://example.com',
+ }
+ invalid_mappings = {
+ 'example.com': ['Enter a valid URL.']
+ }
+ field = fields.URLField()
+
+
# Number types...
class TestIntegerField(ValidAndInvalidValues):
@@ -249,6 +294,34 @@ class TestNaiveDateTimeField(ValidAndInvalidValues):
field = fields.DateTimeField(default_timezone=None)
+class TestTimeField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `TimeField`.
+ """
+ valid_mappings = {
+ '13:00': datetime.time(13, 00),
+ datetime.time(13, 00): datetime.time(13, 00),
+ }
+ invalid_mappings = {
+ 'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
+ '99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
+ }
+ field = fields.TimeField()
+
+
+class TestCustomInputFormatTimeField(ValidAndInvalidValues):
+ """
+ Valid and invalid values for `TimeField` with a custom input format.
+ """
+ valid_mappings = {
+ '1:00pm': datetime.time(13, 00),
+ }
+ invalid_mappings = {
+ '13:00': ['Time has wrong format. Use one of these formats instead: hh:mm[AM|PM]'],
+ }
+ field = fields.TimeField(input_formats=['%I:%M%p'])
+
+
# Choice types...
class TestChoiceField(ValidAndInvalidValues):
--
cgit v1.2.3
From c54f394904c3f93211b8aa073de4e9e50110f831 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 22 Sep 2014 13:57:45 +0100
Subject: Ensure 'messages' in fields are respected in preference to default
validator messages
---
rest_framework/compat.py | 19 +++++++++++++++++++
rest_framework/fields.py | 34 ++++++++++++++++++++++------------
2 files changed, 41 insertions(+), 12 deletions(-)
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 7c05bed9..2b4ddb02 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -121,6 +121,25 @@ else:
return [m.upper() for m in self.http_method_names if hasattr(self, m)]
+
+# MinValueValidator and MaxValueValidator only accept `message` in 1.8+
+if django.VERSION >= (1, 8):
+ from django.core.validators import MinValueValidator, MaxValueValidator
+else:
+ from django.core.validators import MinValueValidator as DjangoMinValueValidator
+ from django.core.validators import MaxValueValidator as DjangoMaxValueValidator
+
+ class MinValueValidator(DjangoMinValueValidator):
+ def __init__(self, *args, **kwargs):
+ self.message = kwargs.pop('message', self.message)
+ super(MinValueValidator, self).__init__(*args, **kwargs)
+
+ class MaxValueValidator(DjangoMaxValueValidator):
+ def __init__(self, *args, **kwargs):
+ self.message = kwargs.pop('message', self.message)
+ super(MaxValueValidator, self).__init__(*args, **kwargs)
+
+
# PATCH method is not implemented by Django
if 'patch' not in View.http_method_names:
View.http_method_names = View.http_method_names + ['patch']
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 35bd5c4b..5105dfcb 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -6,7 +6,7 @@ from django.utils.dateparse import parse_date, parse_datetime, parse_time
from django.utils.encoding import is_protected_type
from django.utils.translation import ugettext_lazy as _
from rest_framework import ISO_8601
-from rest_framework.compat import smart_text
+from rest_framework.compat import smart_text, MinValueValidator, MaxValueValidator
from rest_framework.settings import api_settings
from rest_framework.utils import html, representation, humanize_datetime
import datetime
@@ -330,7 +330,7 @@ class EmailField(CharField):
def __init__(self, **kwargs):
super(EmailField, self).__init__(**kwargs)
validator = validators.EmailValidator(message=self.error_messages['invalid'])
- self.validators = [validator] + self.validators
+ self.validators.append(validator)
def to_internal_value(self, data):
if data == '' and not self.allow_blank:
@@ -353,7 +353,7 @@ class RegexField(CharField):
def __init__(self, regex, **kwargs):
super(RegexField, self).__init__(**kwargs)
validator = validators.RegexValidator(regex, message=self.error_messages['invalid'])
- self.validators = [validator] + self.validators
+ self.validators.append(validator)
class SlugField(CharField):
@@ -365,7 +365,7 @@ class SlugField(CharField):
super(SlugField, self).__init__(**kwargs)
slug_regex = re.compile(r'^[-a-zA-Z0-9_]+$')
validator = validators.RegexValidator(slug_regex, message=self.error_messages['invalid'])
- self.validators = [validator] + self.validators
+ self.validators.append(validator)
class URLField(CharField):
@@ -376,14 +376,16 @@ class URLField(CharField):
def __init__(self, **kwargs):
super(URLField, self).__init__(**kwargs)
validator = validators.URLValidator(message=self.error_messages['invalid'])
- self.validators = [validator] + self.validators
+ self.validators.append(validator)
# Number types...
class IntegerField(Field):
default_error_messages = {
- 'invalid': _('A valid integer is required.')
+ 'invalid': _('A valid integer is required.'),
+ 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
+ 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
}
def __init__(self, **kwargs):
@@ -391,9 +393,11 @@ class IntegerField(Field):
min_value = kwargs.pop('min_value', None)
super(IntegerField, self).__init__(**kwargs)
if max_value is not None:
- self.validators.append(validators.MaxValueValidator(max_value))
+ message = self.error_messages['max_value'].format(max_value=max_value)
+ self.validators.append(MaxValueValidator(max_value, message=message))
if min_value is not None:
- self.validators.append(validators.MinValueValidator(min_value))
+ message = self.error_messages['min_value'].format(min_value=min_value)
+ self.validators.append(MinValueValidator(min_value, message=message))
def to_internal_value(self, data):
try:
@@ -411,6 +415,8 @@ class IntegerField(Field):
class FloatField(Field):
default_error_messages = {
'invalid': _("A valid number is required."),
+ 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
+ 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
}
def __init__(self, **kwargs):
@@ -418,9 +424,11 @@ class FloatField(Field):
min_value = kwargs.pop('min_value', None)
super(FloatField, self).__init__(**kwargs)
if max_value is not None:
- self.validators.append(validators.MaxValueValidator(max_value))
+ message = self.error_messages['max_value'].format(max_value=max_value)
+ self.validators.append(MaxValueValidator(max_value, message=message))
if min_value is not None:
- self.validators.append(validators.MinValueValidator(min_value))
+ message = self.error_messages['min_value'].format(min_value=min_value)
+ self.validators.append(MinValueValidator(min_value, message=message))
def to_internal_value(self, value):
if value is None:
@@ -454,9 +462,11 @@ class DecimalField(Field):
self.coerce_to_string = coerce_to_string if (coerce_to_string is not None) else self.coerce_to_string
super(DecimalField, self).__init__(**kwargs)
if max_value is not None:
- self.validators.append(validators.MaxValueValidator(max_value))
+ message = self.error_messages['max_value'].format(max_value=max_value)
+ self.validators.append(MaxValueValidator(max_value, message=message))
if min_value is not None:
- self.validators.append(validators.MinValueValidator(min_value))
+ message = self.error_messages['min_value'].format(min_value=min_value)
+ self.validators.append(MinValueValidator(min_value, message=message))
def to_internal_value(self, value):
"""
--
cgit v1.2.3
From 249253a144ba4381581809fb3f27959c7bd6e577 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 22 Sep 2014 14:54:33 +0100
Subject: Fix compat issues
---
rest_framework/fields.py | 13 ++++++++++---
tests/test_fields.py | 31 ++++++++++++++++++++-----------
2 files changed, 30 insertions(+), 14 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 5105dfcb..5fb99a42 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -209,8 +209,10 @@ class Field(object):
"""
Validate a simple representation and return the internal value.
- The provided data may be `empty` if no representation was included.
- May return `empty` if the field should not be included in the
+ The provided data may be `empty` if no representation was included
+ in the input.
+
+ May raise `SkipField` if the field should not be included in the
validated data.
"""
if data is empty:
@@ -223,6 +225,10 @@ class Field(object):
return value
def run_validators(self, value):
+ """
+ Test the given value against all the validators on the field,
+ and either raise a `ValidationError` or simply return.
+ """
if value in (None, '', [], (), {}):
return
@@ -753,8 +759,9 @@ class MultipleChoiceField(ChoiceField):
}
def to_internal_value(self, data):
- if not hasattr(data, '__iter__'):
+ if isinstance(data, type('')) or not hasattr(data, '__iter__'):
self.fail('not_a_list', input_type=type(data).__name__)
+
return set([
super(MultipleChoiceField, self).to_internal_value(item)
for item in data
diff --git a/tests/test_fields.py b/tests/test_fields.py
index ae7f1919..e03ece54 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -5,22 +5,31 @@ import datetime
import pytest
+def get_items(mapping_or_list_of_two_tuples):
+ # Tests accept either lists of two tuples, or dictionaries.
+ if isinstance(mapping_or_list_of_two_tuples, dict):
+ # {value: expected}
+ return mapping_or_list_of_two_tuples.items()
+ # [(value, expected), ...]
+ return mapping_or_list_of_two_tuples
+
+
class ValidAndInvalidValues:
"""
- Base class for testing valid and invalid field values.
+ Base class for testing valid and invalid input values.
"""
def test_valid_values(self):
"""
Ensure that valid values return the expected validated data.
"""
- for input_value, expected_output in self.valid_mappings.items():
+ for input_value, expected_output in get_items(self.valid_mappings):
assert self.field.run_validation(input_value) == expected_output
def test_invalid_values(self):
"""
Ensure that invalid values raise the expected validation error.
"""
- for input_value, expected_failure in self.invalid_mappings.items():
+ for input_value, expected_failure in get_items(self.invalid_mappings):
with pytest.raises(fields.ValidationError) as exc_info:
self.field.run_validation(input_value)
assert exc_info.value.messages == expected_failure
@@ -189,14 +198,14 @@ class TestDecimalField(ValidAndInvalidValues):
12.3: Decimal('12.3'),
0.1: Decimal('0.1'),
}
- invalid_mappings = {
- 'abc': ["A valid number is required."],
- Decimal('Nan'): ["A valid number is required."],
- Decimal('Inf'): ["A valid number is required."],
- '12.345': ["Ensure that there are no more than 3 digits in total."],
- '0.01': ["Ensure that there are no more than 1 decimal places."],
- 123: ["Ensure that there are no more than 2 digits before the decimal point."]
- }
+ invalid_mappings = (
+ ('abc', ["A valid number is required."]),
+ (Decimal('Nan'), ["A valid number is required."]),
+ (Decimal('Inf'), ["A valid number is required."]),
+ ('12.345', ["Ensure that there are no more than 3 digits in total."]),
+ ('0.01', ["Ensure that there are no more than 1 decimal places."]),
+ (123, ["Ensure that there are no more than 2 digits before the decimal point."])
+ )
field = fields.DecimalField(max_digits=3, decimal_places=1)
--
cgit v1.2.3
From 4db23cae213decc3e8a8613ad5c76a545f8cfb1a Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 22 Sep 2014 15:34:06 +0100
Subject: Tweaks to DecimalField
---
rest_framework/fields.py | 25 ++---
tests/test_fields.py | 233 ++++++++++++++++++++++++++++++++---------------
2 files changed, 172 insertions(+), 86 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 5fb99a42..db7ceabb 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -521,20 +521,21 @@ class DecimalField(Field):
return value
def to_representation(self, value):
- if isinstance(value, decimal.Decimal):
- context = decimal.getcontext().copy()
- context.prec = self.max_digits
- quantized = value.quantize(
- decimal.Decimal('.1') ** self.decimal_places,
- context=context
- )
- if not self.coerce_to_string:
- return quantized
- return '{0:f}'.format(quantized)
+ if value in (None, ''):
+ return None
+ if not isinstance(value, decimal.Decimal):
+ value = decimal.Decimal(value)
+
+ context = decimal.getcontext().copy()
+ context.prec = self.max_digits
+ quantized = value.quantize(
+ decimal.Decimal('.1') ** self.decimal_places,
+ context=context
+ )
if not self.coerce_to_string:
- return value
- return '%.*f' % (self.max_decimal_places, value)
+ return quantized
+ return '{0:f}'.format(quantized)
# Date & time fields...
diff --git a/tests/test_fields.py b/tests/test_fields.py
index e03ece54..0f445d41 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -14,31 +14,35 @@ def get_items(mapping_or_list_of_two_tuples):
return mapping_or_list_of_two_tuples
-class ValidAndInvalidValues:
+class FieldValues:
"""
Base class for testing valid and invalid input values.
"""
- def test_valid_values(self):
+ def test_valid_inputs(self):
"""
Ensure that valid values return the expected validated data.
"""
- for input_value, expected_output in get_items(self.valid_mappings):
+ for input_value, expected_output in get_items(self.valid_inputs):
assert self.field.run_validation(input_value) == expected_output
- def test_invalid_values(self):
+ def test_invalid_inputs(self):
"""
Ensure that invalid values raise the expected validation error.
"""
- for input_value, expected_failure in get_items(self.invalid_mappings):
+ for input_value, expected_failure in get_items(self.invalid_inputs):
with pytest.raises(fields.ValidationError) as exc_info:
self.field.run_validation(input_value)
assert exc_info.value.messages == expected_failure
+ def test_outputs(self):
+ for output_value, expected_output in get_items(self.outputs):
+ assert self.field.to_representation(output_value) == expected_output
+
# Boolean types...
-class TestBooleanField(ValidAndInvalidValues):
- valid_mappings = {
+class TestBooleanField(FieldValues):
+ valid_inputs = {
'true': True,
'false': False,
'1': True,
@@ -48,73 +52,92 @@ class TestBooleanField(ValidAndInvalidValues):
True: True,
False: False,
}
- invalid_mappings = {
+ invalid_inputs = {
'foo': ['`foo` is not a valid boolean.']
}
+ outputs = {
+ 'true': True,
+ 'false': False,
+ '1': True,
+ '0': False,
+ 1: True,
+ 0: False,
+ True: True,
+ False: False,
+ 'other': True
+ }
field = fields.BooleanField()
# String types...
-class TestCharField(ValidAndInvalidValues):
- valid_mappings = {
+class TestCharField(FieldValues):
+ valid_inputs = {
1: '1',
'abc': 'abc'
}
- invalid_mappings = {
+ invalid_inputs = {
'': ['This field may not be blank.']
}
+ outputs = {
+ 1: '1',
+ 'abc': 'abc'
+ }
field = fields.CharField()
-class TestEmailField(ValidAndInvalidValues):
- valid_mappings = {
+class TestEmailField(FieldValues):
+ valid_inputs = {
'example@example.com': 'example@example.com',
' example@example.com ': 'example@example.com',
}
- invalid_mappings = {
+ invalid_inputs = {
'example.com': ['Enter a valid email address.']
}
+ outputs = {}
field = fields.EmailField()
-class TestRegexField(ValidAndInvalidValues):
- valid_mappings = {
+class TestRegexField(FieldValues):
+ valid_inputs = {
'a9': 'a9',
}
- invalid_mappings = {
+ invalid_inputs = {
'A9': ["This value does not match the required pattern."]
}
+ outputs = {}
field = fields.RegexField(regex='[a-z][0-9]')
-class TestSlugField(ValidAndInvalidValues):
- valid_mappings = {
+class TestSlugField(FieldValues):
+ valid_inputs = {
'slug-99': 'slug-99',
}
- invalid_mappings = {
+ invalid_inputs = {
'slug 99': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]
}
+ outputs = {}
field = fields.SlugField()
-class TestURLField(ValidAndInvalidValues):
- valid_mappings = {
+class TestURLField(FieldValues):
+ valid_inputs = {
'http://example.com': 'http://example.com',
}
- invalid_mappings = {
+ invalid_inputs = {
'example.com': ['Enter a valid URL.']
}
+ outputs = {}
field = fields.URLField()
# Number types...
-class TestIntegerField(ValidAndInvalidValues):
+class TestIntegerField(FieldValues):
"""
Valid and invalid values for `IntegerField`.
"""
- valid_mappings = {
+ valid_inputs = {
'1': 1,
'0': 0,
1: 1,
@@ -122,36 +145,45 @@ class TestIntegerField(ValidAndInvalidValues):
1.0: 1,
0.0: 0
}
- invalid_mappings = {
+ invalid_inputs = {
'abc': ['A valid integer is required.']
}
+ outputs = {
+ '1': 1,
+ '0': 0,
+ 1: 1,
+ 0: 0,
+ 1.0: 1,
+ 0.0: 0
+ }
field = fields.IntegerField()
-class TestMinMaxIntegerField(ValidAndInvalidValues):
+class TestMinMaxIntegerField(FieldValues):
"""
Valid and invalid values for `IntegerField` with min and max limits.
"""
- valid_mappings = {
+ valid_inputs = {
'1': 1,
'3': 3,
1: 1,
3: 3,
}
- invalid_mappings = {
+ invalid_inputs = {
0: ['Ensure this value is greater than or equal to 1.'],
4: ['Ensure this value is less than or equal to 3.'],
'0': ['Ensure this value is greater than or equal to 1.'],
'4': ['Ensure this value is less than or equal to 3.'],
}
+ outputs = {}
field = fields.IntegerField(min_value=1, max_value=3)
-class TestFloatField(ValidAndInvalidValues):
+class TestFloatField(FieldValues):
"""
Valid and invalid values for `FloatField`.
"""
- valid_mappings = {
+ valid_inputs = {
'1': 1.0,
'0': 0.0,
1: 1.0,
@@ -159,17 +191,25 @@ class TestFloatField(ValidAndInvalidValues):
1.0: 1.0,
0.0: 0.0,
}
- invalid_mappings = {
+ invalid_inputs = {
'abc': ["A valid number is required."]
}
+ outputs = {
+ '1': 1.0,
+ '0': 0.0,
+ 1: 1.0,
+ 0: 0.0,
+ 1.0: 1.0,
+ 0.0: 0.0,
+ }
field = fields.FloatField()
-class TestMinMaxFloatField(ValidAndInvalidValues):
+class TestMinMaxFloatField(FieldValues):
"""
Valid and invalid values for `FloatField` with min and max limits.
"""
- valid_mappings = {
+ valid_inputs = {
'1': 1,
'3': 3,
1: 1,
@@ -177,20 +217,21 @@ class TestMinMaxFloatField(ValidAndInvalidValues):
1.0: 1.0,
3.0: 3.0,
}
- invalid_mappings = {
+ invalid_inputs = {
0.9: ['Ensure this value is greater than or equal to 1.'],
3.1: ['Ensure this value is less than or equal to 3.'],
'0.0': ['Ensure this value is greater than or equal to 1.'],
'3.1': ['Ensure this value is less than or equal to 3.'],
}
+ outputs = {}
field = fields.FloatField(min_value=1, max_value=3)
-class TestDecimalField(ValidAndInvalidValues):
+class TestDecimalField(FieldValues):
"""
Valid and invalid values for `DecimalField`.
"""
- valid_mappings = {
+ valid_inputs = {
'12.3': Decimal('12.3'),
'0.1': Decimal('0.1'),
10: Decimal('10'),
@@ -198,7 +239,7 @@ class TestDecimalField(ValidAndInvalidValues):
12.3: Decimal('12.3'),
0.1: Decimal('0.1'),
}
- invalid_mappings = (
+ invalid_inputs = (
('abc', ["A valid number is required."]),
(Decimal('Nan'), ["A valid number is required."]),
(Decimal('Inf'), ["A valid number is required."]),
@@ -206,63 +247,98 @@ class TestDecimalField(ValidAndInvalidValues):
('0.01', ["Ensure that there are no more than 1 decimal places."]),
(123, ["Ensure that there are no more than 2 digits before the decimal point."])
)
+ outputs = {
+ '1': '1.0',
+ '0': '0.0',
+ '1.09': '1.1',
+ '0.04': '0.0',
+ 1: '1.0',
+ 0: '0.0',
+ Decimal('1.0'): '1.0',
+ Decimal('0.0'): '0.0',
+ Decimal('1.09'): '1.1',
+ Decimal('0.04'): '0.0',
+ }
field = fields.DecimalField(max_digits=3, decimal_places=1)
-class TestMinMaxDecimalField(ValidAndInvalidValues):
+class TestMinMaxDecimalField(FieldValues):
"""
Valid and invalid values for `DecimalField` with min and max limits.
"""
- valid_mappings = {
+ valid_inputs = {
'10.0': 10.0,
'20.0': 20.0,
}
- invalid_mappings = {
+ invalid_inputs = {
'9.9': ['Ensure this value is greater than or equal to 10.'],
'20.1': ['Ensure this value is less than or equal to 20.'],
}
+ outputs = {}
field = fields.DecimalField(
max_digits=3, decimal_places=1,
min_value=10, max_value=20
)
+class TestNoStringCoercionDecimalField(FieldValues):
+ """
+ Output values for `DecimalField` with `coerce_to_string=False`.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ 1.09: Decimal('1.1'),
+ 0.04: Decimal('0.0'),
+ '1.09': Decimal('1.1'),
+ '0.04': Decimal('0.0'),
+ Decimal('1.09'): Decimal('1.1'),
+ Decimal('0.04'): Decimal('0.0'),
+ }
+ field = fields.DecimalField(
+ max_digits=3, decimal_places=1,
+ coerce_to_string=False
+ )
+
+
# Date & time fields...
-class TestDateField(ValidAndInvalidValues):
+class TestDateField(FieldValues):
"""
Valid and invalid values for `DateField`.
"""
- valid_mappings = {
+ valid_inputs = {
'2001-01-01': datetime.date(2001, 1, 1),
datetime.date(2001, 1, 1): datetime.date(2001, 1, 1),
}
- invalid_mappings = {
+ invalid_inputs = {
'abc': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
'2001-99-99': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
}
+ outputs = {}
field = fields.DateField()
-class TestCustomInputFormatDateField(ValidAndInvalidValues):
+class TestCustomInputFormatDateField(FieldValues):
"""
Valid and invalid values for `DateField` with a cutom input format.
"""
- valid_mappings = {
+ valid_inputs = {
'1 Jan 2001': datetime.date(2001, 1, 1),
}
- invalid_mappings = {
+ invalid_inputs = {
'2001-01-01': ['Date has wrong format. Use one of these formats instead: DD [Jan-Dec] YYYY']
}
+ outputs = {}
field = fields.DateField(input_formats=['%d %b %Y'])
-class TestDateTimeField(ValidAndInvalidValues):
+class TestDateTimeField(FieldValues):
"""
Valid and invalid values for `DateTimeField`.
"""
- valid_mappings = {
+ valid_inputs = {
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
'2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
'2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
@@ -270,81 +346,87 @@ class TestDateTimeField(ValidAndInvalidValues):
datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
}
- invalid_mappings = {
+ invalid_inputs = {
'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
'2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
}
+ outputs = {}
field = fields.DateTimeField(default_timezone=timezone.UTC())
-class TestCustomInputFormatDateTimeField(ValidAndInvalidValues):
+class TestCustomInputFormatDateTimeField(FieldValues):
"""
Valid and invalid values for `DateTimeField` with a cutom input format.
"""
- valid_mappings = {
+ valid_inputs = {
'1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()),
}
- invalid_mappings = {
+ invalid_inputs = {
'2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY']
}
+ outputs = {}
field = fields.DateTimeField(default_timezone=timezone.UTC(), input_formats=['%I:%M%p, %d %b %Y'])
-class TestNaiveDateTimeField(ValidAndInvalidValues):
+class TestNaiveDateTimeField(FieldValues):
"""
Valid and invalid values for `DateTimeField` with naive datetimes.
"""
- valid_mappings = {
+ valid_inputs = {
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00),
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
}
- invalid_mappings = {}
+ invalid_inputs = {}
+ outputs = {}
field = fields.DateTimeField(default_timezone=None)
-class TestTimeField(ValidAndInvalidValues):
+class TestTimeField(FieldValues):
"""
Valid and invalid values for `TimeField`.
"""
- valid_mappings = {
+ valid_inputs = {
'13:00': datetime.time(13, 00),
datetime.time(13, 00): datetime.time(13, 00),
}
- invalid_mappings = {
+ invalid_inputs = {
'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
'99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
}
+ outputs = {}
field = fields.TimeField()
-class TestCustomInputFormatTimeField(ValidAndInvalidValues):
+class TestCustomInputFormatTimeField(FieldValues):
"""
Valid and invalid values for `TimeField` with a custom input format.
"""
- valid_mappings = {
+ valid_inputs = {
'1:00pm': datetime.time(13, 00),
}
- invalid_mappings = {
+ invalid_inputs = {
'13:00': ['Time has wrong format. Use one of these formats instead: hh:mm[AM|PM]'],
}
+ outputs = {}
field = fields.TimeField(input_formats=['%I:%M%p'])
# Choice types...
-class TestChoiceField(ValidAndInvalidValues):
+class TestChoiceField(FieldValues):
"""
Valid and invalid values for `ChoiceField`.
"""
- valid_mappings = {
+ valid_inputs = {
'poor': 'poor',
'medium': 'medium',
'good': 'good',
}
- invalid_mappings = {
+ invalid_inputs = {
'awful': ['`awful` is not a valid choice.']
}
+ outputs = {}
field = fields.ChoiceField(
choices=[
('poor', 'Poor quality'),
@@ -354,19 +436,20 @@ class TestChoiceField(ValidAndInvalidValues):
)
-class TestChoiceFieldWithType(ValidAndInvalidValues):
+class TestChoiceFieldWithType(FieldValues):
"""
Valid and invalid values for a `Choice` field that uses an integer type,
instead of a char type.
"""
- valid_mappings = {
+ valid_inputs = {
'1': 1,
3: 3,
}
- invalid_mappings = {
+ invalid_inputs = {
5: ['`5` is not a valid choice.'],
'abc': ['`abc` is not a valid choice.']
}
+ outputs = {}
field = fields.ChoiceField(
choices=[
(1, 'Poor quality'),
@@ -376,35 +459,37 @@ class TestChoiceFieldWithType(ValidAndInvalidValues):
)
-class TestChoiceFieldWithListChoices(ValidAndInvalidValues):
+class TestChoiceFieldWithListChoices(FieldValues):
"""
Valid and invalid values for a `Choice` field that uses a flat list for the
choices, rather than a list of pairs of (`value`, `description`).
"""
- valid_mappings = {
+ valid_inputs = {
'poor': 'poor',
'medium': 'medium',
'good': 'good',
}
- invalid_mappings = {
+ invalid_inputs = {
'awful': ['`awful` is not a valid choice.']
}
+ outputs = {}
field = fields.ChoiceField(choices=('poor', 'medium', 'good'))
-class TestMultipleChoiceField(ValidAndInvalidValues):
+class TestMultipleChoiceField(FieldValues):
"""
Valid and invalid values for `MultipleChoiceField`.
"""
- valid_mappings = {
+ valid_inputs = {
(): set(),
('aircon',): set(['aircon']),
('aircon', 'manual'): set(['aircon', 'manual']),
}
- invalid_mappings = {
+ invalid_inputs = {
'abc': ['Expected a list of items but got type `str`'],
('aircon', 'incorrect'): ['`incorrect` is not a valid choice.']
}
+ outputs = {}
field = fields.MultipleChoiceField(
choices=[
('aircon', 'AirCon'),
--
cgit v1.2.3
From 5586b6581d9d8db05276c08f2c6deffec04ade4f Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 22 Sep 2014 16:02:59 +0100
Subject: Support format=None for date/time fields
---
rest_framework/fields.py | 12 +++----
tests/test_fields.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 88 insertions(+), 9 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index db7ceabb..cbd3334a 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -548,8 +548,8 @@ class DateField(Field):
format = api_settings.DATE_FORMAT
input_formats = api_settings.DATE_INPUT_FORMATS
- def __init__(self, format=None, input_formats=None, *args, **kwargs):
- self.format = format if format is not None else self.format
+ def __init__(self, format=empty, input_formats=None, *args, **kwargs):
+ self.format = format if format is not empty else self.format
self.input_formats = input_formats if input_formats is not None else self.input_formats
super(DateField, self).__init__(*args, **kwargs)
@@ -604,8 +604,8 @@ class DateTimeField(Field):
input_formats = api_settings.DATETIME_INPUT_FORMATS
default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None
- def __init__(self, format=None, input_formats=None, default_timezone=None, *args, **kwargs):
- self.format = format if format is not None else self.format
+ def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs):
+ self.format = format if format is not empty else self.format
self.input_formats = input_formats if input_formats is not None else self.input_formats
self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone
super(DateTimeField, self).__init__(*args, **kwargs)
@@ -670,8 +670,8 @@ class TimeField(Field):
format = api_settings.TIME_FORMAT
input_formats = api_settings.TIME_INPUT_FORMATS
- def __init__(self, format=None, input_formats=None, *args, **kwargs):
- self.format = format if format is not None else self.format
+ def __init__(self, format=empty, input_formats=None, *args, **kwargs):
+ self.format = format if format is not empty else self.format
self.input_formats = input_formats if input_formats is not None else self.input_formats
super(TimeField, self).__init__(*args, **kwargs)
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 0f445d41..b221089c 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -316,7 +316,9 @@ class TestDateField(FieldValues):
'2001-99-99': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
}
- outputs = {}
+ outputs = {
+ datetime.date(2001, 1, 1): '2001-01-01',
+ }
field = fields.DateField()
@@ -334,6 +336,30 @@ class TestCustomInputFormatDateField(FieldValues):
field = fields.DateField(input_formats=['%d %b %Y'])
+class TestCustomOutputFormatDateField(FieldValues):
+ """
+ Values for `DateField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.date(2001, 1, 1): '01 Jan 2001'
+ }
+ field = fields.DateField(format='%d %b %Y')
+
+
+class TestNoOutputFormatDateField(FieldValues):
+ """
+ Values for `DateField` with no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.date(2001, 1, 1): datetime.date(2001, 1, 1)
+ }
+ field = fields.DateField(format=None)
+
+
class TestDateTimeField(FieldValues):
"""
Valid and invalid values for `DateTimeField`.
@@ -351,7 +377,10 @@ class TestDateTimeField(FieldValues):
'2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
}
- outputs = {}
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z',
+ }
field = fields.DateTimeField(default_timezone=timezone.UTC())
@@ -369,6 +398,30 @@ class TestCustomInputFormatDateTimeField(FieldValues):
field = fields.DateTimeField(default_timezone=timezone.UTC(), input_formats=['%I:%M%p, %d %b %Y'])
+class TestCustomOutputFormatDateTimeField(FieldValues):
+ """
+ Values for `DateTimeField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): '01:00PM, 01 Jan 2001',
+ }
+ field = fields.DateTimeField(format='%I:%M%p, %d %b %Y')
+
+
+class TestNoOutputFormatDateTimeField(FieldValues):
+ """
+ Values for `DateTimeField` with no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00),
+ }
+ field = fields.DateTimeField(format=None)
+
+
class TestNaiveDateTimeField(FieldValues):
"""
Valid and invalid values for `DateTimeField` with naive datetimes.
@@ -394,7 +447,9 @@ class TestTimeField(FieldValues):
'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
'99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
}
- outputs = {}
+ outputs = {
+ datetime.time(13, 00): '13:00:00'
+ }
field = fields.TimeField()
@@ -412,6 +467,30 @@ class TestCustomInputFormatTimeField(FieldValues):
field = fields.TimeField(input_formats=['%I:%M%p'])
+class TestCustomOutputFormatTimeField(FieldValues):
+ """
+ Values for `TimeField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.time(13, 00): '01:00PM'
+ }
+ field = fields.TimeField(format='%I:%M%p')
+
+
+class TestNoOutputFormatTimeField(FieldValues):
+ """
+ Values for `TimeField` with a no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.time(13, 00): datetime.time(13, 00)
+ }
+ field = fields.TimeField(format=None)
+
+
# Choice types...
class TestChoiceField(FieldValues):
--
cgit v1.2.3
From e5f0a97595ff9280c7876fc917f6feb27b5ea95d Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 22 Sep 2014 16:45:06 +0100
Subject: More compat fixes
---
rest_framework/compat.py | 23 +++++++++++++++++++++++
rest_framework/fields.py | 8 ++++----
tests/test_fields.py | 10 ++++++----
3 files changed, 33 insertions(+), 8 deletions(-)
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 2b4ddb02..7303c32a 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -139,6 +139,29 @@ else:
self.message = kwargs.pop('message', self.message)
super(MaxValueValidator, self).__init__(*args, **kwargs)
+# URLValidator only accept `message` in 1.6+
+if django.VERSION >= (1, 6):
+ from django.core.validators import URLValidator
+else:
+ from django.core.validators import URLValidator as DjangoURLValidator
+
+ class URLValidator(DjangoURLValidator):
+ def __init__(self, *args, **kwargs):
+ self.message = kwargs.pop('message', self.message)
+ super(URLValidator, self).__init__(*args, **kwargs)
+
+
+# EmailValidator requires explicit regex prior to 1.6+
+if django.VERSION >= (1, 6):
+ from django.core.validators import EmailValidator
+else:
+ from django.core.validators import EmailValidator as DjangoEmailValidator
+ from django.core.validators import email_re
+
+ class EmailValidator(DjangoEmailValidator):
+ def __init__(self, *args, **kwargs):
+ super(EmailValidator, self).__init__(email_re, *args, **kwargs)
+
# PATCH method is not implemented by Django
if 'patch' not in View.http_method_names:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index cbd3334a..12975ae4 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -6,7 +6,7 @@ from django.utils.dateparse import parse_date, parse_datetime, parse_time
from django.utils.encoding import is_protected_type
from django.utils.translation import ugettext_lazy as _
from rest_framework import ISO_8601
-from rest_framework.compat import smart_text, MinValueValidator, MaxValueValidator
+from rest_framework.compat import smart_text, EmailValidator, MinValueValidator, MaxValueValidator, URLValidator
from rest_framework.settings import api_settings
from rest_framework.utils import html, representation, humanize_datetime
import datetime
@@ -335,7 +335,7 @@ class EmailField(CharField):
def __init__(self, **kwargs):
super(EmailField, self).__init__(**kwargs)
- validator = validators.EmailValidator(message=self.error_messages['invalid'])
+ validator = EmailValidator(message=self.error_messages['invalid'])
self.validators.append(validator)
def to_internal_value(self, data):
@@ -381,7 +381,7 @@ class URLField(CharField):
def __init__(self, **kwargs):
super(URLField, self).__init__(**kwargs)
- validator = validators.URLValidator(message=self.error_messages['invalid'])
+ validator = URLValidator(message=self.error_messages['invalid'])
self.validators.append(validator)
@@ -525,7 +525,7 @@ class DecimalField(Field):
return None
if not isinstance(value, decimal.Decimal):
- value = decimal.Decimal(value)
+ value = decimal.Decimal(str(value).strip())
context = decimal.getcontext().copy()
context.prec = self.max_digits
diff --git a/tests/test_fields.py b/tests/test_fields.py
index b221089c..8c50aaba 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -2,6 +2,7 @@ from decimal import Decimal
from django.utils import timezone
from rest_framework import fields
import datetime
+import django
import pytest
@@ -92,7 +93,7 @@ class TestEmailField(FieldValues):
' example@example.com ': 'example@example.com',
}
invalid_inputs = {
- 'example.com': ['Enter a valid email address.']
+ 'examplecom': ['Enter a valid email address.']
}
outputs = {}
field = fields.EmailField()
@@ -267,8 +268,8 @@ class TestMinMaxDecimalField(FieldValues):
Valid and invalid values for `DecimalField` with min and max limits.
"""
valid_inputs = {
- '10.0': 10.0,
- '20.0': 20.0,
+ '10.0': Decimal('10.0'),
+ '20.0': Decimal('20.0'),
}
invalid_inputs = {
'9.9': ['Ensure this value is greater than or equal to 10.'],
@@ -368,9 +369,10 @@ class TestDateTimeField(FieldValues):
'2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
'2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
'2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
- '2001-01-01T14:00+0100': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ # Note that 1.4 does not support timezone string parsing.
+ '2001-01-01T14:00+01:00' if (django.VERSION > (1, 4)) else '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())
}
invalid_inputs = {
'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
--
cgit v1.2.3
From b5454dd02290130a7fb0a0e375f3efecc58edc6d Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 22 Sep 2014 16:50:04 +0100
Subject: Tests and tweaks for choice fields
---
rest_framework/fields.py | 4 ++--
tests/test_fields.py | 15 +++++++++++----
2 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 12975ae4..500018f3 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -750,7 +750,7 @@ class ChoiceField(Field):
self.fail('invalid_choice', input=data)
def to_representation(self, value):
- return value
+ return self.choice_strings_to_values[str(value)]
class MultipleChoiceField(ChoiceField):
@@ -769,7 +769,7 @@ class MultipleChoiceField(ChoiceField):
])
def to_representation(self, value):
- return value
+ return [self.choice_strings_to_values[str(item)] for item in value]
# File types...
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 8c50aaba..3343123f 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -505,9 +505,11 @@ class TestChoiceField(FieldValues):
'good': 'good',
}
invalid_inputs = {
- 'awful': ['`awful` is not a valid choice.']
+ 'amazing': ['`amazing` is not a valid choice.']
+ }
+ outputs = {
+ 'good': 'good'
}
- outputs = {}
field = fields.ChoiceField(
choices=[
('poor', 'Poor quality'),
@@ -530,7 +532,10 @@ class TestChoiceFieldWithType(FieldValues):
5: ['`5` is not a valid choice.'],
'abc': ['`abc` is not a valid choice.']
}
- outputs = {}
+ outputs = {
+ '1': 1,
+ 1: 1
+ }
field = fields.ChoiceField(
choices=[
(1, 'Poor quality'),
@@ -553,7 +558,9 @@ class TestChoiceFieldWithListChoices(FieldValues):
invalid_inputs = {
'awful': ['`awful` is not a valid choice.']
}
- outputs = {}
+ outputs = {
+ 'good': 'good'
+ }
field = fields.ChoiceField(choices=('poor', 'medium', 'good'))
--
cgit v1.2.3
From 5a95baf2a2258fb5297062ac18582129c05fb320 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 22 Sep 2014 16:52:57 +0100
Subject: Tests & tweaks for ChoiceField
---
rest_framework/fields.py | 4 +++-
tests/test_fields.py | 4 +++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 500018f3..80eadf1e 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -769,7 +769,9 @@ class MultipleChoiceField(ChoiceField):
])
def to_representation(self, value):
- return [self.choice_strings_to_values[str(item)] for item in value]
+ return set([
+ self.choice_strings_to_values[str(item)] for item in value
+ ])
# File types...
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 3343123f..3cfc1b88 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -577,7 +577,9 @@ class TestMultipleChoiceField(FieldValues):
'abc': ['Expected a list of items but got type `str`'],
('aircon', 'incorrect'): ['`incorrect` is not a valid choice.']
}
- outputs = {}
+ outputs = [
+ (['aircon', 'manual'], set(['aircon', 'manual']))
+ ]
field = fields.MultipleChoiceField(
choices=[
('aircon', 'AirCon'),
--
cgit v1.2.3
From 5d80f7f932bfcc0630ac0fdbf07072a53197b98f Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 22 Sep 2014 17:46:02 +0100
Subject: allow_blank, allow_null
---
rest_framework/fields.py | 40 +-
tests/test_field_options.py | 55 ++
tests/test_field_values.py | 607 ++++++++++++++++
tests/test_fields.py | 1633 -------------------------------------------
4 files changed, 678 insertions(+), 1657 deletions(-)
create mode 100644 tests/test_field_options.py
create mode 100644 tests/test_field_values.py
delete mode 100644 tests/test_fields.py
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 80eadf1e..48a3e1ab 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -98,14 +98,15 @@ class Field(object):
_creation_counter = 0
default_error_messages = {
- 'required': _('This field is required.')
+ 'required': _('This field is required.'),
+ 'null': _('This field may not be null.')
}
default_validators = []
def __init__(self, read_only=False, write_only=False,
required=None, default=empty, initial=None, source=None,
label=None, help_text=None, style=None,
- error_messages=None, validators=[]):
+ error_messages=None, validators=[], allow_null=False):
self._creation_counter = Field._creation_counter
Field._creation_counter += 1
@@ -129,6 +130,7 @@ class Field(object):
self.help_text = help_text
self.style = {} if style is None else style
self.validators = validators or self.default_validators[:]
+ self.allow_null = allow_null
# Collect default error message from self and parent classes
messages = {}
@@ -220,6 +222,11 @@ class Field(object):
self.fail('required')
return self.get_default()
+ if data is None:
+ if not self.allow_null:
+ self.fail('null')
+ return None
+
value = self.to_internal_value(data)
self.run_validators(value)
return value
@@ -315,11 +322,14 @@ class CharField(Field):
self.min_length = kwargs.pop('min_length', None)
super(CharField, self).__init__(**kwargs)
+ def run_validation(self, data=empty):
+ if data == '':
+ if not self.allow_blank:
+ self.fail('blank')
+ return ''
+ return super(CharField, self).run_validation(data)
+
def to_internal_value(self, data):
- if data == '' and not self.allow_blank:
- self.fail('blank')
- if data is None:
- return None
return str(data)
def to_representation(self, value):
@@ -339,10 +349,6 @@ class EmailField(CharField):
self.validators.append(validator)
def to_internal_value(self, data):
- if data == '' and not self.allow_blank:
- self.fail('blank')
- if data is None:
- return None
return str(data).strip()
def to_representation(self, value):
@@ -437,8 +443,6 @@ class FloatField(Field):
self.validators.append(MinValueValidator(min_value, message=message))
def to_internal_value(self, value):
- if value is None:
- return None
try:
return float(value)
except (TypeError, ValueError):
@@ -481,9 +485,6 @@ class DecimalField(Field):
than max_digits in the number, and no more than decimal_places digits
after the decimal point.
"""
- if value in (None, ''):
- return None
-
value = smart_text(value).strip()
try:
value = decimal.Decimal(value)
@@ -554,9 +555,6 @@ class DateField(Field):
super(DateField, self).__init__(*args, **kwargs)
def to_internal_value(self, value):
- if value in (None, ''):
- return None
-
if isinstance(value, datetime.datetime):
self.fail('datetime')
@@ -622,9 +620,6 @@ class DateTimeField(Field):
return value
def to_internal_value(self, value):
- if value in (None, ''):
- return None
-
if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
self.fail('date')
@@ -676,9 +671,6 @@ class TimeField(Field):
super(TimeField, self).__init__(*args, **kwargs)
def to_internal_value(self, value):
- if value in (None, ''):
- return None
-
if isinstance(value, datetime.time):
return value
diff --git a/tests/test_field_options.py b/tests/test_field_options.py
new file mode 100644
index 00000000..444bd424
--- /dev/null
+++ b/tests/test_field_options.py
@@ -0,0 +1,55 @@
+from rest_framework import fields
+import pytest
+
+
+class TestFieldOptions:
+ def test_required(self):
+ """
+ By default a field must be included in the input.
+ """
+ field = fields.IntegerField()
+ with pytest.raises(fields.ValidationError) as exc_info:
+ field.run_validation()
+ assert exc_info.value.messages == ['This field is required.']
+
+ def test_not_required(self):
+ """
+ If `required=False` then a field may be omitted from the input.
+ """
+ field = fields.IntegerField(required=False)
+ with pytest.raises(fields.SkipField):
+ field.run_validation()
+
+ def test_disallow_null(self):
+ """
+ By default `None` is not a valid input.
+ """
+ field = fields.IntegerField()
+ with pytest.raises(fields.ValidationError) as exc_info:
+ field.run_validation(None)
+ assert exc_info.value.messages == ['This field may not be null.']
+
+ def test_allow_null(self):
+ """
+ If `allow_null=True` then `None` is a valid input.
+ """
+ field = fields.IntegerField(allow_null=True)
+ output = field.run_validation(None)
+ assert output is None
+
+ def test_disallow_blank(self):
+ """
+ By default '' is not a valid input.
+ """
+ field = fields.CharField()
+ with pytest.raises(fields.ValidationError) as exc_info:
+ field.run_validation('')
+ assert exc_info.value.messages == ['This field may not be blank.']
+
+ def test_allow_blank(self):
+ """
+ If `allow_blank=True` then '' is a valid input.
+ """
+ field = fields.CharField(allow_blank=True)
+ output = field.run_validation('')
+ assert output is ''
diff --git a/tests/test_field_values.py b/tests/test_field_values.py
new file mode 100644
index 00000000..bac50f0b
--- /dev/null
+++ b/tests/test_field_values.py
@@ -0,0 +1,607 @@
+from decimal import Decimal
+from django.utils import timezone
+from rest_framework import fields
+import datetime
+import django
+import pytest
+
+
+def get_items(mapping_or_list_of_two_tuples):
+ # Tests accept either lists of two tuples, or dictionaries.
+ if isinstance(mapping_or_list_of_two_tuples, dict):
+ # {value: expected}
+ return mapping_or_list_of_two_tuples.items()
+ # [(value, expected), ...]
+ return mapping_or_list_of_two_tuples
+
+
+class FieldValues:
+ """
+ Base class for testing valid and invalid input values.
+ """
+ def test_valid_inputs(self):
+ """
+ Ensure that valid values return the expected validated data.
+ """
+ for input_value, expected_output in get_items(self.valid_inputs):
+ assert self.field.run_validation(input_value) == expected_output
+
+ def test_invalid_inputs(self):
+ """
+ Ensure that invalid values raise the expected validation error.
+ """
+ for input_value, expected_failure in get_items(self.invalid_inputs):
+ with pytest.raises(fields.ValidationError) as exc_info:
+ self.field.run_validation(input_value)
+ assert exc_info.value.messages == expected_failure
+
+ def test_outputs(self):
+ for output_value, expected_output in get_items(self.outputs):
+ assert self.field.to_representation(output_value) == expected_output
+
+
+# Boolean types...
+
+class TestBooleanField(FieldValues):
+ """
+ Valid and invalid values for `BooleanField`.
+ """
+ valid_inputs = {
+ 'true': True,
+ 'false': False,
+ '1': True,
+ '0': False,
+ 1: True,
+ 0: False,
+ True: True,
+ False: False,
+ }
+ invalid_inputs = {
+ 'foo': ['`foo` is not a valid boolean.']
+ }
+ outputs = {
+ 'true': True,
+ 'false': False,
+ '1': True,
+ '0': False,
+ 1: True,
+ 0: False,
+ True: True,
+ False: False,
+ 'other': True
+ }
+ field = fields.BooleanField()
+
+
+# String types...
+
+class TestCharField(FieldValues):
+ """
+ Valid and invalid values for `CharField`.
+ """
+ valid_inputs = {
+ 1: '1',
+ 'abc': 'abc'
+ }
+ invalid_inputs = {
+ '': ['This field may not be blank.']
+ }
+ outputs = {
+ 1: '1',
+ 'abc': 'abc'
+ }
+ field = fields.CharField()
+
+
+class TestEmailField(FieldValues):
+ """
+ Valid and invalid values for `EmailField`.
+ """
+ valid_inputs = {
+ 'example@example.com': 'example@example.com',
+ ' example@example.com ': 'example@example.com',
+ }
+ invalid_inputs = {
+ 'examplecom': ['Enter a valid email address.']
+ }
+ outputs = {}
+ field = fields.EmailField()
+
+
+class TestRegexField(FieldValues):
+ """
+ Valid and invalid values for `RegexField`.
+ """
+ valid_inputs = {
+ 'a9': 'a9',
+ }
+ invalid_inputs = {
+ 'A9': ["This value does not match the required pattern."]
+ }
+ outputs = {}
+ field = fields.RegexField(regex='[a-z][0-9]')
+
+
+class TestSlugField(FieldValues):
+ """
+ Valid and invalid values for `SlugField`.
+ """
+ valid_inputs = {
+ 'slug-99': 'slug-99',
+ }
+ invalid_inputs = {
+ 'slug 99': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]
+ }
+ outputs = {}
+ field = fields.SlugField()
+
+
+class TestURLField(FieldValues):
+ """
+ Valid and invalid values for `URLField`.
+ """
+ valid_inputs = {
+ 'http://example.com': 'http://example.com',
+ }
+ invalid_inputs = {
+ 'example.com': ['Enter a valid URL.']
+ }
+ outputs = {}
+ field = fields.URLField()
+
+
+# Number types...
+
+class TestIntegerField(FieldValues):
+ """
+ Valid and invalid values for `IntegerField`.
+ """
+ valid_inputs = {
+ '1': 1,
+ '0': 0,
+ 1: 1,
+ 0: 0,
+ 1.0: 1,
+ 0.0: 0
+ }
+ invalid_inputs = {
+ 'abc': ['A valid integer is required.']
+ }
+ outputs = {
+ '1': 1,
+ '0': 0,
+ 1: 1,
+ 0: 0,
+ 1.0: 1,
+ 0.0: 0
+ }
+ field = fields.IntegerField()
+
+
+class TestMinMaxIntegerField(FieldValues):
+ """
+ Valid and invalid values for `IntegerField` with min and max limits.
+ """
+ valid_inputs = {
+ '1': 1,
+ '3': 3,
+ 1: 1,
+ 3: 3,
+ }
+ invalid_inputs = {
+ 0: ['Ensure this value is greater than or equal to 1.'],
+ 4: ['Ensure this value is less than or equal to 3.'],
+ '0': ['Ensure this value is greater than or equal to 1.'],
+ '4': ['Ensure this value is less than or equal to 3.'],
+ }
+ outputs = {}
+ field = fields.IntegerField(min_value=1, max_value=3)
+
+
+class TestFloatField(FieldValues):
+ """
+ Valid and invalid values for `FloatField`.
+ """
+ valid_inputs = {
+ '1': 1.0,
+ '0': 0.0,
+ 1: 1.0,
+ 0: 0.0,
+ 1.0: 1.0,
+ 0.0: 0.0,
+ }
+ invalid_inputs = {
+ 'abc': ["A valid number is required."]
+ }
+ outputs = {
+ '1': 1.0,
+ '0': 0.0,
+ 1: 1.0,
+ 0: 0.0,
+ 1.0: 1.0,
+ 0.0: 0.0,
+ }
+ field = fields.FloatField()
+
+
+class TestMinMaxFloatField(FieldValues):
+ """
+ Valid and invalid values for `FloatField` with min and max limits.
+ """
+ valid_inputs = {
+ '1': 1,
+ '3': 3,
+ 1: 1,
+ 3: 3,
+ 1.0: 1.0,
+ 3.0: 3.0,
+ }
+ invalid_inputs = {
+ 0.9: ['Ensure this value is greater than or equal to 1.'],
+ 3.1: ['Ensure this value is less than or equal to 3.'],
+ '0.0': ['Ensure this value is greater than or equal to 1.'],
+ '3.1': ['Ensure this value is less than or equal to 3.'],
+ }
+ outputs = {}
+ field = fields.FloatField(min_value=1, max_value=3)
+
+
+class TestDecimalField(FieldValues):
+ """
+ Valid and invalid values for `DecimalField`.
+ """
+ valid_inputs = {
+ '12.3': Decimal('12.3'),
+ '0.1': Decimal('0.1'),
+ 10: Decimal('10'),
+ 0: Decimal('0'),
+ 12.3: Decimal('12.3'),
+ 0.1: Decimal('0.1'),
+ }
+ invalid_inputs = (
+ ('abc', ["A valid number is required."]),
+ (Decimal('Nan'), ["A valid number is required."]),
+ (Decimal('Inf'), ["A valid number is required."]),
+ ('12.345', ["Ensure that there are no more than 3 digits in total."]),
+ ('0.01', ["Ensure that there are no more than 1 decimal places."]),
+ (123, ["Ensure that there are no more than 2 digits before the decimal point."])
+ )
+ outputs = {
+ '1': '1.0',
+ '0': '0.0',
+ '1.09': '1.1',
+ '0.04': '0.0',
+ 1: '1.0',
+ 0: '0.0',
+ Decimal('1.0'): '1.0',
+ Decimal('0.0'): '0.0',
+ Decimal('1.09'): '1.1',
+ Decimal('0.04'): '0.0',
+ }
+ field = fields.DecimalField(max_digits=3, decimal_places=1)
+
+
+class TestMinMaxDecimalField(FieldValues):
+ """
+ Valid and invalid values for `DecimalField` with min and max limits.
+ """
+ valid_inputs = {
+ '10.0': Decimal('10.0'),
+ '20.0': Decimal('20.0'),
+ }
+ invalid_inputs = {
+ '9.9': ['Ensure this value is greater than or equal to 10.'],
+ '20.1': ['Ensure this value is less than or equal to 20.'],
+ }
+ outputs = {}
+ field = fields.DecimalField(
+ max_digits=3, decimal_places=1,
+ min_value=10, max_value=20
+ )
+
+
+class TestNoStringCoercionDecimalField(FieldValues):
+ """
+ Output values for `DecimalField` with `coerce_to_string=False`.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ 1.09: Decimal('1.1'),
+ 0.04: Decimal('0.0'),
+ '1.09': Decimal('1.1'),
+ '0.04': Decimal('0.0'),
+ Decimal('1.09'): Decimal('1.1'),
+ Decimal('0.04'): Decimal('0.0'),
+ }
+ field = fields.DecimalField(
+ max_digits=3, decimal_places=1,
+ coerce_to_string=False
+ )
+
+
+# Date & time fields...
+
+class TestDateField(FieldValues):
+ """
+ Valid and invalid values for `DateField`.
+ """
+ valid_inputs = {
+ '2001-01-01': datetime.date(2001, 1, 1),
+ datetime.date(2001, 1, 1): datetime.date(2001, 1, 1),
+ }
+ invalid_inputs = {
+ 'abc': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
+ '2001-99-99': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
+ datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
+ }
+ outputs = {
+ datetime.date(2001, 1, 1): '2001-01-01',
+ }
+ field = fields.DateField()
+
+
+class TestCustomInputFormatDateField(FieldValues):
+ """
+ Valid and invalid values for `DateField` with a cutom input format.
+ """
+ valid_inputs = {
+ '1 Jan 2001': datetime.date(2001, 1, 1),
+ }
+ invalid_inputs = {
+ '2001-01-01': ['Date has wrong format. Use one of these formats instead: DD [Jan-Dec] YYYY']
+ }
+ outputs = {}
+ field = fields.DateField(input_formats=['%d %b %Y'])
+
+
+class TestCustomOutputFormatDateField(FieldValues):
+ """
+ Values for `DateField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.date(2001, 1, 1): '01 Jan 2001'
+ }
+ field = fields.DateField(format='%d %b %Y')
+
+
+class TestNoOutputFormatDateField(FieldValues):
+ """
+ Values for `DateField` with no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.date(2001, 1, 1): datetime.date(2001, 1, 1)
+ }
+ field = fields.DateField(format=None)
+
+
+class TestDateTimeField(FieldValues):
+ """
+ Valid and invalid values for `DateTimeField`.
+ """
+ valid_inputs = {
+ '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ '2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ # Note that 1.4 does not support timezone string parsing.
+ '2001-01-01T14:00+01:00' if (django.VERSION > (1, 4)) else '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())
+ }
+ invalid_inputs = {
+ 'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
+ '2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
+ datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
+ }
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z',
+ }
+ field = fields.DateTimeField(default_timezone=timezone.UTC())
+
+
+class TestCustomInputFormatDateTimeField(FieldValues):
+ """
+ Valid and invalid values for `DateTimeField` with a cutom input format.
+ """
+ valid_inputs = {
+ '1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()),
+ }
+ invalid_inputs = {
+ '2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY']
+ }
+ outputs = {}
+ field = fields.DateTimeField(default_timezone=timezone.UTC(), input_formats=['%I:%M%p, %d %b %Y'])
+
+
+class TestCustomOutputFormatDateTimeField(FieldValues):
+ """
+ Values for `DateTimeField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): '01:00PM, 01 Jan 2001',
+ }
+ field = fields.DateTimeField(format='%I:%M%p, %d %b %Y')
+
+
+class TestNoOutputFormatDateTimeField(FieldValues):
+ """
+ Values for `DateTimeField` with no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00),
+ }
+ field = fields.DateTimeField(format=None)
+
+
+class TestNaiveDateTimeField(FieldValues):
+ """
+ Valid and invalid values for `DateTimeField` with naive datetimes.
+ """
+ valid_inputs = {
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00),
+ '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
+ }
+ invalid_inputs = {}
+ outputs = {}
+ field = fields.DateTimeField(default_timezone=None)
+
+
+class TestTimeField(FieldValues):
+ """
+ Valid and invalid values for `TimeField`.
+ """
+ valid_inputs = {
+ '13:00': datetime.time(13, 00),
+ datetime.time(13, 00): datetime.time(13, 00),
+ }
+ invalid_inputs = {
+ 'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
+ '99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
+ }
+ outputs = {
+ datetime.time(13, 00): '13:00:00'
+ }
+ field = fields.TimeField()
+
+
+class TestCustomInputFormatTimeField(FieldValues):
+ """
+ Valid and invalid values for `TimeField` with a custom input format.
+ """
+ valid_inputs = {
+ '1:00pm': datetime.time(13, 00),
+ }
+ invalid_inputs = {
+ '13:00': ['Time has wrong format. Use one of these formats instead: hh:mm[AM|PM]'],
+ }
+ outputs = {}
+ field = fields.TimeField(input_formats=['%I:%M%p'])
+
+
+class TestCustomOutputFormatTimeField(FieldValues):
+ """
+ Values for `TimeField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.time(13, 00): '01:00PM'
+ }
+ field = fields.TimeField(format='%I:%M%p')
+
+
+class TestNoOutputFormatTimeField(FieldValues):
+ """
+ Values for `TimeField` with a no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.time(13, 00): datetime.time(13, 00)
+ }
+ field = fields.TimeField(format=None)
+
+
+# Choice types...
+
+class TestChoiceField(FieldValues):
+ """
+ Valid and invalid values for `ChoiceField`.
+ """
+ valid_inputs = {
+ 'poor': 'poor',
+ 'medium': 'medium',
+ 'good': 'good',
+ }
+ invalid_inputs = {
+ 'amazing': ['`amazing` is not a valid choice.']
+ }
+ outputs = {
+ 'good': 'good'
+ }
+ field = fields.ChoiceField(
+ choices=[
+ ('poor', 'Poor quality'),
+ ('medium', 'Medium quality'),
+ ('good', 'Good quality'),
+ ]
+ )
+
+
+class TestChoiceFieldWithType(FieldValues):
+ """
+ Valid and invalid values for a `Choice` field that uses an integer type,
+ instead of a char type.
+ """
+ valid_inputs = {
+ '1': 1,
+ 3: 3,
+ }
+ invalid_inputs = {
+ 5: ['`5` is not a valid choice.'],
+ 'abc': ['`abc` is not a valid choice.']
+ }
+ outputs = {
+ '1': 1,
+ 1: 1
+ }
+ field = fields.ChoiceField(
+ choices=[
+ (1, 'Poor quality'),
+ (2, 'Medium quality'),
+ (3, 'Good quality'),
+ ]
+ )
+
+
+class TestChoiceFieldWithListChoices(FieldValues):
+ """
+ Valid and invalid values for a `Choice` field that uses a flat list for the
+ choices, rather than a list of pairs of (`value`, `description`).
+ """
+ valid_inputs = {
+ 'poor': 'poor',
+ 'medium': 'medium',
+ 'good': 'good',
+ }
+ invalid_inputs = {
+ 'awful': ['`awful` is not a valid choice.']
+ }
+ outputs = {
+ 'good': 'good'
+ }
+ field = fields.ChoiceField(choices=('poor', 'medium', 'good'))
+
+
+class TestMultipleChoiceField(FieldValues):
+ """
+ Valid and invalid values for `MultipleChoiceField`.
+ """
+ valid_inputs = {
+ (): set(),
+ ('aircon',): set(['aircon']),
+ ('aircon', 'manual'): set(['aircon', 'manual']),
+ }
+ invalid_inputs = {
+ 'abc': ['Expected a list of items but got type `str`'],
+ ('aircon', 'incorrect'): ['`incorrect` is not a valid choice.']
+ }
+ outputs = [
+ (['aircon', 'manual'], set(['aircon', 'manual']))
+ ]
+ field = fields.MultipleChoiceField(
+ choices=[
+ ('aircon', 'AirCon'),
+ ('manual', 'Manual drive'),
+ ('diesel', 'Diesel'),
+ ]
+ )
diff --git a/tests/test_fields.py b/tests/test_fields.py
deleted file mode 100644
index 3cfc1b88..00000000
--- a/tests/test_fields.py
+++ /dev/null
@@ -1,1633 +0,0 @@
-from decimal import Decimal
-from django.utils import timezone
-from rest_framework import fields
-import datetime
-import django
-import pytest
-
-
-def get_items(mapping_or_list_of_two_tuples):
- # Tests accept either lists of two tuples, or dictionaries.
- if isinstance(mapping_or_list_of_two_tuples, dict):
- # {value: expected}
- return mapping_or_list_of_two_tuples.items()
- # [(value, expected), ...]
- return mapping_or_list_of_two_tuples
-
-
-class FieldValues:
- """
- Base class for testing valid and invalid input values.
- """
- def test_valid_inputs(self):
- """
- Ensure that valid values return the expected validated data.
- """
- for input_value, expected_output in get_items(self.valid_inputs):
- assert self.field.run_validation(input_value) == expected_output
-
- def test_invalid_inputs(self):
- """
- Ensure that invalid values raise the expected validation error.
- """
- for input_value, expected_failure in get_items(self.invalid_inputs):
- with pytest.raises(fields.ValidationError) as exc_info:
- self.field.run_validation(input_value)
- assert exc_info.value.messages == expected_failure
-
- def test_outputs(self):
- for output_value, expected_output in get_items(self.outputs):
- assert self.field.to_representation(output_value) == expected_output
-
-
-# Boolean types...
-
-class TestBooleanField(FieldValues):
- valid_inputs = {
- 'true': True,
- 'false': False,
- '1': True,
- '0': False,
- 1: True,
- 0: False,
- True: True,
- False: False,
- }
- invalid_inputs = {
- 'foo': ['`foo` is not a valid boolean.']
- }
- outputs = {
- 'true': True,
- 'false': False,
- '1': True,
- '0': False,
- 1: True,
- 0: False,
- True: True,
- False: False,
- 'other': True
- }
- field = fields.BooleanField()
-
-
-# String types...
-
-class TestCharField(FieldValues):
- valid_inputs = {
- 1: '1',
- 'abc': 'abc'
- }
- invalid_inputs = {
- '': ['This field may not be blank.']
- }
- outputs = {
- 1: '1',
- 'abc': 'abc'
- }
- field = fields.CharField()
-
-
-class TestEmailField(FieldValues):
- valid_inputs = {
- 'example@example.com': 'example@example.com',
- ' example@example.com ': 'example@example.com',
- }
- invalid_inputs = {
- 'examplecom': ['Enter a valid email address.']
- }
- outputs = {}
- field = fields.EmailField()
-
-
-class TestRegexField(FieldValues):
- valid_inputs = {
- 'a9': 'a9',
- }
- invalid_inputs = {
- 'A9': ["This value does not match the required pattern."]
- }
- outputs = {}
- field = fields.RegexField(regex='[a-z][0-9]')
-
-
-class TestSlugField(FieldValues):
- valid_inputs = {
- 'slug-99': 'slug-99',
- }
- invalid_inputs = {
- 'slug 99': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]
- }
- outputs = {}
- field = fields.SlugField()
-
-
-class TestURLField(FieldValues):
- valid_inputs = {
- 'http://example.com': 'http://example.com',
- }
- invalid_inputs = {
- 'example.com': ['Enter a valid URL.']
- }
- outputs = {}
- field = fields.URLField()
-
-
-# Number types...
-
-class TestIntegerField(FieldValues):
- """
- Valid and invalid values for `IntegerField`.
- """
- valid_inputs = {
- '1': 1,
- '0': 0,
- 1: 1,
- 0: 0,
- 1.0: 1,
- 0.0: 0
- }
- invalid_inputs = {
- 'abc': ['A valid integer is required.']
- }
- outputs = {
- '1': 1,
- '0': 0,
- 1: 1,
- 0: 0,
- 1.0: 1,
- 0.0: 0
- }
- field = fields.IntegerField()
-
-
-class TestMinMaxIntegerField(FieldValues):
- """
- Valid and invalid values for `IntegerField` with min and max limits.
- """
- valid_inputs = {
- '1': 1,
- '3': 3,
- 1: 1,
- 3: 3,
- }
- invalid_inputs = {
- 0: ['Ensure this value is greater than or equal to 1.'],
- 4: ['Ensure this value is less than or equal to 3.'],
- '0': ['Ensure this value is greater than or equal to 1.'],
- '4': ['Ensure this value is less than or equal to 3.'],
- }
- outputs = {}
- field = fields.IntegerField(min_value=1, max_value=3)
-
-
-class TestFloatField(FieldValues):
- """
- Valid and invalid values for `FloatField`.
- """
- valid_inputs = {
- '1': 1.0,
- '0': 0.0,
- 1: 1.0,
- 0: 0.0,
- 1.0: 1.0,
- 0.0: 0.0,
- }
- invalid_inputs = {
- 'abc': ["A valid number is required."]
- }
- outputs = {
- '1': 1.0,
- '0': 0.0,
- 1: 1.0,
- 0: 0.0,
- 1.0: 1.0,
- 0.0: 0.0,
- }
- field = fields.FloatField()
-
-
-class TestMinMaxFloatField(FieldValues):
- """
- Valid and invalid values for `FloatField` with min and max limits.
- """
- valid_inputs = {
- '1': 1,
- '3': 3,
- 1: 1,
- 3: 3,
- 1.0: 1.0,
- 3.0: 3.0,
- }
- invalid_inputs = {
- 0.9: ['Ensure this value is greater than or equal to 1.'],
- 3.1: ['Ensure this value is less than or equal to 3.'],
- '0.0': ['Ensure this value is greater than or equal to 1.'],
- '3.1': ['Ensure this value is less than or equal to 3.'],
- }
- outputs = {}
- field = fields.FloatField(min_value=1, max_value=3)
-
-
-class TestDecimalField(FieldValues):
- """
- Valid and invalid values for `DecimalField`.
- """
- valid_inputs = {
- '12.3': Decimal('12.3'),
- '0.1': Decimal('0.1'),
- 10: Decimal('10'),
- 0: Decimal('0'),
- 12.3: Decimal('12.3'),
- 0.1: Decimal('0.1'),
- }
- invalid_inputs = (
- ('abc', ["A valid number is required."]),
- (Decimal('Nan'), ["A valid number is required."]),
- (Decimal('Inf'), ["A valid number is required."]),
- ('12.345', ["Ensure that there are no more than 3 digits in total."]),
- ('0.01', ["Ensure that there are no more than 1 decimal places."]),
- (123, ["Ensure that there are no more than 2 digits before the decimal point."])
- )
- outputs = {
- '1': '1.0',
- '0': '0.0',
- '1.09': '1.1',
- '0.04': '0.0',
- 1: '1.0',
- 0: '0.0',
- Decimal('1.0'): '1.0',
- Decimal('0.0'): '0.0',
- Decimal('1.09'): '1.1',
- Decimal('0.04'): '0.0',
- }
- field = fields.DecimalField(max_digits=3, decimal_places=1)
-
-
-class TestMinMaxDecimalField(FieldValues):
- """
- Valid and invalid values for `DecimalField` with min and max limits.
- """
- valid_inputs = {
- '10.0': Decimal('10.0'),
- '20.0': Decimal('20.0'),
- }
- invalid_inputs = {
- '9.9': ['Ensure this value is greater than or equal to 10.'],
- '20.1': ['Ensure this value is less than or equal to 20.'],
- }
- outputs = {}
- field = fields.DecimalField(
- max_digits=3, decimal_places=1,
- min_value=10, max_value=20
- )
-
-
-class TestNoStringCoercionDecimalField(FieldValues):
- """
- Output values for `DecimalField` with `coerce_to_string=False`.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- 1.09: Decimal('1.1'),
- 0.04: Decimal('0.0'),
- '1.09': Decimal('1.1'),
- '0.04': Decimal('0.0'),
- Decimal('1.09'): Decimal('1.1'),
- Decimal('0.04'): Decimal('0.0'),
- }
- field = fields.DecimalField(
- max_digits=3, decimal_places=1,
- coerce_to_string=False
- )
-
-
-# Date & time fields...
-
-class TestDateField(FieldValues):
- """
- Valid and invalid values for `DateField`.
- """
- valid_inputs = {
- '2001-01-01': datetime.date(2001, 1, 1),
- datetime.date(2001, 1, 1): datetime.date(2001, 1, 1),
- }
- invalid_inputs = {
- 'abc': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
- '2001-99-99': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
- datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
- }
- outputs = {
- datetime.date(2001, 1, 1): '2001-01-01',
- }
- field = fields.DateField()
-
-
-class TestCustomInputFormatDateField(FieldValues):
- """
- Valid and invalid values for `DateField` with a cutom input format.
- """
- valid_inputs = {
- '1 Jan 2001': datetime.date(2001, 1, 1),
- }
- invalid_inputs = {
- '2001-01-01': ['Date has wrong format. Use one of these formats instead: DD [Jan-Dec] YYYY']
- }
- outputs = {}
- field = fields.DateField(input_formats=['%d %b %Y'])
-
-
-class TestCustomOutputFormatDateField(FieldValues):
- """
- Values for `DateField` with a custom output format.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- datetime.date(2001, 1, 1): '01 Jan 2001'
- }
- field = fields.DateField(format='%d %b %Y')
-
-
-class TestNoOutputFormatDateField(FieldValues):
- """
- Values for `DateField` with no output format.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- datetime.date(2001, 1, 1): datetime.date(2001, 1, 1)
- }
- field = fields.DateField(format=None)
-
-
-class TestDateTimeField(FieldValues):
- """
- Valid and invalid values for `DateTimeField`.
- """
- valid_inputs = {
- '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
- '2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
- '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
- datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
- datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
- # Note that 1.4 does not support timezone string parsing.
- '2001-01-01T14:00+01:00' if (django.VERSION > (1, 4)) else '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())
- }
- invalid_inputs = {
- 'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
- '2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
- datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
- }
- outputs = {
- datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
- datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z',
- }
- field = fields.DateTimeField(default_timezone=timezone.UTC())
-
-
-class TestCustomInputFormatDateTimeField(FieldValues):
- """
- Valid and invalid values for `DateTimeField` with a cutom input format.
- """
- valid_inputs = {
- '1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()),
- }
- invalid_inputs = {
- '2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY']
- }
- outputs = {}
- field = fields.DateTimeField(default_timezone=timezone.UTC(), input_formats=['%I:%M%p, %d %b %Y'])
-
-
-class TestCustomOutputFormatDateTimeField(FieldValues):
- """
- Values for `DateTimeField` with a custom output format.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- datetime.datetime(2001, 1, 1, 13, 00): '01:00PM, 01 Jan 2001',
- }
- field = fields.DateTimeField(format='%I:%M%p, %d %b %Y')
-
-
-class TestNoOutputFormatDateTimeField(FieldValues):
- """
- Values for `DateTimeField` with no output format.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00),
- }
- field = fields.DateTimeField(format=None)
-
-
-class TestNaiveDateTimeField(FieldValues):
- """
- Valid and invalid values for `DateTimeField` with naive datetimes.
- """
- valid_inputs = {
- datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00),
- '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
- }
- invalid_inputs = {}
- outputs = {}
- field = fields.DateTimeField(default_timezone=None)
-
-
-class TestTimeField(FieldValues):
- """
- Valid and invalid values for `TimeField`.
- """
- valid_inputs = {
- '13:00': datetime.time(13, 00),
- datetime.time(13, 00): datetime.time(13, 00),
- }
- invalid_inputs = {
- 'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
- '99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
- }
- outputs = {
- datetime.time(13, 00): '13:00:00'
- }
- field = fields.TimeField()
-
-
-class TestCustomInputFormatTimeField(FieldValues):
- """
- Valid and invalid values for `TimeField` with a custom input format.
- """
- valid_inputs = {
- '1:00pm': datetime.time(13, 00),
- }
- invalid_inputs = {
- '13:00': ['Time has wrong format. Use one of these formats instead: hh:mm[AM|PM]'],
- }
- outputs = {}
- field = fields.TimeField(input_formats=['%I:%M%p'])
-
-
-class TestCustomOutputFormatTimeField(FieldValues):
- """
- Values for `TimeField` with a custom output format.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- datetime.time(13, 00): '01:00PM'
- }
- field = fields.TimeField(format='%I:%M%p')
-
-
-class TestNoOutputFormatTimeField(FieldValues):
- """
- Values for `TimeField` with a no output format.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- datetime.time(13, 00): datetime.time(13, 00)
- }
- field = fields.TimeField(format=None)
-
-
-# Choice types...
-
-class TestChoiceField(FieldValues):
- """
- Valid and invalid values for `ChoiceField`.
- """
- valid_inputs = {
- 'poor': 'poor',
- 'medium': 'medium',
- 'good': 'good',
- }
- invalid_inputs = {
- 'amazing': ['`amazing` is not a valid choice.']
- }
- outputs = {
- 'good': 'good'
- }
- field = fields.ChoiceField(
- choices=[
- ('poor', 'Poor quality'),
- ('medium', 'Medium quality'),
- ('good', 'Good quality'),
- ]
- )
-
-
-class TestChoiceFieldWithType(FieldValues):
- """
- Valid and invalid values for a `Choice` field that uses an integer type,
- instead of a char type.
- """
- valid_inputs = {
- '1': 1,
- 3: 3,
- }
- invalid_inputs = {
- 5: ['`5` is not a valid choice.'],
- 'abc': ['`abc` is not a valid choice.']
- }
- outputs = {
- '1': 1,
- 1: 1
- }
- field = fields.ChoiceField(
- choices=[
- (1, 'Poor quality'),
- (2, 'Medium quality'),
- (3, 'Good quality'),
- ]
- )
-
-
-class TestChoiceFieldWithListChoices(FieldValues):
- """
- Valid and invalid values for a `Choice` field that uses a flat list for the
- choices, rather than a list of pairs of (`value`, `description`).
- """
- valid_inputs = {
- 'poor': 'poor',
- 'medium': 'medium',
- 'good': 'good',
- }
- invalid_inputs = {
- 'awful': ['`awful` is not a valid choice.']
- }
- outputs = {
- 'good': 'good'
- }
- field = fields.ChoiceField(choices=('poor', 'medium', 'good'))
-
-
-class TestMultipleChoiceField(FieldValues):
- """
- Valid and invalid values for `MultipleChoiceField`.
- """
- valid_inputs = {
- (): set(),
- ('aircon',): set(['aircon']),
- ('aircon', 'manual'): set(['aircon', 'manual']),
- }
- invalid_inputs = {
- 'abc': ['Expected a list of items but got type `str`'],
- ('aircon', 'incorrect'): ['`incorrect` is not a valid choice.']
- }
- outputs = [
- (['aircon', 'manual'], set(['aircon', 'manual']))
- ]
- field = fields.MultipleChoiceField(
- choices=[
- ('aircon', 'AirCon'),
- ('manual', 'Manual drive'),
- ('diesel', 'Diesel'),
- ]
- )
-
-
-# """
-# General serializer field tests.
-# """
-# from __future__ import unicode_literals
-
-# import datetime
-# import re
-# from decimal import Decimal
-# from uuid import uuid4
-# from django.core import validators
-# from django.db import models
-# from django.test import TestCase
-# from django.utils.datastructures import SortedDict
-# from rest_framework import serializers
-# from tests.models import RESTFrameworkModel
-
-
-# class TimestampedModel(models.Model):
-# added = models.DateTimeField(auto_now_add=True)
-# updated = models.DateTimeField(auto_now=True)
-
-
-# class CharPrimaryKeyModel(models.Model):
-# id = models.CharField(max_length=20, primary_key=True)
-
-
-# class TimestampedModelSerializer(serializers.ModelSerializer):
-# class Meta:
-# model = TimestampedModel
-
-
-# class CharPrimaryKeyModelSerializer(serializers.ModelSerializer):
-# class Meta:
-# model = CharPrimaryKeyModel
-
-
-# class TimeFieldModel(models.Model):
-# clock = models.TimeField()
-
-
-# class TimeFieldModelSerializer(serializers.ModelSerializer):
-# class Meta:
-# model = TimeFieldModel
-
-
-# SAMPLE_CHOICES = [
-# ('red', 'Red'),
-# ('green', 'Green'),
-# ('blue', 'Blue'),
-# ]
-
-
-# class ChoiceFieldModel(models.Model):
-# choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, max_length=255)
-
-
-# class ChoiceFieldModelSerializer(serializers.ModelSerializer):
-# class Meta:
-# model = ChoiceFieldModel
-
-
-# class ChoiceFieldModelWithNull(models.Model):
-# choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, null=True, max_length=255)
-
-
-# class ChoiceFieldModelWithNullSerializer(serializers.ModelSerializer):
-# class Meta:
-# model = ChoiceFieldModelWithNull
-
-
-# class BasicFieldTests(TestCase):
-# def test_auto_now_fields_read_only(self):
-# """
-# auto_now and auto_now_add fields should be read_only by default.
-# """
-# serializer = TimestampedModelSerializer()
-# self.assertEqual(serializer.fields['added'].read_only, True)
-
-# def test_auto_pk_fields_read_only(self):
-# """
-# AutoField fields should be read_only by default.
-# """
-# serializer = TimestampedModelSerializer()
-# self.assertEqual(serializer.fields['id'].read_only, True)
-
-# def test_non_auto_pk_fields_not_read_only(self):
-# """
-# PK fields other than AutoField fields should not be read_only by default.
-# """
-# serializer = CharPrimaryKeyModelSerializer()
-# self.assertEqual(serializer.fields['id'].read_only, False)
-
-# def test_dict_field_ordering(self):
-# """
-# Field should preserve dictionary ordering, if it exists.
-# See: https://github.com/tomchristie/django-rest-framework/issues/832
-# """
-# ret = SortedDict()
-# ret['c'] = 1
-# ret['b'] = 1
-# ret['a'] = 1
-# ret['z'] = 1
-# field = serializers.Field()
-# keys = list(field.to_native(ret).keys())
-# self.assertEqual(keys, ['c', 'b', 'a', 'z'])
-
-# def test_widget_html_attributes(self):
-# """
-# Make sure widget_html() renders the correct attributes
-# """
-# r = re.compile('(\S+)=["\']?((?:.(?!["\']?\s+(?:\S+)=|[>"\']))+.)["\']?')
-# form = TimeFieldModelSerializer().data
-# attributes = r.findall(form.fields['clock'].widget_html())
-# self.assertIn(('name', 'clock'), attributes)
-# self.assertIn(('id', 'clock'), attributes)
-
-
-# class DateFieldTest(TestCase):
-# """
-# Tests for the DateFieldTest from_native() and to_native() behavior
-# """
-
-# def test_from_native_string(self):
-# """
-# Make sure from_native() accepts default iso input formats.
-# """
-# f = serializers.DateField()
-# result_1 = f.from_native('1984-07-31')
-
-# self.assertEqual(datetime.date(1984, 7, 31), result_1)
-
-# def test_from_native_datetime_date(self):
-# """
-# Make sure from_native() accepts a datetime.date instance.
-# """
-# f = serializers.DateField()
-# result_1 = f.from_native(datetime.date(1984, 7, 31))
-
-# self.assertEqual(result_1, datetime.date(1984, 7, 31))
-
-# def test_from_native_custom_format(self):
-# """
-# Make sure from_native() accepts custom input formats.
-# """
-# f = serializers.DateField(input_formats=['%Y -- %d'])
-# result = f.from_native('1984 -- 31')
-
-# self.assertEqual(datetime.date(1984, 1, 31), result)
-
-# def test_from_native_invalid_default_on_custom_format(self):
-# """
-# Make sure from_native() don't accept default formats if custom format is preset
-# """
-# f = serializers.DateField(input_formats=['%Y -- %d'])
-
-# try:
-# f.from_native('1984-07-31')
-# except validators.ValidationError as e:
-# self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY -- DD"])
-# else:
-# self.fail("ValidationError was not properly raised")
-
-# def test_from_native_empty(self):
-# """
-# Make sure from_native() returns None on empty param.
-# """
-# f = serializers.DateField()
-# result = f.from_native('')
-
-# self.assertEqual(result, None)
-
-# def test_from_native_none(self):
-# """
-# Make sure from_native() returns None on None param.
-# """
-# f = serializers.DateField()
-# result = f.from_native(None)
-
-# self.assertEqual(result, None)
-
-# def test_from_native_invalid_date(self):
-# """
-# Make sure from_native() raises a ValidationError on passing an invalid date.
-# """
-# f = serializers.DateField()
-
-# try:
-# f.from_native('1984-13-31')
-# except validators.ValidationError as e:
-# self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]"])
-# else:
-# self.fail("ValidationError was not properly raised")
-
-# def test_from_native_invalid_format(self):
-# """
-# Make sure from_native() raises a ValidationError on passing an invalid format.
-# """
-# f = serializers.DateField()
-
-# try:
-# f.from_native('1984 -- 31')
-# except validators.ValidationError as e:
-# self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]"])
-# else:
-# self.fail("ValidationError was not properly raised")
-
-# def test_to_native(self):
-# """
-# Make sure to_native() returns datetime as default.
-# """
-# f = serializers.DateField()
-
-# result_1 = f.to_native(datetime.date(1984, 7, 31))
-
-# self.assertEqual(datetime.date(1984, 7, 31), result_1)
-
-# def test_to_native_iso(self):
-# """
-# Make sure to_native() with 'iso-8601' returns iso formated date.
-# """
-# f = serializers.DateField(format='iso-8601')
-
-# result_1 = f.to_native(datetime.date(1984, 7, 31))
-
-# self.assertEqual('1984-07-31', result_1)
-
-# def test_to_native_custom_format(self):
-# """
-# Make sure to_native() returns correct custom format.
-# """
-# f = serializers.DateField(format="%Y - %m.%d")
-
-# result_1 = f.to_native(datetime.date(1984, 7, 31))
-
-# self.assertEqual('1984 - 07.31', result_1)
-
-# def test_to_native_none(self):
-# """
-# Make sure from_native() returns None on None param.
-# """
-# f = serializers.DateField(required=False)
-# self.assertEqual(None, f.to_native(None))
-
-
-# class DateTimeFieldTest(TestCase):
-# """
-# Tests for the DateTimeField from_native() and to_native() behavior
-# """
-
-# def test_from_native_string(self):
-# """
-# Make sure from_native() accepts default iso input formats.
-# """
-# f = serializers.DateTimeField()
-# result_1 = f.from_native('1984-07-31 04:31')
-# result_2 = f.from_native('1984-07-31 04:31:59')
-# result_3 = f.from_native('1984-07-31 04:31:59.000200')
-
-# self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result_1)
-# self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59), result_2)
-# self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59, 200), result_3)
-
-# def test_from_native_datetime_datetime(self):
-# """
-# Make sure from_native() accepts a datetime.datetime instance.
-# """
-# f = serializers.DateTimeField()
-# result_1 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31))
-# result_2 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
-# result_3 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
-# self.assertEqual(result_1, datetime.datetime(1984, 7, 31, 4, 31))
-# self.assertEqual(result_2, datetime.datetime(1984, 7, 31, 4, 31, 59))
-# self.assertEqual(result_3, datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
-# def test_from_native_custom_format(self):
-# """
-# Make sure from_native() accepts custom input formats.
-# """
-# f = serializers.DateTimeField(input_formats=['%Y -- %H:%M'])
-# result = f.from_native('1984 -- 04:59')
-
-# self.assertEqual(datetime.datetime(1984, 1, 1, 4, 59), result)
-
-# def test_from_native_invalid_default_on_custom_format(self):
-# """
-# Make sure from_native() don't accept default formats if custom format is preset
-# """
-# f = serializers.DateTimeField(input_formats=['%Y -- %H:%M'])
-
-# try:
-# f.from_native('1984-07-31 04:31:59')
-# except validators.ValidationError as e:
-# self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: YYYY -- hh:mm"])
-# else:
-# self.fail("ValidationError was not properly raised")
-
-# def test_from_native_empty(self):
-# """
-# Make sure from_native() returns None on empty param.
-# """
-# f = serializers.DateTimeField()
-# result = f.from_native('')
-
-# self.assertEqual(result, None)
-
-# def test_from_native_none(self):
-# """
-# Make sure from_native() returns None on None param.
-# """
-# f = serializers.DateTimeField()
-# result = f.from_native(None)
-
-# self.assertEqual(result, None)
-
-# def test_from_native_invalid_datetime(self):
-# """
-# Make sure from_native() raises a ValidationError on passing an invalid datetime.
-# """
-# f = serializers.DateTimeField()
-
-# try:
-# f.from_native('04:61:59')
-# except validators.ValidationError as e:
-# self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
-# "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
-# else:
-# self.fail("ValidationError was not properly raised")
-
-# def test_from_native_invalid_format(self):
-# """
-# Make sure from_native() raises a ValidationError on passing an invalid format.
-# """
-# f = serializers.DateTimeField()
-
-# try:
-# f.from_native('04 -- 31')
-# except validators.ValidationError as e:
-# self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
-# "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
-# else:
-# self.fail("ValidationError was not properly raised")
-
-# def test_to_native(self):
-# """
-# Make sure to_native() returns isoformat as default.
-# """
-# f = serializers.DateTimeField()
-
-# result_1 = f.to_native(datetime.datetime(1984, 7, 31))
-# result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
-# result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
-# result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
-# self.assertEqual(datetime.datetime(1984, 7, 31), result_1)
-# self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result_2)
-# self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59), result_3)
-# self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59, 200), result_4)
-
-# def test_to_native_iso(self):
-# """
-# Make sure to_native() with format=iso-8601 returns iso formatted datetime.
-# """
-# f = serializers.DateTimeField(format='iso-8601')
-
-# result_1 = f.to_native(datetime.datetime(1984, 7, 31))
-# result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
-# result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
-# result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
-# self.assertEqual('1984-07-31T00:00:00', result_1)
-# self.assertEqual('1984-07-31T04:31:00', result_2)
-# self.assertEqual('1984-07-31T04:31:59', result_3)
-# self.assertEqual('1984-07-31T04:31:59.000200', result_4)
-
-# def test_to_native_custom_format(self):
-# """
-# Make sure to_native() returns correct custom format.
-# """
-# f = serializers.DateTimeField(format="%Y - %H:%M")
-
-# result_1 = f.to_native(datetime.datetime(1984, 7, 31))
-# result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
-# result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
-# result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
-# self.assertEqual('1984 - 00:00', result_1)
-# self.assertEqual('1984 - 04:31', result_2)
-# self.assertEqual('1984 - 04:31', result_3)
-# self.assertEqual('1984 - 04:31', result_4)
-
-# def test_to_native_none(self):
-# """
-# Make sure from_native() returns None on None param.
-# """
-# f = serializers.DateTimeField(required=False)
-# self.assertEqual(None, f.to_native(None))
-
-
-# class TimeFieldTest(TestCase):
-# """
-# Tests for the TimeField from_native() and to_native() behavior
-# """
-
-# def test_from_native_string(self):
-# """
-# Make sure from_native() accepts default iso input formats.
-# """
-# f = serializers.TimeField()
-# result_1 = f.from_native('04:31')
-# result_2 = f.from_native('04:31:59')
-# result_3 = f.from_native('04:31:59.000200')
-
-# self.assertEqual(datetime.time(4, 31), result_1)
-# self.assertEqual(datetime.time(4, 31, 59), result_2)
-# self.assertEqual(datetime.time(4, 31, 59, 200), result_3)
-
-# def test_from_native_datetime_time(self):
-# """
-# Make sure from_native() accepts a datetime.time instance.
-# """
-# f = serializers.TimeField()
-# result_1 = f.from_native(datetime.time(4, 31))
-# result_2 = f.from_native(datetime.time(4, 31, 59))
-# result_3 = f.from_native(datetime.time(4, 31, 59, 200))
-
-# self.assertEqual(result_1, datetime.time(4, 31))
-# self.assertEqual(result_2, datetime.time(4, 31, 59))
-# self.assertEqual(result_3, datetime.time(4, 31, 59, 200))
-
-# def test_from_native_custom_format(self):
-# """
-# Make sure from_native() accepts custom input formats.
-# """
-# f = serializers.TimeField(input_formats=['%H -- %M'])
-# result = f.from_native('04 -- 31')
-
-# self.assertEqual(datetime.time(4, 31), result)
-
-# def test_from_native_invalid_default_on_custom_format(self):
-# """
-# Make sure from_native() don't accept default formats if custom format is preset
-# """
-# f = serializers.TimeField(input_formats=['%H -- %M'])
-
-# try:
-# f.from_native('04:31:59')
-# except validators.ValidationError as e:
-# self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: hh -- mm"])
-# else:
-# self.fail("ValidationError was not properly raised")
-
-# def test_from_native_empty(self):
-# """
-# Make sure from_native() returns None on empty param.
-# """
-# f = serializers.TimeField()
-# result = f.from_native('')
-
-# self.assertEqual(result, None)
-
-# def test_from_native_none(self):
-# """
-# Make sure from_native() returns None on None param.
-# """
-# f = serializers.TimeField()
-# result = f.from_native(None)
-
-# self.assertEqual(result, None)
-
-# def test_from_native_invalid_time(self):
-# """
-# Make sure from_native() raises a ValidationError on passing an invalid time.
-# """
-# f = serializers.TimeField()
-
-# try:
-# f.from_native('04:61:59')
-# except validators.ValidationError as e:
-# self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: "
-# "hh:mm[:ss[.uuuuuu]]"])
-# else:
-# self.fail("ValidationError was not properly raised")
-
-# def test_from_native_invalid_format(self):
-# """
-# Make sure from_native() raises a ValidationError on passing an invalid format.
-# """
-# f = serializers.TimeField()
-
-# try:
-# f.from_native('04 -- 31')
-# except validators.ValidationError as e:
-# self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: "
-# "hh:mm[:ss[.uuuuuu]]"])
-# else:
-# self.fail("ValidationError was not properly raised")
-
-# def test_to_native(self):
-# """
-# Make sure to_native() returns time object as default.
-# """
-# f = serializers.TimeField()
-# result_1 = f.to_native(datetime.time(4, 31))
-# result_2 = f.to_native(datetime.time(4, 31, 59))
-# result_3 = f.to_native(datetime.time(4, 31, 59, 200))
-
-# self.assertEqual(datetime.time(4, 31), result_1)
-# self.assertEqual(datetime.time(4, 31, 59), result_2)
-# self.assertEqual(datetime.time(4, 31, 59, 200), result_3)
-
-# def test_to_native_iso(self):
-# """
-# Make sure to_native() with format='iso-8601' returns iso formatted time.
-# """
-# f = serializers.TimeField(format='iso-8601')
-# result_1 = f.to_native(datetime.time(4, 31))
-# result_2 = f.to_native(datetime.time(4, 31, 59))
-# result_3 = f.to_native(datetime.time(4, 31, 59, 200))
-
-# self.assertEqual('04:31:00', result_1)
-# self.assertEqual('04:31:59', result_2)
-# self.assertEqual('04:31:59.000200', result_3)
-
-# def test_to_native_custom_format(self):
-# """
-# Make sure to_native() returns correct custom format.
-# """
-# f = serializers.TimeField(format="%H - %S [%f]")
-# result_1 = f.to_native(datetime.time(4, 31))
-# result_2 = f.to_native(datetime.time(4, 31, 59))
-# result_3 = f.to_native(datetime.time(4, 31, 59, 200))
-
-# self.assertEqual('04 - 00 [000000]', result_1)
-# self.assertEqual('04 - 59 [000000]', result_2)
-# self.assertEqual('04 - 59 [000200]', result_3)
-
-
-# class DecimalFieldTest(TestCase):
-# """
-# Tests for the DecimalField from_native() and to_native() behavior
-# """
-
-# def test_from_native_string(self):
-# """
-# Make sure from_native() accepts string values
-# """
-# f = serializers.DecimalField()
-# result_1 = f.from_native('9000')
-# result_2 = f.from_native('1.00000001')
-
-# self.assertEqual(Decimal('9000'), result_1)
-# self.assertEqual(Decimal('1.00000001'), result_2)
-
-# def test_from_native_invalid_string(self):
-# """
-# Make sure from_native() raises ValidationError on passing invalid string
-# """
-# f = serializers.DecimalField()
-
-# try:
-# f.from_native('123.45.6')
-# except validators.ValidationError as e:
-# self.assertEqual(e.messages, ["Enter a number."])
-# else:
-# self.fail("ValidationError was not properly raised")
-
-# def test_from_native_integer(self):
-# """
-# Make sure from_native() accepts integer values
-# """
-# f = serializers.DecimalField()
-# result = f.from_native(9000)
-
-# self.assertEqual(Decimal('9000'), result)
-
-# def test_from_native_float(self):
-# """
-# Make sure from_native() accepts float values
-# """
-# f = serializers.DecimalField()
-# result = f.from_native(1.00000001)
-
-# self.assertEqual(Decimal('1.00000001'), result)
-
-# def test_from_native_empty(self):
-# """
-# Make sure from_native() returns None on empty param.
-# """
-# f = serializers.DecimalField()
-# result = f.from_native('')
-
-# self.assertEqual(result, None)
-
-# def test_from_native_none(self):
-# """
-# Make sure from_native() returns None on None param.
-# """
-# f = serializers.DecimalField()
-# result = f.from_native(None)
-
-# self.assertEqual(result, None)
-
-# def test_to_native(self):
-# """
-# Make sure to_native() returns Decimal as string.
-# """
-# f = serializers.DecimalField()
-
-# result_1 = f.to_native(Decimal('9000'))
-# result_2 = f.to_native(Decimal('1.00000001'))
-
-# self.assertEqual(Decimal('9000'), result_1)
-# self.assertEqual(Decimal('1.00000001'), result_2)
-
-# def test_to_native_none(self):
-# """
-# Make sure from_native() returns None on None param.
-# """
-# f = serializers.DecimalField(required=False)
-# self.assertEqual(None, f.to_native(None))
-
-# def test_valid_serialization(self):
-# """
-# Make sure the serializer works correctly
-# """
-# class DecimalSerializer(serializers.Serializer):
-# decimal_field = serializers.DecimalField(max_value=9010,
-# min_value=9000,
-# max_digits=6,
-# decimal_places=2)
-
-# self.assertTrue(DecimalSerializer(data={'decimal_field': '9001'}).is_valid())
-# self.assertTrue(DecimalSerializer(data={'decimal_field': '9001.2'}).is_valid())
-# self.assertTrue(DecimalSerializer(data={'decimal_field': '9001.23'}).is_valid())
-
-# self.assertFalse(DecimalSerializer(data={'decimal_field': '8000'}).is_valid())
-# self.assertFalse(DecimalSerializer(data={'decimal_field': '9900'}).is_valid())
-# self.assertFalse(DecimalSerializer(data={'decimal_field': '9001.234'}).is_valid())
-
-# def test_raise_max_value(self):
-# """
-# Make sure max_value violations raises ValidationError
-# """
-# class DecimalSerializer(serializers.Serializer):
-# decimal_field = serializers.DecimalField(max_value=100)
-
-# s = DecimalSerializer(data={'decimal_field': '123'})
-
-# self.assertFalse(s.is_valid())
-# self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is less than or equal to 100.']})
-
-# def test_raise_min_value(self):
-# """
-# Make sure min_value violations raises ValidationError
-# """
-# class DecimalSerializer(serializers.Serializer):
-# decimal_field = serializers.DecimalField(min_value=100)
-
-# s = DecimalSerializer(data={'decimal_field': '99'})
-
-# self.assertFalse(s.is_valid())
-# self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is greater than or equal to 100.']})
-
-# def test_raise_max_digits(self):
-# """
-# Make sure max_digits violations raises ValidationError
-# """
-# class DecimalSerializer(serializers.Serializer):
-# decimal_field = serializers.DecimalField(max_digits=5)
-
-# s = DecimalSerializer(data={'decimal_field': '123.456'})
-
-# self.assertFalse(s.is_valid())
-# self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 5 digits in total.']})
-
-# def test_raise_max_decimal_places(self):
-# """
-# Make sure max_decimal_places violations raises ValidationError
-# """
-# class DecimalSerializer(serializers.Serializer):
-# decimal_field = serializers.DecimalField(decimal_places=3)
-
-# s = DecimalSerializer(data={'decimal_field': '123.4567'})
-
-# self.assertFalse(s.is_valid())
-# self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 3 decimal places.']})
-
-# def test_raise_max_whole_digits(self):
-# """
-# Make sure max_whole_digits violations raises ValidationError
-# """
-# class DecimalSerializer(serializers.Serializer):
-# decimal_field = serializers.DecimalField(max_digits=4, decimal_places=3)
-
-# s = DecimalSerializer(data={'decimal_field': '12345.6'})
-
-# self.assertFalse(s.is_valid())
-# self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
-
-
-# class ChoiceFieldTests(TestCase):
-# """
-# Tests for the ChoiceField options generator
-# """
-# def test_choices_required(self):
-# """
-# Make sure proper choices are rendered if field is required
-# """
-# f = serializers.ChoiceField(required=True, choices=SAMPLE_CHOICES)
-# self.assertEqual(f.choices, SAMPLE_CHOICES)
-
-# def test_choices_not_required(self):
-# """
-# Make sure proper choices (plus blank) are rendered if the field isn't required
-# """
-# f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
-# self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
-
-# def test_blank_choice_display(self):
-# blank = 'No Preference'
-# f = serializers.ChoiceField(
-# required=False,
-# choices=SAMPLE_CHOICES,
-# blank_display_value=blank,
-# )
-# self.assertEqual(f.choices, [('', blank)] + SAMPLE_CHOICES)
-
-# def test_invalid_choice_model(self):
-# s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
-# self.assertFalse(s.is_valid())
-# self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']})
-# self.assertEqual(s.data['choice'], '')
-
-# def test_empty_choice_model(self):
-# """
-# Test that the 'empty' value is correctly passed and used depending on
-# the 'null' property on the model field.
-# """
-# s = ChoiceFieldModelSerializer(data={'choice': ''})
-# self.assertTrue(s.is_valid())
-# self.assertEqual(s.data['choice'], '')
-
-# s = ChoiceFieldModelWithNullSerializer(data={'choice': ''})
-# self.assertTrue(s.is_valid())
-# self.assertEqual(s.data['choice'], None)
-
-# def test_from_native_empty(self):
-# """
-# Make sure from_native() returns an empty string on empty param by default.
-# """
-# f = serializers.ChoiceField(choices=SAMPLE_CHOICES)
-# self.assertEqual(f.from_native(''), '')
-# self.assertEqual(f.from_native(None), '')
-
-# def test_from_native_empty_override(self):
-# """
-# Make sure you can override from_native() behavior regarding empty values.
-# """
-# f = serializers.ChoiceField(choices=SAMPLE_CHOICES, empty=None)
-# self.assertEqual(f.from_native(''), None)
-# self.assertEqual(f.from_native(None), None)
-
-# def test_metadata_choices(self):
-# """
-# Make sure proper choices are included in the field's metadata.
-# """
-# choices = [{'value': v, 'display_name': n} for v, n in SAMPLE_CHOICES]
-# f = serializers.ChoiceField(choices=SAMPLE_CHOICES)
-# self.assertEqual(f.metadata()['choices'], choices)
-
-# def test_metadata_choices_not_required(self):
-# """
-# Make sure proper choices are included in the field's metadata.
-# """
-# choices = [{'value': v, 'display_name': n}
-# for v, n in models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES]
-# f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
-# self.assertEqual(f.metadata()['choices'], choices)
-
-
-# class EmailFieldTests(TestCase):
-# """
-# Tests for EmailField attribute values
-# """
-
-# class EmailFieldModel(RESTFrameworkModel):
-# email_field = models.EmailField(blank=True)
-
-# class EmailFieldWithGivenMaxLengthModel(RESTFrameworkModel):
-# email_field = models.EmailField(max_length=150, blank=True)
-
-# def test_default_model_value(self):
-# class EmailFieldSerializer(serializers.ModelSerializer):
-# class Meta:
-# model = self.EmailFieldModel
-
-# serializer = EmailFieldSerializer(data={})
-# self.assertEqual(serializer.is_valid(), True)
-# self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 75)
-
-# def test_given_model_value(self):
-# class EmailFieldSerializer(serializers.ModelSerializer):
-# class Meta:
-# model = self.EmailFieldWithGivenMaxLengthModel
-
-# serializer = EmailFieldSerializer(data={})
-# self.assertEqual(serializer.is_valid(), True)
-# self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 150)
-
-# def test_given_serializer_value(self):
-# class EmailFieldSerializer(serializers.ModelSerializer):
-# email_field = serializers.EmailField(source='email_field', max_length=20, required=False)
-
-# class Meta:
-# model = self.EmailFieldModel
-
-# serializer = EmailFieldSerializer(data={})
-# self.assertEqual(serializer.is_valid(), True)
-# self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 20)
-
-
-# class SlugFieldTests(TestCase):
-# """
-# Tests for SlugField attribute values
-# """
-
-# class SlugFieldModel(RESTFrameworkModel):
-# slug_field = models.SlugField(blank=True)
-
-# class SlugFieldWithGivenMaxLengthModel(RESTFrameworkModel):
-# slug_field = models.SlugField(max_length=84, blank=True)
-
-# def test_default_model_value(self):
-# class SlugFieldSerializer(serializers.ModelSerializer):
-# class Meta:
-# model = self.SlugFieldModel
-
-# serializer = SlugFieldSerializer(data={})
-# self.assertEqual(serializer.is_valid(), True)
-# self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 50)
-
-# def test_given_model_value(self):
-# class SlugFieldSerializer(serializers.ModelSerializer):
-# class Meta:
-# model = self.SlugFieldWithGivenMaxLengthModel
-
-# serializer = SlugFieldSerializer(data={})
-# self.assertEqual(serializer.is_valid(), True)
-# self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 84)
-
-# def test_given_serializer_value(self):
-# class SlugFieldSerializer(serializers.ModelSerializer):
-# slug_field = serializers.SlugField(source='slug_field',
-# max_length=20, required=False)
-
-# class Meta:
-# model = self.SlugFieldModel
-
-# serializer = SlugFieldSerializer(data={})
-# self.assertEqual(serializer.is_valid(), True)
-# self.assertEqual(getattr(serializer.fields['slug_field'],
-# 'max_length'), 20)
-
-# def test_invalid_slug(self):
-# """
-# Make sure an invalid slug raises ValidationError
-# """
-# class SlugFieldSerializer(serializers.ModelSerializer):
-# slug_field = serializers.SlugField(source='slug_field', max_length=20, required=True)
-
-# class Meta:
-# model = self.SlugFieldModel
-
-# s = SlugFieldSerializer(data={'slug_field': 'a b'})
-
-# self.assertEqual(s.is_valid(), False)
-# self.assertEqual(s.errors, {'slug_field': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]})
-
-
-# class URLFieldTests(TestCase):
-# """
-# Tests for URLField attribute values.
-
-# (Includes test for #1210, checking that validators can be overridden.)
-# """
-
-# class URLFieldModel(RESTFrameworkModel):
-# url_field = models.URLField(blank=True)
-
-# class URLFieldWithGivenMaxLengthModel(RESTFrameworkModel):
-# url_field = models.URLField(max_length=128, blank=True)
-
-# def test_default_model_value(self):
-# class URLFieldSerializer(serializers.ModelSerializer):
-# class Meta:
-# model = self.URLFieldModel
-
-# serializer = URLFieldSerializer(data={})
-# self.assertEqual(serializer.is_valid(), True)
-# self.assertEqual(getattr(serializer.fields['url_field'],
-# 'max_length'), 200)
-
-# def test_given_model_value(self):
-# class URLFieldSerializer(serializers.ModelSerializer):
-# class Meta:
-# model = self.URLFieldWithGivenMaxLengthModel
-
-# serializer = URLFieldSerializer(data={})
-# self.assertEqual(serializer.is_valid(), True)
-# self.assertEqual(getattr(serializer.fields['url_field'],
-# 'max_length'), 128)
-
-# def test_given_serializer_value(self):
-# class URLFieldSerializer(serializers.ModelSerializer):
-# url_field = serializers.URLField(source='url_field',
-# max_length=20, required=False)
-
-# class Meta:
-# model = self.URLFieldWithGivenMaxLengthModel
-
-# serializer = URLFieldSerializer(data={})
-# self.assertEqual(serializer.is_valid(), True)
-# self.assertEqual(getattr(serializer.fields['url_field'],
-# 'max_length'), 20)
-
-# def test_validators_can_be_overridden(self):
-# url_field = serializers.URLField(validators=[])
-# validators = url_field.validators
-# self.assertEqual([], validators, 'Passing `validators` kwarg should have overridden default validators')
-
-
-# class FieldMetadata(TestCase):
-# def setUp(self):
-# self.required_field = serializers.Field()
-# self.required_field.label = uuid4().hex
-# self.required_field.required = True
-
-# self.optional_field = serializers.Field()
-# self.optional_field.label = uuid4().hex
-# self.optional_field.required = False
-
-# def test_required(self):
-# self.assertEqual(self.required_field.metadata()['required'], True)
-
-# def test_optional(self):
-# self.assertEqual(self.optional_field.metadata()['required'], False)
-
-# def test_label(self):
-# for field in (self.required_field, self.optional_field):
-# self.assertEqual(field.metadata()['label'], field.label)
-
-
-# class FieldCallableDefault(TestCase):
-# def setUp(self):
-# self.simple_callable = lambda: 'foo bar'
-
-# def test_default_can_be_simple_callable(self):
-# """
-# Ensure that the 'default' argument can also be a simple callable.
-# """
-# field = serializers.WritableField(default=self.simple_callable)
-# into = {}
-# field.field_from_native({}, {}, 'field', into)
-# self.assertEqual(into, {'field': 'foo bar'})
-
-
-# class CustomIntegerField(TestCase):
-# """
-# Test that custom fields apply min_value and max_value constraints
-# """
-# def test_custom_fields_can_be_validated_for_value(self):
-
-# class MoneyField(models.PositiveIntegerField):
-# pass
-
-# class EntryModel(models.Model):
-# bank = MoneyField(validators=[validators.MaxValueValidator(100)])
-
-# class EntrySerializer(serializers.ModelSerializer):
-# class Meta:
-# model = EntryModel
-
-# entry = EntryModel(bank=1)
-
-# serializer = EntrySerializer(entry, data={"bank": 11})
-# self.assertTrue(serializer.is_valid())
-
-# serializer = EntrySerializer(entry, data={"bank": -1})
-# self.assertFalse(serializer.is_valid())
-
-# serializer = EntrySerializer(entry, data={"bank": 101})
-# self.assertFalse(serializer.is_valid())
-
-
-# class BooleanField(TestCase):
-# """
-# Tests for BooleanField
-# """
-# def test_boolean_required(self):
-# class BooleanRequiredSerializer(serializers.Serializer):
-# bool_field = serializers.BooleanField(required=True)
-
-# self.assertFalse(BooleanRequiredSerializer(data={}).is_valid())
-
-
-# class SerializerMethodFieldTest(TestCase):
-# """
-# Tests for the SerializerMethodField field_to_native() behavior
-# """
-# class SerializerTest(serializers.Serializer):
-# def get_my_test(self, obj):
-# return obj.my_test[0:5]
-
-# class ModelCharField(TestCase):
-# """
-# Tests for CharField
-# """
-# def test_none_serializing(self):
-# class CharFieldSerializer(serializers.Serializer):
-# char = serializers.CharField(allow_none=True, required=False)
-# serializer = CharFieldSerializer(data={'char': None})
-# self.assertTrue(serializer.is_valid())
-# self.assertIsNone(serializer.object['char'])
-
-
-# class SerializerMethodFieldTest(TestCase):
-# """
-# Tests for the SerializerMethodField field_to_native() behavior
-# """
-# class SerializerTest(serializers.Serializer):
-# def get_my_test(self, obj):
-# return obj.my_test[0:5]
-
-# class Example():
-# my_test = 'Hey, this is a test !'
-
-# def test_field_to_native(self):
-# s = serializers.SerializerMethodField('get_my_test')
-# s.initialize(self.SerializerTest(), 'name')
-# result = s.field_to_native(self.Example(), None)
-# self.assertEqual(result, 'Hey, ')
--
cgit v1.2.3
From b187f53453d3885cd918f5f9f4490bcc8e3e2410 Mon Sep 17 00:00:00 2001
From: Danilo Bargen
Date: Mon, 2 Jun 2014 00:41:58 +0200
Subject: Changed return status for CSRF failures to HTTP 403
By default, Django returns "HTTP 403 Forbidden" responses when CSRF
validation failed[1]. CSRF is a case of authorization, not of
authentication. Therefore `PermissionDenied` should be raised instead
of `AuthenticationFailed`.
[1] https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#rejected-requests
---
rest_framework/authentication.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index f3fec05e..36d74dd9 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -129,7 +129,7 @@ class SessionAuthentication(BaseAuthentication):
reason = CSRFCheck().process_view(request, None, (), {})
if reason:
# CSRF failed, bail with explicit error message
- raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason)
+ raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
class TokenAuthentication(BaseAuthentication):
--
cgit v1.2.3
From f22d0afc3dfc7478e084d1d6ed6b53f71641dec6 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 23 Sep 2014 14:15:00 +0100
Subject: Tests for field choices
---
rest_framework/fields.py | 21 +-
rest_framework/serializers.py | 3 +
rest_framework/utils/field_mapping.py | 58 +--
tests/test_field_options.py | 55 ---
tests/test_field_values.py | 607 ------------------------------
tests/test_fields.py | 674 ++++++++++++++++++++++++++++++++++
tests/test_model_serializer.py | 47 ++-
7 files changed, 756 insertions(+), 709 deletions(-)
delete mode 100644 tests/test_field_options.py
delete mode 100644 tests/test_field_values.py
create mode 100644 tests/test_fields.py
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 48a3e1ab..f5bae734 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -102,6 +102,7 @@ class Field(object):
'null': _('This field may not be null.')
}
default_validators = []
+ default_empty_html = None
def __init__(self, read_only=False, write_only=False,
required=None, default=empty, initial=None, source=None,
@@ -185,6 +186,11 @@ class Field(object):
Given the *incoming* primative data, return the value for this field
that should be validated and transformed to a native value.
"""
+ if html.is_html_input(dictionary):
+ # HTML forms will represent empty fields as '', and cannot
+ # represent None or False values directly.
+ ret = dictionary.get(self.field_name, '')
+ return self.default_empty_html if (ret == '') else ret
return dictionary.get(self.field_name, empty)
def get_attribute(self, instance):
@@ -236,9 +242,6 @@ class Field(object):
Test the given value against all the validators on the field,
and either raise a `ValidationError` or simply return.
"""
- if value in (None, '', [], (), {}):
- return
-
errors = []
for validator in self.validators:
try:
@@ -282,16 +285,10 @@ class BooleanField(Field):
default_error_messages = {
'invalid': _('`{input}` is not a valid boolean.')
}
+ default_empty_html = False
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
- def get_value(self, dictionary):
- if html.is_html_input(dictionary):
- # HTML forms do not send a `False` value on an empty checkbox,
- # so we override the default empty value to be False.
- return dictionary.get(self.field_name, False)
- return dictionary.get(self.field_name, empty)
-
def to_internal_value(self, data):
if data in self.TRUE_VALUES:
return True
@@ -315,6 +312,7 @@ class CharField(Field):
default_error_messages = {
'blank': _('This field may not be blank.')
}
+ default_empty_html = ''
def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
@@ -323,6 +321,9 @@ class CharField(Field):
super(CharField, self).__init__(**kwargs)
def run_validation(self, data=empty):
+ # Test for the empty string here so that it does not get validated,
+ # and so that subclasses do not need to handle it explicitly
+ # inside the `to_internal_value()` method.
if data == '':
if not self.allow_blank:
self.fail('blank')
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index d9f9c8cb..949f5915 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -411,6 +411,9 @@ class ModelSerializer(Serializer):
# `ModelField`, which is used when no other typed field
# matched to the model field.
kwargs.pop('model_field', None)
+ if not issubclass(field_cls, CharField):
+ # `allow_blank` is only valid for textual fields.
+ kwargs.pop('allow_blank', None)
elif field_name in info.relations:
# Create forward and reverse relationships.
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index be72e444..1c718ccb 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -49,8 +49,9 @@ def get_field_kwargs(field_name, model_field):
kwargs = {}
validator_kwarg = model_field.validators
- if model_field.null or model_field.blank:
- kwargs['required'] = False
+ # The following will only be used by ModelField classes.
+ # Gets removed for everything else.
+ kwargs['model_field'] = model_field
if model_field.verbose_name and needs_label(model_field, field_name):
kwargs['label'] = capfirst(model_field.verbose_name)
@@ -59,23 +60,26 @@ def get_field_kwargs(field_name, model_field):
kwargs['help_text'] = model_field.help_text
if isinstance(model_field, models.AutoField) or not model_field.editable:
+ # If this field is read-only, then return early.
+ # Further keyword arguments are not valid.
kwargs['read_only'] = True
- # Read only implies that the field is not required.
- # We have a cleaner repr on the instance if we don't set it.
- kwargs.pop('required', None)
+ return kwargs
if model_field.has_default():
- kwargs['default'] = model_field.get_default()
- # Having a default implies that the field is not required.
- # We have a cleaner repr on the instance if we don't set it.
- kwargs.pop('required', None)
+ kwargs['required'] = False
if model_field.flatchoices:
- # If this model field contains choices, then return now,
- # any further keyword arguments are not valid.
+ # If this model field contains choices, then return early.
+ # Further keyword arguments are not valid.
kwargs['choices'] = model_field.flatchoices
return kwargs
+ if model_field.null:
+ kwargs['allow_null'] = True
+
+ if model_field.blank:
+ kwargs['allow_blank'] = True
+
# Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator.
max_length = getattr(model_field, 'max_length', None)
@@ -88,7 +92,10 @@ def get_field_kwargs(field_name, model_field):
# Ensure that min_length is passed explicitly as a keyword arg,
# rather than as a validator.
- min_length = getattr(model_field, 'min_length', None)
+ min_length = next((
+ validator.limit_value for validator in validator_kwarg
+ if isinstance(validator, validators.MinLengthValidator)
+ ), None)
if min_length is not None:
kwargs['min_length'] = min_length
validator_kwarg = [
@@ -153,20 +160,9 @@ def get_field_kwargs(field_name, model_field):
if decimal_places is not None:
kwargs['decimal_places'] = decimal_places
- if isinstance(model_field, models.BooleanField):
- # models.BooleanField has `blank=True`, but *is* actually
- # required *unless* a default is provided.
- # Also note that Django<1.6 uses `default=False` for
- # models.BooleanField, but Django>=1.6 uses `default=None`.
- kwargs.pop('required', None)
-
if validator_kwarg:
kwargs['validators'] = validator_kwarg
- # The following will only be used by ModelField classes.
- # Gets removed for everything else.
- kwargs['model_field'] = model_field
-
return kwargs
@@ -188,16 +184,22 @@ def get_relation_kwargs(field_name, relation_info):
kwargs.pop('queryset', None)
if model_field:
- if model_field.null or model_field.blank:
- kwargs['required'] = False
if model_field.verbose_name and needs_label(model_field, field_name):
kwargs['label'] = capfirst(model_field.verbose_name)
- if not model_field.editable:
- kwargs['read_only'] = True
- kwargs.pop('queryset', None)
help_text = clean_manytomany_helptext(model_field.help_text)
if help_text:
kwargs['help_text'] = help_text
+ if not model_field.editable:
+ kwargs['read_only'] = True
+ kwargs.pop('queryset', None)
+ if kwargs.get('read_only', False):
+ # If this field is read-only, then return early.
+ # No further keyword arguments are valid.
+ return kwargs
+ if model_field.has_default():
+ kwargs['required'] = False
+ if model_field.null:
+ kwargs['allow_null'] = True
return kwargs
diff --git a/tests/test_field_options.py b/tests/test_field_options.py
deleted file mode 100644
index 444bd424..00000000
--- a/tests/test_field_options.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from rest_framework import fields
-import pytest
-
-
-class TestFieldOptions:
- def test_required(self):
- """
- By default a field must be included in the input.
- """
- field = fields.IntegerField()
- with pytest.raises(fields.ValidationError) as exc_info:
- field.run_validation()
- assert exc_info.value.messages == ['This field is required.']
-
- def test_not_required(self):
- """
- If `required=False` then a field may be omitted from the input.
- """
- field = fields.IntegerField(required=False)
- with pytest.raises(fields.SkipField):
- field.run_validation()
-
- def test_disallow_null(self):
- """
- By default `None` is not a valid input.
- """
- field = fields.IntegerField()
- with pytest.raises(fields.ValidationError) as exc_info:
- field.run_validation(None)
- assert exc_info.value.messages == ['This field may not be null.']
-
- def test_allow_null(self):
- """
- If `allow_null=True` then `None` is a valid input.
- """
- field = fields.IntegerField(allow_null=True)
- output = field.run_validation(None)
- assert output is None
-
- def test_disallow_blank(self):
- """
- By default '' is not a valid input.
- """
- field = fields.CharField()
- with pytest.raises(fields.ValidationError) as exc_info:
- field.run_validation('')
- assert exc_info.value.messages == ['This field may not be blank.']
-
- def test_allow_blank(self):
- """
- If `allow_blank=True` then '' is a valid input.
- """
- field = fields.CharField(allow_blank=True)
- output = field.run_validation('')
- assert output is ''
diff --git a/tests/test_field_values.py b/tests/test_field_values.py
deleted file mode 100644
index bac50f0b..00000000
--- a/tests/test_field_values.py
+++ /dev/null
@@ -1,607 +0,0 @@
-from decimal import Decimal
-from django.utils import timezone
-from rest_framework import fields
-import datetime
-import django
-import pytest
-
-
-def get_items(mapping_or_list_of_two_tuples):
- # Tests accept either lists of two tuples, or dictionaries.
- if isinstance(mapping_or_list_of_two_tuples, dict):
- # {value: expected}
- return mapping_or_list_of_two_tuples.items()
- # [(value, expected), ...]
- return mapping_or_list_of_two_tuples
-
-
-class FieldValues:
- """
- Base class for testing valid and invalid input values.
- """
- def test_valid_inputs(self):
- """
- Ensure that valid values return the expected validated data.
- """
- for input_value, expected_output in get_items(self.valid_inputs):
- assert self.field.run_validation(input_value) == expected_output
-
- def test_invalid_inputs(self):
- """
- Ensure that invalid values raise the expected validation error.
- """
- for input_value, expected_failure in get_items(self.invalid_inputs):
- with pytest.raises(fields.ValidationError) as exc_info:
- self.field.run_validation(input_value)
- assert exc_info.value.messages == expected_failure
-
- def test_outputs(self):
- for output_value, expected_output in get_items(self.outputs):
- assert self.field.to_representation(output_value) == expected_output
-
-
-# Boolean types...
-
-class TestBooleanField(FieldValues):
- """
- Valid and invalid values for `BooleanField`.
- """
- valid_inputs = {
- 'true': True,
- 'false': False,
- '1': True,
- '0': False,
- 1: True,
- 0: False,
- True: True,
- False: False,
- }
- invalid_inputs = {
- 'foo': ['`foo` is not a valid boolean.']
- }
- outputs = {
- 'true': True,
- 'false': False,
- '1': True,
- '0': False,
- 1: True,
- 0: False,
- True: True,
- False: False,
- 'other': True
- }
- field = fields.BooleanField()
-
-
-# String types...
-
-class TestCharField(FieldValues):
- """
- Valid and invalid values for `CharField`.
- """
- valid_inputs = {
- 1: '1',
- 'abc': 'abc'
- }
- invalid_inputs = {
- '': ['This field may not be blank.']
- }
- outputs = {
- 1: '1',
- 'abc': 'abc'
- }
- field = fields.CharField()
-
-
-class TestEmailField(FieldValues):
- """
- Valid and invalid values for `EmailField`.
- """
- valid_inputs = {
- 'example@example.com': 'example@example.com',
- ' example@example.com ': 'example@example.com',
- }
- invalid_inputs = {
- 'examplecom': ['Enter a valid email address.']
- }
- outputs = {}
- field = fields.EmailField()
-
-
-class TestRegexField(FieldValues):
- """
- Valid and invalid values for `RegexField`.
- """
- valid_inputs = {
- 'a9': 'a9',
- }
- invalid_inputs = {
- 'A9': ["This value does not match the required pattern."]
- }
- outputs = {}
- field = fields.RegexField(regex='[a-z][0-9]')
-
-
-class TestSlugField(FieldValues):
- """
- Valid and invalid values for `SlugField`.
- """
- valid_inputs = {
- 'slug-99': 'slug-99',
- }
- invalid_inputs = {
- 'slug 99': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]
- }
- outputs = {}
- field = fields.SlugField()
-
-
-class TestURLField(FieldValues):
- """
- Valid and invalid values for `URLField`.
- """
- valid_inputs = {
- 'http://example.com': 'http://example.com',
- }
- invalid_inputs = {
- 'example.com': ['Enter a valid URL.']
- }
- outputs = {}
- field = fields.URLField()
-
-
-# Number types...
-
-class TestIntegerField(FieldValues):
- """
- Valid and invalid values for `IntegerField`.
- """
- valid_inputs = {
- '1': 1,
- '0': 0,
- 1: 1,
- 0: 0,
- 1.0: 1,
- 0.0: 0
- }
- invalid_inputs = {
- 'abc': ['A valid integer is required.']
- }
- outputs = {
- '1': 1,
- '0': 0,
- 1: 1,
- 0: 0,
- 1.0: 1,
- 0.0: 0
- }
- field = fields.IntegerField()
-
-
-class TestMinMaxIntegerField(FieldValues):
- """
- Valid and invalid values for `IntegerField` with min and max limits.
- """
- valid_inputs = {
- '1': 1,
- '3': 3,
- 1: 1,
- 3: 3,
- }
- invalid_inputs = {
- 0: ['Ensure this value is greater than or equal to 1.'],
- 4: ['Ensure this value is less than or equal to 3.'],
- '0': ['Ensure this value is greater than or equal to 1.'],
- '4': ['Ensure this value is less than or equal to 3.'],
- }
- outputs = {}
- field = fields.IntegerField(min_value=1, max_value=3)
-
-
-class TestFloatField(FieldValues):
- """
- Valid and invalid values for `FloatField`.
- """
- valid_inputs = {
- '1': 1.0,
- '0': 0.0,
- 1: 1.0,
- 0: 0.0,
- 1.0: 1.0,
- 0.0: 0.0,
- }
- invalid_inputs = {
- 'abc': ["A valid number is required."]
- }
- outputs = {
- '1': 1.0,
- '0': 0.0,
- 1: 1.0,
- 0: 0.0,
- 1.0: 1.0,
- 0.0: 0.0,
- }
- field = fields.FloatField()
-
-
-class TestMinMaxFloatField(FieldValues):
- """
- Valid and invalid values for `FloatField` with min and max limits.
- """
- valid_inputs = {
- '1': 1,
- '3': 3,
- 1: 1,
- 3: 3,
- 1.0: 1.0,
- 3.0: 3.0,
- }
- invalid_inputs = {
- 0.9: ['Ensure this value is greater than or equal to 1.'],
- 3.1: ['Ensure this value is less than or equal to 3.'],
- '0.0': ['Ensure this value is greater than or equal to 1.'],
- '3.1': ['Ensure this value is less than or equal to 3.'],
- }
- outputs = {}
- field = fields.FloatField(min_value=1, max_value=3)
-
-
-class TestDecimalField(FieldValues):
- """
- Valid and invalid values for `DecimalField`.
- """
- valid_inputs = {
- '12.3': Decimal('12.3'),
- '0.1': Decimal('0.1'),
- 10: Decimal('10'),
- 0: Decimal('0'),
- 12.3: Decimal('12.3'),
- 0.1: Decimal('0.1'),
- }
- invalid_inputs = (
- ('abc', ["A valid number is required."]),
- (Decimal('Nan'), ["A valid number is required."]),
- (Decimal('Inf'), ["A valid number is required."]),
- ('12.345', ["Ensure that there are no more than 3 digits in total."]),
- ('0.01', ["Ensure that there are no more than 1 decimal places."]),
- (123, ["Ensure that there are no more than 2 digits before the decimal point."])
- )
- outputs = {
- '1': '1.0',
- '0': '0.0',
- '1.09': '1.1',
- '0.04': '0.0',
- 1: '1.0',
- 0: '0.0',
- Decimal('1.0'): '1.0',
- Decimal('0.0'): '0.0',
- Decimal('1.09'): '1.1',
- Decimal('0.04'): '0.0',
- }
- field = fields.DecimalField(max_digits=3, decimal_places=1)
-
-
-class TestMinMaxDecimalField(FieldValues):
- """
- Valid and invalid values for `DecimalField` with min and max limits.
- """
- valid_inputs = {
- '10.0': Decimal('10.0'),
- '20.0': Decimal('20.0'),
- }
- invalid_inputs = {
- '9.9': ['Ensure this value is greater than or equal to 10.'],
- '20.1': ['Ensure this value is less than or equal to 20.'],
- }
- outputs = {}
- field = fields.DecimalField(
- max_digits=3, decimal_places=1,
- min_value=10, max_value=20
- )
-
-
-class TestNoStringCoercionDecimalField(FieldValues):
- """
- Output values for `DecimalField` with `coerce_to_string=False`.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- 1.09: Decimal('1.1'),
- 0.04: Decimal('0.0'),
- '1.09': Decimal('1.1'),
- '0.04': Decimal('0.0'),
- Decimal('1.09'): Decimal('1.1'),
- Decimal('0.04'): Decimal('0.0'),
- }
- field = fields.DecimalField(
- max_digits=3, decimal_places=1,
- coerce_to_string=False
- )
-
-
-# Date & time fields...
-
-class TestDateField(FieldValues):
- """
- Valid and invalid values for `DateField`.
- """
- valid_inputs = {
- '2001-01-01': datetime.date(2001, 1, 1),
- datetime.date(2001, 1, 1): datetime.date(2001, 1, 1),
- }
- invalid_inputs = {
- 'abc': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
- '2001-99-99': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
- datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
- }
- outputs = {
- datetime.date(2001, 1, 1): '2001-01-01',
- }
- field = fields.DateField()
-
-
-class TestCustomInputFormatDateField(FieldValues):
- """
- Valid and invalid values for `DateField` with a cutom input format.
- """
- valid_inputs = {
- '1 Jan 2001': datetime.date(2001, 1, 1),
- }
- invalid_inputs = {
- '2001-01-01': ['Date has wrong format. Use one of these formats instead: DD [Jan-Dec] YYYY']
- }
- outputs = {}
- field = fields.DateField(input_formats=['%d %b %Y'])
-
-
-class TestCustomOutputFormatDateField(FieldValues):
- """
- Values for `DateField` with a custom output format.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- datetime.date(2001, 1, 1): '01 Jan 2001'
- }
- field = fields.DateField(format='%d %b %Y')
-
-
-class TestNoOutputFormatDateField(FieldValues):
- """
- Values for `DateField` with no output format.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- datetime.date(2001, 1, 1): datetime.date(2001, 1, 1)
- }
- field = fields.DateField(format=None)
-
-
-class TestDateTimeField(FieldValues):
- """
- Valid and invalid values for `DateTimeField`.
- """
- valid_inputs = {
- '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
- '2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
- '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
- datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
- datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
- # Note that 1.4 does not support timezone string parsing.
- '2001-01-01T14:00+01:00' if (django.VERSION > (1, 4)) else '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())
- }
- invalid_inputs = {
- 'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
- '2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
- datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
- }
- outputs = {
- datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
- datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z',
- }
- field = fields.DateTimeField(default_timezone=timezone.UTC())
-
-
-class TestCustomInputFormatDateTimeField(FieldValues):
- """
- Valid and invalid values for `DateTimeField` with a cutom input format.
- """
- valid_inputs = {
- '1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()),
- }
- invalid_inputs = {
- '2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY']
- }
- outputs = {}
- field = fields.DateTimeField(default_timezone=timezone.UTC(), input_formats=['%I:%M%p, %d %b %Y'])
-
-
-class TestCustomOutputFormatDateTimeField(FieldValues):
- """
- Values for `DateTimeField` with a custom output format.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- datetime.datetime(2001, 1, 1, 13, 00): '01:00PM, 01 Jan 2001',
- }
- field = fields.DateTimeField(format='%I:%M%p, %d %b %Y')
-
-
-class TestNoOutputFormatDateTimeField(FieldValues):
- """
- Values for `DateTimeField` with no output format.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00),
- }
- field = fields.DateTimeField(format=None)
-
-
-class TestNaiveDateTimeField(FieldValues):
- """
- Valid and invalid values for `DateTimeField` with naive datetimes.
- """
- valid_inputs = {
- datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00),
- '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
- }
- invalid_inputs = {}
- outputs = {}
- field = fields.DateTimeField(default_timezone=None)
-
-
-class TestTimeField(FieldValues):
- """
- Valid and invalid values for `TimeField`.
- """
- valid_inputs = {
- '13:00': datetime.time(13, 00),
- datetime.time(13, 00): datetime.time(13, 00),
- }
- invalid_inputs = {
- 'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
- '99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
- }
- outputs = {
- datetime.time(13, 00): '13:00:00'
- }
- field = fields.TimeField()
-
-
-class TestCustomInputFormatTimeField(FieldValues):
- """
- Valid and invalid values for `TimeField` with a custom input format.
- """
- valid_inputs = {
- '1:00pm': datetime.time(13, 00),
- }
- invalid_inputs = {
- '13:00': ['Time has wrong format. Use one of these formats instead: hh:mm[AM|PM]'],
- }
- outputs = {}
- field = fields.TimeField(input_formats=['%I:%M%p'])
-
-
-class TestCustomOutputFormatTimeField(FieldValues):
- """
- Values for `TimeField` with a custom output format.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- datetime.time(13, 00): '01:00PM'
- }
- field = fields.TimeField(format='%I:%M%p')
-
-
-class TestNoOutputFormatTimeField(FieldValues):
- """
- Values for `TimeField` with a no output format.
- """
- valid_inputs = {}
- invalid_inputs = {}
- outputs = {
- datetime.time(13, 00): datetime.time(13, 00)
- }
- field = fields.TimeField(format=None)
-
-
-# Choice types...
-
-class TestChoiceField(FieldValues):
- """
- Valid and invalid values for `ChoiceField`.
- """
- valid_inputs = {
- 'poor': 'poor',
- 'medium': 'medium',
- 'good': 'good',
- }
- invalid_inputs = {
- 'amazing': ['`amazing` is not a valid choice.']
- }
- outputs = {
- 'good': 'good'
- }
- field = fields.ChoiceField(
- choices=[
- ('poor', 'Poor quality'),
- ('medium', 'Medium quality'),
- ('good', 'Good quality'),
- ]
- )
-
-
-class TestChoiceFieldWithType(FieldValues):
- """
- Valid and invalid values for a `Choice` field that uses an integer type,
- instead of a char type.
- """
- valid_inputs = {
- '1': 1,
- 3: 3,
- }
- invalid_inputs = {
- 5: ['`5` is not a valid choice.'],
- 'abc': ['`abc` is not a valid choice.']
- }
- outputs = {
- '1': 1,
- 1: 1
- }
- field = fields.ChoiceField(
- choices=[
- (1, 'Poor quality'),
- (2, 'Medium quality'),
- (3, 'Good quality'),
- ]
- )
-
-
-class TestChoiceFieldWithListChoices(FieldValues):
- """
- Valid and invalid values for a `Choice` field that uses a flat list for the
- choices, rather than a list of pairs of (`value`, `description`).
- """
- valid_inputs = {
- 'poor': 'poor',
- 'medium': 'medium',
- 'good': 'good',
- }
- invalid_inputs = {
- 'awful': ['`awful` is not a valid choice.']
- }
- outputs = {
- 'good': 'good'
- }
- field = fields.ChoiceField(choices=('poor', 'medium', 'good'))
-
-
-class TestMultipleChoiceField(FieldValues):
- """
- Valid and invalid values for `MultipleChoiceField`.
- """
- valid_inputs = {
- (): set(),
- ('aircon',): set(['aircon']),
- ('aircon', 'manual'): set(['aircon', 'manual']),
- }
- invalid_inputs = {
- 'abc': ['Expected a list of items but got type `str`'],
- ('aircon', 'incorrect'): ['`incorrect` is not a valid choice.']
- }
- outputs = [
- (['aircon', 'manual'], set(['aircon', 'manual']))
- ]
- field = fields.MultipleChoiceField(
- choices=[
- ('aircon', 'AirCon'),
- ('manual', 'Manual drive'),
- ('diesel', 'Diesel'),
- ]
- )
diff --git a/tests/test_fields.py b/tests/test_fields.py
new file mode 100644
index 00000000..6bf9aed4
--- /dev/null
+++ b/tests/test_fields.py
@@ -0,0 +1,674 @@
+from decimal import Decimal
+from django.utils import timezone
+from rest_framework import fields
+import datetime
+import django
+import pytest
+
+
+# Tests for field keyword arguments and core functionality.
+# ---------------------------------------------------------
+
+class TestFieldOptions:
+ def test_required(self):
+ """
+ By default a field must be included in the input.
+ """
+ field = fields.IntegerField()
+ with pytest.raises(fields.ValidationError) as exc_info:
+ field.run_validation()
+ assert exc_info.value.messages == ['This field is required.']
+
+ def test_not_required(self):
+ """
+ If `required=False` then a field may be omitted from the input.
+ """
+ field = fields.IntegerField(required=False)
+ with pytest.raises(fields.SkipField):
+ field.run_validation()
+
+ def test_disallow_null(self):
+ """
+ By default `None` is not a valid input.
+ """
+ field = fields.IntegerField()
+ with pytest.raises(fields.ValidationError) as exc_info:
+ field.run_validation(None)
+ assert exc_info.value.messages == ['This field may not be null.']
+
+ def test_allow_null(self):
+ """
+ If `allow_null=True` then `None` is a valid input.
+ """
+ field = fields.IntegerField(allow_null=True)
+ output = field.run_validation(None)
+ assert output is None
+
+ def test_disallow_blank(self):
+ """
+ By default '' is not a valid input.
+ """
+ field = fields.CharField()
+ with pytest.raises(fields.ValidationError) as exc_info:
+ field.run_validation('')
+ assert exc_info.value.messages == ['This field may not be blank.']
+
+ def test_allow_blank(self):
+ """
+ If `allow_blank=True` then '' is a valid input.
+ """
+ field = fields.CharField(allow_blank=True)
+ output = field.run_validation('')
+ assert output is ''
+
+ def test_default(self):
+ """
+ If `default` is set, then omitted values get the default input.
+ """
+ field = fields.IntegerField(default=123)
+ output = field.run_validation()
+ assert output is 123
+
+
+# Tests for field input and output values.
+# ----------------------------------------
+
+def get_items(mapping_or_list_of_two_tuples):
+ # Tests accept either lists of two tuples, or dictionaries.
+ if isinstance(mapping_or_list_of_two_tuples, dict):
+ # {value: expected}
+ return mapping_or_list_of_two_tuples.items()
+ # [(value, expected), ...]
+ return mapping_or_list_of_two_tuples
+
+
+class FieldValues:
+ """
+ Base class for testing valid and invalid input values.
+ """
+ def test_valid_inputs(self):
+ """
+ Ensure that valid values return the expected validated data.
+ """
+ for input_value, expected_output in get_items(self.valid_inputs):
+ assert self.field.run_validation(input_value) == expected_output
+
+ def test_invalid_inputs(self):
+ """
+ Ensure that invalid values raise the expected validation error.
+ """
+ for input_value, expected_failure in get_items(self.invalid_inputs):
+ with pytest.raises(fields.ValidationError) as exc_info:
+ self.field.run_validation(input_value)
+ assert exc_info.value.messages == expected_failure
+
+ def test_outputs(self):
+ for output_value, expected_output in get_items(self.outputs):
+ assert self.field.to_representation(output_value) == expected_output
+
+
+# Boolean types...
+
+class TestBooleanField(FieldValues):
+ """
+ Valid and invalid values for `BooleanField`.
+ """
+ valid_inputs = {
+ 'true': True,
+ 'false': False,
+ '1': True,
+ '0': False,
+ 1: True,
+ 0: False,
+ True: True,
+ False: False,
+ }
+ invalid_inputs = {
+ 'foo': ['`foo` is not a valid boolean.']
+ }
+ outputs = {
+ 'true': True,
+ 'false': False,
+ '1': True,
+ '0': False,
+ 1: True,
+ 0: False,
+ True: True,
+ False: False,
+ 'other': True
+ }
+ field = fields.BooleanField()
+
+
+# String types...
+
+class TestCharField(FieldValues):
+ """
+ Valid and invalid values for `CharField`.
+ """
+ valid_inputs = {
+ 1: '1',
+ 'abc': 'abc'
+ }
+ invalid_inputs = {
+ '': ['This field may not be blank.']
+ }
+ outputs = {
+ 1: '1',
+ 'abc': 'abc'
+ }
+ field = fields.CharField()
+
+
+class TestEmailField(FieldValues):
+ """
+ Valid and invalid values for `EmailField`.
+ """
+ valid_inputs = {
+ 'example@example.com': 'example@example.com',
+ ' example@example.com ': 'example@example.com',
+ }
+ invalid_inputs = {
+ 'examplecom': ['Enter a valid email address.']
+ }
+ outputs = {}
+ field = fields.EmailField()
+
+
+class TestRegexField(FieldValues):
+ """
+ Valid and invalid values for `RegexField`.
+ """
+ valid_inputs = {
+ 'a9': 'a9',
+ }
+ invalid_inputs = {
+ 'A9': ["This value does not match the required pattern."]
+ }
+ outputs = {}
+ field = fields.RegexField(regex='[a-z][0-9]')
+
+
+class TestSlugField(FieldValues):
+ """
+ Valid and invalid values for `SlugField`.
+ """
+ valid_inputs = {
+ 'slug-99': 'slug-99',
+ }
+ invalid_inputs = {
+ 'slug 99': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]
+ }
+ outputs = {}
+ field = fields.SlugField()
+
+
+class TestURLField(FieldValues):
+ """
+ Valid and invalid values for `URLField`.
+ """
+ valid_inputs = {
+ 'http://example.com': 'http://example.com',
+ }
+ invalid_inputs = {
+ 'example.com': ['Enter a valid URL.']
+ }
+ outputs = {}
+ field = fields.URLField()
+
+
+# Number types...
+
+class TestIntegerField(FieldValues):
+ """
+ Valid and invalid values for `IntegerField`.
+ """
+ valid_inputs = {
+ '1': 1,
+ '0': 0,
+ 1: 1,
+ 0: 0,
+ 1.0: 1,
+ 0.0: 0
+ }
+ invalid_inputs = {
+ 'abc': ['A valid integer is required.']
+ }
+ outputs = {
+ '1': 1,
+ '0': 0,
+ 1: 1,
+ 0: 0,
+ 1.0: 1,
+ 0.0: 0
+ }
+ field = fields.IntegerField()
+
+
+class TestMinMaxIntegerField(FieldValues):
+ """
+ Valid and invalid values for `IntegerField` with min and max limits.
+ """
+ valid_inputs = {
+ '1': 1,
+ '3': 3,
+ 1: 1,
+ 3: 3,
+ }
+ invalid_inputs = {
+ 0: ['Ensure this value is greater than or equal to 1.'],
+ 4: ['Ensure this value is less than or equal to 3.'],
+ '0': ['Ensure this value is greater than or equal to 1.'],
+ '4': ['Ensure this value is less than or equal to 3.'],
+ }
+ outputs = {}
+ field = fields.IntegerField(min_value=1, max_value=3)
+
+
+class TestFloatField(FieldValues):
+ """
+ Valid and invalid values for `FloatField`.
+ """
+ valid_inputs = {
+ '1': 1.0,
+ '0': 0.0,
+ 1: 1.0,
+ 0: 0.0,
+ 1.0: 1.0,
+ 0.0: 0.0,
+ }
+ invalid_inputs = {
+ 'abc': ["A valid number is required."]
+ }
+ outputs = {
+ '1': 1.0,
+ '0': 0.0,
+ 1: 1.0,
+ 0: 0.0,
+ 1.0: 1.0,
+ 0.0: 0.0,
+ }
+ field = fields.FloatField()
+
+
+class TestMinMaxFloatField(FieldValues):
+ """
+ Valid and invalid values for `FloatField` with min and max limits.
+ """
+ valid_inputs = {
+ '1': 1,
+ '3': 3,
+ 1: 1,
+ 3: 3,
+ 1.0: 1.0,
+ 3.0: 3.0,
+ }
+ invalid_inputs = {
+ 0.9: ['Ensure this value is greater than or equal to 1.'],
+ 3.1: ['Ensure this value is less than or equal to 3.'],
+ '0.0': ['Ensure this value is greater than or equal to 1.'],
+ '3.1': ['Ensure this value is less than or equal to 3.'],
+ }
+ outputs = {}
+ field = fields.FloatField(min_value=1, max_value=3)
+
+
+class TestDecimalField(FieldValues):
+ """
+ Valid and invalid values for `DecimalField`.
+ """
+ valid_inputs = {
+ '12.3': Decimal('12.3'),
+ '0.1': Decimal('0.1'),
+ 10: Decimal('10'),
+ 0: Decimal('0'),
+ 12.3: Decimal('12.3'),
+ 0.1: Decimal('0.1'),
+ }
+ invalid_inputs = (
+ ('abc', ["A valid number is required."]),
+ (Decimal('Nan'), ["A valid number is required."]),
+ (Decimal('Inf'), ["A valid number is required."]),
+ ('12.345', ["Ensure that there are no more than 3 digits in total."]),
+ ('0.01', ["Ensure that there are no more than 1 decimal places."]),
+ (123, ["Ensure that there are no more than 2 digits before the decimal point."])
+ )
+ outputs = {
+ '1': '1.0',
+ '0': '0.0',
+ '1.09': '1.1',
+ '0.04': '0.0',
+ 1: '1.0',
+ 0: '0.0',
+ Decimal('1.0'): '1.0',
+ Decimal('0.0'): '0.0',
+ Decimal('1.09'): '1.1',
+ Decimal('0.04'): '0.0',
+ }
+ field = fields.DecimalField(max_digits=3, decimal_places=1)
+
+
+class TestMinMaxDecimalField(FieldValues):
+ """
+ Valid and invalid values for `DecimalField` with min and max limits.
+ """
+ valid_inputs = {
+ '10.0': Decimal('10.0'),
+ '20.0': Decimal('20.0'),
+ }
+ invalid_inputs = {
+ '9.9': ['Ensure this value is greater than or equal to 10.'],
+ '20.1': ['Ensure this value is less than or equal to 20.'],
+ }
+ outputs = {}
+ field = fields.DecimalField(
+ max_digits=3, decimal_places=1,
+ min_value=10, max_value=20
+ )
+
+
+class TestNoStringCoercionDecimalField(FieldValues):
+ """
+ Output values for `DecimalField` with `coerce_to_string=False`.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ 1.09: Decimal('1.1'),
+ 0.04: Decimal('0.0'),
+ '1.09': Decimal('1.1'),
+ '0.04': Decimal('0.0'),
+ Decimal('1.09'): Decimal('1.1'),
+ Decimal('0.04'): Decimal('0.0'),
+ }
+ field = fields.DecimalField(
+ max_digits=3, decimal_places=1,
+ coerce_to_string=False
+ )
+
+
+# Date & time fields...
+
+class TestDateField(FieldValues):
+ """
+ Valid and invalid values for `DateField`.
+ """
+ valid_inputs = {
+ '2001-01-01': datetime.date(2001, 1, 1),
+ datetime.date(2001, 1, 1): datetime.date(2001, 1, 1),
+ }
+ invalid_inputs = {
+ 'abc': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
+ '2001-99-99': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]'],
+ datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
+ }
+ outputs = {
+ datetime.date(2001, 1, 1): '2001-01-01',
+ }
+ field = fields.DateField()
+
+
+class TestCustomInputFormatDateField(FieldValues):
+ """
+ Valid and invalid values for `DateField` with a cutom input format.
+ """
+ valid_inputs = {
+ '1 Jan 2001': datetime.date(2001, 1, 1),
+ }
+ invalid_inputs = {
+ '2001-01-01': ['Date has wrong format. Use one of these formats instead: DD [Jan-Dec] YYYY']
+ }
+ outputs = {}
+ field = fields.DateField(input_formats=['%d %b %Y'])
+
+
+class TestCustomOutputFormatDateField(FieldValues):
+ """
+ Values for `DateField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.date(2001, 1, 1): '01 Jan 2001'
+ }
+ field = fields.DateField(format='%d %b %Y')
+
+
+class TestNoOutputFormatDateField(FieldValues):
+ """
+ Values for `DateField` with no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.date(2001, 1, 1): datetime.date(2001, 1, 1)
+ }
+ field = fields.DateField(format=None)
+
+
+class TestDateTimeField(FieldValues):
+ """
+ Valid and invalid values for `DateTimeField`.
+ """
+ valid_inputs = {
+ '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ '2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ # Note that 1.4 does not support timezone string parsing.
+ '2001-01-01T14:00+01:00' if (django.VERSION > (1, 4)) else '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())
+ }
+ invalid_inputs = {
+ 'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
+ '2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'],
+ datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
+ }
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z',
+ }
+ field = fields.DateTimeField(default_timezone=timezone.UTC())
+
+
+class TestCustomInputFormatDateTimeField(FieldValues):
+ """
+ Valid and invalid values for `DateTimeField` with a cutom input format.
+ """
+ valid_inputs = {
+ '1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()),
+ }
+ invalid_inputs = {
+ '2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY']
+ }
+ outputs = {}
+ field = fields.DateTimeField(default_timezone=timezone.UTC(), input_formats=['%I:%M%p, %d %b %Y'])
+
+
+class TestCustomOutputFormatDateTimeField(FieldValues):
+ """
+ Values for `DateTimeField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): '01:00PM, 01 Jan 2001',
+ }
+ field = fields.DateTimeField(format='%I:%M%p, %d %b %Y')
+
+
+class TestNoOutputFormatDateTimeField(FieldValues):
+ """
+ Values for `DateTimeField` with no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00),
+ }
+ field = fields.DateTimeField(format=None)
+
+
+class TestNaiveDateTimeField(FieldValues):
+ """
+ Valid and invalid values for `DateTimeField` with naive datetimes.
+ """
+ valid_inputs = {
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00),
+ '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
+ }
+ invalid_inputs = {}
+ outputs = {}
+ field = fields.DateTimeField(default_timezone=None)
+
+
+class TestTimeField(FieldValues):
+ """
+ Valid and invalid values for `TimeField`.
+ """
+ valid_inputs = {
+ '13:00': datetime.time(13, 00),
+ datetime.time(13, 00): datetime.time(13, 00),
+ }
+ invalid_inputs = {
+ 'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
+ '99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]]'],
+ }
+ outputs = {
+ datetime.time(13, 00): '13:00:00'
+ }
+ field = fields.TimeField()
+
+
+class TestCustomInputFormatTimeField(FieldValues):
+ """
+ Valid and invalid values for `TimeField` with a custom input format.
+ """
+ valid_inputs = {
+ '1:00pm': datetime.time(13, 00),
+ }
+ invalid_inputs = {
+ '13:00': ['Time has wrong format. Use one of these formats instead: hh:mm[AM|PM]'],
+ }
+ outputs = {}
+ field = fields.TimeField(input_formats=['%I:%M%p'])
+
+
+class TestCustomOutputFormatTimeField(FieldValues):
+ """
+ Values for `TimeField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.time(13, 00): '01:00PM'
+ }
+ field = fields.TimeField(format='%I:%M%p')
+
+
+class TestNoOutputFormatTimeField(FieldValues):
+ """
+ Values for `TimeField` with a no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.time(13, 00): datetime.time(13, 00)
+ }
+ field = fields.TimeField(format=None)
+
+
+# Choice types...
+
+class TestChoiceField(FieldValues):
+ """
+ Valid and invalid values for `ChoiceField`.
+ """
+ valid_inputs = {
+ 'poor': 'poor',
+ 'medium': 'medium',
+ 'good': 'good',
+ }
+ invalid_inputs = {
+ 'amazing': ['`amazing` is not a valid choice.']
+ }
+ outputs = {
+ 'good': 'good'
+ }
+ field = fields.ChoiceField(
+ choices=[
+ ('poor', 'Poor quality'),
+ ('medium', 'Medium quality'),
+ ('good', 'Good quality'),
+ ]
+ )
+
+
+class TestChoiceFieldWithType(FieldValues):
+ """
+ Valid and invalid values for a `Choice` field that uses an integer type,
+ instead of a char type.
+ """
+ valid_inputs = {
+ '1': 1,
+ 3: 3,
+ }
+ invalid_inputs = {
+ 5: ['`5` is not a valid choice.'],
+ 'abc': ['`abc` is not a valid choice.']
+ }
+ outputs = {
+ '1': 1,
+ 1: 1
+ }
+ field = fields.ChoiceField(
+ choices=[
+ (1, 'Poor quality'),
+ (2, 'Medium quality'),
+ (3, 'Good quality'),
+ ]
+ )
+
+
+class TestChoiceFieldWithListChoices(FieldValues):
+ """
+ Valid and invalid values for a `Choice` field that uses a flat list for the
+ choices, rather than a list of pairs of (`value`, `description`).
+ """
+ valid_inputs = {
+ 'poor': 'poor',
+ 'medium': 'medium',
+ 'good': 'good',
+ }
+ invalid_inputs = {
+ 'awful': ['`awful` is not a valid choice.']
+ }
+ outputs = {
+ 'good': 'good'
+ }
+ field = fields.ChoiceField(choices=('poor', 'medium', 'good'))
+
+
+class TestMultipleChoiceField(FieldValues):
+ """
+ Valid and invalid values for `MultipleChoiceField`.
+ """
+ valid_inputs = {
+ (): set(),
+ ('aircon',): set(['aircon']),
+ ('aircon', 'manual'): set(['aircon', 'manual']),
+ }
+ invalid_inputs = {
+ 'abc': ['Expected a list of items but got type `str`'],
+ ('aircon', 'incorrect'): ['`incorrect` is not a valid choice.']
+ }
+ outputs = [
+ (['aircon', 'manual'], set(['aircon', 'manual']))
+ ]
+ field = fields.MultipleChoiceField(
+ choices=[
+ ('aircon', 'AirCon'),
+ ('manual', 'Manual drive'),
+ ('diesel', 'Diesel'),
+ ]
+ )
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index d9f9efbe..731ed2fb 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -6,6 +6,7 @@ These tests deal with ensuring that we correctly map the model fields onto
an appropriate set of serializer fields for each case.
"""
from django.core.exceptions import ImproperlyConfigured
+from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator
from django.db import models
from django.test import TestCase
from rest_framework import serializers
@@ -15,7 +16,8 @@ def dedent(blocktext):
return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]])
-# Testing regular field mappings
+# Tests for regular field mappings.
+# ---------------------------------
class CustomField(models.Field):
"""
@@ -24,9 +26,6 @@ class CustomField(models.Field):
pass
-COLOR_CHOICES = (('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))
-
-
class RegularFieldsModel(models.Model):
"""
A model class for testing regular flat fields.
@@ -35,7 +34,6 @@ class RegularFieldsModel(models.Model):
big_integer_field = models.BigIntegerField()
boolean_field = models.BooleanField(default=False)
char_field = models.CharField(max_length=100)
- choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES)
comma_seperated_integer_field = models.CommaSeparatedIntegerField(max_length=100)
date_field = models.DateField()
datetime_field = models.DateTimeField()
@@ -57,6 +55,19 @@ class RegularFieldsModel(models.Model):
return 'method'
+COLOR_CHOICES = (('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))
+
+
+class FieldOptionsModel(models.Model):
+ value_limit_field = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)])
+ length_limit_field = models.CharField(validators=[MinLengthValidator(3)], max_length=12)
+ blank_field = models.CharField(blank=True, max_length=10)
+ null_field = models.IntegerField(null=True)
+ default_field = models.IntegerField(default=0)
+ descriptive_field = models.IntegerField(help_text='Some help text', verbose_name='A label')
+ choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES)
+
+
class TestRegularFieldMappings(TestCase):
def test_regular_fields(self):
"""
@@ -70,9 +81,8 @@ class TestRegularFieldMappings(TestCase):
TestSerializer():
auto_field = IntegerField(read_only=True)
big_integer_field = IntegerField()
- boolean_field = BooleanField(default=False)
+ boolean_field = BooleanField(required=False)
char_field = CharField(max_length=100)
- choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')])
comma_seperated_integer_field = CharField(max_length=100, validators=[])
date_field = DateField()
datetime_field = DateTimeField()
@@ -80,7 +90,7 @@ class TestRegularFieldMappings(TestCase):
email_field = EmailField(max_length=100)
float_field = FloatField()
integer_field = IntegerField()
- null_boolean_field = BooleanField(required=False)
+ null_boolean_field = BooleanField(allow_null=True)
positive_integer_field = IntegerField()
positive_small_integer_field = IntegerField()
slug_field = SlugField(max_length=100)
@@ -92,6 +102,24 @@ class TestRegularFieldMappings(TestCase):
""")
self.assertEqual(repr(TestSerializer()), expected)
+ def test_field_options(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = FieldOptionsModel
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ value_limit_field = IntegerField(max_value=10, min_value=1)
+ length_limit_field = CharField(max_length=12, min_length=3)
+ blank_field = CharField(allow_blank=True, max_length=10)
+ null_field = IntegerField(allow_null=True)
+ default_field = IntegerField(required=False)
+ descriptive_field = IntegerField(help_text='Some help text', label='A label')
+ choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')])
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
def test_method_field(self):
"""
Properties and methods on the model should be allowed as `Meta.fields`
@@ -178,7 +206,8 @@ class TestRegularFieldMappings(TestCase):
assert str(excinfo.exception) == expected
-# Testing relational field mappings
+# Tests for relational field mappings.
+# ------------------------------------
class ForeignKeyTargetModel(models.Model):
name = models.CharField(max_length=100)
--
cgit v1.2.3
From 0404f09a7e69f533038d47ca25caad90c0c2659f Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 23 Sep 2014 14:30:17 +0100
Subject: NullBooleanField
---
rest_framework/fields.py | 37 ++++++++++++++++++++++++++++++++++-
rest_framework/serializers.py | 2 +-
rest_framework/utils/field_mapping.py | 2 +-
tests/test_fields.py | 29 ++++++++++++++++++++++++++-
tests/test_model_serializer.py | 2 +-
5 files changed, 67 insertions(+), 5 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index f5bae734..f859658a 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -289,6 +289,10 @@ class BooleanField(Field):
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
+ def __init__(self, **kwargs):
+ assert 'allow_null' not in kwargs, '`allow_null` is not a valid option. Use `NullBooleanField` instead.'
+ super(BooleanField, self).__init__(**kwargs)
+
def to_internal_value(self, data):
if data in self.TRUE_VALUES:
return True
@@ -297,7 +301,38 @@ class BooleanField(Field):
self.fail('invalid', input=data)
def to_representation(self, value):
- if value is None:
+ if value in self.TRUE_VALUES:
+ return True
+ elif value in self.FALSE_VALUES:
+ return False
+ return bool(value)
+
+
+class NullBooleanField(Field):
+ default_error_messages = {
+ 'invalid': _('`{input}` is not a valid boolean.')
+ }
+ default_empty_html = None
+ TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
+ FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
+ NULL_VALUES = set(('n', 'N', 'null', 'Null', 'NULL', '', None))
+
+ def __init__(self, **kwargs):
+ assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.'
+ kwargs['allow_null'] = True
+ super(NullBooleanField, self).__init__(**kwargs)
+
+ def to_internal_value(self, data):
+ if data in self.TRUE_VALUES:
+ return True
+ elif data in self.FALSE_VALUES:
+ return False
+ elif data in self.NULL_VALUES:
+ return None
+ self.fail('invalid', input=data)
+
+ def to_representation(self, value):
+ if value in self.NULL_VALUES:
return None
if value in self.TRUE_VALUES:
return True
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 949f5915..d8d72a4c 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -333,7 +333,7 @@ class ModelSerializer(Serializer):
models.FloatField: FloatField,
models.ImageField: ImageField,
models.IntegerField: IntegerField,
- models.NullBooleanField: BooleanField,
+ models.NullBooleanField: NullBooleanField,
models.PositiveIntegerField: IntegerField,
models.PositiveSmallIntegerField: IntegerField,
models.SlugField: SlugField,
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index 1c718ccb..c208afdc 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -74,7 +74,7 @@ def get_field_kwargs(field_name, model_field):
kwargs['choices'] = model_field.flatchoices
return kwargs
- if model_field.null:
+ if model_field.null and not isinstance(model_field, models.NullBooleanField):
kwargs['allow_null'] = True
if model_field.blank:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 6bf9aed4..91f3f5db 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -124,7 +124,8 @@ class TestBooleanField(FieldValues):
False: False,
}
invalid_inputs = {
- 'foo': ['`foo` is not a valid boolean.']
+ 'foo': ['`foo` is not a valid boolean.'],
+ None: ['This field may not be null.']
}
outputs = {
'true': True,
@@ -140,6 +141,32 @@ class TestBooleanField(FieldValues):
field = fields.BooleanField()
+class TestNullBooleanField(FieldValues):
+ """
+ Valid and invalid values for `BooleanField`.
+ """
+ valid_inputs = {
+ 'true': True,
+ 'false': False,
+ 'null': None,
+ True: True,
+ False: False,
+ None: None
+ }
+ invalid_inputs = {
+ 'foo': ['`foo` is not a valid boolean.'],
+ }
+ outputs = {
+ 'true': True,
+ 'false': False,
+ 'null': None,
+ True: True,
+ False: False,
+ None: None
+ }
+ field = fields.NullBooleanField()
+
+
# String types...
class TestCharField(FieldValues):
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index 731ed2fb..f7475024 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -90,7 +90,7 @@ class TestRegularFieldMappings(TestCase):
email_field = EmailField(max_length=100)
float_field = FloatField()
integer_field = IntegerField()
- null_boolean_field = BooleanField(allow_null=True)
+ null_boolean_field = NullBooleanField()
positive_integer_field = IntegerField()
positive_small_integer_field = IntegerField()
slug_field = SlugField(max_length=100)
--
cgit v1.2.3
From f4b1dcb167be0bbdaae2cc2a92f651536896dc16 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 24 Sep 2014 14:09:49 +0100
Subject: OPTIONS support
---
rest_framework/generics.py | 51 +----------
rest_framework/metadata.py | 126 ++++++++++++++++++++++++++
rest_framework/serializers.py | 8 +-
rest_framework/settings.py | 2 +
rest_framework/utils/field_mapping.py | 20 ++--
rest_framework/views.py | 24 +----
tests/test_metadata.py | 166 ++++++++++++++++++++++++++++++++++
7 files changed, 316 insertions(+), 81 deletions(-)
create mode 100644 rest_framework/metadata.py
create mode 100644 tests/test_metadata.py
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index eb6b64ef..f49b0a43 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -4,13 +4,11 @@ Generic views that provide commonly needed behaviour.
from __future__ import unicode_literals
from django.db.models.query import QuerySet
-from django.core.exceptions import PermissionDenied
from django.core.paginator import Paginator, InvalidPage
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
@@ -249,53 +247,6 @@ 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 metadata(self, request):
- """
- Return a dictionary of metadata about the view.
- Used to return responses for OPTIONS requests.
-
- We override the default behavior, and add some extra information
- about the required request body for POST and PUT operations.
- """
- 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':
- try:
- self.get_object()
- except Http404:
- # Http404 should be acceptable and the serializer
- # metadata should be populated. Except this so the
- # outer "else" clause of the try-except-else block
- # will be executed.
- pass
- except (exceptions.APIException, PermissionDenied):
- pass
- 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()
-
- if actions:
- ret['actions'] = actions
-
- return ret
-
# Concrete view classes that provide method handlers
# by composing the mixin classes with the base view.
diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py
new file mode 100644
index 00000000..580259de
--- /dev/null
+++ b/rest_framework/metadata.py
@@ -0,0 +1,126 @@
+"""
+The metadata API is used to allow cusomization of how `OPTIONS` requests
+are handled. We currently provide a single default implementation that returns
+some fairly ad-hoc information about the view.
+
+Future implementations might use JSON schema or other definations in order
+to return this information in a more standardized way.
+"""
+from __future__ import unicode_literals
+
+from django.core.exceptions import PermissionDenied
+from django.http import Http404
+from django.utils import six
+from django.utils.datastructures import SortedDict
+from rest_framework import exceptions, serializers
+from rest_framework.compat import force_text
+from rest_framework.request import clone_request
+from rest_framework.utils.field_mapping import ClassLookupDict
+
+
+class BaseMetadata(object):
+ def determine_metadata(self, request, view):
+ """
+ Return a dictionary of metadata about the view.
+ Used to return responses for OPTIONS requests.
+ """
+ raise NotImplementedError(".determine_metadata() must be overridden.")
+
+
+class SimpleMetadata(BaseMetadata):
+ """
+ This is the default metadata implementation.
+ It returns an ad-hoc set of information about the view.
+ There are not any formalized standards for `OPTIONS` responses
+ for us to base this on.
+ """
+ label_lookup = ClassLookupDict({
+ serializers.Field: 'field',
+ serializers.BooleanField: 'boolean',
+ serializers.CharField: 'string',
+ serializers.URLField: 'url',
+ serializers.EmailField: 'email',
+ serializers.RegexField: 'regex',
+ serializers.SlugField: 'slug',
+ serializers.IntegerField: 'integer',
+ serializers.FloatField: 'float',
+ serializers.DecimalField: 'decimal',
+ serializers.DateField: 'date',
+ serializers.DateTimeField: 'datetime',
+ serializers.TimeField: 'time',
+ serializers.ChoiceField: 'choice',
+ serializers.MultipleChoiceField: 'multiple choice',
+ serializers.FileField: 'file upload',
+ serializers.ImageField: 'image upload',
+ })
+
+ def determine_metadata(self, request, view):
+ metadata = SortedDict()
+ metadata['name'] = view.get_view_name()
+ metadata['description'] = view.get_view_description()
+ metadata['renders'] = [renderer.media_type for renderer in view.renderer_classes]
+ metadata['parses'] = [parser.media_type for parser in view.parser_classes]
+ if hasattr(view, 'get_serializer'):
+ actions = self.determine_actions(request, view)
+ if actions:
+ metadata['actions'] = actions
+ return metadata
+
+ def determine_actions(self, request, view):
+ """
+ For generic class based views we return information about
+ the fields that are accepted for 'PUT' and 'POST' methods.
+ """
+ actions = {}
+ for method in set(['PUT', 'POST']) & set(view.allowed_methods):
+ view.request = clone_request(request, method)
+ try:
+ # Test global permissions
+ if hasattr(view, 'check_permissions'):
+ view.check_permissions(view.request)
+ # Test object permissions
+ if method == 'PUT' and hasattr(view, 'get_object'):
+ view.get_object()
+ except (exceptions.APIException, PermissionDenied, Http404):
+ pass
+ else:
+ # If user has appropriate permissions for the view, include
+ # appropriate metadata about the fields that should be supplied.
+ serializer = view.get_serializer()
+ actions[method] = self.get_serializer_info(serializer)
+ finally:
+ view.request = request
+
+ return actions
+
+ def get_serializer_info(self, serializer):
+ """
+ Given an instance of a serializer, return a dictionary of metadata
+ about its fields.
+ """
+ return SortedDict([
+ (field_name, self.get_field_info(field))
+ for field_name, field in six.iteritems(serializer.fields)
+ ])
+
+ def get_field_info(self, field):
+ """
+ Given an instance of a serializer field, return a dictionary
+ of metadata about it.
+ """
+ field_info = SortedDict()
+ field_info['type'] = self.label_lookup[field]
+ field_info['required'] = getattr(field, 'required', False)
+
+ for attr in ['read_only', 'label', 'help_text', 'min_length', 'max_length']:
+ value = getattr(field, attr, None)
+ if value is not None and value != '':
+ field_info[attr] = force_text(value, strings_only=True)
+
+ if hasattr(field, 'choices'):
+ field_info['choices'] = [
+ {'value': choice_value, 'display_name': choice_name}
+ for choice_value, choice_name in field.choices.items()
+ ]
+
+ return field_info
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index d8d72a4c..8902294b 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -21,7 +21,7 @@ from rest_framework.utils import html, model_meta, representation
from rest_framework.utils.field_mapping import (
get_url_kwargs, get_field_kwargs,
get_relation_kwargs, get_nested_relation_kwargs,
- lookup_class
+ ClassLookupDict
)
import copy
import inspect
@@ -318,7 +318,7 @@ class ListSerializer(BaseSerializer):
class ModelSerializer(Serializer):
- _field_mapping = {
+ _field_mapping = ClassLookupDict({
models.AutoField: IntegerField,
models.BigIntegerField: IntegerField,
models.BooleanField: BooleanField,
@@ -341,7 +341,7 @@ class ModelSerializer(Serializer):
models.TextField: CharField,
models.TimeField: TimeField,
models.URLField: URLField,
- }
+ })
_related_class = PrimaryKeyRelatedField
def create(self, attrs):
@@ -400,7 +400,7 @@ class ModelSerializer(Serializer):
elif field_name in info.fields_and_pk:
# Create regular model fields.
model_field = info.fields_and_pk[field_name]
- field_cls = lookup_class(self._field_mapping, model_field)
+ field_cls = self._field_mapping[model_field]
kwargs = get_field_kwargs(field_name, model_field)
if 'choices' in kwargs:
# Fields with choices get coerced into `ChoiceField`
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 421e146c..d7fb0a43 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -45,6 +45,7 @@ DEFAULTS = {
),
'DEFAULT_THROTTLE_CLASSES': (),
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
+ 'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
# Genric view behavior
'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer',
@@ -121,6 +122,7 @@ IMPORT_STRINGS = (
'DEFAULT_PERMISSION_CLASSES',
'DEFAULT_THROTTLE_CLASSES',
'DEFAULT_CONTENT_NEGOTIATION_CLASS',
+ 'DEFAULT_METADATA_CLASS',
'DEFAULT_MODEL_SERIALIZER_CLASS',
'DEFAULT_PAGINATION_SERIALIZER_CLASS',
'DEFAULT_FILTER_BACKENDS',
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index c208afdc..c3794083 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -9,17 +9,21 @@ from rest_framework.compat import clean_manytomany_helptext
import inspect
-def lookup_class(mapping, instance):
+class ClassLookupDict(object):
"""
- Takes a dictionary with classes as keys, and an object.
- Traverses the object's inheritance hierarchy in method
- resolution order, and returns the first matching value
+ Takes a dictionary with classes as keys.
+ Lookups against this object will traverses the object's inheritance
+ hierarchy in method resolution order, and returns the first matching value
from the dictionary or raises a KeyError if nothing matches.
"""
- for cls in inspect.getmro(instance.__class__):
- if cls in mapping:
- return mapping[cls]
- raise KeyError('Class %s not found in lookup.', cls.__name__)
+ def __init__(self, mapping):
+ self.mapping = mapping
+
+ def __getitem__(self, key):
+ for cls in inspect.getmro(key.__class__):
+ if cls in self.mapping:
+ return self.mapping[cls]
+ raise KeyError('Class %s not found in lookup.', cls.__name__)
def needs_label(model_field, field_name):
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 9f08a4ad..835e223a 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -5,7 +5,6 @@ from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied, ValidationError, NON_FIELD_ERRORS
from django.http import Http404
-from django.utils.datastructures import SortedDict
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status, exceptions
from rest_framework.compat import smart_text, HttpResponseBase, View
@@ -99,6 +98,7 @@ class APIView(View):
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
+ metadata_class = api_settings.DEFAULT_METADATA_CLASS
# Allow dependancy injection of other settings to make testing easier.
settings = api_settings
@@ -418,22 +418,8 @@ class APIView(View):
def options(self, request, *args, **kwargs):
"""
Handler method for HTTP 'OPTIONS' request.
- We may as well implement this as Django will otherwise provide
- a less useful default implementation.
"""
- return Response(self.metadata(request), status=status.HTTP_200_OK)
-
- def metadata(self, request):
- """
- Return a dictionary of metadata about the view.
- Used to return responses for OPTIONS requests.
- """
- # By default we can't provide any form-like information, however the
- # generic views override this implementation and add additional
- # information for POST and PUT methods, based on the serializer.
- ret = SortedDict()
- ret['name'] = self.get_view_name()
- ret['description'] = self.get_view_description()
- ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
- ret['parses'] = [parser.media_type for parser in self.parser_classes]
- return ret
+ if self.metadata_class is None:
+ return self.http_method_not_allowed(request, *args, **kwargs)
+ data = self.metadata_class().determine_metadata(request, self)
+ return Response(data, status=status.HTTP_200_OK)
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
new file mode 100644
index 00000000..0ebea935
--- /dev/null
+++ b/tests/test_metadata.py
@@ -0,0 +1,166 @@
+from __future__ import unicode_literals
+
+from rest_framework import exceptions, serializers, views
+from rest_framework.request import Request
+from rest_framework.test import APIRequestFactory
+import pytest
+
+request = Request(APIRequestFactory().options('/'))
+
+
+class TestMetadata:
+ def test_metadata(self):
+ """
+ OPTIONS requests to views should return a valid 200 response.
+ """
+ class ExampleView(views.APIView):
+ """Example view."""
+ pass
+
+ response = ExampleView().options(request=request)
+ expected = {
+ 'name': 'Example',
+ 'description': 'Example view.',
+ 'renders': [
+ 'application/json',
+ 'text/html'
+ ],
+ 'parses': [
+ 'application/json',
+ 'application/x-www-form-urlencoded',
+ 'multipart/form-data'
+ ]
+ }
+ assert response.status_code == 200
+ assert response.data == expected
+
+ def test_none_metadata(self):
+ """
+ OPTIONS requests to views where `metadata_class = None` should raise
+ a MethodNotAllowed exception, which will result in an HTTP 405 response.
+ """
+ class ExampleView(views.APIView):
+ metadata_class = None
+
+ with pytest.raises(exceptions.MethodNotAllowed):
+ ExampleView().options(request=request)
+
+ def test_actions(self):
+ """
+ On generic views OPTIONS should return an 'actions' key with metadata
+ on the fields that may be supplied to PUT and POST requests.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
+ integer_field = serializers.IntegerField(max_value=10)
+ char_field = serializers.CharField(required=False)
+
+ class ExampleView(views.APIView):
+ """Example view."""
+ def post(self, request):
+ pass
+
+ def get_serializer(self):
+ return ExampleSerializer()
+
+ response = ExampleView().options(request=request)
+ expected = {
+ 'name': 'Example',
+ 'description': 'Example view.',
+ 'renders': [
+ 'application/json',
+ 'text/html'
+ ],
+ 'parses': [
+ 'application/json',
+ 'application/x-www-form-urlencoded',
+ 'multipart/form-data'
+ ],
+ 'actions': {
+ 'POST': {
+ 'choice_field': {
+ 'type': 'choice',
+ 'required': True,
+ 'read_only': False,
+ 'label': 'Choice field',
+ 'choices': [
+ {'display_name': 'blue', 'value': 'blue'},
+ {'display_name': 'green', 'value': 'green'},
+ {'display_name': 'red', 'value': 'red'}
+ ]
+ },
+ 'integer_field': {
+ 'type': 'integer',
+ 'required': True,
+ 'read_only': False,
+ 'label': 'Integer field'
+ },
+ 'char_field': {
+ 'type': 'string',
+ 'required': False,
+ 'read_only': False,
+ 'label': 'Char field'
+ }
+ }
+ }
+ }
+ assert response.status_code == 200
+ assert response.data == expected
+
+ def test_global_permissions(self):
+ """
+ If a user does not have global permissions on an action, then any
+ metadata associated with it should not be included in OPTION responses.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
+ integer_field = serializers.IntegerField(max_value=10)
+ char_field = serializers.CharField(required=False)
+
+ class ExampleView(views.APIView):
+ """Example view."""
+ def post(self, request):
+ pass
+
+ def put(self, request):
+ pass
+
+ def get_serializer(self):
+ return ExampleSerializer()
+
+ def check_permissions(self, request):
+ if request.method == 'POST':
+ raise exceptions.PermissionDenied()
+
+ response = ExampleView().options(request=request)
+ assert response.status_code == 200
+ assert list(response.data['actions'].keys()) == ['PUT']
+
+ def test_object_permissions(self):
+ """
+ If a user does not have object permissions on an action, then any
+ metadata associated with it should not be included in OPTION responses.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
+ integer_field = serializers.IntegerField(max_value=10)
+ char_field = serializers.CharField(required=False)
+
+ class ExampleView(views.APIView):
+ """Example view."""
+ def post(self, request):
+ pass
+
+ def put(self, request):
+ pass
+
+ def get_serializer(self):
+ return ExampleSerializer()
+
+ def get_object(self):
+ if self.request.method == 'PUT':
+ raise exceptions.PermissionDenied()
+
+ response = ExampleView().options(request=request)
+ assert response.status_code == 200
+ assert list(response.data['actions'].keys()) == ['POST']
--
cgit v1.2.3
From 358445c174629bdd55894c48eaf965bbfd4414b2 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 24 Sep 2014 14:52:34 +0100
Subject: Drop redundant OPTIONS tests
---
tests/test_generics.py | 180 -------------------------------------------------
1 file changed, 180 deletions(-)
diff --git a/tests/test_generics.py b/tests/test_generics.py
index 51004edf..89f9def0 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -23,25 +23,16 @@ class ForeignKeySerializer(serializers.ModelSerializer):
class RootView(generics.ListCreateAPIView):
- """
- Example description for OPTIONS.
- """
queryset = BasicModel.objects.all()
serializer_class = BasicSerializer
class InstanceView(generics.RetrieveUpdateDestroyAPIView):
- """
- Example description for OPTIONS.
- """
queryset = BasicModel.objects.exclude(text='filtered out')
serializer_class = BasicSerializer
class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
- """
- FK: example description for OPTIONS.
- """
queryset = ForeignKeySource.objects.all()
serializer_class = ForeignKeySerializer
@@ -122,47 +113,6 @@ class TestRootView(TestCase):
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
self.assertEqual(response.data, {"detail": "Method 'DELETE' not allowed."})
- # def test_options_root_view(self):
- # """
- # OPTIONS requests to ListCreateAPIView should return metadata
- # """
- # request = factory.options('/')
- # with self.assertNumQueries(0):
- # response = self.view(request).render()
- # expected = {
- # 'parses': [
- # 'application/json',
- # 'application/x-www-form-urlencoded',
- # 'multipart/form-data'
- # ],
- # 'renders': [
- # 'application/json',
- # 'text/html'
- # ],
- # 'name': 'Root',
- # 'description': 'Example description for OPTIONS.',
- # 'actions': {
- # 'POST': {
- # 'text': {
- # 'max_length': 100,
- # 'read_only': False,
- # 'required': True,
- # 'type': 'string',
- # "label": "Text comes here",
- # "help_text": "Text description."
- # },
- # 'id': {
- # 'read_only': True,
- # 'required': False,
- # 'type': 'integer',
- # 'label': 'ID',
- # },
- # }
- # }
- # }
- # self.assertEqual(response.status_code, status.HTTP_200_OK)
- # self.assertEqual(response.data, expected)
-
def test_post_cannot_set_id(self):
"""
POST requests to create a new object should not be able to set the id.
@@ -256,89 +206,6 @@ class TestInstanceView(TestCase):
ids = [obj.id for obj in self.objects.all()]
self.assertEqual(ids, [2, 3])
- # def test_options_instance_view(self):
- # """
- # OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
- # """
- # request = factory.options('/1')
- # with self.assertNumQueries(1):
- # response = self.view(request, pk=1).render()
- # expected = {
- # 'parses': [
- # 'application/json',
- # 'application/x-www-form-urlencoded',
- # 'multipart/form-data'
- # ],
- # 'renders': [
- # 'application/json',
- # 'text/html'
- # ],
- # 'name': 'Instance',
- # 'description': 'Example description for OPTIONS.',
- # 'actions': {
- # 'PUT': {
- # 'text': {
- # 'max_length': 100,
- # 'read_only': False,
- # 'required': True,
- # 'type': 'string',
- # 'label': 'Text comes here',
- # 'help_text': 'Text description.'
- # },
- # 'id': {
- # 'read_only': True,
- # 'required': False,
- # 'type': 'integer',
- # 'label': 'ID',
- # },
- # }
- # }
- # }
- # self.assertEqual(response.status_code, status.HTTP_200_OK)
- # self.assertEqual(response.data, expected)
-
- # def test_options_before_instance_create(self):
- # """
- # OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
- # before the instance has been created
- # """
- # request = factory.options('/999')
- # with self.assertNumQueries(1):
- # response = self.view(request, pk=999).render()
- # expected = {
- # 'parses': [
- # 'application/json',
- # 'application/x-www-form-urlencoded',
- # 'multipart/form-data'
- # ],
- # 'renders': [
- # 'application/json',
- # 'text/html'
- # ],
- # 'name': 'Instance',
- # 'description': 'Example description for OPTIONS.',
- # 'actions': {
- # 'PUT': {
- # 'text': {
- # 'max_length': 100,
- # 'read_only': False,
- # 'required': True,
- # 'type': 'string',
- # 'label': 'Text comes here',
- # 'help_text': 'Text description.'
- # },
- # 'id': {
- # 'read_only': True,
- # 'required': False,
- # 'type': 'integer',
- # 'label': 'ID',
- # },
- # }
- # }
- # }
- # self.assertEqual(response.status_code, status.HTTP_200_OK)
- # self.assertEqual(response.data, expected)
-
def test_get_instance_view_incorrect_arg(self):
"""
GET requests with an incorrect pk type, should raise 404, not 500.
@@ -415,53 +282,6 @@ class TestFKInstanceView(TestCase):
]
self.view = FKInstanceView.as_view()
- # def test_options_root_view(self):
- # """
- # OPTIONS requests to ListCreateAPIView should return metadata
- # """
- # request = factory.options('/999')
- # with self.assertNumQueries(1):
- # response = self.view(request, pk=999).render()
- # expected = {
- # 'name': 'Fk Instance',
- # 'description': 'FK: example description for OPTIONS.',
- # 'renders': [
- # 'application/json',
- # 'text/html'
- # ],
- # 'parses': [
- # 'application/json',
- # 'application/x-www-form-urlencoded',
- # 'multipart/form-data'
- # ],
- # 'actions': {
- # 'PUT': {
- # 'id': {
- # 'type': 'integer',
- # 'required': False,
- # 'read_only': True,
- # 'label': 'ID'
- # },
- # 'name': {
- # 'type': 'string',
- # 'required': True,
- # 'read_only': False,
- # 'label': 'name',
- # 'max_length': 100
- # },
- # 'target': {
- # 'type': 'field',
- # 'required': True,
- # 'read_only': False,
- # 'label': 'Target',
- # 'help_text': 'Target'
- # }
- # }
- # }
- # }
- # self.assertEqual(response.status_code, status.HTTP_200_OK)
- # self.assertEqual(response.data, expected)
-
class TestOverriddenGetObject(TestCase):
"""
--
cgit v1.2.3
From 127c0bd3d68860dd6567d81047257fbc3e70b4b9 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 24 Sep 2014 20:25:59 +0100
Subject: Custom deepcopy on Field classes
---
rest_framework/fields.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index f859658a..1f7d964a 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -9,6 +9,7 @@ from rest_framework import ISO_8601
from rest_framework.compat import smart_text, EmailValidator, MinValueValidator, MaxValueValidator, URLValidator
from rest_framework.settings import api_settings
from rest_framework.utils import html, representation, humanize_datetime
+import copy
import datetime
import decimal
import inspect
@@ -150,6 +151,11 @@ class Field(object):
instance._kwargs = kwargs
return instance
+ def __deepcopy__(self, memo):
+ args = copy.deepcopy(self._args)
+ kwargs = copy.deepcopy(self._kwargs)
+ return self.__class__(*args, **kwargs)
+
def bind(self, field_name, parent, root):
"""
Setup the context for the field instance.
--
cgit v1.2.3
From fb1546ee50953faae8af505a0c90da00ac08ad92 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 24 Sep 2014 20:53:37 +0100
Subject: Enforce field_name != source
---
rest_framework/fields.py | 11 +++++++++++
tests/test_fields.py | 13 ++++++++++++-
2 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 1f7d964a..9280ea3a 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -160,6 +160,17 @@ class Field(object):
"""
Setup the context for the field instance.
"""
+
+ # In order to enforce a consistent style, we error if a redundant
+ # 'source' argument has been used. For example:
+ # my_field = serializer.CharField(source='my_field')
+ assert self._kwargs.get('source') != field_name, (
+ "It is redundant to specify `source='%s'` on field '%s' in "
+ "serializer '%s', as it is the same the field name. "
+ "Remove the `source` keyword argument." %
+ (field_name, self.__class__.__name__, parent.__class__.__name__)
+ )
+
self.field_name = field_name
self.parent = parent
self.root = root
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 91f3f5db..c2e03023 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1,6 +1,6 @@
from decimal import Decimal
from django.utils import timezone
-from rest_framework import fields
+from rest_framework import fields, serializers
import datetime
import django
import pytest
@@ -69,6 +69,17 @@ class TestFieldOptions:
output = field.run_validation()
assert output is 123
+ def test_redundant_source(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.CharField(source='example_field')
+ with pytest.raises(AssertionError) as exc_info:
+ ExampleSerializer()
+ assert str(exc_info.value) == (
+ "It is redundant to specify `source='example_field'` on field "
+ "'CharField' in serializer 'ExampleSerializer', as it is the "
+ "same the field name. Remove the `source` keyword argument."
+ )
+
# Tests for field input and output values.
# ----------------------------------------
--
cgit v1.2.3
From 1420c76453c37c023a901dd0938d717b7b5e52ca Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 25 Sep 2014 10:49:25 +0100
Subject: Ensure proper sorting of 'choices' attribute on ChoiceField
---
rest_framework/fields.py | 9 ++++++---
tests/test_fields.py | 4 ++--
tests/test_metadata.py | 4 ++--
3 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 9280ea3a..d1aebbaf 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -2,6 +2,7 @@ from django.conf import settings
from django.core import validators
from django.core.exceptions import ValidationError
from django.utils import timezone
+from django.utils.datastructures import SortedDict
from django.utils.dateparse import parse_date, parse_datetime, parse_time
from django.utils.encoding import is_protected_type
from django.utils.translation import ugettext_lazy as _
@@ -166,7 +167,7 @@ class Field(object):
# my_field = serializer.CharField(source='my_field')
assert self._kwargs.get('source') != field_name, (
"It is redundant to specify `source='%s'` on field '%s' in "
- "serializer '%s', as it is the same the field name. "
+ "serializer '%s', because it is the same as the field name. "
"Remove the `source` keyword argument." %
(field_name, self.__class__.__name__, parent.__class__.__name__)
)
@@ -303,6 +304,7 @@ class BooleanField(Field):
'invalid': _('`{input}` is not a valid boolean.')
}
default_empty_html = False
+ initial = False
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
@@ -365,6 +367,7 @@ class CharField(Field):
'blank': _('This field may not be blank.')
}
default_empty_html = ''
+ initial = ''
def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
@@ -775,9 +778,9 @@ class ChoiceField(Field):
for item in choices
]
if all(pairs):
- self.choices = dict([(key, display_value) for key, display_value in choices])
+ self.choices = SortedDict([(key, display_value) for key, display_value in choices])
else:
- self.choices = dict([(item, item) for item in choices])
+ self.choices = SortedDict([(item, item) for item in choices])
# Map the string representation of choices to the underlying value.
# Allows us to deal with eg. integer choices while supporting either
diff --git a/tests/test_fields.py b/tests/test_fields.py
index c2e03023..ebb88d3d 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -76,8 +76,8 @@ class TestFieldOptions:
ExampleSerializer()
assert str(exc_info.value) == (
"It is redundant to specify `source='example_field'` on field "
- "'CharField' in serializer 'ExampleSerializer', as it is the "
- "same the field name. Remove the `source` keyword argument."
+ "'CharField' in serializer 'ExampleSerializer', because it is the "
+ "same as the field name. Remove the `source` keyword argument."
)
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
index 0ebea935..5ff59c72 100644
--- a/tests/test_metadata.py
+++ b/tests/test_metadata.py
@@ -84,9 +84,9 @@ class TestMetadata:
'read_only': False,
'label': 'Choice field',
'choices': [
- {'display_name': 'blue', 'value': 'blue'},
+ {'display_name': 'red', 'value': 'red'},
{'display_name': 'green', 'value': 'green'},
- {'display_name': 'red', 'value': 'red'}
+ {'display_name': 'blue', 'value': 'blue'}
]
},
'integer_field': {
--
cgit v1.2.3
From b22c9602fa0f717b688fdb35e4f6f42c189af3f3 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 25 Sep 2014 11:04:18 +0100
Subject: Automatic field binding
---
rest_framework/metadata.py | 3 +--
rest_framework/pagination.py | 1 -
rest_framework/serializers.py | 30 +++++++++++++++++++++++++-----
3 files changed, 26 insertions(+), 8 deletions(-)
diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py
index 580259de..af4bc396 100644
--- a/rest_framework/metadata.py
+++ b/rest_framework/metadata.py
@@ -10,7 +10,6 @@ from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied
from django.http import Http404
-from django.utils import six
from django.utils.datastructures import SortedDict
from rest_framework import exceptions, serializers
from rest_framework.compat import force_text
@@ -100,7 +99,7 @@ class SimpleMetadata(BaseMetadata):
"""
return SortedDict([
(field_name, self.get_field_info(field))
- for field_name, field in six.iteritems(serializer.fields)
+ for field_name, field in serializer.fields.items()
])
def get_field_info(self, field):
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index c5a9270a..fb451285 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -72,7 +72,6 @@ class BasePaginationSerializer(serializers.Serializer):
child=object_serializer(),
source='object_list'
)
- self.fields[results_field].bind(results_field, self, self)
class PaginationSerializer(BasePaginationSerializer):
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 8902294b..12e38090 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -149,6 +149,28 @@ class SerializerMetaclass(type):
return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
+class BindingDict(object):
+ def __init__(self, serializer):
+ self.serializer = serializer
+ self.fields = SortedDict()
+
+ def __setitem__(self, key, field):
+ self.fields[key] = field
+ field.bind(field_name=key, parent=self.serializer, root=self.serializer)
+
+ def __getitem__(self, key):
+ return self.fields[key]
+
+ def __delitem__(self, key):
+ del self.fields[key]
+
+ def items(self):
+ return self.fields.items()
+
+ def values(self):
+ return self.fields.values()
+
+
@six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer):
def __init__(self, *args, **kwargs):
@@ -161,11 +183,9 @@ class Serializer(BaseSerializer):
# Every new serializer is created with a clone of the field instances.
# This allows users to dynamically modify the fields on a serializer
# instance without affecting every other serializer class.
- self.fields = self._get_base_fields()
-
- # Setup all the child fields, to provide them with the current context.
- for field_name, field in self.fields.items():
- field.bind(field_name, self, self)
+ self.fields = BindingDict(self)
+ for key, value in self._get_base_fields().items():
+ self.fields[key] = value
def __new__(cls, *args, **kwargs):
# We override this method in order to automagically create
--
cgit v1.2.3
From 64632da3718f501cb8174243385d38b547c2fefd Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 25 Sep 2014 11:40:32 +0100
Subject: Clean up bind - no longer needs to be called multiple times in nested
fields
---
rest_framework/fields.py | 21 ++++++++++++++++-----
rest_framework/relations.py | 5 -----
rest_framework/serializers.py | 26 +++++++++-----------------
tests/test_relations.py | 8 ++++----
4 files changed, 29 insertions(+), 31 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index d1aebbaf..446732c3 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -109,7 +109,8 @@ class Field(object):
def __init__(self, read_only=False, write_only=False,
required=None, default=empty, initial=None, source=None,
label=None, help_text=None, style=None,
- error_messages=None, validators=[], allow_null=False):
+ error_messages=None, validators=[], allow_null=False,
+ context=None):
self._creation_counter = Field._creation_counter
Field._creation_counter += 1
@@ -135,6 +136,11 @@ class Field(object):
self.validators = validators or self.default_validators[:]
self.allow_null = allow_null
+ # These are set up by `.bind()` when the field is added to a serializer.
+ self.field_name = None
+ self.parent = None
+ self._context = {} if (context is None) else context
+
# Collect default error message from self and parent classes
messages = {}
for cls in reversed(self.__class__.__mro__):
@@ -157,7 +163,14 @@ class Field(object):
kwargs = copy.deepcopy(self._kwargs)
return self.__class__(*args, **kwargs)
- def bind(self, field_name, parent, root):
+ @property
+ def context(self):
+ root = self
+ while root.parent is not None:
+ root = root.parent
+ return root._context
+
+ def bind(self, field_name, parent):
"""
Setup the context for the field instance.
"""
@@ -174,10 +187,8 @@ class Field(object):
self.field_name = field_name
self.parent = parent
- self.root = root
- self.context = parent.context
- # `self.label` should deafult to being based on the field name.
+ # `self.label` should default to being based on the field name.
if self.label is None:
self.label = field_name.replace('_', ' ').capitalize()
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 5aa1f8bd..b37a6fed 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -243,11 +243,6 @@ class ManyRelation(Field):
assert child_relation is not None, '`child_relation` is a required argument.'
super(ManyRelation, self).__init__(*args, **kwargs)
- def bind(self, field_name, parent, root):
- # ManyRelation needs to provide the current context to the child relation.
- super(ManyRelation, self).bind(field_name, parent, root)
- self.child_relation.bind(field_name, parent, root)
-
def to_internal_value(self, data):
return [
self.child_relation.to_internal_value(item)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 12e38090..04721c7a 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -150,13 +150,20 @@ class SerializerMetaclass(type):
class BindingDict(object):
+ """
+ This dict-like object is used to store fields on a serializer.
+
+ This ensures that whenever fields are added to the serializer we call
+ `field.bind()` so that the `field_name` and `parent` attributes
+ can be set correctly.
+ """
def __init__(self, serializer):
self.serializer = serializer
self.fields = SortedDict()
def __setitem__(self, key, field):
self.fields[key] = field
- field.bind(field_name=key, parent=self.serializer, root=self.serializer)
+ field.bind(field_name=key, parent=self.serializer)
def __getitem__(self, key):
return self.fields[key]
@@ -174,7 +181,6 @@ class BindingDict(object):
@six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer):
def __init__(self, *args, **kwargs):
- self.context = kwargs.pop('context', {})
kwargs.pop('partial', None)
kwargs.pop('many', None)
@@ -198,13 +204,6 @@ class Serializer(BaseSerializer):
def _get_base_fields(self):
return copy.deepcopy(self._declared_fields)
- def bind(self, field_name, parent, root):
- # If the serializer is used as a field then when it becomes bound
- # it also needs to bind all its child fields.
- super(Serializer, self).bind(field_name, parent, root)
- for field_name, field in self.fields.items():
- field.bind(field_name, self, root)
-
def get_initial(self):
return dict([
(field.field_name, field.get_initial())
@@ -290,17 +289,10 @@ class ListSerializer(BaseSerializer):
self.child = kwargs.pop('child', copy.deepcopy(self.child))
assert self.child is not None, '`child` is a required argument.'
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
- self.context = kwargs.pop('context', {})
kwargs.pop('partial', None)
super(ListSerializer, self).__init__(*args, **kwargs)
- self.child.bind('', self, self)
-
- def bind(self, field_name, parent, root):
- # If the list is used as a field then it needs to provide
- # the current context to the child serializer.
- super(ListSerializer, self).bind(field_name, parent, root)
- self.child.bind(field_name, self, root)
+ self.child.bind(field_name='', parent=self)
def get_value(self, dictionary):
# We override the default field access in order to support
diff --git a/tests/test_relations.py b/tests/test_relations.py
index c29618ce..2d11672b 100644
--- a/tests/test_relations.py
+++ b/tests/test_relations.py
@@ -51,7 +51,7 @@ class TestHyperlinkedIdentityField(APISimpleTestCase):
self.instance = MockObject(pk=1, name='foo')
self.field = serializers.HyperlinkedIdentityField(view_name='example')
self.field.reverse = mock_reverse
- self.field.context = {'request': True}
+ self.field._context = {'request': True}
def test_representation(self):
representation = self.field.to_representation(self.instance)
@@ -62,7 +62,7 @@ class TestHyperlinkedIdentityField(APISimpleTestCase):
assert representation is None
def test_representation_with_format(self):
- self.field.context['format'] = 'xml'
+ self.field._context['format'] = 'xml'
representation = self.field.to_representation(self.instance)
assert representation == 'http://example.org/example/1.xml/'
@@ -91,14 +91,14 @@ class TestHyperlinkedIdentityFieldWithFormat(APISimpleTestCase):
self.instance = MockObject(pk=1, name='foo')
self.field = serializers.HyperlinkedIdentityField(view_name='example', format='json')
self.field.reverse = mock_reverse
- self.field.context = {'request': True}
+ self.field._context = {'request': True}
def test_representation(self):
representation = self.field.to_representation(self.instance)
assert representation == 'http://example.org/example/1/'
def test_representation_with_format(self):
- self.field.context['format'] = 'xml'
+ self.field._context['format'] = 'xml'
representation = self.field.to_representation(self.instance)
assert representation == 'http://example.org/example/1.json/'
--
cgit v1.2.3
From b47ca158b9ba9733baad080e648d24b0465ec697 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 25 Sep 2014 12:09:12 +0100
Subject: Check for redundant on SerializerMethodField
---
rest_framework/fields.py | 29 ++++++++++++++++++++++-------
tests/test_fields.py | 30 ++++++++++++++++++++++++++++++
2 files changed, 52 insertions(+), 7 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 446732c3..328e93ef 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -178,7 +178,7 @@ class Field(object):
# In order to enforce a consistent style, we error if a redundant
# 'source' argument has been used. For example:
# my_field = serializer.CharField(source='my_field')
- assert self._kwargs.get('source') != field_name, (
+ assert self.source != field_name, (
"It is redundant to specify `source='%s'` on field '%s' in "
"serializer '%s', because it is the same as the field name. "
"Remove the `source` keyword argument." %
@@ -883,17 +883,32 @@ class SerializerMethodField(Field):
def get_extra_info(self, obj):
return ... # Calculate some data to return.
"""
- def __init__(self, method_attr=None, **kwargs):
- self.method_attr = method_attr
+ def __init__(self, method_name=None, **kwargs):
+ self.method_name = method_name
kwargs['source'] = '*'
kwargs['read_only'] = True
super(SerializerMethodField, self).__init__(**kwargs)
+ def bind(self, field_name, parent):
+ # In order to enforce a consistent style, we error if a redundant
+ # 'method_name' argument has been used. For example:
+ # my_field = serializer.CharField(source='my_field')
+ default_method_name = 'get_{field_name}'.format(field_name=field_name)
+ assert self.method_name != default_method_name, (
+ "It is redundant to specify `%s` on SerializerMethodField '%s' in "
+ "serializer '%s', because it is the same as the default method name. "
+ "Remove the `method_name` argument." %
+ (self.method_name, field_name, parent.__class__.__name__)
+ )
+
+ # The method name should default to `get_{field_name}`.
+ if self.method_name is None:
+ self.method_name = default_method_name
+
+ super(SerializerMethodField, self).bind(field_name, parent)
+
def to_representation(self, value):
- method_attr = self.method_attr
- if method_attr is None:
- method_attr = 'get_{field_name}'.format(field_name=self.field_name)
- method = getattr(self.parent, method_attr)
+ method = getattr(self.parent, self.method_name)
return method(value)
diff --git a/tests/test_fields.py b/tests/test_fields.py
index ebb88d3d..003b4b8c 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -710,3 +710,33 @@ class TestMultipleChoiceField(FieldValues):
('diesel', 'Diesel'),
]
)
+
+
+# Tests for SerializerMethodField.
+# --------------------------------
+
+class TestSerializerMethodField:
+ def test_serializer_method_field(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.SerializerMethodField()
+
+ def get_example_field(self, obj):
+ return 'ran get_example_field(%d)' % obj['example_field']
+
+ serializer = ExampleSerializer({'example_field': 123})
+ assert serializer.data == {
+ 'example_field': 'ran get_example_field(123)'
+ }
+
+ def test_redundant_method_name(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.SerializerMethodField('get_example_field')
+
+ with pytest.raises(AssertionError) as exc_info:
+ ExampleSerializer()
+ assert str(exc_info.value) == (
+ "It is redundant to specify `get_example_field` on "
+ "SerializerMethodField 'example_field' in serializer "
+ "'ExampleSerializer', because it is the same as the default "
+ "method name. Remove the `method_name` argument."
+ )
--
cgit v1.2.3
From 8ee92f8a18c3a31a2a95233f36754203dc60bb18 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 25 Sep 2014 13:10:33 +0100
Subject: Refuse to downcast from datetime to date or time
---
rest_framework/fields.py | 118 ++++++++++++++++++++++++++---------------------
tests/test_fields.py | 3 +-
2 files changed, 67 insertions(+), 54 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 328e93ef..d855e6fd 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -608,120 +608,126 @@ class DecimalField(Field):
# Date & time fields...
-class DateField(Field):
+class DateTimeField(Field):
default_error_messages = {
- 'invalid': _('Date has wrong format. Use one of these formats instead: {format}'),
- 'datetime': _('Expected a date but got a datetime.'),
+ 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'),
+ 'date': _('Expected a datetime but got a date.'),
}
- format = api_settings.DATE_FORMAT
- input_formats = api_settings.DATE_INPUT_FORMATS
+ format = api_settings.DATETIME_FORMAT
+ input_formats = api_settings.DATETIME_INPUT_FORMATS
+ default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None
- def __init__(self, format=empty, input_formats=None, *args, **kwargs):
+ def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs):
self.format = format if format is not empty else self.format
self.input_formats = input_formats if input_formats is not None else self.input_formats
- super(DateField, self).__init__(*args, **kwargs)
+ self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone
+ super(DateTimeField, self).__init__(*args, **kwargs)
+
+ def enforce_timezone(self, value):
+ """
+ When `self.default_timezone` is `None`, always return naive datetimes.
+ When `self.default_timezone` is not `None`, always return aware datetimes.
+ """
+ if (self.default_timezone is not None) and not timezone.is_aware(value):
+ return timezone.make_aware(value, self.default_timezone)
+ elif (self.default_timezone is None) and timezone.is_aware(value):
+ return timezone.make_naive(value, timezone.UTC())
+ return value
def to_internal_value(self, value):
- if isinstance(value, datetime.datetime):
- self.fail('datetime')
+ if (isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
+ self.fail('date')
- if isinstance(value, datetime.date):
- return value
+ if isinstance(value, datetime.datetime):
+ return self.enforce_timezone(value)
for format in self.input_formats:
if format.lower() == ISO_8601:
try:
- parsed = parse_date(value)
+ parsed = parse_datetime(value)
except (ValueError, TypeError):
pass
else:
if parsed is not None:
- return parsed
+ return self.enforce_timezone(parsed)
else:
try:
parsed = datetime.datetime.strptime(value, format)
except (ValueError, TypeError):
pass
else:
- return parsed.date()
+ return self.enforce_timezone(parsed)
- humanized_format = humanize_datetime.date_formats(self.input_formats)
+ humanized_format = humanize_datetime.datetime_formats(self.input_formats)
self.fail('invalid', format=humanized_format)
def to_representation(self, value):
if value is None or self.format is None:
return value
- if isinstance(value, datetime.datetime):
- value = value.date()
-
if self.format.lower() == ISO_8601:
- return value.isoformat()
+ ret = value.isoformat()
+ if ret.endswith('+00:00'):
+ ret = ret[:-6] + 'Z'
+ return ret
return value.strftime(self.format)
-class DateTimeField(Field):
+class DateField(Field):
default_error_messages = {
- 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}'),
- 'date': _('Expected a datetime but got a date.'),
+ 'invalid': _('Date has wrong format. Use one of these formats instead: {format}'),
+ 'datetime': _('Expected a date but got a datetime.'),
}
- format = api_settings.DATETIME_FORMAT
- input_formats = api_settings.DATETIME_INPUT_FORMATS
- default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None
+ format = api_settings.DATE_FORMAT
+ input_formats = api_settings.DATE_INPUT_FORMATS
- def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs):
+ def __init__(self, format=empty, input_formats=None, *args, **kwargs):
self.format = format if format is not empty else self.format
self.input_formats = input_formats if input_formats is not None else self.input_formats
- self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone
- super(DateTimeField, self).__init__(*args, **kwargs)
-
- def enforce_timezone(self, value):
- """
- When `self.default_timezone` is `None`, always return naive datetimes.
- When `self.default_timezone` is not `None`, always return aware datetimes.
- """
- if (self.default_timezone is not None) and not timezone.is_aware(value):
- return timezone.make_aware(value, self.default_timezone)
- elif (self.default_timezone is None) and timezone.is_aware(value):
- return timezone.make_naive(value, timezone.UTC())
- return value
+ super(DateField, self).__init__(*args, **kwargs)
def to_internal_value(self, value):
- if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
- self.fail('date')
-
if isinstance(value, datetime.datetime):
- return self.enforce_timezone(value)
+ self.fail('datetime')
+
+ if isinstance(value, datetime.date):
+ return value
for format in self.input_formats:
if format.lower() == ISO_8601:
try:
- parsed = parse_datetime(value)
+ parsed = parse_date(value)
except (ValueError, TypeError):
pass
else:
if parsed is not None:
- return self.enforce_timezone(parsed)
+ return parsed
else:
try:
parsed = datetime.datetime.strptime(value, format)
except (ValueError, TypeError):
pass
else:
- return self.enforce_timezone(parsed)
+ return parsed.date()
- humanized_format = humanize_datetime.datetime_formats(self.input_formats)
+ humanized_format = humanize_datetime.date_formats(self.input_formats)
self.fail('invalid', format=humanized_format)
def to_representation(self, value):
if value is None or self.format is None:
return value
+ # Applying a `DateField` to a datetime value is almost always
+ # not a sensible thing to do, as it means naively dropping
+ # any explicit or implicit timezone info.
+ assert not isinstance(value, datetime.datetime), (
+ 'Expected a `date`, but got a `datetime`. Refusing to coerce, '
+ 'as this may mean losing timezone information. Use a custom '
+ 'read-only field and deal with timezone issues explicitly.'
+ )
+
if self.format.lower() == ISO_8601:
- ret = value.isoformat()
- if ret.endswith('+00:00'):
- ret = ret[:-6] + 'Z'
- return ret
+ return value.isoformat()
return value.strftime(self.format)
@@ -765,8 +771,14 @@ class TimeField(Field):
if value is None or self.format is None:
return value
- if isinstance(value, datetime.datetime):
- value = value.time()
+ # Applying a `TimeField` to a datetime value is almost always
+ # not a sensible thing to do, as it means naively dropping
+ # any explicit or implicit timezone info.
+ assert not isinstance(value, datetime.datetime), (
+ 'Expected a `time`, but got a `datetime`. Refusing to coerce, '
+ 'as this may mean losing timezone information. Use a custom '
+ 'read-only field and deal with timezone issues explicitly.'
+ )
if self.format.lower() == ISO_8601:
return value.isoformat()
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 003b4b8c..b29acad8 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -173,7 +173,8 @@ class TestNullBooleanField(FieldValues):
'null': None,
True: True,
False: False,
- None: None
+ None: None,
+ 'other': True
}
field = fields.NullBooleanField()
--
cgit v1.2.3
From 3a5335f09f58439f8e3c0bddbed8e4c7eeb32482 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 25 Sep 2014 13:12:02 +0100
Subject: Fix syntax error
---
rest_framework/fields.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index d855e6fd..7beccbb7 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -635,7 +635,7 @@ class DateTimeField(Field):
return value
def to_internal_value(self, value):
- if (isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
+ if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
self.fail('date')
if isinstance(value, datetime.datetime):
--
cgit v1.2.3
From 417fe1b675bd1d42518fb89a6f81547caef5b735 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Thu, 25 Sep 2014 13:37:26 +0100
Subject: Partial support
---
rest_framework/fields.py | 35 +++++++++++++++++++++++++----------
rest_framework/serializers.py | 6 ++++--
tests/test_serializer.py | 33 ++++++++++++++++++++++++++++++++-
3 files changed, 61 insertions(+), 13 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 7beccbb7..032bfd04 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -109,8 +109,7 @@ class Field(object):
def __init__(self, read_only=False, write_only=False,
required=None, default=empty, initial=None, source=None,
label=None, help_text=None, style=None,
- error_messages=None, validators=[], allow_null=False,
- context=None):
+ error_messages=None, validators=[], allow_null=False):
self._creation_counter = Field._creation_counter
Field._creation_counter += 1
@@ -139,7 +138,6 @@ class Field(object):
# These are set up by `.bind()` when the field is added to a serializer.
self.field_name = None
self.parent = None
- self._context = {} if (context is None) else context
# Collect default error message from self and parent classes
messages = {}
@@ -163,13 +161,6 @@ class Field(object):
kwargs = copy.deepcopy(self._kwargs)
return self.__class__(*args, **kwargs)
- @property
- def context(self):
- root = self
- while root.parent is not None:
- root = root.parent
- return root._context
-
def bind(self, field_name, parent):
"""
Setup the context for the field instance.
@@ -254,6 +245,8 @@ class Field(object):
"""
if data is empty:
if self.required:
+ if getattr(self.root, 'partial', False):
+ raise SkipField()
self.fail('required')
return self.get_default()
@@ -304,7 +297,29 @@ class Field(object):
raise AssertionError(msg)
raise ValidationError(msg.format(**kwargs))
+ @property
+ def root(self):
+ """
+ Returns the top-level serializer for this field.
+ """
+ root = self
+ while root.parent is not None:
+ root = root.parent
+ return root
+
+ @property
+ def context(self):
+ """
+ Returns the context as passed to the root serializer on initialization.
+ """
+ return getattr(self.root, '_context', {})
+
def __repr__(self):
+ """
+ Fields are represented using their initial calling arguments.
+ This allows us to create descriptive representations for serializer
+ instances that show all the declared fields on the serializer.
+ """
return representation.field_repr(self)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 04721c7a..b6a1898c 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -181,8 +181,9 @@ class BindingDict(object):
@six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer):
def __init__(self, *args, **kwargs):
- kwargs.pop('partial', None)
kwargs.pop('many', None)
+ self.partial = kwargs.pop('partial', False)
+ self._context = kwargs.pop('context', {})
super(Serializer, self).__init__(*args, **kwargs)
@@ -289,7 +290,8 @@ class ListSerializer(BaseSerializer):
self.child = kwargs.pop('child', copy.deepcopy(self.child))
assert self.child is not None, '`child` is a required argument.'
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
- kwargs.pop('partial', None)
+ self.partial = kwargs.pop('partial', False)
+ self._context = kwargs.pop('context', {})
super(ListSerializer, self).__init__(*args, **kwargs)
self.child.bind(field_name='', parent=self)
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index b0eb4e27..5646f994 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -1,3 +1,35 @@
+from rest_framework import serializers
+
+
+# Tests for core functionality.
+# -----------------------------
+
+class TestSerializer:
+ def setup(self):
+ class ExampleSerializer(serializers.Serializer):
+ char = serializers.CharField()
+ integer = serializers.IntegerField()
+ self.Serializer = ExampleSerializer
+
+ def test_valid_serializer(self):
+ serializer = self.Serializer(data={'char': 'abc', 'integer': 123})
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'char': 'abc', 'integer': 123}
+ assert serializer.errors == {}
+
+ def test_invalid_serializer(self):
+ serializer = self.Serializer(data={'char': 'abc'})
+ assert not serializer.is_valid()
+ assert serializer.validated_data == {}
+ assert serializer.errors == {'integer': ['This field is required.']}
+
+ def test_partial_validation(self):
+ serializer = self.Serializer(data={'char': 'abc'}, partial=True)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'char': 'abc'}
+ assert serializer.errors == {}
+
+
# # -*- coding: utf-8 -*-
# from __future__ import unicode_literals
# from django.db import models
@@ -334,7 +366,6 @@
# Check _data attribute is cleared on `save()`
# Regression test for #1116
-# — id field is not populated if `data` is accessed prior to `save()`
# """
# serializer = ActionItemSerializer(self.actionitem)
# self.assertIsNone(serializer.data.get('id', None), 'New instance. `id` should not be set.')
--
cgit v1.2.3
From 2859eaf524bca23f27e666d24a0b63ba61698a76 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 10:46:52 +0100
Subject: request.data attribute
---
docs/topics/3.0-announcement.md | 344 +++++++++++++++++++++++++++++++++++---
rest_framework/authtoken/views.py | 2 +-
rest_framework/fields.py | 51 +++---
rest_framework/filters.py | 6 +-
rest_framework/generics.py | 4 +-
rest_framework/mixins.py | 6 +-
rest_framework/negotiation.py | 4 +-
rest_framework/renderers.py | 4 +-
rest_framework/request.py | 37 +++-
rest_framework/serializers.py | 21 +--
tests/test_fields.py | 140 +++++++++++++++-
tests/test_generics.py | 2 +-
tests/test_serializer.py | 62 +++++++
13 files changed, 610 insertions(+), 73 deletions(-)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index cd883cdd..1795611c 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -4,36 +4,65 @@ See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-fr
# REST framework 3.0
-**Note incremental nature, discuss upgrading.**
+**TODO**: Note incremental nature, discuss upgrading, motivation, features.
-## Motivation
-
-**TODO**
+* Serializer reprs.
+* Non-magical model serializers.
+* Base serializer class.
+* Clean logic in views, serializers, fields.
---
## Request objects
-#### The `request.data` property.
+#### The `.data` and `.query_params` properties.
-**TODO**
+The usage of `request.DATA` and `request.FILES` is now discouraged in favor of a single `request.data` attribute that contains *all* the parsed data.
-#### The parser API.
+Having seperate attributes is reasonable for web applications that only ever parse URL encoded or MultiPart requests, but makes less sense for the general-purpose request parsing that REST framework supports.
-**TODO**
+You may now pass all the request data to a serializer class in a single argument:
+
+ ExampleSerializer(data=request.data)
+
+Instead of passing the files argument seperately:
+
+ # Don't do this...
+ ExampleSerializer(data=request.DATA, files=request.FILES)
+
+
+The usage of `request.QUERY_PARAMS` is now discouraged in favor of the lowercased `request.query_params`.
## Serializers
#### Single-step object creation.
+#### The `.create()` and `.update()` methods.
+
**TODO**: Drop `.restore_object()`, use `.create()` and `.update()` which should save the instance.
-**TODO**: Drop`.object`, use `.validated_data` or get the instance with `.save()`.
+#### Use `.validated_data` instead of `.object`.
-#### The `BaseSerializer` class.
+You must now use the `.validated_data` attribute if you need to inspect the data before saving, rather than using the `.object` attribute, which no longer exists.
-**TODO**
+For example the following code *is no longer valid*:
+
+ if serializer.is_valid():
+ name = serializer.object.name # Inspect validated field data.
+ logging.info('Creating ticket "%s"' % name)
+ serializer.object.user = request.user # Include the user when saving.
+ serializer.save()
+Instead of using `.object` to inspect a partially constructed instance, you would now use `.validated_data` to inspect the cleaned incoming values. Also you can't set extra attributes on the instance directly, but instead pass them to the `.save()` method using the `extras` keyword argument.
+
+The corresponding code would now look like this:
+
+ if serializer.is_valid():
+ name = serializer.validated_data['name'] # Inspect validated field data.
+ logging.info('Creating ticket "%s"' % name)
+ extras = {'user': request.user} # Include the user when saving.
+ serializer.save(extras=extras)
+
#### Always use `fields`, not `exclude`.
The `exclude` option is no longer available. You should use the more explicit `fields` option instead.
@@ -111,42 +140,287 @@ These fields will be mapped to `serializers.ReadOnlyField()` instances.
message = CharField(max_length=1000)
expiry_date = ReadOnlyField()
+#### The `ListSerializer` class.
+
+The `ListSerializer` class has now been added, and allows you to create base serializer classes for only accepting multiple inputs.
+
+ class MultipleUserSerializer(ListSerializer):
+ child = UserSerializer()
+
+You can also still use the `many=True` argument to serializer classes. It's worth noting that `many=True` argument transparently creates a `ListSerializer` instance, allowing the validation logic for list and non-list data to be cleanly seperated in the REST framework codebase.
+
+See also the new `ListField` class, which validates input in the same way, but does not include the serializer interfaces of `.is_valid()`, `.data`, `.save()` and so on.
+
+#### The `BaseSerializer` class.
+
+REST framework now includes a simple `BaseSerializer` class that can be used to easily support alternative serialization and deserialization styles.
+
+This class implements the same basic API as the `Serializer` class:
+
+* `.data` - Returns the outgoing primative representation.
+* `.is_valid()` - Deserializes and validates incoming data.
+* `.validated_data` - Returns the validated incoming data.
+* `.errors` - Returns an errors during validation.
+* `.save()` - Persists the validated data into an object instance.
+
+There are four mathods that can be overriding, depending on what functionality you want the serializer class to support:
+
+* `.to_representation()` - Override this to support serialization, for read operations.
+* `.to_internal_value()` - Override this to support deserialization, for write operations.
+* `.create()` and `.update()` - Overide either or both of these to support saving instances.
+
+##### Read-only serializers.
+
+To implement a read-only serializer using the `BaseSerializer` class, we just need to override the `.to_representation()` method. Let's take a look at an example using a simple Django model:
+
+ class HighScore(models.Model):
+ created = models.DateTimeField(auto_now_add=True)
+ player_name = models.CharField(max_length=10)
+ score = models.IntegerField()
+
+It's simple to create a read-only serializer for converting `HighScore` instances into primative data types.
+
+ class HighScoreSerializer(serializers.BaseSerializer):
+ def to_representation(self, obj):
+ return {
+ 'score': obj.score,
+ 'player_name': obj.player_name
+ }
+
+We can now use this class to serialize single `HighScore` instances:
+
+ @api_view(['GET'])
+ def high_score(request, pk):
+ instance = HighScore.objects.get(pk=pk)
+ serializer = HighScoreSerializer(instance)
+ return Response(serializer.data)
+
+Or use it to serialize multiple instances:
+
+ @api_view(['GET'])
+ def all_high_scores(request):
+ queryset = HighScore.objects.order_by('-score')
+ serializer = HighScoreSerializer(queryset, many=True)
+ return Response(serializer.data)
+
+##### Read-write serializers.
+
+To create a read-write serializer we first need to implement a `.to_internal_value()` method. This method returns the validated values that will be used to construct the object instance, and may raise a `ValidationError` if the supplied data is in an incorrect format.
+
+Once you've implemented `.to_internal_value()`, the basic validation API will be available on the serializer, and you will be able to use `.is_valid()`, `.validated_data` and `.errors`.
+
+If you want to also support `.save()` you'll need to also implement either or both of the `.create()` and `.update()` methods.
+
+Here's a complete example of our previous `HighScoreSerializer`, that's been updated to support both read and write operations.
+
+ class HighScoreSerializer(serializers.BaseSerializer):
+ def to_internal_value(self, data):
+ score = data.get('score')
+ player_name = data.get('player_name')
+
+ # Perform the data validation.
+ if not score:
+ raise ValidationError({
+ 'score': 'This field is required.'
+ })
+ if not player_name:
+ raise ValidationError({
+ 'player_name': 'This field is required.'
+ })
+ if len(player_name) > 10:
+ raise ValidationError({
+ 'player_name': 'May not be more than 10 characters.'
+ })
+
+ # Return the validated values. This will be available as
+ # the `.validated_data` property.
+ return {
+ 'score': int(score),
+ 'player_name': player_name
+ }
+
+ def to_representation(self, obj):
+ return {
+ 'score': obj.score,
+ 'player_name': obj.player_name
+ }
+
+ def create(self, validated_data):
+ return HighScore.objects.create(**validated_data)
+
+#### Creating new base classes with `BaseSerializer`.
+
+The `BaseSerializer` class is also useful if you want to implement new generic serializer classes for dealing with particular serialization styles or for integrating with different storage backends.
+
+The following class is an example of a generic serializer that can handle coercing aribitrary objects into primative representations.
+
+ class ObjectSerializer(serializers.BaseSerializer):
+ """
+ A read-only serializer that coerces arbitrary complex objects
+ into primative representations.
+ """
+ def to_representation(self, obj):
+ for attribute_name in dir(obj):
+ attribute = getattr(obj, attribute_name)
+ if attribute_name('_'):
+ # Ignore private attributes.
+ pass
+ elif hasattr(attribute, '__call__'):
+ # Ignore methods and other callables.
+ pass
+ elif isinstance(attribute, (str, int, bool, float, type(None))):
+ # Primative types can be passed through unmodified.
+ output[attribute_name] = attribute
+ elif isinstance(attribute, list):
+ # Recursivly deal with items in lists.
+ output[attribute_name] = [
+ self.to_representation(item) for item in attribute
+ ]
+ elif isinstance(attribute, dict):
+ # Recursivly deal with items in dictionarys.
+ output[attribute_name] = {
+ str(key): self.to_representation(value)
+ for key, value in attribute.items()
+ }
+ else:
+ # Force anything else to its string representation.
+ output[attribute_name] = str(attribute)
## Serializer fields
#### The `Field` and `ReadOnly` field classes.
-**TODO**
+There are some minor tweaks to the field base classes.
+
+Previously we had these two base classes:
+
+* `Field` as the base class for read-only fields. A default implementation was included for serializing data.
+* `WriteableField` as the base class for read-write fields.
+
+We now use the following:
+
+* `Field` is the base class for all fields. It does not include any default implementation for either serializing or deserializing data.
+* `ReadOnlyField` is a concrete implementation for read-only fields that simply returns the attribute value without modification.
+
+#### The `required`, `allow_none`, `allow_blank` and `default` arguments.
+
+REST framework now has more explict and clear control over validating empty values for fields.
+
+Previously the meaning of the `required=False` keyword argument was underspecified. In practice it's use meant that a field could either be not included in the input, or it could be included, but be `None`.
+
+We now have a better seperation, with seperate `required` and `allow_none` arguments.
+
+The following set of arguments are used to control validation of empty values:
+
+* `required=False`: The value does not need to be present in the input, and will not be passed to `.create()` or `.update()` if it is not seen.
+* `default=`: The value does not need to be present in the input, and a default value will be passed to `.create()` or `.update()` if it is not seen.
+* `allow_none=True`: `None` is a valid input.
+* `allow_blank=True`: `''` is valid input. For `CharField` and subclasses only.
+
+Typically you'll want to use `required=False` if the corresponding model field has a default value, and additionally set either `allow_none=True` or `allow_blank=True` if required.
+
+The `default` argument is there if you need it, but you'll more typically want defaults to be set on model fields, rather than serializer fields.
#### Coercing output types.
-**TODO**
+The previous field implementations did not forcibly coerce returned values into the correct type in many cases. For example, an `IntegerField` would return a string output if the attribute value was a string. We now more strictly coerce to the correct return type, leading to more constrained and expected behavior.
-#### The `ListSerializer` class.
+#### The `ListField` class.
-**TODO**
+The `ListField` class has now been added. This field validates list input. It takes a `child` keyword argument which is used to specify the field used to validate each item in the list. For example:
+
+ scores = ListField(child=IntegerField(min_value=0, max_value=100))
+
+You can also use a declarative style to create new subclasses of `ListField`, like this:
+
+ class ScoresField(ListField):
+ child = IntegerField(min_value=0, max_value=100)
+
+We can now use the `ScoresField` class inside another serializer:
+
+ scores = ScoresField()
+
+See also the new `ListSerializer` class, which validates input in the same way, but also includes the serializer interfaces of `.is_valid()`, `.data`, `.save()` and so on.
+
+#### The `ChoiceField` class may now accept a flat list.
+
+The `ChoiceField` class may now accept a list of choices in addition to the existing style of using a list of pairs of `(name, display_value)`. The following is now valid:
+
+ color = ChoiceField(choices=['red', 'green', 'blue'])
#### The `MultipleChoiceField` class.
-**TODO**
+The `MultipleChoiceField` class has been added. This field acts like `ChoiceField`, but returns a set, which may include none, one or many of the valid choices.
#### Changes to the custom field API.
-**TODO** `to_representation`, `to_internal_value`.
+The `from_native(self, value)` and `to_native(self, data)` method names have been replaced with the more obviously named `to_representation(self, value)` and `to_internal_value(self, data)`.
-#### Explicit `querysets` required on relational fields.
+The `field_from_native()` and `field_to_native()` methods are removed.
-**TODO**
+#### Explicit `queryset` required on relational fields.
+
+Previously relational fields that were explicitly declared on a serializer class could omit the queryset argument if (and only if) they were declared on a `ModelSerializer`.
+
+This code *would be valid* in `2.4.3`:
+
+ class AccountSerializer(serializers.ModelSerializer):
+ organisations = serializers.SlugRelatedField(slug_field='name')
+
+ class Meta:
+ model = Account
+
+However this code *would not be valid* in `2.4.3`:
+
+ # Missing `queryset`
+ class AccountSerializer(serializers.Serializer):
+ organisations = serializers.SlugRelatedField(slug_field='name')
+
+ def restore_object(self, attrs, instance=None):
+ # ...
+
+The queryset argument is now always required for writable relational fields.
+This removes some magic and makes it easier and more obvious to move between implict `ModelSerializer` classes and explicit `Serializer` classes.
+
+ class AccountSerializer(serializers.ModelSerializer):
+ organisations = serializers.SlugRelatedField(
+ slug_field='name',
+ queryset=Organisation.objects.all()
+ )
+
+ class Meta:
+ model = Account
+
+The `queryset` argument is only ever required for writable fields, and is not required or valid for fields with `read_only=True`.
#### Optional argument to `SerializerMethodField`.
-**TODO**
+The argument to `SerializerMethodField` is now optional, and defaults to `get_`. For example the following is valid:
+
+ class AccountSerializer(serializers.Serializer):
+ # `method_name='get_billing_details'` by default.
+ billing_details = serializers.SerializerMethodField()
+
+ def get_billing_details(self, account):
+ return calculate_billing(account)
+
+In order to ensure a consistent code style an assertion error will be raised if you include a redundant method name argument that matches the default method name. For example, the following code *will raise an error*:
+
+ billing_details = serializers.SerializerMethodField('get_billing_details')
+
+#### Enforcing consistent `source` usage.
+
+I've see several codebases that unneccessarily include the `source` argument, setting it to the same value as the field name. This usage is redundant and confusing, making it less obvious that `source` is usually not required.
+
+The following usage will *now raise an error*:
+
+ email = serializers.EmailField(source='email')
## Generic views
#### Simplification of view logic.
-**TODO**
+The view logic for the default method handlers has been significantly simplified, due to the new serializers API.
#### Removal of pre/post save hooks.
@@ -169,6 +443,20 @@ I would personally recommend that developers treat view instances as immutable o
#### PUT as create.
+Allowing `PUT` as create operations is problematic, as it neccessarily exposes information about the existence or non-existance of objects. It's also not obvious that transparently allowing re-creating of previously deleted instances is neccessarily a better default behavior than simply returning `404` responses.
+
+Both styles "`PUT` as 404" and "`PUT` as create" can be valid in different circumstances, but we've now opted for the 404 behavior as the default, due to it being simpler and more obvious.
+
+If you need to restore the previous behavior you can include the `AllowPUTAsCreateMixin` class in your view. This class can be imported from `rest_framework.mixins`.
+
+#### Customizing error responses.
+
+The generic views now raise `ValidationError` for invalid data. This exception is then dealt with by the exception handler, rather than the view returning a `400 Bad Request` response directly.
+
+This change means that you can now easily cusomize the style of error responses across your entire API, without having to modify any of the generic views.
+
+## The metadata API
+
**TODO**
## API style
@@ -241,3 +529,17 @@ Or modify it on an individual serializer field, using the `corece_to_string` key
)
The default JSON renderer will return float objects for uncoerced `Decimal` instances. This allows you to easily switch between string or float representations for decimals depending on your API design needs.
+
+## What's coming next.
+
+3.0 is an incremental release, and there are several upcoming features that will build on the baseline improvements that it makes.
+
+The 3.1 release is planned to address improvements in the following components:
+
+* Request parsing, mediatypes & the implementation of the browsable API.
+* Introduction of a new pagination API.
+* Better support for API versioning.
+
+The 3.2 release is planned to introduce an alternative admin-style interface to the browsable API.
+
+You can follow development on the GitHub site, where we use [milestones to indicate planning timescales](https://github.com/tomchristie/django-rest-framework/milestones).
diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py
index 94e6f061..103abb27 100644
--- a/rest_framework/authtoken/views.py
+++ b/rest_framework/authtoken/views.py
@@ -16,7 +16,7 @@ class ObtainAuthToken(APIView):
model = Token
def post(self, request):
- serializer = self.serializer_class(data=request.DATA)
+ serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
user = serializer.validated_data['user']
token, created = Token.objects.get_or_create(user=user)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 032bfd04..ec07a413 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -56,7 +56,7 @@ def get_attribute(instance, attrs):
except AttributeError as exc:
try:
return instance[attr]
- except (KeyError, TypeError):
+ except (KeyError, TypeError, AttributeError):
raise exc
return instance
@@ -90,6 +90,7 @@ NOT_READ_ONLY_WRITE_ONLY = 'May not set both `read_only` and `write_only`'
NOT_READ_ONLY_REQUIRED = 'May not set both `read_only` and `required`'
NOT_READ_ONLY_DEFAULT = 'May not set both `read_only` and `default`'
NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`'
+USE_READONLYFIELD = 'Field(read_only=True) should be ReadOnlyField'
MISSING_ERROR_MESSAGE = (
'ValidationError raised by `{class_name}`, but error key `{key}` does '
'not exist in the `error_messages` dictionary.'
@@ -105,9 +106,10 @@ class Field(object):
}
default_validators = []
default_empty_html = None
+ initial = None
def __init__(self, read_only=False, write_only=False,
- required=None, default=empty, initial=None, source=None,
+ required=None, default=empty, initial=empty, source=None,
label=None, help_text=None, style=None,
error_messages=None, validators=[], allow_null=False):
self._creation_counter = Field._creation_counter
@@ -122,13 +124,14 @@ class Field(object):
assert not (read_only and required), NOT_READ_ONLY_REQUIRED
assert not (read_only and default is not empty), NOT_READ_ONLY_DEFAULT
assert not (required and default is not empty), NOT_REQUIRED_DEFAULT
+ assert not (read_only and self.__class__ == Field), USE_READONLYFIELD
self.read_only = read_only
self.write_only = write_only
self.required = required
self.default = default
self.source = source
- self.initial = initial
+ self.initial = self.initial if (initial is empty) else initial
self.label = label
self.help_text = help_text
self.style = {} if style is None else style
@@ -146,24 +149,10 @@ class Field(object):
messages.update(error_messages or {})
self.error_messages = messages
- def __new__(cls, *args, **kwargs):
- """
- When a field is instantiated, we store the arguments that were used,
- so that we can present a helpful representation of the object.
- """
- instance = super(Field, cls).__new__(cls)
- instance._args = args
- instance._kwargs = kwargs
- return instance
-
- def __deepcopy__(self, memo):
- args = copy.deepcopy(self._args)
- kwargs = copy.deepcopy(self._kwargs)
- return self.__class__(*args, **kwargs)
-
def bind(self, field_name, parent):
"""
- Setup the context for the field instance.
+ Initializes the field name and parent for the field instance.
+ Called when a field is added to the parent serializer instance.
"""
# In order to enforce a consistent style, we error if a redundant
@@ -244,9 +233,9 @@ class Field(object):
validated data.
"""
if data is empty:
+ if getattr(self.root, 'partial', False):
+ raise SkipField()
if self.required:
- if getattr(self.root, 'partial', False):
- raise SkipField()
self.fail('required')
return self.get_default()
@@ -314,6 +303,25 @@ class Field(object):
"""
return getattr(self.root, '_context', {})
+ def __new__(cls, *args, **kwargs):
+ """
+ When a field is instantiated, we store the arguments that were used,
+ so that we can present a helpful representation of the object.
+ """
+ instance = super(Field, cls).__new__(cls)
+ instance._args = args
+ instance._kwargs = kwargs
+ return instance
+
+ def __deepcopy__(self, memo):
+ """
+ When cloning fields we instantiate using the arguments it was
+ originally created with, rather than copying the complete state.
+ """
+ args = copy.deepcopy(self._args)
+ kwargs = copy.deepcopy(self._kwargs)
+ return self.__class__(*args, **kwargs)
+
def __repr__(self):
"""
Fields are represented using their initial calling arguments.
@@ -358,6 +366,7 @@ class NullBooleanField(Field):
'invalid': _('`{input}` is not a valid boolean.')
}
default_empty_html = None
+ initial = None
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
NULL_VALUES = set(('n', 'N', 'null', 'Null', 'NULL', '', None))
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index c580f935..085dfe65 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -64,7 +64,7 @@ class DjangoFilterBackend(BaseFilterBackend):
filter_class = self.get_filter_class(view, queryset)
if filter_class:
- return filter_class(request.QUERY_PARAMS, queryset=queryset).qs
+ return filter_class(request.query_params, queryset=queryset).qs
return queryset
@@ -78,7 +78,7 @@ class SearchFilter(BaseFilterBackend):
Search terms are set by a ?search=... query parameter,
and may be comma and/or whitespace delimited.
"""
- params = request.QUERY_PARAMS.get(self.search_param, '')
+ params = request.query_params.get(self.search_param, '')
return params.replace(',', ' ').split()
def construct_search(self, field_name):
@@ -121,7 +121,7 @@ class OrderingFilter(BaseFilterBackend):
the `ordering_param` value on the OrderingFilter or by
specifying an `ORDERING_PARAM` value in the API settings.
"""
- params = request.QUERY_PARAMS.get(self.ordering_param)
+ params = request.query_params.get(self.ordering_param)
if params:
return [param.strip() for param in params.split(',')]
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index f49b0a43..cf903dab 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -112,7 +112,7 @@ class GenericAPIView(views.APIView):
paginator = self.paginator_class(queryset, page_size)
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
try:
page_number = paginator.validate_number(page)
@@ -166,7 +166,7 @@ class GenericAPIView(views.APIView):
if self.paginate_by_param:
try:
return strict_positive_int(
- self.request.QUERY_PARAMS[self.paginate_by_param],
+ self.request.query_params[self.paginate_by_param],
cutoff=self.max_paginate_by
)
except (KeyError, ValueError):
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 14a6b44b..04b7a763 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -18,7 +18,7 @@ class CreateModelMixin(object):
Create a model instance.
"""
def create(self, request, *args, **kwargs):
- serializer = self.get_serializer(data=request.DATA)
+ serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
headers = self.get_success_headers(serializer.data)
@@ -62,7 +62,7 @@ class UpdateModelMixin(object):
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
- serializer = self.get_serializer(instance, data=request.DATA, partial=partial)
+ serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
@@ -95,7 +95,7 @@ class AllowPUTAsCreateMixin(object):
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object_or_none()
- serializer = self.get_serializer(instance, data=request.DATA, partial=partial)
+ serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
if instance is None:
diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py
index ca7b5397..1838130a 100644
--- a/rest_framework/negotiation.py
+++ b/rest_framework/negotiation.py
@@ -38,7 +38,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
"""
# Allow URL style format override. eg. "?format=json
format_query_param = self.settings.URL_FORMAT_OVERRIDE
- format = format_suffix or request.QUERY_PARAMS.get(format_query_param)
+ format = format_suffix or request.query_params.get(format_query_param)
if format:
renderers = self.filter_renderers(renderers, format)
@@ -87,5 +87,5 @@ class DefaultContentNegotiation(BaseContentNegotiation):
Allows URL style accept override. eg. "?accept=application/json"
"""
header = request.META.get('HTTP_ACCEPT', '*/*')
- header = request.QUERY_PARAMS.get(self.settings.URL_ACCEPT_OVERRIDE, header)
+ header = request.query_params.get(self.settings.URL_ACCEPT_OVERRIDE, header)
return [token.strip() for token in header.split(',')]
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 3bf03e62..225f9fe8 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -120,7 +120,7 @@ class JSONPRenderer(JSONRenderer):
Determine the name of the callback to wrap around the json output.
"""
request = renderer_context.get('request', None)
- params = request and request.QUERY_PARAMS or {}
+ params = request and request.query_params or {}
return params.get(self.callback_parameter, self.default_callback)
def render(self, data, accepted_media_type=None, renderer_context=None):
@@ -426,7 +426,7 @@ class BrowsableAPIRenderer(BaseRenderer):
"""
if request.method == method:
try:
- data = request.DATA
+ data = request.data
# files = request.FILES
except ParseError:
data = None
diff --git a/rest_framework/request.py b/rest_framework/request.py
index 27532661..d80baa70 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -4,7 +4,7 @@ The Request class is used as a wrapper around the standard request object.
The wrapped request then offers a richer API, in particular :
- content automatically parsed according to `Content-Type` header,
- and available as `request.DATA`
+ and available as `request.data`
- full support of PUT method, including support for file uploads
- form overloading of HTTP method, content type and content
"""
@@ -13,6 +13,7 @@ from django.conf import settings
from django.http import QueryDict
from django.http.multipartparser import parse_header
from django.utils.datastructures import MultiValueDict
+from django.utils.datastructures import MergeDict as DjangoMergeDict
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions
from rest_framework.compat import BytesIO
@@ -58,6 +59,15 @@ class override_method(object):
self.view.action = self.action
+class MergeDict(DjangoMergeDict, dict):
+ """
+ Using this as a workaround until the parsers API is properly
+ addressed in 3.1.
+ """
+ def __init__(self, *dicts):
+ self.dicts = dicts
+
+
class Empty(object):
"""
Placeholder for unset attributes.
@@ -82,6 +92,7 @@ def clone_request(request, method):
parser_context=request.parser_context)
ret._data = request._data
ret._files = request._files
+ ret._full_data = request._full_data
ret._content_type = request._content_type
ret._stream = request._stream
ret._method = method
@@ -133,6 +144,7 @@ class Request(object):
self.parser_context = parser_context
self._data = Empty
self._files = Empty
+ self._full_data = Empty
self._method = Empty
self._content_type = Empty
self._stream = Empty
@@ -186,12 +198,25 @@ class Request(object):
return self._stream
@property
- def QUERY_PARAMS(self):
+ def query_params(self):
"""
More semantically correct name for request.GET.
"""
return self._request.GET
+ @property
+ def QUERY_PARAMS(self):
+ """
+ Synonym for `.query_params`, for backwards compatibility.
+ """
+ return self._request.GET
+
+ @property
+ def data(self):
+ if not _hasattr(self, '_full_data'):
+ self._load_data_and_files()
+ return self._full_data
+
@property
def DATA(self):
"""
@@ -272,6 +297,10 @@ class Request(object):
if not _hasattr(self, '_data'):
self._data, self._files = self._parse()
+ if self._files:
+ self._full_data = MergeDict(self._data, self._files)
+ else:
+ self._full_data = self._data
def _load_method_and_content_type(self):
"""
@@ -333,6 +362,7 @@ class Request(object):
# At this point we're committed to parsing the request as form data.
self._data = self._request.POST
self._files = self._request.FILES
+ self._full_data = MergeDict(self._data, self._files)
# Method overloading - change the method and remove the param from the content.
if (
@@ -350,7 +380,7 @@ class Request(object):
):
self._content_type = self._data[self._CONTENTTYPE_PARAM]
self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
- self._data, self._files = (Empty, Empty)
+ self._data, self._files, self._full_data = (Empty, Empty, Empty)
def _parse(self):
"""
@@ -380,6 +410,7 @@ class Request(object):
# logging the request or similar.
self._data = QueryDict('', encoding=self._request._encoding)
self._files = MultiValueDict()
+ self._full_data = self._data
raise
# Parser classes may return the raw data, or a
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index b6a1898c..a2b878ec 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -57,21 +57,24 @@ class BaseSerializer(Field):
def to_representation(self, instance):
raise NotImplementedError('`to_representation()` must be implemented.')
- def update(self, instance, attrs):
+ def update(self, instance, validated_data):
raise NotImplementedError('`update()` must be implemented.')
- def create(self, attrs):
+ def create(self, validated_data):
raise NotImplementedError('`create()` must be implemented.')
def save(self, extras=None):
- attrs = self.validated_data
+ validated_data = self.validated_data
if extras is not None:
- attrs = dict(list(attrs.items()) + list(extras.items()))
+ validated_data = dict(
+ list(validated_data.items()) +
+ list(extras.items())
+ )
if self.instance is not None:
- self.update(self.instance, attrs)
+ self.update(self.instance, validated_data)
else:
- self.instance = self.create(attrs)
+ self.instance = self.create(validated_data)
return self.instance
@@ -321,12 +324,6 @@ class ListSerializer(BaseSerializer):
def create(self, attrs_list):
return [self.child.create(attrs) for attrs in attrs_list]
- def save(self):
- if self.instance is not None:
- self.update(self.instance, self.validated_data)
- self.instance = self.create(self.validated_data)
- return self.instance
-
def __repr__(self):
return representation.list_repr(self, indent=1)
diff --git a/tests/test_fields.py b/tests/test_fields.py
index b29acad8..1539a210 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -9,7 +9,10 @@ import pytest
# Tests for field keyword arguments and core functionality.
# ---------------------------------------------------------
-class TestFieldOptions:
+class TestEmpty:
+ """
+ Tests for `required`, `allow_null`, `allow_blank`, `default`.
+ """
def test_required(self):
"""
By default a field must be included in the input.
@@ -69,6 +72,17 @@ class TestFieldOptions:
output = field.run_validation()
assert output is 123
+
+class TestSource:
+ def test_source(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.CharField(source='other')
+ serializer = ExampleSerializer(data={'example_field': 'abc'})
+ print serializer.is_valid()
+ print serializer.data
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'other': 'abc'}
+
def test_redundant_source(self):
class ExampleSerializer(serializers.Serializer):
example_field = serializers.CharField(source='example_field')
@@ -81,6 +95,128 @@ class TestFieldOptions:
)
+class TestReadOnly:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ read_only = fields.ReadOnlyField()
+ writable = fields.IntegerField()
+ self.Serializer = TestSerializer
+
+ def test_validate_read_only(self):
+ """
+ Read-only fields should not be included in validation.
+ """
+ data = {'read_only': 123, 'writable': 456}
+ serializer = self.Serializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'writable': 456}
+
+ def test_serialize_read_only(self):
+ """
+ Read-only fields should be serialized.
+ """
+ instance = {'read_only': 123, 'writable': 456}
+ serializer = self.Serializer(instance)
+ assert serializer.data == {'read_only': 123, 'writable': 456}
+
+
+class TestWriteOnly:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ write_only = fields.IntegerField(write_only=True)
+ readable = fields.IntegerField()
+ self.Serializer = TestSerializer
+
+ def test_validate_write_only(self):
+ """
+ Write-only fields should be included in validation.
+ """
+ data = {'write_only': 123, 'readable': 456}
+ serializer = self.Serializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'write_only': 123, 'readable': 456}
+
+ def test_serialize_write_only(self):
+ """
+ Write-only fields should not be serialized.
+ """
+ instance = {'write_only': 123, 'readable': 456}
+ serializer = self.Serializer(instance)
+ assert serializer.data == {'readable': 456}
+
+
+class TestInitial:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ initial_field = fields.IntegerField(initial=123)
+ blank_field = fields.IntegerField()
+ self.serializer = TestSerializer()
+
+ def test_initial(self):
+ """
+ Initial values should be included when serializing a new representation.
+ """
+ assert self.serializer.data == {
+ 'initial_field': 123,
+ 'blank_field': None
+ }
+
+
+class TestLabel:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ labeled = fields.IntegerField(label='My label')
+ self.serializer = TestSerializer()
+
+ def test_label(self):
+ """
+ A field's label may be set with the `label` argument.
+ """
+ fields = self.serializer.fields
+ assert fields['labeled'].label == 'My label'
+
+
+class TestInvalidErrorKey:
+ def setup(self):
+ class ExampleField(serializers.Field):
+ def to_native(self, data):
+ self.fail('incorrect')
+ self.field = ExampleField()
+
+ def test_invalid_error_key(self):
+ """
+ If a field raises a validation error, but does not have a corresponding
+ error message, then raise an appropriate assertion error.
+ """
+ with pytest.raises(AssertionError) as exc_info:
+ self.field.to_native(123)
+ expected = (
+ 'ValidationError raised by `ExampleField`, but error key '
+ '`incorrect` does not exist in the `error_messages` dictionary.'
+ )
+ assert str(exc_info.value) == expected
+
+
+class TestBooleanHTMLInput:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ archived = fields.BooleanField()
+ self.Serializer = TestSerializer
+
+ def test_empty_html_checkbox(self):
+ """
+ HTML checkboxes do not send any value, but should be treated
+ as `False` by BooleanField.
+ """
+ # This class mocks up a dictionary like object, that behaves
+ # as if it was returned for multipart or urlencoded data.
+ class MockHTMLDict(dict):
+ getlist = None
+ serializer = self.Serializer(data=MockHTMLDict())
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'archived': False}
+
+
# Tests for field input and output values.
# ----------------------------------------
@@ -495,7 +631,7 @@ class TestDateTimeField(FieldValues):
'2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
- # Note that 1.4 does not support timezone string parsing.
+ # Django 1.4 does not support timezone string parsing.
'2001-01-01T14:00+01:00' if (django.VERSION > (1, 4)) else '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())
}
invalid_inputs = {
diff --git a/tests/test_generics.py b/tests/test_generics.py
index 89f9def0..2690fb47 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -38,7 +38,7 @@ class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
class SlugSerializer(serializers.ModelSerializer):
- slug = serializers.Field(read_only=True)
+ slug = serializers.ReadOnlyField()
class Meta:
model = SlugBasedModel
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index 5646f994..256a12e6 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -1,4 +1,5 @@
from rest_framework import serializers
+import pytest
# Tests for core functionality.
@@ -29,6 +30,67 @@ class TestSerializer:
assert serializer.validated_data == {'char': 'abc'}
assert serializer.errors == {}
+ def test_empty_serializer(self):
+ serializer = self.Serializer()
+ assert serializer.data == {'char': '', 'integer': None}
+
+ def test_missing_attribute_during_serialization(self):
+ class MissingAttributes:
+ pass
+ instance = MissingAttributes()
+ serializer = self.Serializer(instance)
+ with pytest.raises(AttributeError):
+ serializer.data
+
+
+class TestStarredSource:
+ """
+ Tests for `source='*'` argument, which is used for nested representations.
+
+ For example:
+
+ nested_field = NestedField(source='*')
+ """
+ data = {
+ 'nested1': {'a': 1, 'b': 2},
+ 'nested2': {'c': 3, 'd': 4}
+ }
+
+ def setup(self):
+ class NestedSerializer1(serializers.Serializer):
+ a = serializers.IntegerField()
+ b = serializers.IntegerField()
+
+ class NestedSerializer2(serializers.Serializer):
+ c = serializers.IntegerField()
+ d = serializers.IntegerField()
+
+ class TestSerializer(serializers.Serializer):
+ nested1 = NestedSerializer1(source='*')
+ nested2 = NestedSerializer2(source='*')
+
+ self.Serializer = TestSerializer
+
+ def test_nested_validate(self):
+ """
+ A nested representation is validated into a flat internal object.
+ """
+ serializer = self.Serializer(data=self.data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'a': 1,
+ 'b': 2,
+ 'c': 3,
+ 'd': 4
+ }
+
+ def test_nested_serialize(self):
+ """
+ An object can be serialized into a nested representation.
+ """
+ instance = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
+ serializer = self.Serializer(instance)
+ assert serializer.data == self.data
# # -*- coding: utf-8 -*-
# from __future__ import unicode_literals
--
cgit v1.2.3
From 43e80c74b225e17edfe8a90da893823bf50b946f Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 11:56:29 +0100
Subject: Release notes
---
docs/topics/3.0-announcement.md | 79 +++++++++++++++++++++++++++++++++++++----
rest_framework/serializers.py | 3 ++
2 files changed, 75 insertions(+), 7 deletions(-)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 1795611c..35d725ff 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -4,12 +4,25 @@ See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-fr
# REST framework 3.0
-**TODO**: Note incremental nature, discuss upgrading, motivation, features.
+The 3.0 release of Django REST framework is the result of almost four years of iteration and refinement. It comprehensively addresses some of the previous remaining design issues in serializers, fields and the generic views.
-* Serializer reprs.
-* Non-magical model serializers.
-* Base serializer class.
-* Clean logic in views, serializers, fields.
+This release is incremental in nature. There *are* some breaking API changes, and upgrading *will* require you to read the release notes carefully, but the migration path should otherwise be relatively straightforward.
+
+The difference in quality of the REST framework API and implementation should make writing, maintaining and debugging your application far easier.
+
+## New features
+
+Notable features of this new release include:
+
+* Printable representations on serializers that allow you to inspect exactly what fields are present on the instance.
+* Simple model serializers that are vastly easier to understand and debug, and that make it easy to switch between the implicit `ModelSerializer` class and the explicit `Serializer` class.
+* A new `BaseSerializer` class, making it easier to write serializers for alternative storage backends, or to completely customize your serialization and validation logic.
+* A cleaner fields API plus new `ListField` and `MultipleChoiceField` classes.
+* Super simple default implementations for the generic views.
+* Support for overriding how validation errors are handled by your API.
+* A metadata API that allows you to customize how `OPTIONS` requests are handled by your API.
+
+Below is an in-depth guide to the API changes and migration notes for 3.0.
---
@@ -37,9 +50,59 @@ The usage of `request.QUERY_PARAMS` is now discouraged in favor of the lowercase
#### Single-step object creation.
+Previously the serializers used a two-step object creation, as follows:
+
+1. Validating the data would create an object instance. This instance would be available as `serializer.object`.
+2. Calling `serializer.save()` would then save the object instance to the database.
+
+This style is in line with how the `ModelForm` class works in Django, but is problematic for a number of reasons:
+
+* Some data, such as many-to-many relationships, cannot be added to the object instance until after it has been save. This type of data needed to be hidden in some undocumentated state on the object instance, or kept as state on the serializer instance so that it could be used when `.save()` is called.
+* Instantiating model instances directly means that you cannot use model manager classes for instance creation, eg `ExampleModel.objects.create(...)`. Manager classes are an excellent layer at which to enforce business logic and application-level data constraints.
+* The two step process makes it unclear where to put deserialization logic. For example, should extra attributes such as the current user get added to the instance during object creation or during object save?
+
+We now use single-step object creation, like so:
+
+1. Validating the data makes the cleaned data available as `serializer.validated_data`.
+2. Calling `serializer.save()` then saves and returns the new object instance.
+
+The resulting API changes are further detailed below.
+
#### The `.create()` and `.update()` methods.
-**TODO**: Drop `.restore_object()`, use `.create()` and `.update()` which should save the instance.
+The `.restore_object()` method is now replaced with two seperate methods, `.create()` and `.update()`.
+
+When using the `.create()` and `.update()` methods you should both create *and save* the object instance. This is in contrast to the previous `.restore_object()` behavior that would instantiate the object but not save it.
+
+The following example from the tutorial previously used `restore_object()` to handle both creating and updating object instances.
+
+ def restore_object(self, attrs, instance=None):
+ if instance:
+ # Update existing instance
+ instance.title = attrs.get('title', instance.title)
+ instance.code = attrs.get('code', instance.code)
+ instance.linenos = attrs.get('linenos', instance.linenos)
+ instance.language = attrs.get('language', instance.language)
+ instance.style = attrs.get('style', instance.style)
+ return instance
+
+ # Create new instance
+ return Snippet(**attrs)
+
+This would now be split out into two seperate methods.
+
+ def update(self, instance, validated_attrs)
+ instance.title = validated_attrs.get('title', instance.title)
+ instance.code = validated_attrs.get('code', instance.code)
+ instance.linenos = validated_attrs.get('linenos', instance.linenos)
+ instance.language = validated_attrs.get('language', instance.language)
+ instance.style = validated_attrs.get('style', instance.style)
+ instance.save()
+
+ def create(self, validated_attrs):
+ return Snippet.objects.create(**validated_attrs)
+
+Note that the `.create` method should return the newly created object instance.
#### Use `.validated_data` instead of `.object`.
@@ -457,7 +520,9 @@ This change means that you can now easily cusomize the style of error responses
## The metadata API
-**TODO**
+Behavior for dealing with `OPTIONS` requests was previously built directly into the class based views. This has now been properly seperated out into a Metadata API that allows the same pluggable style as other API policies in REST framework.
+
+This makes it far easier to use a different style for `OPTIONS` responses throughout your API, and makes it possible to create third-party metadata policies.
## API style
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index a2b878ec..86bed773 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -75,6 +75,9 @@ class BaseSerializer(Field):
self.update(self.instance, validated_data)
else:
self.instance = self.create(validated_data)
+ assert self.instance is not None, (
+ '`create()` did not return an object instance.'
+ )
return self.instance
--
cgit v1.2.3
From e8af73d144d73a55aecde6a1fda8516f15f027c1 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 12:17:20 +0100
Subject: Release notes
---
docs/topics/3.0-announcement.md | 35 +++++++++++++++++++++++++++++++----
1 file changed, 31 insertions(+), 4 deletions(-)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 35d725ff..144d3550 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -125,14 +125,41 @@ The corresponding code would now look like this:
logging.info('Creating ticket "%s"' % name)
extras = {'user': request.user} # Include the user when saving.
serializer.save(extras=extras)
-
+
+#### Printable serializer reprensentations.
+
+Serializer instances now support a printable representation that allows you to inspect the fields present on the instance.
+
+For instance, given the following example model:
+
+ class LocationRating(models.Model):
+ location = models.CharField(max_length=100)
+ rating = models.IntegerField()
+ created_by = models.ForeignKey(User)
+
+Let's create a simple `ModelSerializer` class c.
+
+ class LocationRatingSerializer(serializer.ModelSerializer):
+ class Meta:
+ model = LocationRating
+
+We can now inspect its representation in the Django shell, using `python manage.py shell`...
+
+ >>> serializer = LocationRatingSerializer()
+ >>> print(serializer) # Or use `print serializer` in Python 2.x
+ LocationRatingSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ location = CharField(max_length=100)
+ rating = IntegerField()
+ created_by = PrimaryKeyRelatedField(queryset=User.objects.all())
+
#### Always use `fields`, not `exclude`.
-The `exclude` option is no longer available. You should use the more explicit `fields` option instead.
+The `exclude` option on `ModelSerializer` is no longer available. You should use the more explicit `fields` option instead.
#### The `extra_kwargs` option.
-The `read_only_fields` and `write_only_fields` options have been removed and replaced with a more generic `extra_kwargs`.
+The `read_only_fields` and `write_only_fields` options on `ModelSerializer` have been removed and replaced with a more generic `extra_kwargs`.
class MySerializer(serializer.ModelSerializer):
class Meta:
@@ -177,7 +204,7 @@ Alternatively, specify the field explicitly on the serializer class:
#### Fields for model methods and properties.
-You can now specify field names in the `fields` option that refer to model methods or properties. For example, suppose you have the following model:
+With `ModelSerilizer` you can now specify field names in the `fields` option that refer to model methods or properties. For example, suppose you have the following model:
class Invitation(models.Model):
created = models.DateTimeField()
--
cgit v1.2.3
From 90311357add780433c79e97346ed85f1f4224877 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 12:18:27 +0100
Subject: Release notes
---
docs/topics/3.0-announcement.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 144d3550..daacbba0 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -137,13 +137,13 @@ For instance, given the following example model:
rating = models.IntegerField()
created_by = models.ForeignKey(User)
-Let's create a simple `ModelSerializer` class c.
+Let's create a simple `ModelSerializer` class corresponding to the `LocationRating` model.
class LocationRatingSerializer(serializer.ModelSerializer):
class Meta:
model = LocationRating
-We can now inspect its representation in the Django shell, using `python manage.py shell`...
+We can now inspect the serializer representation in the Django shell, using `python manage.py shell`...
>>> serializer = LocationRatingSerializer()
>>> print(serializer) # Or use `print serializer` in Python 2.x
--
cgit v1.2.3
From fde934d33c8692bab5e0e7b6009d358101a25dd7 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 12:21:05 +0100
Subject: Release notes
---
docs/topics/3.0-announcement.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index daacbba0..a4e4db14 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -21,6 +21,7 @@ Notable features of this new release include:
* Super simple default implementations for the generic views.
* Support for overriding how validation errors are handled by your API.
* A metadata API that allows you to customize how `OPTIONS` requests are handled by your API.
+* A more compact JSON output with unicode style encoding turned on by default.
Below is an in-depth guide to the API changes and migration notes for 3.0.
--
cgit v1.2.3
From 8b8623c5f84d443d26804cac52a793a3037a1dd0 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 12:48:20 +0100
Subject: Allow many, partial and context in BaseSerializer
---
docs/topics/3.0-announcement.md | 7 +++++
rest_framework/serializers.py | 28 +++++++++-----------
tests/test_serializer.py | 58 +++++++++++++++++++++++++++++++++++++++++
3 files changed, 77 insertions(+), 16 deletions(-)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index a4e4db14..faba2d35 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -2,6 +2,13 @@
See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-framework/pull/1800) for more details.
+Most notable outstanding issues still to resolved on the `version-3.0` branch.
+
+* `FileField` and `ImageField` support.
+* Forms support for serializers and in the browsable API.
+* Enforcing uniqueness on `unique=True` and `unique_together` fields.
+* Optimisations for serialializing primary keys.
+
# REST framework 3.0
The 3.0 release of Django REST framework is the result of almost four years of iteration and refinement. It comprehensively addresses some of the previous remaining design issues in serializers, fields and the generic views.
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 86bed773..245ec26f 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -47,9 +47,20 @@ class BaseSerializer(Field):
"""
def __init__(self, instance=None, data=None, **kwargs):
- super(BaseSerializer, self).__init__(**kwargs)
self.instance = instance
self._initial_data = data
+ self.partial = kwargs.pop('partial', False)
+ self._context = kwargs.pop('context', {})
+ kwargs.pop('many', None)
+ super(BaseSerializer, self).__init__(**kwargs)
+
+ def __new__(cls, *args, **kwargs):
+ # We override this method in order to automagically create
+ # `ListSerializer` classes instead when `many=True` is set.
+ if kwargs.pop('many', False):
+ kwargs['child'] = cls()
+ return ListSerializer(*args, **kwargs)
+ return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)
def to_internal_value(self, data):
raise NotImplementedError('`to_internal_value()` must be implemented.')
@@ -187,10 +198,6 @@ class BindingDict(object):
@six.add_metaclass(SerializerMetaclass)
class Serializer(BaseSerializer):
def __init__(self, *args, **kwargs):
- kwargs.pop('many', None)
- self.partial = kwargs.pop('partial', False)
- self._context = kwargs.pop('context', {})
-
super(Serializer, self).__init__(*args, **kwargs)
# Every new serializer is created with a clone of the field instances.
@@ -200,14 +207,6 @@ class Serializer(BaseSerializer):
for key, value in self._get_base_fields().items():
self.fields[key] = value
- def __new__(cls, *args, **kwargs):
- # We override this method in order to automagically create
- # `ListSerializer` classes instead when `many=True` is set.
- if kwargs.pop('many', False):
- kwargs['child'] = cls()
- return ListSerializer(*args, **kwargs)
- return super(Serializer, cls).__new__(cls, *args, **kwargs)
-
def _get_base_fields(self):
return copy.deepcopy(self._declared_fields)
@@ -296,9 +295,6 @@ class ListSerializer(BaseSerializer):
self.child = kwargs.pop('child', copy.deepcopy(self.child))
assert self.child is not None, '`child` is a required argument.'
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
- self.partial = kwargs.pop('partial', False)
- self._context = kwargs.pop('context', {})
-
super(ListSerializer, self).__init__(*args, **kwargs)
self.child.bind(field_name='', parent=self)
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index 256a12e6..4df1b736 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -43,6 +43,64 @@ class TestSerializer:
serializer.data
+class TestBaseSerializer:
+ def setup(self):
+ class ExampleSerializer(serializers.BaseSerializer):
+ def to_representation(self, obj):
+ return {
+ 'id': obj['id'],
+ 'email': obj['name'] + '@' + obj['domain']
+ }
+
+ def to_internal_value(self, data):
+ name, domain = str(data['email']).split('@')
+ return {
+ 'id': int(data['id']),
+ 'name': name,
+ 'domain': domain,
+ }
+
+ self.Serializer = ExampleSerializer
+
+ def test_serialize_instance(self):
+ instance = {'id': 1, 'name': 'tom', 'domain': 'example.com'}
+ serializer = self.Serializer(instance)
+ assert serializer.data == {'id': 1, 'email': 'tom@example.com'}
+
+ def test_serialize_list(self):
+ instances = [
+ {'id': 1, 'name': 'tom', 'domain': 'example.com'},
+ {'id': 2, 'name': 'ann', 'domain': 'example.com'},
+ ]
+ serializer = self.Serializer(instances, many=True)
+ assert serializer.data == [
+ {'id': 1, 'email': 'tom@example.com'},
+ {'id': 2, 'email': 'ann@example.com'}
+ ]
+
+ def test_validate_data(self):
+ data = {'id': 1, 'email': 'tom@example.com'}
+ serializer = self.Serializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'id': 1,
+ 'name': 'tom',
+ 'domain': 'example.com'
+ }
+
+ def test_validate_list(self):
+ data = [
+ {'id': 1, 'email': 'tom@example.com'},
+ {'id': 2, 'email': 'ann@example.com'},
+ ]
+ serializer = self.Serializer(data=data, many=True)
+ assert serializer.is_valid()
+ assert serializer.validated_data == [
+ {'id': 1, 'name': 'tom', 'domain': 'example.com'},
+ {'id': 2, 'name': 'ann', 'domain': 'example.com'}
+ ]
+
+
class TestStarredSource:
"""
Tests for `source='*'` argument, which is used for nested representations.
--
cgit v1.2.3
From 2e87de01430d7fec83f00948e60c8d61b317053b Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 13:08:20 +0100
Subject: Added ListField
---
rest_framework/fields.py | 38 ++++++++++++++++++++++++++++++++++++++
rest_framework/serializers.py | 6 ++++--
tests/test_fields.py | 19 +++++++++++++++++++
3 files changed, 61 insertions(+), 2 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index ec07a413..cf42d36c 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -881,6 +881,44 @@ class ImageField(Field):
# Advanced field types...
+class ListField(Field):
+ child = None
+ initial = []
+ default_error_messages = {
+ 'not_a_list': _('Expected a list of items but got type `{input_type}`')
+ }
+
+ def __init__(self, *args, **kwargs):
+ self.child = kwargs.pop('child', copy.deepcopy(self.child))
+ assert self.child is not None, '`child` is a required argument.'
+ assert not inspect.isclass(self.child), '`child` has not been instantiated.'
+ super(ListField, self).__init__(*args, **kwargs)
+ self.child.bind(field_name='', parent=self)
+
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # lists in HTML forms.
+ if html.is_html_input(dictionary):
+ return html.parse_html_list(dictionary, prefix=self.field_name)
+ return dictionary.get(self.field_name, empty)
+
+ def to_internal_value(self, data):
+ """
+ List of dicts of native values <- List of dicts of primitive datatypes.
+ """
+ if html.is_html_input(data):
+ data = html.parse_html_list(data)
+ if isinstance(data, type('')) or not hasattr(data, '__iter__'):
+ self.fail('not_a_list', input_type=type(data).__name__)
+ return [self.child.run_validation(item) for item in data]
+
+ def to_representation(self, data):
+ """
+ List of object instances -> List of dicts of primitive datatypes.
+ """
+ return [self.child.to_representation(item) for item in data]
+
+
class ReadOnlyField(Field):
"""
A read-only field that simply returns the field value.
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 245ec26f..fa2e8fb1 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -287,6 +287,9 @@ class Serializer(BaseSerializer):
return representation.serializer_repr(self, indent=1)
+# There's some replication of `ListField` here,
+# but that's probably better than obfuscating the call hierarchy.
+
class ListSerializer(BaseSerializer):
child = None
initial = []
@@ -301,7 +304,7 @@ class ListSerializer(BaseSerializer):
def get_value(self, dictionary):
# We override the default field access in order to support
# lists in HTML forms.
- if is_html_input(dictionary):
+ if html.is_html_input(dictionary):
return html.parse_html_list(dictionary, prefix=self.field_name)
return dictionary.get(self.field_name, empty)
@@ -311,7 +314,6 @@ class ListSerializer(BaseSerializer):
"""
if html.is_html_input(data):
data = html.parse_html_list(data)
-
return [self.child.run_validation(item) for item in data]
def to_representation(self, data):
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 1539a210..68112748 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -849,6 +849,25 @@ class TestMultipleChoiceField(FieldValues):
)
+class TestListField(FieldValues):
+ """
+ Values for `ListField`.
+ """
+ valid_inputs = [
+ ([1, 2, 3], [1, 2, 3]),
+ (['1', '2', '3'], [1, 2, 3])
+ ]
+ invalid_inputs = [
+ ('not a list', ['Expected a list of items but got type `str`']),
+ ([1, 2, 'error'], ['A valid integer is required.'])
+ ]
+ outputs = [
+ ([1, 2, 3], [1, 2, 3]),
+ (['1', '2', '3'], [1, 2, 3])
+ ]
+ field = fields.ListField(child=fields.IntegerField())
+
+
# Tests for SerializerMethodField.
# --------------------------------
--
cgit v1.2.3
From 0eb6a4de8a3293c3b326fadccf7aa0be67c2f5b5 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 13:10:58 +0100
Subject: Release notes
---
docs/topics/3.0-announcement.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index faba2d35..9769e884 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -8,6 +8,7 @@ Most notable outstanding issues still to resolved on the `version-3.0` branch.
* Forms support for serializers and in the browsable API.
* Enforcing uniqueness on `unique=True` and `unique_together` fields.
* Optimisations for serialializing primary keys.
+* Refine style of validation errors in some cases, such as validation errors in `ListField`.
# REST framework 3.0
--
cgit v1.2.3
From 24f7db2fc156b0af7749a5cc17c1df3f5522bf88 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 13:12:10 +0100
Subject: Release notes
---
docs/topics/3.0-announcement.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 9769e884..a48d22ea 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -2,13 +2,15 @@
See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-framework/pull/1800) for more details.
-Most notable outstanding issues still to resolved on the `version-3.0` branch.
+The most notable outstanding issues still to resolved on the `version-3.0` branch are as follows:
* `FileField` and `ImageField` support.
* Forms support for serializers and in the browsable API.
* Enforcing uniqueness on `unique=True` and `unique_together` fields.
* Optimisations for serialializing primary keys.
* Refine style of validation errors in some cases, such as validation errors in `ListField`.
+* `.validate()` method on fields.
+* `.transform_()` method on serializers.
# REST framework 3.0
--
cgit v1.2.3
From 33ccf40b76ddae790c34c294a133219e68efb946 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 13:14:08 +0100
Subject: Update version number
---
rest_framework/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 7f724c18..261c9c98 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -8,7 +8,7 @@ ______ _____ _____ _____ __
"""
__title__ = 'Django REST framework'
-__version__ = '2.4.3'
+__version__ = '3.0.0'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2014 Tom Christie'
--
cgit v1.2.3
From ee79b453974f3dc8ead83bff86784366d59a4fb1 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 13:19:32 +0100
Subject: Prepend some pre-release notes
---
docs/topics/3.0-announcement.md | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index a48d22ea..21052ca0 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -1,6 +1,10 @@
-**THIS DOCUMENT IS CURRENTLY A WORK IN PROGRESS**
+## Pre-release notes:
-See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-framework/pull/1800) for more details.
+The 3.0 release is now ready for some tentative testing and upgrades for super keen early adopters. You can install the development version directly from GitHub like so:
+
+ pip install https://github.com/tomchristie/django-rest-framework/archive/version-3.0.zip
+
+See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-framework/pull/1800) for more details on remaining work.
The most notable outstanding issues still to resolved on the `version-3.0` branch are as follows:
@@ -12,6 +16,12 @@ The most notable outstanding issues still to resolved on the `version-3.0` branc
* `.validate()` method on fields.
* `.transform_()` method on serializers.
+**Your feedback on the upgrade process and 3.0 changes is hugely important!**
+
+Please do get in touch via twitter, IRC, a GitHub ticket, or the discussion group.
+
+---
+
# REST framework 3.0
The 3.0 release of Django REST framework is the result of almost four years of iteration and refinement. It comprehensively addresses some of the previous remaining design issues in serializers, fields and the generic views.
--
cgit v1.2.3
From 8be4496586519f84b839b07efc74148a3559349e Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 13:59:37 +0100
Subject: Drop erronous print statements
---
tests/test_fields.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 68112748..342ae192 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -78,8 +78,6 @@ class TestSource:
class ExampleSerializer(serializers.Serializer):
example_field = serializers.CharField(source='other')
serializer = ExampleSerializer(data={'example_field': 'abc'})
- print serializer.is_valid()
- print serializer.data
assert serializer.is_valid()
assert serializer.validated_data == {'other': 'abc'}
--
cgit v1.2.3
From 609014460861fdfe82054551790d6439292dde7b Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 14:32:44 +0100
Subject: Simplify serialization slightly
---
rest_framework/fields.py | 11 +++++++----
rest_framework/serializers.py | 3 +--
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index cf42d36c..4c49aaba 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -202,12 +202,15 @@ class Field(object):
return self.default_empty_html if (ret == '') else ret
return dictionary.get(self.field_name, empty)
- def get_attribute(self, instance):
+ def get_field_representation(self, instance):
"""
- Given the *outgoing* object instance, return the value for this field
- that should be returned as a primative value.
+ Given the outgoing object instance, return the primative value
+ that should be used for this field.
"""
- return get_attribute(instance, self.source_attrs)
+ attribute = get_attribute(instance, self.source_attrs)
+ if attribute is None:
+ return None
+ return self.to_representation(attribute)
def get_default(self):
"""
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index fa2e8fb1..080b958d 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -268,8 +268,7 @@ class Serializer(BaseSerializer):
fields = [field for field in self.fields.values() if not field.write_only]
for field in fields:
- native_value = field.get_attribute(instance)
- ret[field.field_name] = field.to_representation(native_value)
+ ret[field.field_name] = field.get_field_representation(instance)
return ret
--
cgit v1.2.3
From dee3f78cb688b1bee892ef78d6eec23ccf67a80e Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 17:06:20 +0100
Subject: FileField and ImageField
---
requirements-test.txt | 1 -
rest_framework/compat.py | 9 -----
rest_framework/fields.py | 82 +++++++++++++++++++++++++++++++---------
rest_framework/settings.py | 3 +-
tests/test_fields.py | 93 ++++++++++++++++++++++++++++++++++++++++++++--
tox.ini | 16 --------
6 files changed, 156 insertions(+), 48 deletions(-)
diff --git a/requirements-test.txt b/requirements-test.txt
index d6ee5c6f..06c8849a 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -13,4 +13,3 @@ django-filter>=0.5.4
django-oauth-plus>=2.2.1
oauth2>=1.5.211
django-oauth2-provider>=0.2.4
-Pillow==2.3.0
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 7303c32a..89af9b48 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -84,15 +84,6 @@ except ImportError:
from collections import UserDict
from collections import MutableMapping as DictMixin
-# Try to import PIL in either of the two ways it can end up installed.
-try:
- from PIL import Image
-except ImportError:
- try:
- import Image
- except ImportError:
- Image = None
-
def get_model_name(model_cls):
try:
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 4c49aaba..f4b53279 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1,3 +1,4 @@
+from django import forms
from django.conf import settings
from django.core import validators
from django.core.exceptions import ValidationError
@@ -427,8 +428,6 @@ class CharField(Field):
return str(data)
def to_representation(self, value):
- if value is None:
- return None
return str(value)
@@ -446,8 +445,6 @@ class EmailField(CharField):
return str(data).strip()
def to_representation(self, value):
- if value is None:
- return None
return str(value).strip()
@@ -513,8 +510,6 @@ class IntegerField(Field):
return data
def to_representation(self, value):
- if value is None:
- return None
return int(value)
@@ -543,8 +538,6 @@ class FloatField(Field):
self.fail('invalid')
def to_representation(self, value):
- if value is None:
- return None
return float(value)
@@ -616,9 +609,6 @@ class DecimalField(Field):
return value
def to_representation(self, value):
- if value in (None, ''):
- return None
-
if not isinstance(value, decimal.Decimal):
value = decimal.Decimal(str(value).strip())
@@ -689,7 +679,7 @@ class DateTimeField(Field):
self.fail('invalid', format=humanized_format)
def to_representation(self, value):
- if value is None or self.format is None:
+ if self.format is None:
return value
if self.format.lower() == ISO_8601:
@@ -741,7 +731,7 @@ class DateField(Field):
self.fail('invalid', format=humanized_format)
def to_representation(self, value):
- if value is None or self.format is None:
+ if self.format is None:
return value
# Applying a `DateField` to a datetime value is almost always
@@ -795,7 +785,7 @@ class TimeField(Field):
self.fail('invalid', format=humanized_format)
def to_representation(self, value):
- if value is None or self.format is None:
+ if self.format is None:
return value
# Applying a `TimeField` to a datetime value is almost always
@@ -875,14 +865,68 @@ class MultipleChoiceField(ChoiceField):
# File types...
class FileField(Field):
- pass # TODO
+ default_error_messages = {
+ 'required': _("No file was submitted."),
+ 'invalid': _("The submitted data was not a file. Check the encoding type on the form."),
+ 'no_name': _("No filename could be determined."),
+ 'empty': _("The submitted file is empty."),
+ 'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'),
+ }
+ use_url = api_settings.UPLOADED_FILES_USE_URL
+ def __init__(self, *args, **kwargs):
+ self.max_length = kwargs.pop('max_length', None)
+ self.allow_empty_file = kwargs.pop('allow_empty_file', False)
+ self.use_url = kwargs.pop('use_url', self.use_url)
+ super(FileField, self).__init__(*args, **kwargs)
-class ImageField(Field):
- pass # TODO
+ def to_internal_value(self, data):
+ try:
+ # `UploadedFile` objects should have name and size attributes.
+ file_name = data.name
+ file_size = data.size
+ except AttributeError:
+ self.fail('invalid')
+ if not file_name:
+ self.fail('no_name')
+ if not self.allow_empty_file and not file_size:
+ self.fail('empty')
+ if self.max_length and len(file_name) > self.max_length:
+ self.fail('max_length', max_length=self.max_length, length=len(file_name))
-# Advanced field types...
+ return data
+
+ def to_representation(self, value):
+ if self.use_url:
+ return settings.MEDIA_URL + value.url
+ return value.name
+
+
+class ImageField(FileField):
+ default_error_messages = {
+ 'invalid_image': _(
+ 'Upload a valid image. The file you uploaded was either not an '
+ 'image or a corrupted image.'
+ ),
+ }
+
+ def __init__(self, *args, **kwargs):
+ self._DjangoImageField = kwargs.pop('_DjangoImageField', forms.ImageField)
+ super(ImageField, self).__init__(*args, **kwargs)
+
+ def to_internal_value(self, data):
+ # Image validation is a bit grungy, so we'll just outright
+ # defer to Django's implementation so we don't need to
+ # consider it, or treat PIL as a test dependancy.
+ file_object = super(ImageField, self).to_internal_value(data)
+ django_field = self._DjangoImageField()
+ django_field.error_messages = self.error_messages
+ django_field.to_python(file_object)
+ return file_object
+
+
+# Composite field types...
class ListField(Field):
child = None
@@ -922,6 +966,8 @@ class ListField(Field):
return [self.child.to_representation(item) for item in data]
+# Miscellaneous field types...
+
class ReadOnlyField(Field):
"""
A read-only field that simply returns the field value.
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index d7fb0a43..1e8c27fc 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -110,7 +110,8 @@ DEFAULTS = {
# Encoding
'UNICODE_JSON': True,
'COMPACT_JSON': True,
- 'COERCE_DECIMAL_TO_STRING': True
+ 'COERCE_DECIMAL_TO_STRING': True,
+ 'UPLOADED_FILES_USE_URL': True
}
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 342ae192..aa8c3a68 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1,4 +1,5 @@
from decimal import Decimal
+from django.core.exceptions import ValidationError
from django.utils import timezone
from rest_framework import fields, serializers
import datetime
@@ -516,7 +517,7 @@ class TestDecimalField(FieldValues):
Decimal('1.0'): '1.0',
Decimal('0.0'): '0.0',
Decimal('1.09'): '1.1',
- Decimal('0.04'): '0.0',
+ Decimal('0.04'): '0.0'
}
field = fields.DecimalField(max_digits=3, decimal_places=1)
@@ -576,7 +577,7 @@ class TestDateField(FieldValues):
datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
}
outputs = {
- datetime.date(2001, 1, 1): '2001-01-01',
+ datetime.date(2001, 1, 1): '2001-01-01'
}
field = fields.DateField()
@@ -639,7 +640,7 @@ class TestDateTimeField(FieldValues):
}
outputs = {
datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
- datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z',
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z'
}
field = fields.DateTimeField(default_timezone=timezone.UTC())
@@ -847,6 +848,92 @@ class TestMultipleChoiceField(FieldValues):
)
+# File fields...
+
+class MockFile:
+ def __init__(self, name='', size=0, url=''):
+ self.name = name
+ self.size = size
+ self.url = url
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, MockFile) and
+ self.name == other.name and
+ self.size == other.size and
+ self.url == other.url
+ )
+
+
+class TestFileField(FieldValues):
+ """
+ Values for `FileField`.
+ """
+ valid_inputs = [
+ (MockFile(name='example', size=10), MockFile(name='example', size=10))
+ ]
+ invalid_inputs = [
+ ('invalid', ['The submitted data was not a file. Check the encoding type on the form.']),
+ (MockFile(name='example.txt', size=0), ['The submitted file is empty.']),
+ (MockFile(name='', size=10), ['No filename could be determined.']),
+ (MockFile(name='x' * 100, size=10), ['Ensure this filename has at most 10 characters (it has 100).'])
+ ]
+ outputs = [
+ (MockFile(name='example.txt', url='/example.txt'), '/example.txt')
+ ]
+ field = fields.FileField(max_length=10)
+
+
+class TestFieldFieldWithName(FieldValues):
+ """
+ Values for `FileField` with a filename output instead of URLs.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = [
+ (MockFile(name='example.txt', url='/example.txt'), 'example.txt')
+ ]
+ field = fields.FileField(use_url=False)
+
+
+# Stub out mock Django `forms.ImageField` class so we don't *actually*
+# call into it's regular validation, or require PIL for testing.
+class FailImageValidation(object):
+ def to_python(self, value):
+ raise ValidationError(self.error_messages['invalid_image'])
+
+
+class PassImageValidation(object):
+ def to_python(self, value):
+ return value
+
+
+class TestInvalidImageField(FieldValues):
+ """
+ Values for an invalid `ImageField`.
+ """
+ valid_inputs = {}
+ invalid_inputs = [
+ (MockFile(name='example.txt', size=10), ['Upload a valid image. The file you uploaded was either not an image or a corrupted image.'])
+ ]
+ outputs = {}
+ field = fields.ImageField(_DjangoImageField=FailImageValidation)
+
+
+class TestValidImageField(FieldValues):
+ """
+ Values for an valid `ImageField`.
+ """
+ valid_inputs = [
+ (MockFile(name='example.txt', size=10), MockFile(name='example.txt', size=10))
+ ]
+ invalid_inputs = {}
+ outputs = {}
+ field = fields.ImageField(_DjangoImageField=PassImageValidation)
+
+
+# Composite fields...
+
class TestListField(FieldValues):
"""
Values for `ListField`.
diff --git a/tox.ini b/tox.ini
index d40a7079..5b9a0ffe 100644
--- a/tox.ini
+++ b/tox.ini
@@ -21,7 +21,6 @@ basepython = python3.4
deps = Django==1.7
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.3-django1.7]
@@ -29,7 +28,6 @@ basepython = python3.3
deps = Django==1.7
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.2-django1.7]
@@ -37,7 +35,6 @@ basepython = python3.2
deps = Django==1.7
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.7-django1.7]
@@ -49,7 +46,6 @@ deps = Django==1.7
# oauth2==1.5.211
# django-oauth2-provider==0.2.4
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.4-django1.6]
@@ -57,7 +53,6 @@ basepython = python3.4
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.3-django1.6]
@@ -65,7 +60,6 @@ basepython = python3.3
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.2-django1.6]
@@ -73,7 +67,6 @@ basepython = python3.2
deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.7-django1.6]
@@ -85,7 +78,6 @@ deps = Django==1.6.3
oauth2==1.5.211
django-oauth2-provider==0.2.4
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.6-django1.6]
@@ -97,7 +89,6 @@ deps = Django==1.6.3
oauth2==1.5.211
django-oauth2-provider==0.2.4
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.4-django1.5]
@@ -105,7 +96,6 @@ basepython = python3.4
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.3-django1.5]
@@ -113,7 +103,6 @@ basepython = python3.3
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py3.2-django1.5]
@@ -121,7 +110,6 @@ basepython = python3.2
deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.7-django1.5]
@@ -133,7 +121,6 @@ deps = django==1.5.6
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.6-django1.5]
@@ -145,7 +132,6 @@ deps = django==1.5.6
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.7-django1.4]
@@ -157,7 +143,6 @@ deps = django==1.4.11
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1
[testenv:py2.6-django1.4]
@@ -169,5 +154,4 @@ deps = django==1.4.11
oauth2==1.5.211
django-oauth2-provider==0.2.3
django-guardian==1.2.3
- Pillow==2.3.0
pytest-django==2.6.1
--
cgit v1.2.3
From ce04d59a53df45715c4805831406b2105c9594a8 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Fri, 26 Sep 2014 17:07:47 +0100
Subject: Update release notes
---
docs/topics/3.0-announcement.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 21052ca0..24f4ed4c 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -8,7 +8,6 @@ See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-fr
The most notable outstanding issues still to resolved on the `version-3.0` branch are as follows:
-* `FileField` and `ImageField` support.
* Forms support for serializers and in the browsable API.
* Enforcing uniqueness on `unique=True` and `unique_together` fields.
* Optimisations for serialializing primary keys.
--
cgit v1.2.3
From 43fd5a873051c99600386c1fdc9fa368edeb6eda Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 29 Sep 2014 09:24:03 +0100
Subject: Uniqueness validation
---
rest_framework/fields.py | 4 +++
rest_framework/utils/field_mapping.py | 5 +++
rest_framework/validators.py | 57 +++++++++++++++++++++++++++++++++++
tests/test_validators.py | 35 +++++++++++++++++++++
4 files changed, 101 insertions(+)
create mode 100644 rest_framework/validators.py
create mode 100644 tests/test_validators.py
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index f4b53279..231f693c 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -150,6 +150,10 @@ class Field(object):
messages.update(error_messages or {})
self.error_messages = messages
+ for validator in validators:
+ if getattr(validator, 'requires_context', False):
+ validator.serializer_field = self
+
def bind(self, field_name, parent):
"""
Initializes the field name and parent for the field instance.
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index c3794083..cf9d910a 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -6,6 +6,7 @@ from django.core import validators
from django.db import models
from django.utils.text import capfirst
from rest_framework.compat import clean_manytomany_helptext
+from rest_framework.validators import UniqueValidator
import inspect
@@ -156,6 +157,10 @@ def get_field_kwargs(field_name, model_field):
if validator is not validators.validate_slug
]
+ if getattr(model_field, 'unique', False):
+ validator = UniqueValidator(queryset=model_field.model._default_manager)
+ validator_kwarg.append(validator)
+
max_digits = getattr(model_field, 'max_digits', None)
if max_digits is not None:
kwargs['max_digits'] = max_digits
diff --git a/rest_framework/validators.py b/rest_framework/validators.py
new file mode 100644
index 00000000..f5fbeb3c
--- /dev/null
+++ b/rest_framework/validators.py
@@ -0,0 +1,57 @@
+from django.core.exceptions import ValidationError
+
+
+class UniqueValidator:
+ # Validators with `requires_context` will have the field instance
+ # passed to them when the field is instantiated.
+ requires_context = True
+
+ def __init__(self, queryset):
+ self.queryset = queryset
+ self.serializer_field = None
+
+ def get_queryset(self):
+ return self.queryset.all()
+
+ def __call__(self, value):
+ field = self.serializer_field
+
+ # Determine the model field name that the serializer field corresponds to.
+ field_name = field.source_attrs[0] if field.source_attrs else field.field_name
+
+ # Determine the existing instance, if this is an update operation.
+ instance = getattr(field.parent, 'instance', None)
+
+ # Ensure uniqueness.
+ filter_kwargs = {field_name: value}
+ queryset = self.get_queryset().filter(**filter_kwargs)
+ if instance:
+ queryset = queryset.exclude(pk=instance.pk)
+ if queryset.exists():
+ raise ValidationError('This field must be unique.')
+
+
+class UniqueTogetherValidator:
+ requires_context = True
+
+ def __init__(self, queryset, fields):
+ self.queryset = queryset
+ self.fields = fields
+ self.serializer_field = None
+
+ def __call__(self, value):
+ serializer = self.serializer_field
+
+ # Determine the existing instance, if this is an update operation.
+ instance = getattr(serializer, 'instance', None)
+
+ # Ensure uniqueness.
+ filter_kwargs = dict([
+ (field_name, value[field_name]) for field_name in self.fields
+ ])
+ queryset = self.get_queryset().filter(**filter_kwargs)
+ if instance:
+ queryset = queryset.exclude(pk=instance.pk)
+ if queryset.exists():
+ field_names = ' and '.join(self.fields)
+ raise ValidationError('The fields %s must make a unique set.' % field_names)
diff --git a/tests/test_validators.py b/tests/test_validators.py
new file mode 100644
index 00000000..a1366a1a
--- /dev/null
+++ b/tests/test_validators.py
@@ -0,0 +1,35 @@
+from django.db import models
+from django.test import TestCase
+from rest_framework import serializers
+
+
+class ExampleModel(models.Model):
+ username = models.CharField(unique=True, max_length=100)
+
+
+class ExampleSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ExampleModel
+
+
+class TestUniquenessValidation(TestCase):
+ def setUp(self):
+ self.instance = ExampleModel.objects.create(username='existing')
+
+ def test_is_not_unique(self):
+ data = {'username': 'existing'}
+ serializer = ExampleSerializer(data=data)
+ assert not serializer.is_valid()
+ assert serializer.errors == {'username': ['This field must be unique.']}
+
+ def test_is_unique(self):
+ data = {'username': 'other'}
+ serializer = ExampleSerializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'username': 'other'}
+
+ def test_updated_instance_excluded(self):
+ data = {'username': 'existing'}
+ serializer = ExampleSerializer(self.instance, data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'username': 'existing'}
--
cgit v1.2.3
From 9805a085fb115785f272489dc24b51ba6f8e6329 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 29 Sep 2014 11:23:02 +0100
Subject: UniqueTogetherValidator
---
rest_framework/serializers.py | 80 +++++++++++++++++++++++---
rest_framework/validators.py | 38 ++++++++++---
tests/test_validators.py | 129 +++++++++++++++++++++++++++++++++++++++---
3 files changed, 223 insertions(+), 24 deletions(-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 080b958d..09ad376a 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -23,6 +23,7 @@ from rest_framework.utils.field_mapping import (
get_relation_kwargs, get_nested_relation_kwargs,
ClassLookupDict
)
+from rest_framework.validators import UniqueTogetherValidator
import copy
import inspect
@@ -95,7 +96,7 @@ class BaseSerializer(Field):
def is_valid(self, raise_exception=False):
if not hasattr(self, '_validated_data'):
try:
- self._validated_data = self.to_internal_value(self._initial_data)
+ self._validated_data = self.run_validation(self._initial_data)
except ValidationError as exc:
self._validated_data = {}
self._errors = exc.message_dict
@@ -223,15 +224,43 @@ class Serializer(BaseSerializer):
return html.parse_html_dict(dictionary, prefix=self.field_name)
return dictionary.get(self.field_name, empty)
- def to_internal_value(self, data):
+ def run_validation(self, data=empty):
"""
- Dict of native values <- Dict of primitive datatypes.
+ We override the default `run_validation`, because the validation
+ performed by validators and the `.validate()` method should
+ be coerced into an error dictionary with a 'non_fields_error' key.
"""
+ if data is empty:
+ if getattr(self.root, 'partial', False):
+ raise SkipField()
+ if self.required:
+ self.fail('required')
+ return self.get_default()
+
+ if data is None:
+ if not self.allow_null:
+ self.fail('null')
+ return None
+
if not isinstance(data, dict):
raise ValidationError({
api_settings.NON_FIELD_ERRORS_KEY: ['Invalid data']
})
+ value = self.to_internal_value(data)
+ try:
+ self.run_validators(value)
+ self.validate(value)
+ except ValidationError as exc:
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: exc.messages
+ })
+ return value
+
+ def to_internal_value(self, data):
+ """
+ Dict of native values <- Dict of primitive datatypes.
+ """
ret = {}
errors = {}
fields = [field for field in self.fields.values() if not field.read_only]
@@ -253,12 +282,7 @@ class Serializer(BaseSerializer):
if errors:
raise ValidationError(errors)
- try:
- return self.validate(ret)
- except ValidationError as exc:
- raise ValidationError({
- api_settings.NON_FIELD_ERRORS_KEY: exc.messages
- })
+ return ret
def to_representation(self, instance):
"""
@@ -355,6 +379,14 @@ class ModelSerializer(Serializer):
})
_related_class = PrimaryKeyRelatedField
+ def __init__(self, *args, **kwargs):
+ super(ModelSerializer, self).__init__(*args, **kwargs)
+ if 'validators' not in kwargs:
+ validators = self.get_unique_together_validators()
+ if validators:
+ self.validators.extend(validators)
+ self._kwargs['validators'] = validators
+
def create(self, attrs):
ModelClass = self.Meta.model
@@ -381,6 +413,36 @@ class ModelSerializer(Serializer):
setattr(obj, attr, value)
obj.save()
+ def get_unique_together_validators(self):
+ field_names = set([
+ field.source for field in self.fields.values()
+ if (field.source != '*') and ('.' not in field.source)
+ ])
+
+ validators = []
+ model_class = self.Meta.model
+
+ for unique_together in model_class._meta.unique_together:
+ if field_names.issuperset(set(unique_together)):
+ validator = UniqueTogetherValidator(
+ queryset=model_class._default_manager,
+ fields=unique_together
+ )
+ validator.serializer_field = self
+ validators.append(validator)
+
+ for parent_class in model_class._meta.parents.keys():
+ for unique_together in parent_class._meta.unique_together:
+ if field_names.issuperset(set(unique_together)):
+ validator = UniqueTogetherValidator(
+ queryset=parent_class._default_manager,
+ fields=unique_together
+ )
+ validator.serializer_field = self
+ validators.append(validator)
+
+ return validators
+
def _get_base_fields(self):
declared_fields = copy.deepcopy(self._declared_fields)
diff --git a/rest_framework/validators.py b/rest_framework/validators.py
index f5fbeb3c..20de4b42 100644
--- a/rest_framework/validators.py
+++ b/rest_framework/validators.py
@@ -1,18 +1,26 @@
+"""
+We perform uniqueness checks explicitly on the serializer class, rather
+the using Django's `.full_clean()`.
+
+This gives us better seperation of concerns, allows us to use single-step
+object creation, and makes it possible to switch between using the implicit
+`ModelSerializer` class and an equivelent explicit `Serializer` class.
+"""
from django.core.exceptions import ValidationError
+from django.utils.translation import ugettext_lazy as _
+from rest_framework.utils.representation import smart_repr
class UniqueValidator:
# Validators with `requires_context` will have the field instance
# passed to them when the field is instantiated.
requires_context = True
+ message = _('This field must be unique.')
def __init__(self, queryset):
self.queryset = queryset
self.serializer_field = None
- def get_queryset(self):
- return self.queryset.all()
-
def __call__(self, value):
field = self.serializer_field
@@ -24,15 +32,22 @@ class UniqueValidator:
# Ensure uniqueness.
filter_kwargs = {field_name: value}
- queryset = self.get_queryset().filter(**filter_kwargs)
+ queryset = self.queryset.filter(**filter_kwargs)
if instance:
queryset = queryset.exclude(pk=instance.pk)
if queryset.exists():
- raise ValidationError('This field must be unique.')
+ raise ValidationError(self.message)
+
+ def __repr__(self):
+ return '<%s(queryset=%s)>' % (
+ self.__class__.__name__,
+ smart_repr(self.queryset)
+ )
class UniqueTogetherValidator:
requires_context = True
+ message = _('The fields {field_names} must make a unique set.')
def __init__(self, queryset, fields):
self.queryset = queryset
@@ -49,9 +64,16 @@ class UniqueTogetherValidator:
filter_kwargs = dict([
(field_name, value[field_name]) for field_name in self.fields
])
- queryset = self.get_queryset().filter(**filter_kwargs)
+ queryset = self.queryset.filter(**filter_kwargs)
if instance:
queryset = queryset.exclude(pk=instance.pk)
if queryset.exists():
- field_names = ' and '.join(self.fields)
- raise ValidationError('The fields %s must make a unique set.' % field_names)
+ field_names = ', '.join(self.fields)
+ raise ValidationError(self.message.format(field_names=field_names))
+
+ def __repr__(self):
+ return '<%s(queryset=%s, fields=%s)>' % (
+ self.__class__.__name__,
+ smart_repr(self.queryset),
+ smart_repr(self.fields)
+ )
diff --git a/tests/test_validators.py b/tests/test_validators.py
index a1366a1a..c35ecb51 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -3,33 +3,148 @@ from django.test import TestCase
from rest_framework import serializers
-class ExampleModel(models.Model):
+def dedent(blocktext):
+ return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]])
+
+
+# Tests for `UniqueValidator`
+# ---------------------------
+
+class UniquenessModel(models.Model):
username = models.CharField(unique=True, max_length=100)
-class ExampleSerializer(serializers.ModelSerializer):
+class UniquenessSerializer(serializers.ModelSerializer):
class Meta:
- model = ExampleModel
+ model = UniquenessModel
class TestUniquenessValidation(TestCase):
def setUp(self):
- self.instance = ExampleModel.objects.create(username='existing')
+ self.instance = UniquenessModel.objects.create(username='existing')
+
+ def test_repr(self):
+ serializer = UniquenessSerializer()
+ expected = dedent("""
+ UniquenessSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ username = CharField(max_length=100, validators=[])
+ """)
+ assert repr(serializer) == expected
def test_is_not_unique(self):
data = {'username': 'existing'}
- serializer = ExampleSerializer(data=data)
+ serializer = UniquenessSerializer(data=data)
assert not serializer.is_valid()
assert serializer.errors == {'username': ['This field must be unique.']}
def test_is_unique(self):
data = {'username': 'other'}
- serializer = ExampleSerializer(data=data)
+ serializer = UniquenessSerializer(data=data)
assert serializer.is_valid()
assert serializer.validated_data == {'username': 'other'}
def test_updated_instance_excluded(self):
data = {'username': 'existing'}
- serializer = ExampleSerializer(self.instance, data=data)
+ serializer = UniquenessSerializer(self.instance, data=data)
assert serializer.is_valid()
assert serializer.validated_data == {'username': 'existing'}
+
+
+# Tests for `UniqueTogetherValidator`
+# -----------------------------------
+
+class UniquenessTogetherModel(models.Model):
+ race_name = models.CharField(max_length=100)
+ position = models.IntegerField()
+
+ class Meta:
+ unique_together = ('race_name', 'position')
+
+
+class UniquenessTogetherSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = UniquenessTogetherModel
+
+
+class TestUniquenessTogetherValidation(TestCase):
+ def setUp(self):
+ self.instance = UniquenessTogetherModel.objects.create(
+ race_name='example',
+ position=1
+ )
+ UniquenessTogetherModel.objects.create(
+ race_name='example',
+ position=2
+ )
+ UniquenessTogetherModel.objects.create(
+ race_name='other',
+ position=1
+ )
+
+ def test_repr(self):
+ serializer = UniquenessTogetherSerializer()
+ expected = dedent("""
+ UniquenessTogetherSerializer(validators=[]):
+ id = IntegerField(label='ID', read_only=True)
+ race_name = CharField(max_length=100)
+ position = IntegerField()
+ """)
+ assert repr(serializer) == expected
+
+ def test_is_not_unique_together(self):
+ """
+ Failing unique together validation should result in non field errors.
+ """
+ data = {'race_name': 'example', 'position': 2}
+ serializer = UniquenessTogetherSerializer(data=data)
+ print serializer.validators
+ assert not serializer.is_valid()
+ assert serializer.errors == {
+ 'non_field_errors': [
+ 'The fields race_name, position must make a unique set.'
+ ]
+ }
+
+ def test_is_unique_together(self):
+ """
+ In a unique together validation, one field may be non-unique
+ so long as the set as a whole is unique.
+ """
+ data = {'race_name': 'other', 'position': 2}
+ serializer = UniquenessTogetherSerializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'race_name': 'other',
+ 'position': 2
+ }
+
+ def test_updated_instance_excluded_from_unique_together(self):
+ """
+ When performing an update, the existing instance does not count
+ as a match against uniqueness.
+ """
+ data = {'race_name': 'example', 'position': 1}
+ serializer = UniquenessTogetherSerializer(self.instance, data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'race_name': 'example',
+ 'position': 1
+ }
+
+ def test_ignore_exlcuded_fields(self):
+ """
+ When model fields are not included in a serializer, then uniqueness
+ validtors should not be added for that field.
+ """
+ class ExcludedFieldSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = UniquenessTogetherModel
+ fields = ('id', 'race_name',)
+ serializer = ExcludedFieldSerializer()
+ expected = dedent("""
+ ExcludedFieldSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ race_name = CharField(max_length=100)
+ """)
+ assert repr(serializer) == expected
--
cgit v1.2.3
From d2d412993f537952fd7809ded3e981f85ec318e9 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 29 Sep 2014 11:24:21 +0100
Subject: .validate() on serializer fields
---
rest_framework/fields.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 231f693c..fee6080a 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -254,6 +254,7 @@ class Field(object):
value = self.to_internal_value(data)
self.run_validators(value)
+ self.validate(value)
return value
def run_validators(self, value):
@@ -270,6 +271,9 @@ class Field(object):
if errors:
raise ValidationError(errors)
+ def validate(self, value):
+ pass
+
def to_internal_value(self, data):
"""
Transform the *incoming* primative data into a native value.
--
cgit v1.2.3
From 4798df52df5d59cc570043e3eb7e26f7ce57b54f Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 29 Sep 2014 12:57:05 +0100
Subject: Update release notes
---
docs/topics/3.0-announcement.md | 44 ++++++++++++++++++++++++++++++++++++-----
tests/test_validators.py | 1 -
2 files changed, 39 insertions(+), 6 deletions(-)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 24f4ed4c..92062552 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -146,7 +146,41 @@ The corresponding code would now look like this:
extras = {'user': request.user} # Include the user when saving.
serializer.save(extras=extras)
-#### Printable serializer reprensentations.
+#### Limitations of ModelSerializer validation.
+
+This change also means that we no longer use the `.full_clean()` method on model instances, but instead perform all validation explicitly on the serializer. This gives a cleaner seperation, and ensures that there's no automatic validation behavior on `ModelSerializer` classes that can't also be easily replicated on regular `Serializer` classes.
+
+This change comes with the following limitations:
+
+* The model `.clean()` method will not be called as part of serializer validation. Use the serializer `.validate()` method to perform a final validation step on incoming data where required.
+* The `.unique_for_date`, `.unique_for_month` and `.unique_for_year` options on model fields are not automatically validated. Again, you'll need to handle these explicitly on the serializer if required.
+
+#### Writable nested serialization.
+
+REST framework 2.x attempted to automatically support writable nested serialization, but the behavior was complex and non-obvious. Attempting to automatically handle these case is problematic:
+
+* There can be complex dependancies involved in order of saving multiple related model instances.
+* It's unclear what behavior the user should expect when related models are passed `None` data.
+* It's unclear how the user should expect to-many relationships to handle updates, creations and deletions of multiple records.
+
+Using the `depth` option on `ModelSerializer` will now create **read-only nested serializers** by default. To use writable nested serialization you'll want to declare a nested field on the serializer class, and write the `create()` and/or `update()` methods explicitly.
+
+ class UserSerializer(serializers.ModelSerializer):
+ profile = ProfileSerializer()
+
+ class Meta:
+ model = User
+ fields = ('username', 'email', 'profile')
+
+ def create(self, validated_data):
+ profile_data = validated_data.pop['profile']
+ user = User.objects.create(**validated_data)
+ profile = Profile.objects.create(user=user, **profile_data)
+ return user
+
+The single-step object creation makes this far simpler and more obvious than the previous `.restore_object()` behavior.
+
+#### Printable serializer representations.
Serializer instances now support a printable representation that allows you to inspect the fields present on the instance.
@@ -279,7 +313,7 @@ There are four mathods that can be overriding, depending on what functionality y
* `.to_internal_value()` - Override this to support deserialization, for write operations.
* `.create()` and `.update()` - Overide either or both of these to support saving instances.
-##### Read-only serializers.
+##### Read-only `BaseSerializer` classes.
To implement a read-only serializer using the `BaseSerializer` class, we just need to override the `.to_representation()` method. Let's take a look at an example using a simple Django model:
@@ -313,7 +347,7 @@ Or use it to serialize multiple instances:
serializer = HighScoreSerializer(queryset, many=True)
return Response(serializer.data)
-##### Read-write serializers.
+##### Read-write `BaseSerializer` classes.
To create a read-write serializer we first need to implement a `.to_internal_value()` method. This method returns the validated values that will be used to construct the object instance, and may raise a `ValidationError` if the supplied data is in an incorrect format.
@@ -358,9 +392,9 @@ Here's a complete example of our previous `HighScoreSerializer`, that's been upd
def create(self, validated_data):
return HighScore.objects.create(**validated_data)
-#### Creating new base classes with `BaseSerializer`.
+#### Creating new generic serializers with `BaseSerializer`.
-The `BaseSerializer` class is also useful if you want to implement new generic serializer classes for dealing with particular serialization styles or for integrating with different storage backends.
+The `BaseSerializer` class is also useful if you want to implement new generic serializer classes for dealing with particular serialization styles, or for integrating with alternative storage backends.
The following class is an example of a generic serializer that can handle coercing aribitrary objects into primative representations.
diff --git a/tests/test_validators.py b/tests/test_validators.py
index c35ecb51..ac04d2b4 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -98,7 +98,6 @@ class TestUniquenessTogetherValidation(TestCase):
"""
data = {'race_name': 'example', 'position': 2}
serializer = UniquenessTogetherSerializer(data=data)
- print serializer.validators
assert not serializer.is_valid()
assert serializer.errors == {
'non_field_errors': [
--
cgit v1.2.3
From 657d1de032bfa392609d53751e89366b972cd678 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 29 Sep 2014 14:12:09 +0100
Subject: Latest release notes
---
docs/topics/3.0-announcement.md | 39 +++++++++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 92062552..584c4979 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -560,6 +560,35 @@ The following usage will *now raise an error*:
email = serializers.EmailField(source='email')
+#### The `UniqueValidator` and `UniqueTogetherValidator` classes.
+
+REST framework now provides two new validators that allow you to ensure field uniqueness, while still using a completely explicit `Serializer` class instead of using `ModelSerializer`.
+
+The `UniqueValidator` should be applied to a serializer field, and takes a single `queryset` argument.
+
+ from rest_framework import serializers
+ from rest_framework.validators import UniqueValidator
+
+ class OrganizationSerializer(serializers.Serializer):
+ url = serializers.HyperlinkedIdentityField(view_name='organisation_detail')
+ created = serializers.DateTimeField(read_only=True)
+ name = serializers.CharField(
+ max_length=100,
+ validators=UniqueValidator(queryset=Organisation.objects.all())
+ )
+
+The `UniqueTogetherValidator` should be applied to a serializer, and takes a `queryset` argument and a `fields` argument which should be a list or tuple of field names.
+
+ class RaceResultSerializer(serializers.Serializer):
+ category = serializers.ChoiceField(['5k', '10k'])
+ position = serializers.IntegerField()
+ name = serializers.CharField(max_length=100)
+
+ default_validators = [UniqueTogetherValidator(
+ queryset=RaceResult.objects.all(),
+ fields=('category', 'position')
+ )]
+
## Generic views
#### Simplification of view logic.
@@ -633,6 +662,16 @@ The `COMPACT_JSON` setting has been added, and can be used to revert this behavi
'COMPACT_JSON': False
}
+#### File fields as URLs
+
+The `FileField` and `ImageField` classes are now represented as URLs by default. You should ensure you set Django's standard `MEDIA_URL` setting appropriately.
+
+You can revert this behavior, and display filenames as the representation, using the `UPLOADED_FILES_USE_URL` settings key:
+
+ REST_FRAMEWORK = {
+ 'UPLOADED_FILES_USE_URL': False
+ }
+
#### Throttle headers using `Retry-After`.
The custom `X-Throttle-Wait-Second` header has now been dropped in favor of the standard `Retry-After` header. You can revert this behavior if needed by writing a custom exception handler for your application.
--
cgit v1.2.3
From d1b2c8ac7faec65483cbddf4f1718ca4f5805246 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 29 Sep 2014 14:12:26 +0100
Subject: Absolute URLs for file fields
---
rest_framework/fields.py | 12 +++++++-----
rest_framework/serializers.py | 2 --
tests/test_fields.py | 17 +++++++++++++++++
3 files changed, 24 insertions(+), 7 deletions(-)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index fee6080a..f7ea3b0c 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -150,10 +150,6 @@ class Field(object):
messages.update(error_messages or {})
self.error_messages = messages
- for validator in validators:
- if getattr(validator, 'requires_context', False):
- validator.serializer_field = self
-
def bind(self, field_name, parent):
"""
Initializes the field name and parent for the field instance.
@@ -264,6 +260,8 @@ class Field(object):
"""
errors = []
for validator in self.validators:
+ if getattr(validator, 'requires_context', False):
+ validator.serializer_field = self
try:
validator(value)
except ValidationError as exc:
@@ -907,7 +905,11 @@ class FileField(Field):
def to_representation(self, value):
if self.use_url:
- return settings.MEDIA_URL + value.url
+ url = settings.MEDIA_URL + value.url
+ request = self.context.get('request', None)
+ if request is not None:
+ return request.build_absolute_uri(url)
+ return url
return value.name
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 09ad376a..0faa5671 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -428,7 +428,6 @@ class ModelSerializer(Serializer):
queryset=model_class._default_manager,
fields=unique_together
)
- validator.serializer_field = self
validators.append(validator)
for parent_class in model_class._meta.parents.keys():
@@ -438,7 +437,6 @@ class ModelSerializer(Serializer):
queryset=parent_class._default_manager,
fields=unique_together
)
- validator.serializer_field = self
validators.append(validator)
return validators
diff --git a/tests/test_fields.py b/tests/test_fields.py
index aa8c3a68..bbd9f93d 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -953,6 +953,23 @@ class TestListField(FieldValues):
field = fields.ListField(child=fields.IntegerField())
+# Tests for FieldField.
+# ---------------------
+
+class MockRequest:
+ def build_absolute_uri(self, value):
+ return 'http://example.com' + value
+
+
+class TestFileFieldContext:
+ def test_fully_qualified_when_request_in_context(self):
+ field = fields.FileField(max_length=10)
+ field._context = {'request': MockRequest()}
+ obj = MockFile(name='example.txt', url='/example.txt')
+ value = field.to_representation(obj)
+ assert value == 'http://example.com/example.txt'
+
+
# Tests for SerializerMethodField.
# --------------------------------
--
cgit v1.2.3
From 83a5ea8db27b9452a5539f1e0574f493392a91ad Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 29 Sep 2014 21:17:13 +0100
Subject: Update release notes
---
docs/topics/3.0-announcement.md | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 584c4979..12ab5a0d 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -664,14 +664,26 @@ The `COMPACT_JSON` setting has been added, and can be used to revert this behavi
#### File fields as URLs
-The `FileField` and `ImageField` classes are now represented as URLs by default. You should ensure you set Django's standard `MEDIA_URL` setting appropriately.
+The `FileField` and `ImageField` classes are now represented as URLs by default. You should ensure you set Django's [standard `MEDIA_URL` setting](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-MEDIA_URL) appropriately, and ensure your application [serves the uploaded files](https://docs.djangoproject.com/en/dev/howto/static-files/#serving-uploaded-files-in-development).
-You can revert this behavior, and display filenames as the representation, using the `UPLOADED_FILES_USE_URL` settings key:
+You can revert this behavior, and display filenames in the representation by using the `UPLOADED_FILES_USE_URL` settings key:
REST_FRAMEWORK = {
'UPLOADED_FILES_USE_URL': False
}
+You can also modify serializer fields individually, using the `use_url` argument:
+
+ uploaded_file = serializers.FileField(user_url=False)
+
+Also note that you should pass the `request` object to the serializer as context when instantiating it, so that a fully qualified URL can be returned. Returned URLs will then be of the form `https://example.com/url_path/filename.txt`. For example:
+
+ context = {'request': request}
+ serializer = ExampleSerializer(instance, context=context)
+ return Response(serializer.data)
+
+If the request is omitted from the context, the returned URLs will be of the form `/url_path/filename.txt`.
+
#### Throttle headers using `Retry-After`.
The custom `X-Throttle-Wait-Second` header has now been dropped in favor of the standard `Retry-After` header. You can revert this behavior if needed by writing a custom exception handler for your application.
--
cgit v1.2.3
From da4900a353bd1136aa96cb5444b34f7beefa8e85 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 30 Sep 2014 11:10:13 +0100
Subject: Update 3.0-announcement.md
---
docs/topics/3.0-announcement.md | 2 --
1 file changed, 2 deletions(-)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 12ab5a0d..1c7e016e 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -9,10 +9,8 @@ See the [Version 3.0 GitHub issue](https://github.com/tomchristie/django-rest-fr
The most notable outstanding issues still to resolved on the `version-3.0` branch are as follows:
* Forms support for serializers and in the browsable API.
-* Enforcing uniqueness on `unique=True` and `unique_together` fields.
* Optimisations for serialializing primary keys.
* Refine style of validation errors in some cases, such as validation errors in `ListField`.
-* `.validate()` method on fields.
* `.transform_()` method on serializers.
**Your feedback on the upgrade process and 3.0 changes is hugely important!**
--
cgit v1.2.3
From 770d63fb046917f9fe1f08449f07bf13f1adfa4f Mon Sep 17 00:00:00 2001
From: Omer Katz
Date: Wed, 1 Oct 2014 13:12:33 +0300
Subject: Fixed documentation typo.
---
docs/topics/3.0-announcement.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 1c7e016e..d2505a1b 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -75,7 +75,7 @@ Previously the serializers used a two-step object creation, as follows:
This style is in line with how the `ModelForm` class works in Django, but is problematic for a number of reasons:
-* Some data, such as many-to-many relationships, cannot be added to the object instance until after it has been save. This type of data needed to be hidden in some undocumentated state on the object instance, or kept as state on the serializer instance so that it could be used when `.save()` is called.
+* Some data, such as many-to-many relationships, cannot be added to the object instance until after it has been saved. This type of data needed to be hidden in some undocumentated state on the object instance, or kept as state on the serializer instance so that it could be used when `.save()` is called.
* Instantiating model instances directly means that you cannot use model manager classes for instance creation, eg `ExampleModel.objects.create(...)`. Manager classes are an excellent layer at which to enforce business logic and application-level data constraints.
* The two step process makes it unclear where to put deserialization logic. For example, should extra attributes such as the current user get added to the instance during object creation or during object save?
--
cgit v1.2.3
From 381771731f48c75e7d5951e353049cceec386512 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 1 Oct 2014 13:09:14 +0100
Subject: Use six.text_type instead of str everywhere
---
rest_framework/compat.py | 9 +++++----
rest_framework/fields.py | 22 +++++++++++-----------
rest_framework/filters.py | 3 ++-
rest_framework/generics.py | 5 +++--
rest_framework/parsers.py | 3 ++-
rest_framework/relations.py | 3 ++-
rest_framework/reverse.py | 3 ++-
rest_framework/utils/encoders.py | 6 +++---
8 files changed, 30 insertions(+), 24 deletions(-)
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 89af9b48..3993cee6 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -5,11 +5,12 @@ versions of django/python, and compatibility wrappers around optional packages.
# flake8: noqa
from __future__ import unicode_literals
-import django
-import inspect
+
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
from django.utils import six
+import django
+import inspect
# Handle django.utils.encoding rename in 1.5 onwards.
@@ -177,12 +178,12 @@ class RequestFactory(DjangoRequestFactory):
r = {
'PATH_INFO': self._get_path(parsed),
'QUERY_STRING': force_text(parsed[4]),
- 'REQUEST_METHOD': str(method),
+ 'REQUEST_METHOD': six.text_type(method),
}
if data:
r.update({
'CONTENT_LENGTH': len(data),
- 'CONTENT_TYPE': str(content_type),
+ 'CONTENT_TYPE': six.text_type(content_type),
'wsgi.input': FakePayload(data),
})
elif django.VERSION <= (1, 4):
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index f7ea3b0c..f3ff2233 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -2,7 +2,7 @@ from django import forms
from django.conf import settings
from django.core import validators
from django.core.exceptions import ValidationError
-from django.utils import timezone
+from django.utils import six, timezone
from django.utils.datastructures import SortedDict
from django.utils.dateparse import parse_date, parse_datetime, parse_time
from django.utils.encoding import is_protected_type
@@ -431,10 +431,10 @@ class CharField(Field):
return super(CharField, self).run_validation(data)
def to_internal_value(self, data):
- return str(data)
+ return six.text_type(data)
def to_representation(self, value):
- return str(value)
+ return six.text_type(value)
class EmailField(CharField):
@@ -448,10 +448,10 @@ class EmailField(CharField):
self.validators.append(validator)
def to_internal_value(self, data):
- return str(data).strip()
+ return six.text_type(data).strip()
def to_representation(self, value):
- return str(value).strip()
+ return six.text_type(value).strip()
class RegexField(CharField):
@@ -510,7 +510,7 @@ class IntegerField(Field):
def to_internal_value(self, data):
try:
- data = int(str(data))
+ data = int(six.text_type(data))
except (ValueError, TypeError):
self.fail('invalid')
return data
@@ -616,7 +616,7 @@ class DecimalField(Field):
def to_representation(self, value):
if not isinstance(value, decimal.Decimal):
- value = decimal.Decimal(str(value).strip())
+ value = decimal.Decimal(six.text_type(value).strip())
context = decimal.getcontext().copy()
context.prec = self.max_digits
@@ -832,19 +832,19 @@ class ChoiceField(Field):
# Allows us to deal with eg. integer choices while supporting either
# integer or string input, but still get the correct datatype out.
self.choice_strings_to_values = dict([
- (str(key), key) for key in self.choices.keys()
+ (six.text_type(key), key) for key in self.choices.keys()
])
super(ChoiceField, self).__init__(**kwargs)
def to_internal_value(self, data):
try:
- return self.choice_strings_to_values[str(data)]
+ return self.choice_strings_to_values[six.text_type(data)]
except KeyError:
self.fail('invalid_choice', input=data)
def to_representation(self, value):
- return self.choice_strings_to_values[str(value)]
+ return self.choice_strings_to_values[six.text_type(value)]
class MultipleChoiceField(ChoiceField):
@@ -864,7 +864,7 @@ class MultipleChoiceField(ChoiceField):
def to_representation(self, value):
return set([
- self.choice_strings_to_values[str(item)] for item in value
+ self.choice_strings_to_values[six.text_type(item)] for item in value
])
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index 085dfe65..4c485668 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -3,6 +3,7 @@ Provides generic filtering backends that can be used to filter the results
returned by list views.
"""
from __future__ import unicode_literals
+
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils import six
@@ -97,7 +98,7 @@ class SearchFilter(BaseFilterBackend):
if not search_fields:
return queryset
- orm_lookups = [self.construct_search(str(search_field))
+ orm_lookups = [self.construct_search(six.text_type(search_field))
for search_field in search_fields]
for search_term in self.get_search_terms(request):
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index cf903dab..3d6cf168 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -3,10 +3,11 @@ Generic views that provide commonly needed behaviour.
"""
from __future__ import unicode_literals
-from django.db.models.query import QuerySet
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 import six
from django.utils.translation import ugettext as _
from rest_framework import views, mixins
from rest_framework.settings import api_settings
@@ -127,7 +128,7 @@ class GenericAPIView(views.APIView):
error_format = _('Invalid page (%(page_number)s): %(message)s')
raise Http404(error_format % {
'page_number': page_number,
- 'message': str(exc)
+ 'message': six.text_type(exc)
})
return page
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index fa02ecf1..ccb82f03 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -5,6 +5,7 @@ They give us a generic way of being able to handle various media types
on the request, such as form content or json encoded data.
"""
from __future__ import unicode_literals
+
from django.conf import settings
from django.core.files.uploadhandler import StopFutureHandlers
from django.http import QueryDict
@@ -132,7 +133,7 @@ class MultiPartParser(BaseParser):
data, files = parser.parse()
return DataAndFiles(data, files)
except MultiPartParserError as exc:
- raise ParseError('Multipart form parse error - %s' % str(exc))
+ raise ParseError('Multipart form parse error - %s' % six.text_type(exc))
class XMLParser(BaseParser):
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index b37a6fed..b5effc6c 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -4,6 +4,7 @@ from rest_framework.reverse import reverse
from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch, Resolver404
from django.db.models.query import QuerySet
+from django.utils import six
from django.utils.translation import ugettext_lazy as _
@@ -49,7 +50,7 @@ class StringRelatedField(Field):
super(StringRelatedField, self).__init__(**kwargs)
def to_representation(self, value):
- return str(value)
+ return six.text_type(value)
class PrimaryKeyRelatedField(RelatedField):
diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py
index a51b07f5..a74e8aa2 100644
--- a/rest_framework/reverse.py
+++ b/rest_framework/reverse.py
@@ -3,6 +3,7 @@ Provide reverse functions that return fully qualified URLs
"""
from __future__ import unicode_literals
from django.core.urlresolvers import reverse as django_reverse
+from django.utils import six
from django.utils.functional import lazy
@@ -20,4 +21,4 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra
return url
-reverse_lazy = lazy(reverse, str)
+reverse_lazy = lazy(reverse, six.text_type)
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index 174b08b8..7c4179a1 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -2,8 +2,8 @@
Helper classes for parsers.
"""
from __future__ import unicode_literals
-from django.utils import timezone
from django.db.models.query import QuerySet
+from django.utils import six, timezone
from django.utils.datastructures import SortedDict
from django.utils.functional import Promise
from rest_framework.compat import force_text
@@ -40,7 +40,7 @@ class JSONEncoder(json.JSONEncoder):
representation = representation[:12]
return representation
elif isinstance(obj, datetime.timedelta):
- return str(obj.total_seconds())
+ return six.text_type(obj.total_seconds())
elif isinstance(obj, decimal.Decimal):
# Serializers will coerce decimals to strings by default.
return float(obj)
@@ -72,7 +72,7 @@ else:
than the usual behaviour of sorting the keys.
"""
def represent_decimal(self, data):
- return self.represent_scalar('tag:yaml.org,2002:str', str(data))
+ return self.represent_scalar('tag:yaml.org,2002:str', six.text_type(data))
def represent_mapping(self, tag, mapping, flow_style=None):
value = []
--
cgit v1.2.3
From c630a12e26f29145784523dd1b01ab0b3576f42c Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 1 Oct 2014 13:24:47 +0100
Subject: Deal with lazy strings in serializer reprs
---
rest_framework/utils/representation.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/rest_framework/utils/representation.py b/rest_framework/utils/representation.py
index e64fdd22..180b51f8 100644
--- a/rest_framework/utils/representation.py
+++ b/rest_framework/utils/representation.py
@@ -3,6 +3,8 @@ Helper functions for creating user-friendly representations
of serializer classes and serializer fields.
"""
from django.db import models
+from django.utils.functional import Promise
+from rest_framework.compat import force_text
import re
@@ -19,6 +21,9 @@ def smart_repr(value):
if isinstance(value, models.Manager):
return manager_repr(value)
+ if isinstance(value, Promise) and value._delegate_text:
+ value = force_text(value)
+
value = repr(value)
# Representations like u'help text'
--
cgit v1.2.3
From c171fa21ac62538331755524057d2435f33ec8a5 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Wed, 1 Oct 2014 19:44:46 +0100
Subject: First pass at HTML form rendering
---
rest_framework/renderers.py | 47 ++++++++++++++++++++--
rest_framework/serializers.py | 2 +
.../templates/rest_framework/fields/attrs.html | 1 +
.../rest_framework/fields/horizontal/checkbox.html | 10 +++++
.../rest_framework/fields/horizontal/fieldset.html | 10 +++++
.../rest_framework/fields/horizontal/input.html | 7 ++++
.../rest_framework/fields/horizontal/label.html | 1 +
.../rest_framework/fields/horizontal/select.html | 10 +++++
.../fields/horizontal/select_checkbox.html | 22 ++++++++++
.../fields/horizontal/select_multiple.html | 10 +++++
.../fields/horizontal/select_radio.html | 22 ++++++++++
.../rest_framework/fields/horizontal/textarea.html | 7 ++++
.../rest_framework/fields/inline/checkbox.html | 6 +++
.../rest_framework/fields/inline/fieldset.html | 3 ++
.../rest_framework/fields/inline/input.html | 4 ++
.../rest_framework/fields/inline/label.html | 1 +
.../rest_framework/fields/inline/select.html | 8 ++++
.../fields/inline/select_checkbox.html | 11 +++++
.../fields/inline/select_multiple.html | 8 ++++
.../rest_framework/fields/inline/select_radio.html | 11 +++++
.../rest_framework/fields/inline/textarea.html | 4 ++
.../rest_framework/fields/vertical/checkbox.html | 6 +++
.../rest_framework/fields/vertical/fieldset.html | 6 +++
.../rest_framework/fields/vertical/input.html | 5 +++
.../rest_framework/fields/vertical/label.html | 1 +
.../rest_framework/fields/vertical/select.html | 8 ++++
.../fields/vertical/select_checkbox.html | 22 ++++++++++
.../fields/vertical/select_multiple.html | 8 ++++
.../fields/vertical/select_radio.html | 22 ++++++++++
.../rest_framework/fields/vertical/textarea.html | 5 +++
rest_framework/templates/rest_framework/form.html | 40 ++++++++++++------
rest_framework/templatetags/rest_framework.py | 8 ++++
rest_framework/utils/field_mapping.py | 3 ++
tests/test_model_serializer.py | 2 +-
34 files changed, 325 insertions(+), 16 deletions(-)
create mode 100644 rest_framework/templates/rest_framework/fields/attrs.html
create mode 100644 rest_framework/templates/rest_framework/fields/horizontal/checkbox.html
create mode 100644 rest_framework/templates/rest_framework/fields/horizontal/fieldset.html
create mode 100644 rest_framework/templates/rest_framework/fields/horizontal/input.html
create mode 100644 rest_framework/templates/rest_framework/fields/horizontal/label.html
create mode 100644 rest_framework/templates/rest_framework/fields/horizontal/select.html
create mode 100644 rest_framework/templates/rest_framework/fields/horizontal/select_checkbox.html
create mode 100644 rest_framework/templates/rest_framework/fields/horizontal/select_multiple.html
create mode 100644 rest_framework/templates/rest_framework/fields/horizontal/select_radio.html
create mode 100644 rest_framework/templates/rest_framework/fields/horizontal/textarea.html
create mode 100644 rest_framework/templates/rest_framework/fields/inline/checkbox.html
create mode 100644 rest_framework/templates/rest_framework/fields/inline/fieldset.html
create mode 100644 rest_framework/templates/rest_framework/fields/inline/input.html
create mode 100644 rest_framework/templates/rest_framework/fields/inline/label.html
create mode 100644 rest_framework/templates/rest_framework/fields/inline/select.html
create mode 100644 rest_framework/templates/rest_framework/fields/inline/select_checkbox.html
create mode 100644 rest_framework/templates/rest_framework/fields/inline/select_multiple.html
create mode 100644 rest_framework/templates/rest_framework/fields/inline/select_radio.html
create mode 100644 rest_framework/templates/rest_framework/fields/inline/textarea.html
create mode 100644 rest_framework/templates/rest_framework/fields/vertical/checkbox.html
create mode 100644 rest_framework/templates/rest_framework/fields/vertical/fieldset.html
create mode 100644 rest_framework/templates/rest_framework/fields/vertical/input.html
create mode 100644 rest_framework/templates/rest_framework/fields/vertical/label.html
create mode 100644 rest_framework/templates/rest_framework/fields/vertical/select.html
create mode 100644 rest_framework/templates/rest_framework/fields/vertical/select_checkbox.html
create mode 100644 rest_framework/templates/rest_framework/fields/vertical/select_multiple.html
create mode 100644 rest_framework/templates/rest_framework/fields/vertical/select_radio.html
create mode 100644 rest_framework/templates/rest_framework/fields/vertical/textarea.html
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 225f9fe8..6483a47c 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -13,17 +13,18 @@ import django
from django import forms
from django.core.exceptions import ImproperlyConfigured
from django.http.multipartparser import parse_header
-from django.template import RequestContext, loader, Template
+from django.template import Context, RequestContext, loader, Template
from django.test.client import encode_multipart
from django.utils import six
from django.utils.xmlutils import SimplerXMLGenerator
+from rest_framework import exceptions, serializers, status, VERSION
from rest_framework.compat import StringIO, smart_text, yaml
from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings
from rest_framework.request import is_form_media_type, override_method
from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs
-from rest_framework import exceptions, status, VERSION
+from rest_framework.utils.field_mapping import ClassLookupDict
def zero_as_none(value):
@@ -341,6 +342,42 @@ class HTMLFormRenderer(BaseRenderer):
template = 'rest_framework/form.html'
charset = 'utf-8'
+ field_templates = ClassLookupDict({
+ serializers.Field: {
+ 'default': 'input.html'
+ },
+ serializers.BooleanField: {
+ 'default': 'checkbox.html'
+ },
+ serializers.CharField: {
+ 'default': 'input.html',
+ 'textarea': 'textarea.html'
+ },
+ serializers.ChoiceField: {
+ 'default': 'select.html',
+ 'radio': 'select_radio.html'
+ },
+ serializers.MultipleChoiceField: {
+ 'default': 'select_multiple.html',
+ 'checkbox': 'select_checkbox.html'
+ }
+ })
+
+ def render_field(self, field, value, errors, layout=None):
+ layout = layout or 'vertical'
+ style_type = field.style.get('type', 'default')
+ if style_type == 'textarea' and layout == 'inline':
+ style_type = 'default'
+ base = self.field_templates[field][style_type]
+ template_name = 'rest_framework/fields/' + layout + '/' + base
+ template = loader.get_template(template_name)
+ context = Context({
+ 'field': field,
+ 'value': value,
+ 'errors': errors
+ })
+ return template.render(context)
+
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
Render serializer data and return an HTML form, as a string.
@@ -349,7 +386,11 @@ class HTMLFormRenderer(BaseRenderer):
request = renderer_context['request']
template = loader.get_template(self.template)
- context = RequestContext(request, {'form': data})
+ context = RequestContext(request, {
+ 'form': data,
+ 'layout': getattr(getattr(data, 'Meta', None), 'layout', 'vertical'),
+ 'renderer': self
+ })
return template.render(context)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 0faa5671..5da81247 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -302,6 +302,8 @@ class Serializer(BaseSerializer):
def __iter__(self):
errors = self.errors if hasattr(self, '_errors') else {}
for field in self.fields.values():
+ if field.read_only:
+ continue
value = self.data.get(field.field_name) if self.data else None
error = errors.get(field.field_name)
yield FieldResult(field, value, error)
diff --git a/rest_framework/templates/rest_framework/fields/attrs.html b/rest_framework/templates/rest_framework/fields/attrs.html
new file mode 100644
index 00000000..b5a4dbcf
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/attrs.html
@@ -0,0 +1 @@
+name="{{ field.field_name }}" {% if field.style.placeholder %}placeholder="{{ field.style.placeholder }}"{% endif %} {% if field.style.rows %}rows="{{ field.style.rows }}"{% endif %}
diff --git a/rest_framework/templates/rest_framework/fields/horizontal/checkbox.html b/rest_framework/templates/rest_framework/fields/horizontal/checkbox.html
new file mode 100644
index 00000000..dce4a5cf
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/horizontal/checkbox.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/rest_framework/templates/rest_framework/fields/horizontal/fieldset.html b/rest_framework/templates/rest_framework/fields/horizontal/fieldset.html
new file mode 100644
index 00000000..86417633
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/horizontal/fieldset.html
@@ -0,0 +1,10 @@
+
diff --git a/rest_framework/templates/rest_framework/fields/horizontal/input.html b/rest_framework/templates/rest_framework/fields/horizontal/input.html
new file mode 100644
index 00000000..310154bb
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/horizontal/input.html
@@ -0,0 +1,7 @@
+
+ {% include "rest_framework/fields/horizontal/label.html" %}
+
+
+ {% if field.help_text %}
{{ field.help_text }}
{% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/fields/horizontal/label.html b/rest_framework/templates/rest_framework/fields/horizontal/label.html
new file mode 100644
index 00000000..bf21f78c
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/horizontal/label.html
@@ -0,0 +1 @@
+{% if field.label %}{% endif %}
diff --git a/rest_framework/templates/rest_framework/fields/horizontal/select.html b/rest_framework/templates/rest_framework/fields/horizontal/select.html
new file mode 100644
index 00000000..3f8cab0a
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/horizontal/select.html
@@ -0,0 +1,10 @@
+
+ {% include "rest_framework/fields/horizontal/label.html" %}
+
+
+
+
diff --git a/rest_framework/templates/rest_framework/fields/horizontal/select_checkbox.html b/rest_framework/templates/rest_framework/fields/horizontal/select_checkbox.html
new file mode 100644
index 00000000..659eede8
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/horizontal/select_checkbox.html
@@ -0,0 +1,22 @@
+
+ {% include "rest_framework/fields/horizontal/label.html" %}
+
+ {% if field.style.inline %}
+ {% for key, text in field.choices.items %}
+
+ {% endfor %}
+ {% else %}
+ {% for key, text in field.choices.items %}
+
+
+
+ {% endfor %}
+ {% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/fields/horizontal/select_multiple.html b/rest_framework/templates/rest_framework/fields/horizontal/select_multiple.html
new file mode 100644
index 00000000..da25eb2b
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/horizontal/select_multiple.html
@@ -0,0 +1,10 @@
+
+ {% include "rest_framework/fields/horizontal/label.html" %}
+
+
+
+
diff --git a/rest_framework/templates/rest_framework/fields/horizontal/select_radio.html b/rest_framework/templates/rest_framework/fields/horizontal/select_radio.html
new file mode 100644
index 00000000..188f05e2
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/horizontal/select_radio.html
@@ -0,0 +1,22 @@
+
+ {% include "rest_framework/fields/horizontal/label.html" %}
+
+ {% if field.style.inline %}
+ {% for key, text in field.choices.items %}
+
+ {% endfor %}
+ {% else %}
+ {% for key, text in field.choices.items %}
+
+
+
+ {% endfor %}
+ {% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/fields/horizontal/textarea.html b/rest_framework/templates/rest_framework/fields/horizontal/textarea.html
new file mode 100644
index 00000000..e99266f3
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/horizontal/textarea.html
@@ -0,0 +1,7 @@
+
+ {% include "rest_framework/fields/horizontal/label.html" %}
+
+
+ {% if field.help_text %}
{{ field.help_text }}
{% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/fields/inline/checkbox.html b/rest_framework/templates/rest_framework/fields/inline/checkbox.html
new file mode 100644
index 00000000..01d30aae
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/inline/checkbox.html
@@ -0,0 +1,6 @@
+
+
+
diff --git a/rest_framework/templates/rest_framework/fields/inline/fieldset.html b/rest_framework/templates/rest_framework/fields/inline/fieldset.html
new file mode 100644
index 00000000..d22982fd
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/inline/fieldset.html
@@ -0,0 +1,3 @@
+{% for field_item in value.field_items.values() %}
+ {{ renderer.render_field(field_item, layout=layout) }}
+{% endfor %}
diff --git a/rest_framework/templates/rest_framework/fields/inline/input.html b/rest_framework/templates/rest_framework/fields/inline/input.html
new file mode 100644
index 00000000..aefd1672
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/inline/input.html
@@ -0,0 +1,4 @@
+
+ {% include "rest_framework/fields/inline/label.html" %}
+
+
diff --git a/rest_framework/templates/rest_framework/fields/inline/label.html b/rest_framework/templates/rest_framework/fields/inline/label.html
new file mode 100644
index 00000000..7d546a57
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/inline/label.html
@@ -0,0 +1 @@
+{% if field.label %}{% endif %}
diff --git a/rest_framework/templates/rest_framework/fields/inline/select.html b/rest_framework/templates/rest_framework/fields/inline/select.html
new file mode 100644
index 00000000..cb9a7013
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/inline/select.html
@@ -0,0 +1,8 @@
+
+ {% include "rest_framework/fields/inline/label.html" %}
+
+
diff --git a/rest_framework/templates/rest_framework/fields/inline/select_checkbox.html b/rest_framework/templates/rest_framework/fields/inline/select_checkbox.html
new file mode 100644
index 00000000..424df93e
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/inline/select_checkbox.html
@@ -0,0 +1,11 @@
+
+ {% include "rest_framework/fields/inline/label.html" %}
+ {% for key, text in field.choices.items %}
+
+
+
+ {% endfor %}
+
diff --git a/rest_framework/templates/rest_framework/fields/inline/select_multiple.html b/rest_framework/templates/rest_framework/fields/inline/select_multiple.html
new file mode 100644
index 00000000..6fdfd672
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/inline/select_multiple.html
@@ -0,0 +1,8 @@
+
+ {% include "rest_framework/fields/inline/label.html" %}
+
+
diff --git a/rest_framework/templates/rest_framework/fields/inline/select_radio.html b/rest_framework/templates/rest_framework/fields/inline/select_radio.html
new file mode 100644
index 00000000..ddabc9e9
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/inline/select_radio.html
@@ -0,0 +1,11 @@
+
+ {% include "rest_framework/fields/inline/label.html" %}
+ {% for key, text in field.choices.items %}
+
+
+
+ {% endfor %}
+
diff --git a/rest_framework/templates/rest_framework/fields/inline/textarea.html b/rest_framework/templates/rest_framework/fields/inline/textarea.html
new file mode 100644
index 00000000..31366809
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/inline/textarea.html
@@ -0,0 +1,4 @@
+
+ {% include "rest_framework/fields/inline/label.html" %}
+
+
diff --git a/rest_framework/templates/rest_framework/fields/vertical/checkbox.html b/rest_framework/templates/rest_framework/fields/vertical/checkbox.html
new file mode 100644
index 00000000..01d30aae
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/vertical/checkbox.html
@@ -0,0 +1,6 @@
+
+
+
diff --git a/rest_framework/templates/rest_framework/fields/vertical/fieldset.html b/rest_framework/templates/rest_framework/fields/vertical/fieldset.html
new file mode 100644
index 00000000..cad32df9
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/vertical/fieldset.html
@@ -0,0 +1,6 @@
+
diff --git a/rest_framework/templates/rest_framework/fields/vertical/input.html b/rest_framework/templates/rest_framework/fields/vertical/input.html
new file mode 100644
index 00000000..c25407d1
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/vertical/input.html
@@ -0,0 +1,5 @@
+
+ {% include "rest_framework/fields/vertical/label.html" %}
+
+ {% if field.help_text %}
{{ field.help_text }}
{% endif %}
+
diff --git a/rest_framework/templates/rest_framework/fields/vertical/label.html b/rest_framework/templates/rest_framework/fields/vertical/label.html
new file mode 100644
index 00000000..651939b2
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/vertical/label.html
@@ -0,0 +1 @@
+{% if field.label %}{% endif %}
diff --git a/rest_framework/templates/rest_framework/fields/vertical/select.html b/rest_framework/templates/rest_framework/fields/vertical/select.html
new file mode 100644
index 00000000..44679d8a
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/vertical/select.html
@@ -0,0 +1,8 @@
+
+ {% include "rest_framework/fields/vertical/label.html" %}
+
+
diff --git a/rest_framework/templates/rest_framework/fields/vertical/select_checkbox.html b/rest_framework/templates/rest_framework/fields/vertical/select_checkbox.html
new file mode 100644
index 00000000..e60574c0
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/vertical/select_checkbox.html
@@ -0,0 +1,22 @@
+
+ {% include "rest_framework/fields/vertical/label.html" %}
+ {% if field.style.inline %}
+
+ {% for key, text in field.choices.items %}
+
+ {% endfor %}
+
+ {% else %}
+ {% for key, text in field.choices.items %}
+
+
+
+ {% endfor %}
+ {% endif %}
+
diff --git a/rest_framework/templates/rest_framework/fields/vertical/select_multiple.html b/rest_framework/templates/rest_framework/fields/vertical/select_multiple.html
new file mode 100644
index 00000000..f0fa418b
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/vertical/select_multiple.html
@@ -0,0 +1,8 @@
+
+ {% include "rest_framework/fields/vertical/label.html" %}
+
+
diff --git a/rest_framework/templates/rest_framework/fields/vertical/select_radio.html b/rest_framework/templates/rest_framework/fields/vertical/select_radio.html
new file mode 100644
index 00000000..4ffe38ea
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/vertical/select_radio.html
@@ -0,0 +1,22 @@
+
+ {% include "rest_framework/fields/vertical/label.html" %}
+ {% if field.style.inline %}
+
+ {% for key, text in field.choices.items %}
+
+ {% endfor %}
+
+ {% else %}
+ {% for key, text in field.choices.items %}
+
+
+
+ {% endfor %}
+ {% endif %}
+
diff --git a/rest_framework/templates/rest_framework/fields/vertical/textarea.html b/rest_framework/templates/rest_framework/fields/vertical/textarea.html
new file mode 100644
index 00000000..33cb27c7
--- /dev/null
+++ b/rest_framework/templates/rest_framework/fields/vertical/textarea.html
@@ -0,0 +1,5 @@
+
+ {% include "rest_framework/fields/vertical/label.html" %}
+
+ {% if field.help_text %}