aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/authtoken/serializers.py2
-rw-r--r--rest_framework/authtoken/views.py5
-rw-r--r--rest_framework/compat.py10
-rw-r--r--rest_framework/fields.py28
-rw-r--r--rest_framework/renderers.py3
-rwxr-xr-xrest_framework/runtests/runtests.py5
-rw-r--r--rest_framework/serializers.py94
-rw-r--r--rest_framework/tests/authentication.py16
-rw-r--r--rest_framework/tests/hyperlinkedserializers.py25
-rw-r--r--rest_framework/tests/models.py27
-rw-r--r--rest_framework/tests/pagination.py45
-rw-r--r--rest_framework/tests/pk_relations.py138
-rw-r--r--rest_framework/tests/serializer.py180
14 files changed, 508 insertions, 72 deletions
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 48cebbc5..d61632bc 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,3 +1,3 @@
-__version__ = '2.1.6'
+__version__ = '2.1.10'
VERSION = __version__ # synonym
diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py
index a5ed6e6d..60a3740e 100644
--- a/rest_framework/authtoken/serializers.py
+++ b/rest_framework/authtoken/serializers.py
@@ -1,6 +1,7 @@
from django.contrib.auth import authenticate
from rest_framework import serializers
+
class AuthTokenSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField()
@@ -21,4 +22,3 @@ class AuthTokenSerializer(serializers.Serializer):
raise serializers.ValidationError('Unable to login with provided credentials.')
else:
raise serializers.ValidationError('Must include "username" and "password"')
-
diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py
index cfaacbe9..d318c723 100644
--- a/rest_framework/authtoken/views.py
+++ b/rest_framework/authtoken/views.py
@@ -6,11 +6,12 @@ from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.serializers import AuthTokenSerializer
+
class ObtainAuthToken(APIView):
throttle_classes = ()
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
- renderer_classes = (renderers.JSONRenderer,)
+ renderer_classes = (renderers.JSONRenderer,)
model = Token
def post(self, request):
@@ -18,7 +19,7 @@ class ObtainAuthToken(APIView):
if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.object['user'])
return Response({'token': token.key})
- return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
obtain_auth_token = ObtainAuthToken.as_view()
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 09b76368..d4901437 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -19,6 +19,16 @@ except ImportError:
import StringIO
+# 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_concrete_model(model_cls):
try:
return model_cls._meta.concrete_model
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index f57dc57f..d3470854 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -32,6 +32,7 @@ def is_simple_callable(obj):
class Field(object):
+ read_only = True
creation_counter = 0
empty = ''
type_name = None
@@ -139,7 +140,7 @@ class WritableField(Field):
if required is None:
self.required = not(read_only)
else:
- assert not read_only, "Cannot set required=True and read_only=True"
+ assert not (read_only and required), "Cannot set required=True and read_only=True"
self.required = required
messages = {}
@@ -275,6 +276,7 @@ class RelatedField(WritableField):
def __init__(self, *args, **kwargs):
self.queryset = kwargs.pop('queryset', None)
+ self.null = kwargs.pop('null', False)
super(RelatedField, self).__init__(*args, **kwargs)
self.read_only = kwargs.pop('read_only', self.default_read_only)
@@ -356,7 +358,13 @@ class RelatedField(WritableField):
return
value = data.get(field_name)
- into[(self.source or field_name)] = self.from_native(value)
+
+ if value in (None, '') and not self.null:
+ raise ValidationError('Value may not be null')
+ elif value in (None, '') and self.null:
+ into[(self.source or field_name)] = None
+ else:
+ into[(self.source or field_name)] = self.from_native(value)
class ManyRelatedMixin(object):
@@ -580,7 +588,7 @@ class HyperlinkedRelatedField(RelatedField):
except:
pass
- raise ValidationError('Could not resolve URL for field using view name "%s"', view_name)
+ raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
def from_native(self, value):
# Convert URL -> model instance pk
@@ -679,7 +687,7 @@ class HyperlinkedIdentityField(Field):
except:
pass
- raise ValidationError('Could not resolve URL for field using view name "%s"', view_name)
+ raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)
##### Typed Fields #####
@@ -699,9 +707,9 @@ class BooleanField(WritableField):
default = False
def from_native(self, value):
- if value in ('t', 'True', '1'):
+ if value in ('true', 't', 'True', '1'):
return True
- if value in ('f', 'False', '0'):
+ if value in ('false', 'f', 'False', '0'):
return False
return bool(value)
@@ -823,6 +831,7 @@ class EmailField(CharField):
class RegexField(CharField):
type_name = 'RegexField'
+ 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)
@@ -1054,11 +1063,8 @@ class ImageField(FileField):
if f is None:
return None
- # Try to import PIL in either of the two ways it can end up installed.
- try:
- from PIL import Image
- except ImportError:
- import Image
+ from compat import Image
+ assert Image is not None, '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.
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 25a32baa..1220bca1 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -320,6 +320,9 @@ class BrowsableAPIRenderer(BaseRenderer):
if getattr(v, 'choices', None) is not None:
kwargs['choices'] = v.choices
+ if getattr(v, 'regex', None) is not None:
+ kwargs['regex'] = v.regex
+
if getattr(v, 'widget', None):
widget = copy.deepcopy(v.widget)
kwargs['widget'] = widget
diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py
index 1bd0a5fc..729ef26a 100755
--- a/rest_framework/runtests/runtests.py
+++ b/rest_framework/runtests/runtests.py
@@ -5,6 +5,11 @@
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
import os
import sys
+"""
+Need to fix sys path so following works without specifically messing with PYTHONPATH
+python ./rest_framework/runtests/runtests.py
+"""
+sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
from django.conf import settings
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 37496be3..ed6835fa 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -22,7 +22,16 @@ class DictWithMetadata(dict):
"""
A dict-like object, that can have additional properties attached.
"""
- pass
+ def __getstate__(self):
+ """
+ Used by pickle (e.g., caching).
+ Overriden to remove metadata from the dict, since it shouldn't be pickled
+ and may in some instances be unpickleable.
+ """
+ # return an instance of the first dict in MRO that isn't a DictWithMetadata
+ for base in self.__class__.__mro__:
+ if not isinstance(base, DictWithMetadata) and isinstance(base, dict):
+ return base(self)
class SortedDictWithMetadata(SortedDict, DictWithMetadata):
@@ -91,7 +100,8 @@ class BaseSerializer(Field):
_options_class = SerializerOptions
_dict_class = SortedDictWithMetadata # Set to unsorted dict for backwards compatibility with unsorted implementations.
- def __init__(self, instance=None, data=None, files=None, context=None, partial=False, **kwargs):
+ def __init__(self, instance=None, data=None, files=None,
+ context=None, partial=False, **kwargs):
super(BaseSerializer, self).__init__(**kwargs)
self.opts = self._options_class(self.Meta)
self.parent = None
@@ -131,8 +141,6 @@ class BaseSerializer(Field):
base_fields = copy.deepcopy(self.base_fields)
for key, field in base_fields.items():
ret[key] = field
- # Set up the field
- field.initialize(parent=self, field_name=key)
# Add in the default fields
default_fields = self.get_default_fields()
@@ -166,6 +174,13 @@ class BaseSerializer(Field):
if parent.opts.depth:
self.opts.depth = parent.opts.depth - 1
+ # We need to call initialize here to ensure any nested
+ # serializers that will have already called initialize on their
+ # descendants get updated with *their* parent.
+ # We could be a bit more smart about this, but it'll do for now.
+ for key, field in self.fields.items():
+ field.initialize(parent=self, field_name=key)
+
#####
# Methods to convert or revert from objects <--> primitive representations.
@@ -184,6 +199,7 @@ class BaseSerializer(Field):
ret.fields = {}
for field_name, field in self.fields.items():
+ field.initialize(parent=self, field_name=field_name)
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
ret[key] = value
@@ -197,6 +213,7 @@ class BaseSerializer(Field):
"""
reverted_data = {}
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:
@@ -217,10 +234,18 @@ class BaseSerializer(Field):
except ValidationError as err:
self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
- try:
- attrs = self.validate(attrs)
- except ValidationError as err:
- self._errors['non_field_errors'] = 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
@@ -272,10 +297,15 @@ class BaseSerializer(Field):
Override default so that we can apply ModelSerializer as a nested
field to relationships.
"""
- obj = getattr(obj, self.source or field_name)
-
- if is_simple_callable(obj):
- obj = obj()
+ if self.source:
+ for component in self.source.split('.'):
+ obj = getattr(obj, component)
+ if is_simple_callable(obj):
+ obj = obj()
+ else:
+ obj = getattr(obj, field_name)
+ if is_simple_callable(obj):
+ obj = value()
# If the object has an "all" method, assume it's a relationship
if is_simple_callable(getattr(obj, 'all', None)):
@@ -364,7 +394,6 @@ class ModelSerializer(Serializer):
field = self.get_field(model_field)
if field:
- field.initialize(parent=self, field_name=model_field.name)
ret[model_field.name] = field
for field_name in self.opts.read_only_fields:
@@ -396,10 +425,14 @@ class ModelSerializer(Serializer):
"""
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
- queryset = model_field.rel.to._default_manager
+ kwargs = {
+ 'null': model_field.null,
+ 'queryset': model_field.rel.to._default_manager
+ }
+
if to_many:
- return ManyPrimaryKeyRelatedField(queryset=queryset)
- return PrimaryKeyRelatedField(queryset=queryset)
+ return ManyPrimaryKeyRelatedField(**kwargs)
+ return PrimaryKeyRelatedField(**kwargs)
def get_field(self, model_field):
"""
@@ -424,10 +457,6 @@ class ModelSerializer(Serializer):
kwargs['choices'] = model_field.flatchoices
return ChoiceField(**kwargs)
- max_length = getattr(model_field, 'max_length', None)
- if max_length:
- kwargs['max_length'] = max_length
-
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
@@ -457,6 +486,18 @@ class ModelSerializer(Serializer):
except KeyError:
return ModelField(model_field=model_field, **kwargs)
+ def get_validation_exclusions(self):
+ """
+ Return a list of field names to exclude from model validation.
+ """
+ cls = self.opts.model
+ opts = get_concrete_model(cls)._meta
+ exclusions = [field.name for field in opts.fields + opts.many_to_many]
+ for field_name, field in self.fields.items():
+ if field_name in exclusions and not field.read_only:
+ exclusions.remove(field_name)
+ return exclusions
+
def restore_object(self, attrs, instance=None):
"""
Restore the model instance.
@@ -478,7 +519,14 @@ class ModelSerializer(Serializer):
for field in self.opts.model._meta.many_to_many:
if field.name in attrs:
self.m2m_data[field.name] = attrs.pop(field.name)
- return self.opts.model(**attrs)
+
+ instance = self.opts.model(**attrs)
+ try:
+ instance.full_clean(exclude=self.get_validation_exclusions())
+ except ValidationError, err:
+ self._errors = err.message_dict
+ return None
+ return instance
def save(self, save_m2m=True):
"""
@@ -537,9 +585,9 @@ class HyperlinkedModelSerializer(ModelSerializer):
# TODO: filter queryset using:
# .using(db).complex_filter(self.rel.limit_choices_to)
rel = model_field.rel.to
- queryset = rel._default_manager
kwargs = {
- 'queryset': queryset,
+ 'null': model_field.null,
+ 'queryset': rel._default_manager,
'view_name': self._get_default_view_name(rel)
}
if to_many:
diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py
index 802bc6c1..d498ae3e 100644
--- a/rest_framework/tests/authentication.py
+++ b/rest_framework/tests/authentication.py
@@ -1,4 +1,4 @@
-from django.conf.urls.defaults import patterns, include
+from django.conf.urls.defaults import patterns
from django.contrib.auth.models import User
from django.test import Client, TestCase
@@ -27,7 +27,7 @@ MockView.authentication_classes += (TokenAuthentication,)
urlpatterns = patterns('',
(r'^$', MockView.as_view()),
- (r'^auth-token/', 'rest_framework.authtoken.views.obtain_auth_token'),
+ (r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
)
@@ -157,7 +157,7 @@ class TokenAuthTests(TestCase):
def test_token_login_json(self):
"""Ensure token login view using JSON POST works."""
client = Client(enforce_csrf_checks=True)
- response = client.post('/auth-token/login/',
+ response = client.post('/auth-token/',
json.dumps({'username': self.username, 'password': self.password}), 'application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content)['token'], self.key)
@@ -165,21 +165,21 @@ class TokenAuthTests(TestCase):
def test_token_login_json_bad_creds(self):
"""Ensure token login view using JSON POST fails if bad credentials are used."""
client = Client(enforce_csrf_checks=True)
- response = client.post('/auth-token/login/',
+ response = client.post('/auth-token/',
json.dumps({'username': self.username, 'password': "badpass"}), 'application/json')
- self.assertEqual(response.status_code, 401)
+ self.assertEqual(response.status_code, 400)
def test_token_login_json_missing_fields(self):
"""Ensure token login view using JSON POST fails if missing fields."""
client = Client(enforce_csrf_checks=True)
- response = client.post('/auth-token/login/',
+ response = client.post('/auth-token/',
json.dumps({'username': self.username}), 'application/json')
- self.assertEqual(response.status_code, 401)
+ self.assertEqual(response.status_code, 400)
def test_token_login_form(self):
"""Ensure token login view using form POST works."""
client = Client(enforce_csrf_checks=True)
- response = client.post('/auth-token/login/',
+ response = client.post('/auth-token/',
{'username': self.username, 'password': self.password})
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content)['token'], self.key)
diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py
index d7effce7..24bf61bf 100644
--- a/rest_framework/tests/hyperlinkedserializers.py
+++ b/rest_framework/tests/hyperlinkedserializers.py
@@ -1,6 +1,7 @@
from django.conf.urls.defaults import patterns, url
from django.test import TestCase
from django.test.client import RequestFactory
+from django.utils import simplejson as json
from rest_framework import generics, status, serializers
from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel
@@ -54,10 +55,12 @@ class BlogPostCommentListCreate(generics.ListCreateAPIView):
model = BlogPostComment
serializer_class = BlogPostCommentSerializer
+
class BlogPostCommentDetail(generics.RetrieveAPIView):
model = BlogPostComment
serializer_class = BlogPostCommentSerializer
+
class BlogPostDetail(generics.RetrieveAPIView):
model = BlogPost
@@ -71,7 +74,7 @@ class AlbumDetail(generics.RetrieveAPIView):
model = Album
-class OptionalRelationDetail(generics.RetrieveAPIView):
+class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
model = OptionalRelationModel
model_serializer_class = serializers.HyperlinkedModelSerializer
@@ -162,7 +165,7 @@ class TestManyToManyHyperlinkedView(TestCase):
GET requests to ListCreateAPIView should return list of objects.
"""
request = factory.get('/manytomany/')
- response = self.list_view(request).render()
+ response = self.list_view(request)
self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, self.data)
@@ -171,7 +174,7 @@ class TestManyToManyHyperlinkedView(TestCase):
GET requests to ListCreateAPIView should return list of objects.
"""
request = factory.get('/manytomany/1/')
- response = self.detail_view(request, pk=1).render()
+ response = self.detail_view(request, pk=1)
self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, self.data[0])
@@ -194,7 +197,7 @@ class TestCreateWithForeignKeys(TestCase):
}
request = factory.post('/comments/', data=data)
- response = self.create_view(request).render()
+ 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)
@@ -219,7 +222,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase):
}
request = factory.post('/photos/', data=data)
- response = self.list_create_view(request).render()
+ 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)
@@ -244,6 +247,16 @@ class TestOptionalRelationHyperlinkedView(TestCase):
for non existing relations.
"""
request = factory.get('/optionalrelationmodel-detail/1')
- response = self.detail_view(request, pk=1).render()
+ response = self.detail_view(request, pk=1)
self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(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)
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
index a13f1ef3..5cc7eb9a 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -125,8 +125,21 @@ class ActionItem(RESTFrameworkModel):
# Models for reverse relations
+class Person(RESTFrameworkModel):
+ name = models.CharField(max_length=10)
+ age = models.IntegerField(null=True, blank=True)
+
+ @property
+ def info(self):
+ return {
+ 'name': self.name,
+ 'age': self.age,
+ }
+
+
class BlogPost(RESTFrameworkModel):
title = models.CharField(max_length=100)
+ writer = models.ForeignKey(Person, null=True, blank=True)
def get_first_comment(self):
return self.blogpostcomment_set.all()[0]
@@ -146,21 +159,9 @@ class Photo(RESTFrameworkModel):
album = models.ForeignKey(Album)
-class Person(RESTFrameworkModel):
- name = models.CharField(max_length=10)
- age = models.IntegerField(null=True, blank=True)
-
- @property
- def info(self):
- return {
- 'name': self.name,
- 'age': self.age,
- }
-
-
# Model for issue #324
class BlankFieldModel(RESTFrameworkModel):
- title = models.CharField(max_length=100, blank=True)
+ title = models.CharField(max_length=100, blank=True, null=True)
# Model for issue #380
diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py
index 3062007d..81d297a1 100644
--- a/rest_framework/tests/pagination.py
+++ b/rest_framework/tests/pagination.py
@@ -4,7 +4,7 @@ from django.core.paginator import Paginator
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import unittest
-from rest_framework import generics, status, pagination, filters
+from rest_framework import generics, status, pagination, filters, serializers
from rest_framework.compat import django_filters
from rest_framework.tests.models import BasicModel, FilterableItem
@@ -148,6 +148,11 @@ class IntegrationTestPaginationAndFiltering(TestCase):
self.assertEquals(response.data['previous'], None)
+class PassOnContextPaginationSerializer(pagination.PaginationSerializer):
+ class Meta:
+ object_serializer_class = serializers.Serializer
+
+
class UnitTestPagination(TestCase):
"""
Unit tests for pagination of primitive objects.
@@ -172,6 +177,15 @@ class UnitTestPagination(TestCase):
self.assertEquals(serializer.data['previous'], '?page=2')
self.assertEquals(serializer.data['results'], self.objects[20:])
+ def test_context_available_in_result(self):
+ """
+ Ensure context gets passed through to the object serializer.
+ """
+ serializer = PassOnContextPaginationSerializer(self.first_page)
+ serializer.data
+ results = serializer.fields[serializer.results_field]
+ self.assertTrue(serializer.context is results.context)
+
class TestUnpaginated(TestCase):
"""
@@ -236,3 +250,32 @@ class TestCustomPaginateByParam(TestCase):
response = self.view(request).render()
self.assertEquals(response.data['count'], 13)
self.assertEquals(response.data['results'], self.data[:5])
+
+
+class CustomField(serializers.Field):
+ def to_native(self, value):
+ if not 'view' in self.context:
+ raise RuntimeError("context isn't getting passed into custom field")
+ return "value"
+
+
+class BasicModelSerializer(serializers.Serializer):
+ text = CustomField()
+
+
+class TestContextPassedToCustomField(TestCase):
+ def setUp(self):
+ BasicModel.objects.create(text='ala ma kota')
+
+ def test_with_pagination(self):
+ class ListView(generics.ListCreateAPIView):
+ model = BasicModel
+ serializer_class = BasicModelSerializer
+ paginate_by = 1
+
+ self.view = ListView.as_view()
+ request = factory.get('/')
+ response = self.view(request).render()
+
+ self.assertEquals(response.status_code, status.HTTP_200_OK)
+
diff --git a/rest_framework/tests/pk_relations.py b/rest_framework/tests/pk_relations.py
index 3dcc76f9..e3360939 100644
--- a/rest_framework/tests/pk_relations.py
+++ b/rest_framework/tests/pk_relations.py
@@ -49,9 +49,22 @@ class ForeignKeySourceSerializer(serializers.ModelSerializer):
model = ForeignKeySource
+# Nullable ForeignKey
+
+class NullableForeignKeySource(models.Model):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
+ related_name='nullable_sources')
+
+
+class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = NullableForeignKeySource
+
+
# TODO: Add test that .data cannot be accessed prior to .is_valid
-class PrimaryKeyManyToManyTests(TestCase):
+class PKManyToManyTests(TestCase):
def setUp(self):
for idx in range(1, 4):
target = ManyToManyTarget(name='target-%d' % idx)
@@ -117,6 +130,25 @@ class PrimaryKeyManyToManyTests(TestCase):
]
self.assertEquals(serializer.data, expected)
+ def test_many_to_many_create(self):
+ data = {'id': 4, 'name': u'source-4', 'targets': [1, 3]}
+ serializer = ManyToManySourceSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 4 is added, and everything else is as expected
+ queryset = ManyToManySource.objects.all()
+ serializer = ManyToManySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'targets': [1]},
+ {'id': 2, 'name': u'source-2', 'targets': [1, 2]},
+ {'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]},
+ {'id': 4, 'name': u'source-4', 'targets': [1, 3]},
+ ]
+ self.assertEquals(serializer.data, expected)
+
def test_reverse_many_to_many_create(self):
data = {'id': 4, 'name': u'target-4', 'sources': [1, 3]}
serializer = ManyToManyTargetSerializer(data=data)
@@ -137,7 +169,7 @@ class PrimaryKeyManyToManyTests(TestCase):
self.assertEquals(serializer.data, expected)
-class PrimaryKeyForeignKeyTests(TestCase):
+class PKForeignKeyTests(TestCase):
def setUp(self):
target = ForeignKeyTarget(name='target-1')
target.save()
@@ -174,7 +206,7 @@ class PrimaryKeyForeignKeyTests(TestCase):
self.assertEquals(serializer.data, data)
serializer.save()
- # # Ensure source 1 is updated, and everything else is as expected
+ # Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
serializer = ForeignKeySourceSerializer(queryset)
expected = [
@@ -184,6 +216,106 @@ class PrimaryKeyForeignKeyTests(TestCase):
]
self.assertEquals(serializer.data, expected)
+ def test_foreign_key_update_with_invalid_null(self):
+ data = {'id': 1, 'name': u'source-1', 'target': None}
+ instance = ForeignKeySource.objects.get(pk=1)
+ serializer = ForeignKeySourceSerializer(instance, data=data)
+ self.assertFalse(serializer.is_valid())
+ self.assertEquals(serializer.errors, {'target': [u'Value may not be null']})
+
+
+class PKNullableForeignKeyTests(TestCase):
+ def setUp(self):
+ target = ForeignKeyTarget(name='target-1')
+ target.save()
+ for idx in range(1, 4):
+ source = NullableForeignKeySource(name='source-%d' % idx, target=target)
+ source.save()
+
+ def test_foreign_key_create_with_valid_null(self):
+ data = {'id': 4, 'name': u'source-4', 'target': None}
+ serializer = NullableForeignKeySourceSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 4 is created, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 1},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': 1},
+ {'id': 4, 'name': u'source-4', 'target': None}
+ ]
+ self.assertEquals(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': u'source-4', 'target': ''}
+ expected_data = {'id': 4, 'name': u'source-4', 'target': None}
+ serializer = NullableForeignKeySourceSerializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ obj = serializer.save()
+ self.assertEquals(serializer.data, expected_data)
+ self.assertEqual(obj.name, u'source-4')
+
+ # Ensure source 4 is created, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': 1},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': 1},
+ {'id': 4, 'name': u'source-4', 'target': None}
+ ]
+ self.assertEquals(serializer.data, expected)
+
+ def test_foreign_key_update_with_valid_null(self):
+ data = {'id': 1, 'name': u'source-1', 'target': None}
+ instance = NullableForeignKeySource.objects.get(pk=1)
+ serializer = NullableForeignKeySourceSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, data)
+ serializer.save()
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': None},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': 1}
+ ]
+ self.assertEquals(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': u'source-1', 'target': ''}
+ expected_data = {'id': 1, 'name': u'source-1', 'target': None}
+ instance = NullableForeignKeySource.objects.get(pk=1)
+ serializer = NullableForeignKeySourceSerializer(instance, data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.data, expected_data)
+ serializer.save()
+
+ # Ensure source 1 is updated, and everything else is as expected
+ queryset = NullableForeignKeySource.objects.all()
+ serializer = NullableForeignKeySourceSerializer(queryset)
+ expected = [
+ {'id': 1, 'name': u'source-1', 'target': None},
+ {'id': 2, 'name': u'source-2', 'target': 1},
+ {'id': 3, 'name': u'source-3', 'target': 1}
+ ]
+ self.assertEquals(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.
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 44adf92e..4cfb0aae 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -1,9 +1,10 @@
import datetime
+import pickle
from django.test import TestCase
from rest_framework import serializers, fields
-from rest_framework.tests.models import (ActionItem, Anchor, BasicModel,
+from rest_framework.tests.models import (Album, ActionItem, Anchor, BasicModel,
BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel,
- ManyToManyModel, Person, ReadOnlyManyToManyModel)
+ ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo)
class SubComment(object):
@@ -62,6 +63,13 @@ class PersonSerializer(serializers.ModelSerializer):
read_only_fields = ('age',)
+class AlbumsSerializer(serializers.ModelSerializer):
+
+ class Meta:
+ model = Album
+ fields = ['title'] # lists are also valid options
+
+
class BasicTests(TestCase):
def setUp(self):
self.comment = Comment(
@@ -169,7 +177,7 @@ class ValidationTests(TestCase):
'content': 'x' * 1001,
'created': datetime.datetime(2012, 1, 1)
}
- self.actionitem = ActionItem('Some to do item',
+ self.actionitem = ActionItem(title='Some to do item',
)
def test_create(self):
@@ -277,6 +285,19 @@ class ValidationTests(TestCase):
self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']})
+class ModelValidationTests(TestCase):
+ def test_validate_unique(self):
+ """
+ Just check if serializers.ModelSerializer handles unique checks via .full_clean()
+ """
+ serializer = AlbumsSerializer(data={'title': 'a'})
+ serializer.is_valid()
+ serializer.save()
+ second_serializer = AlbumsSerializer(data={'title': 'a'})
+ self.assertFalse(second_serializer.is_valid())
+ self.assertEqual(second_serializer.errors, {'title': [u'Album with this Title already exists.']})
+
+
class RegexValidationTest(TestCase):
def test_create_failed(self):
serializer = BookSerializer(data={'isbn': '1234567890'})
@@ -560,6 +581,47 @@ class ManyRelatedTests(TestCase):
self.assertEqual(serializer.data, expected)
+class RelatedTraversalTest(TestCase):
+ def test_nested_traversal(self):
+ 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")
+
+ from rest_framework.tests.models import BlogPostComment
+
+ class PersonSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Person
+ fields = ("name", "age")
+
+ class BlogPostCommentSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BlogPostComment
+ fields = ("text", "post_owner")
+
+ text = serializers.CharField()
+ post_owner = PersonSerializer(source='blog_post.writer')
+
+ class BlogPostSerializer(serializers.Serializer):
+ title = serializers.CharField()
+ comments = BlogPostCommentSerializer(source='blogpostcomment_set')
+
+ serializer = BlogPostSerializer(instance=post)
+
+ expected = {
+ 'title': u'Test blog post',
+ 'comments': [{
+ 'text': u'I love this blog post',
+ 'post_owner': {
+ "name": u"django",
+ "age": None
+ }
+ }]
+ }
+
+ self.assertEqual(serializer.data, expected)
+
+
class SerializerMethodFieldTests(TestCase):
def setUp(self):
@@ -643,6 +705,118 @@ class BlankFieldTests(TestCase):
self.assertEquals(serializer.is_valid(), False)
+#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)
+
+
+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)
+
+ class BlogPostSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BlogPost
+ depth = 1
+
+ serializer = BlogPostSerializer(instance=post)
+ expected = {'id': 1, 'title': u'Test blog post',
+ 'writer': {'id': 1, 'name': u'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)
+
+ class PersonSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Person
+
+ class BlogPostSerializer(serializers.ModelSerializer):
+ writer = PersonSerializer()
+
+ class Meta:
+ model = BlogPost
+
+ serializer = BlogPostSerializer(instance=post)
+ expected = {'id': 1, 'title': u'Test blog post',
+ 'writer': {'id': 1, 'name': u'django', 'age': 1}}
+
+ self.assertEqual(serializer.data, expected)
+
+
+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")
+
+ callable = serializers.SerializerMethodField('_callable')
+
+ def _callable(self, instance):
+ if not 'context_item' 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")
+ callable = serializers.SerializerMethodField("_callable")
+
+ def _callable(self, instance):
+ if not 'context_item' in self.context:
+ raise RuntimeError("context isn't getting passed into 1st level nested serializer")
+ return "success"
+
+ class AlbumCollection(object):
+ albums = None
+
+ class AlbumCollectionSerializer(serializers.Serializer):
+ albums = AlbumSerializer(source="albums")
+
+ 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
+
# Test for issue #467
class FieldLabelTest(TestCase):
def setUp(self):