{% block script %}
-
+
+
+
+
{% endblock %}
{% endblock %}
diff --git a/rest_framework/templates/rest_framework/raw_data_form.html b/rest_framework/templates/rest_framework/raw_data_form.html
index 2aa0beecd..ebf6fa706 100644
--- a/rest_framework/templates/rest_framework/raw_data_form.html
+++ b/rest_framework/templates/rest_framework/raw_data_form.html
@@ -1,5 +1,4 @@
{% load rest_framework %}
-{% csrf_token %}
{{ form.non_field_errors }}
{% for field in form %}
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index 0069d9a5e..08acecef7 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -41,8 +41,9 @@ def optional_login(request):
except NoReverseMatch:
return ''
- snippet = "
Log in".format(href=login_url, next=escape(request.path))
- return snippet
+ snippet = "
Log in"
+ snippet = snippet.format(href=login_url, next=escape(request.path))
+ return mark_safe(snippet)
@register.simple_tag
@@ -64,8 +65,8 @@ def optional_logout(request, user):
Log out
"""
-
- return snippet.format(user=user, href=logout_url, next=escape(request.path))
+ snippet = snippet.format(user=escape(user), href=logout_url, next=escape(request.path))
+ return mark_safe(snippet)
@register.simple_tag
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index f2598974e..8206578ee 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -8,7 +8,6 @@ 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
NUMERIC_FIELD_TYPES = (
@@ -113,6 +112,14 @@ def get_field_kwargs(field_name, model_field):
kwargs['choices'] = model_field.choices
return kwargs
+ # Our decimal validation is handled in the field code, not validator code.
+ # (In Django 1.9+ this differs from previous style)
+ if isinstance(model_field, models.DecimalField):
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.DecimalValidator)
+ ]
+
# Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator.
max_length = getattr(model_field, 'max_length', None)
@@ -193,7 +200,15 @@ def get_field_kwargs(field_name, model_field):
]
if getattr(model_field, 'unique', False):
- validator = UniqueValidator(queryset=model_field.model._default_manager)
+ unique_error_message = model_field.error_messages.get('unique', None)
+ if unique_error_message:
+ unique_error_message = unique_error_message % {
+ 'model_name': model_field.model._meta.object_name,
+ 'field_label': model_field.verbose_name
+ }
+ validator = UniqueValidator(
+ queryset=model_field.model._default_manager,
+ message=unique_error_message)
validator_kwarg.append(validator)
if validator_kwarg:
@@ -222,7 +237,7 @@ def get_relation_kwargs(field_name, relation_info):
if model_field:
if model_field.verbose_name and needs_label(model_field, field_name):
kwargs['label'] = capfirst(model_field.verbose_name)
- help_text = clean_manytomany_helptext(model_field.help_text)
+ help_text = model_field.help_text
if help_text:
kwargs['help_text'] = help_text
if not model_field.editable:
diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py
index 3bcd9049c..d16630ed6 100644
--- a/rest_framework/utils/model_meta.py
+++ b/rest_framework/utils/model_meta.py
@@ -6,14 +6,13 @@ relationships and their associated metadata.
Usage: `get_field_info(model)` returns a `FieldInfo` instance.
"""
import inspect
-from collections import namedtuple
+from collections import OrderedDict, namedtuple
+from django.apps import apps
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils import six
-from rest_framework.compat import OrderedDict
-
FieldInfo = namedtuple('FieldResult', [
'pk', # Model field instance
'fields', # Dict of field name -> model field instance
@@ -45,7 +44,7 @@ def _resolve_model(obj):
"""
if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
app_name, model_name = obj.split('.')
- resolved_model = models.get_model(app_name, model_name)
+ resolved_model = apps.get_model(app_name, model_name)
if resolved_model is None:
msg = "Django did not return a model for {0}.{1}"
raise ImproperlyConfigured(msg.format(app_name, model_name))
diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py
index 0aede90f7..ddf160868 100644
--- a/rest_framework/utils/serializer_helpers.py
+++ b/rest_framework/utils/serializer_helpers.py
@@ -1,10 +1,11 @@
from __future__ import unicode_literals
import collections
+from collections import OrderedDict
from django.utils.encoding import force_text
-from rest_framework.compat import OrderedDict, unicode_to_repr
+from rest_framework.compat import unicode_to_repr
class ReturnDict(OrderedDict):
diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py
index 89bc0ac66..3e1d41725 100644
--- a/tests/test_atomic_requests.py
+++ b/tests/test_atomic_requests.py
@@ -1,11 +1,12 @@
from __future__ import unicode_literals
-from django.conf.urls import patterns, url
+import unittest
+
+from django.conf.urls import url
from django.db import connection, connections, transaction
from django.http import Http404
from django.test import TestCase, TransactionTestCase
from django.utils.decorators import method_decorator
-from django.utils.unittest import skipUnless
from tests.models import BasicModel
from rest_framework import status
@@ -35,8 +36,10 @@ class APIExceptionView(APIView):
raise APIException
-@skipUnless(connection.features.uses_savepoints,
- "'atomic' requires transactions and savepoints.")
+@unittest.skipUnless(
+ connection.features.uses_savepoints,
+ "'atomic' requires transactions and savepoints."
+)
class DBTransactionTests(TestCase):
def setUp(self):
self.view = BasicView.as_view()
@@ -45,7 +48,7 @@ class DBTransactionTests(TestCase):
def tearDown(self):
connections.databases['default']['ATOMIC_REQUESTS'] = False
- def test_no_exception_conmmit_transaction(self):
+ def test_no_exception_commit_transaction(self):
request = factory.post('/')
with self.assertNumQueries(1):
@@ -55,8 +58,10 @@ class DBTransactionTests(TestCase):
assert BasicModel.objects.count() == 1
-@skipUnless(connection.features.uses_savepoints,
- "'atomic' requires transactions and savepoints.")
+@unittest.skipUnless(
+ connection.features.uses_savepoints,
+ "'atomic' requires transactions and savepoints."
+)
class DBTransactionErrorTests(TestCase):
def setUp(self):
self.view = ErrorView.as_view()
@@ -83,8 +88,10 @@ class DBTransactionErrorTests(TestCase):
assert BasicModel.objects.count() == 1
-@skipUnless(connection.features.uses_savepoints,
- "'atomic' requires transactions and savepoints.")
+@unittest.skipUnless(
+ connection.features.uses_savepoints,
+ "'atomic' requires transactions and savepoints."
+)
class DBTransactionAPIExceptionTests(TestCase):
def setUp(self):
self.view = APIExceptionView.as_view()
@@ -113,8 +120,10 @@ class DBTransactionAPIExceptionTests(TestCase):
assert BasicModel.objects.count() == 0
-@skipUnless(connection.features.uses_savepoints,
- "'atomic' requires transactions and savepoints.")
+@unittest.skipUnless(
+ connection.features.uses_savepoints,
+ "'atomic' requires transactions and savepoints."
+)
class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase):
@property
def urls(self):
@@ -127,9 +136,8 @@ class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase):
BasicModel.objects.all()
raise Http404
- return patterns(
- '',
- url(r'^$', NonAtomicAPIExceptionView.as_view())
+ return (
+ url(r'^$', NonAtomicAPIExceptionView.as_view()),
)
def setUp(self):
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 6048f49d0..104337627 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1525,6 +1525,58 @@ class TestUnvalidatedDictField(FieldValues):
field = serializers.DictField()
+class TestJSONField(FieldValues):
+ """
+ Values for `JSONField`.
+ """
+ valid_inputs = [
+ ({
+ 'a': 1,
+ 'b': ['some', 'list', True, 1.23],
+ '3': None
+ }, {
+ 'a': 1,
+ 'b': ['some', 'list', True, 1.23],
+ '3': None
+ }),
+ ]
+ invalid_inputs = [
+ ({'a': set()}, ['Value must be valid JSON.']),
+ ]
+ outputs = [
+ ({
+ 'a': 1,
+ 'b': ['some', 'list', True, 1.23],
+ '3': 3
+ }, {
+ 'a': 1,
+ 'b': ['some', 'list', True, 1.23],
+ '3': 3
+ }),
+ ]
+ field = serializers.JSONField()
+
+
+class TestBinaryJSONField(FieldValues):
+ """
+ Values for `JSONField` with binary=True.
+ """
+ valid_inputs = [
+ (b'{"a": 1, "3": null, "b": ["some", "list", true, 1.23]}', {
+ 'a': 1,
+ 'b': ['some', 'list', True, 1.23],
+ '3': None
+ }),
+ ]
+ invalid_inputs = [
+ ('{"a": "unterminated string}', ['Value must be valid JSON.']),
+ ]
+ outputs = [
+ (['some', 'list', True, 1.23], b'["some", "list", true, 1.23]'),
+ ]
+ field = serializers.JSONField(binary=True)
+
+
# Tests for FieldField.
# ---------------------
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 0610b0855..729a7b75b 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -1,6 +1,7 @@
from __future__ import unicode_literals
import datetime
+import unittest
from decimal import Decimal
from django.conf.urls import url
@@ -8,7 +9,6 @@ from django.core.urlresolvers import reverse
from django.db import models
from django.test import TestCase
from django.test.utils import override_settings
-from django.utils import unittest
from django.utils.dateparse import parse_date
from django.utils.six.moves import reload_module
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index 777b956c4..aa62ec4ae 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -8,6 +8,7 @@ an appropriate set of serializer fields for each case.
from __future__ import unicode_literals
import decimal
+from collections import OrderedDict
import django
import pytest
@@ -21,7 +22,7 @@ from django.utils import six
from rest_framework import serializers
from rest_framework.compat import DurationField as ModelDurationField
-from rest_framework.compat import OrderedDict, unicode_repr
+from rest_framework.compat import unicode_repr
def dedent(blocktext):
@@ -321,6 +322,21 @@ class TestRegularFieldMappings(TestCase):
ExampleSerializer()
+ def test_fields_and_exclude_behavior(self):
+ class ImplicitFieldsSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+
+ class ExplicitFieldsSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ fields = '__all__'
+
+ implicit = ImplicitFieldsSerializer()
+ explicit = ExplicitFieldsSerializer()
+
+ assert implicit.data == explicit.data
+
@pytest.mark.skipif(django.VERSION < (1, 8),
reason='DurationField is only available for django1.8+')
diff --git a/tests/test_permissions.py b/tests/test_permissions.py
index ac0ecab90..f36b8f4da 100644
--- a/tests/test_permissions.py
+++ b/tests/test_permissions.py
@@ -1,19 +1,19 @@
from __future__ import unicode_literals
import base64
+import unittest
from django.contrib.auth.models import Group, Permission, User
from django.core.urlresolvers import ResolverMatch
from django.db import models
from django.test import TestCase
-from django.utils import unittest
from tests.models import BasicModel
from rest_framework import (
HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers,
status
)
-from rest_framework.compat import get_model_name, guardian
+from rest_framework.compat import guardian
from rest_framework.filters import DjangoObjectPermissionsFilter
from rest_framework.routers import DefaultRouter
from rest_framework.test import APIRequestFactory
@@ -279,7 +279,7 @@ class ObjectPermissionsIntegrationTests(TestCase):
# give everyone model level permissions, as we are not testing those
everyone = Group.objects.create(name='everyone')
- model_name = get_model_name(BasicPermModel)
+ model_name = BasicPermModel._meta.model_name
app_label = BasicPermModel._meta.app_label
f = '{0}_{1}'.format
perms = {
diff --git a/tests/test_relations_generic.py b/tests/test_relations_generic.py
index 962857365..340d4d1d1 100644
--- a/tests/test_relations_generic.py
+++ b/tests/test_relations_generic.py
@@ -1,6 +1,6 @@
from __future__ import unicode_literals
-from django.contrib.contenttypes.generic import (
+from django.contrib.contenttypes.fields import (
GenericForeignKey, GenericRelation
)
from django.contrib.contenttypes.models import ContentType
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
index 03b95243f..b4b2db22e 100644
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
import json
import re
-from collections import MutableMapping
+from collections import MutableMapping, OrderedDict
from django.conf.urls import include, url
from django.core.cache import cache
@@ -13,7 +13,6 @@ from django.utils import six
from django.utils.translation import ugettext_lazy as _
from rest_framework import permissions, serializers, status
-from rest_framework.compat import OrderedDict
from rest_framework.renderers import (
BaseRenderer, BrowsableAPIRenderer, HTMLFormRenderer, JSONRenderer
)
@@ -192,17 +191,6 @@ class RendererEndToEndTests(TestCase):
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
- def test_specified_renderer_serializes_content_on_accept_query(self):
- """The '_accept' query string should behave in the same way as the Accept header."""
- param = '?%s=%s' % (
- api_settings.URL_ACCEPT_OVERRIDE,
- RendererB.media_type
- )
- resp = self.client.get('/' + param)
- self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
- self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
- self.assertEqual(resp.status_code, DUMMYSTATUS)
-
def test_unsatisfiable_accept_header_on_request_returns_406_status(self):
"""If the Accept header is unsatisfiable we should return a 406 Not Acceptable response."""
resp = self.client.get('/', HTTP_ACCEPT='foo/bar')
diff --git a/tests/test_request.py b/tests/test_request.py
index f4b381aeb..a2bd4cade 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -3,27 +3,20 @@ Tests for content parsing, and form-overloaded content parsing.
"""
from __future__ import unicode_literals
-import json
-from io import BytesIO
-
import django
import pytest
from django.conf.urls import url
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
from django.contrib.sessions.middleware import SessionMiddleware
-from django.core.handlers.wsgi import WSGIRequest
from django.test import TestCase
from django.utils import six
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
-from rest_framework.parsers import (
- BaseParser, FormParser, JSONParser, MultiPartParser
-)
-from rest_framework.request import Empty, Request
+from rest_framework.parsers import BaseParser, FormParser, MultiPartParser
+from rest_framework.request import Request
from rest_framework.response import Response
-from rest_framework.settings import api_settings
from rest_framework.test import APIClient, APIRequestFactory
from rest_framework.views import APIView
@@ -43,36 +36,6 @@ class PlainTextParser(BaseParser):
return stream.read()
-class TestMethodOverloading(TestCase):
- def test_method(self):
- """
- Request methods should be same as underlying request.
- """
- request = Request(factory.get('/'))
- self.assertEqual(request.method, 'GET')
- request = Request(factory.post('/'))
- self.assertEqual(request.method, 'POST')
-
- def test_overloaded_method(self):
- """
- POST requests can be overloaded to another method by setting a
- reserved form field
- """
- request = Request(factory.post('/', {api_settings.FORM_METHOD_OVERRIDE: 'DELETE'}))
- self.assertEqual(request.method, 'DELETE')
-
- def test_x_http_method_override_header(self):
- """
- POST requests can also be overloaded to another method by setting
- the X-HTTP-Method-Override header.
- """
- request = Request(factory.post('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
- self.assertEqual(request.method, 'DELETE')
-
- request = Request(factory.get('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
- self.assertEqual(request.method, 'DELETE')
-
-
class TestContentParsing(TestCase):
def test_standard_behaviour_determines_no_content_GET(self):
"""
@@ -137,49 +100,6 @@ class TestContentParsing(TestCase):
request.parsers = (PlainTextParser(), )
self.assertEqual(request.data, content)
- def test_overloaded_behaviour_allows_content_tunnelling(self):
- """
- Ensure request.data returns content for overloaded POST request.
- """
- json_data = {'foobar': 'qwerty'}
- content = json.dumps(json_data)
- content_type = 'application/json'
- form_data = {
- api_settings.FORM_CONTENT_OVERRIDE: content,
- api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type
- }
- request = Request(factory.post('/', form_data))
- request.parsers = (JSONParser(), )
- self.assertEqual(request.data, json_data)
-
- def test_form_POST_unicode(self):
- """
- JSON POST via default web interface with unicode data
- """
- # Note: environ and other variables here have simplified content compared to real Request
- CONTENT = b'_content_type=application%2Fjson&_content=%7B%22request%22%3A+4%2C+%22firm%22%3A+1%2C+%22text%22%3A+%22%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%21%22%7D'
- environ = {
- 'REQUEST_METHOD': 'POST',
- 'CONTENT_TYPE': 'application/x-www-form-urlencoded',
- 'CONTENT_LENGTH': len(CONTENT),
- 'wsgi.input': BytesIO(CONTENT),
- }
- wsgi_request = WSGIRequest(environ=environ)
- wsgi_request._load_post_and_files()
- parsers = (JSONParser(), FormParser(), MultiPartParser())
- parser_context = {
- 'encoding': 'utf-8',
- 'kwargs': {},
- 'args': (),
- }
- request = Request(wsgi_request, parsers=parsers, parser_context=parser_context)
- method = request.method
- self.assertEqual(method, 'POST')
- self.assertEqual(request._content_type, 'application/json')
- self.assertEqual(request._stream.getvalue(), b'{"request": 4, "firm": 1, "text": "\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!"}')
- self.assertEqual(request._data, Empty)
- self.assertEqual(request._files, Empty)
-
class MockView(APIView):
authentication_classes = (SessionAuthentication,)
diff --git a/tests/test_response.py b/tests/test_response.py
index 84b0935d7..595227bd7 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -6,11 +6,11 @@ from django.utils import six
from tests.models import BasicModel
from rest_framework import generics, routers, serializers, status, viewsets
+from rest_framework.parsers import JSONParser
from rest_framework.renderers import (
BaseRenderer, BrowsableAPIRenderer, JSONRenderer
)
from rest_framework.response import Response
-from rest_framework.settings import api_settings
from rest_framework.views import APIView
@@ -79,6 +79,14 @@ class MockViewSettingContentType(APIView):
return Response(DUMMYCONTENT, status=DUMMYSTATUS, content_type='setbyview')
+class JSONView(APIView):
+ parser_classes = (JSONParser,)
+
+ def post(self, request, **kwargs):
+ assert request.data
+ return Response(DUMMYCONTENT)
+
+
class HTMLView(APIView):
renderer_classes = (BrowsableAPIRenderer, )
@@ -114,6 +122,7 @@ urlpatterns = [
url(r'^.*\.(?P
.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^html$', HTMLView.as_view()),
+ url(r'^json$', JSONView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
url(r'^html_new_model$', HTMLNewModelView.as_view()),
url(r'^html_new_model_viewset', include(new_model_viewset_router.urls)),
@@ -166,17 +175,6 @@ class RendererIntegrationTests(TestCase):
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
- def test_specified_renderer_serializes_content_on_accept_query(self):
- """The '_accept' query string should behave in the same way as the Accept header."""
- param = '?%s=%s' % (
- api_settings.URL_ACCEPT_OVERRIDE,
- RendererB.media_type
- )
- resp = self.client.get('/' + param)
- self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
- self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
- self.assertEqual(resp.status_code, DUMMYSTATUS)
-
def test_specified_renderer_serializes_content_on_format_query(self):
"""If a 'format' query is specified, the renderer with the matching
format attribute should serialize the response."""
@@ -203,6 +201,25 @@ class RendererIntegrationTests(TestCase):
self.assertEqual(resp.status_code, DUMMYSTATUS)
+class UnsupportedMediaTypeTests(TestCase):
+ urls = 'tests.test_response'
+
+ def test_should_allow_posting_json(self):
+ response = self.client.post('/json', data='{"test": 123}', content_type='application/json')
+
+ self.assertEqual(response.status_code, 200)
+
+ def test_should_not_allow_posting_xml(self):
+ response = self.client.post('/json', data='123', content_type='application/xml')
+
+ self.assertEqual(response.status_code, 415)
+
+ def test_should_not_allow_posting_a_form(self):
+ response = self.client.post('/json', data={'test': 123})
+
+ self.assertEqual(response.status_code, 415)
+
+
class Issue122Tests(TestCase):
"""
Tests that covers #122.
@@ -270,16 +287,6 @@ class Issue807Tests(TestCase):
resp = self.client.get('/setbyview', **headers)
self.assertEqual('setbyview', resp['Content-Type'])
- def test_viewset_label_help_text(self):
- param = '?%s=%s' % (
- api_settings.URL_ACCEPT_OVERRIDE,
- 'text/html'
- )
- 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.')
-
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')
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index c18cbb584..741c6ab17 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -51,6 +51,16 @@ class TestSerializer:
with pytest.raises(AttributeError):
serializer.data
+ def test_data_access_before_save_raises_error(self):
+ def create(validated_data):
+ return validated_data
+ serializer = self.Serializer(data={'char': 'abc', 'integer': 123})
+ serializer.create = create
+ assert serializer.is_valid()
+ assert serializer.data == {'char': 'abc', 'integer': 123}
+ with pytest.raises(AssertionError):
+ serializer.save()
+
class TestValidateMethod:
def test_non_field_error_validate_method(self):
diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py
index e9234d8f7..607ddba04 100644
--- a/tests/test_serializer_lists.py
+++ b/tests/test_serializer_lists.py
@@ -289,3 +289,32 @@ class TestListSerializerClass:
serializer = TestSerializer(data=[], many=True)
assert not serializer.is_valid()
assert serializer.errors == {'non_field_errors': ['Non field error']}
+
+
+class TestSerializerPartialUsage:
+ """
+ When not submitting key for list fields or multiple choice, partial
+ serialization should result in an empty state (key not there), not
+ an empty list.
+
+ Regression test for Github issue #2761.
+ """
+ def test_partial_listfield(self):
+ class ListSerializer(serializers.Serializer):
+ listdata = serializers.ListField()
+ serializer = ListSerializer(data=MultiValueDict(), partial=True)
+ result = serializer.to_internal_value(data={})
+ assert "listdata" not in result
+ assert serializer.is_valid()
+ assert serializer.validated_data == {}
+ assert serializer.errors == {}
+
+ def test_partial_multiplechoice(self):
+ class MultipleChoiceSerializer(serializers.Serializer):
+ multiplechoice = serializers.MultipleChoiceField(choices=[1, 2, 3])
+ serializer = MultipleChoiceSerializer(data=MultiValueDict(), partial=True)
+ result = serializer.to_internal_value(data={})
+ assert "multiplechoice" not in result
+ assert serializer.is_valid()
+ assert serializer.validated_data == {}
+ assert serializer.errors == {}
diff --git a/tests/test_utils.py b/tests/test_utils.py
index acda7adf8..5d3cdad7e 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -150,16 +150,16 @@ class ResolveModelWithPatchedDjangoTests(TestCase):
def setUp(self):
"""Monkeypatch get_model."""
- self.get_model = rest_framework.utils.model_meta.models.get_model
+ self.get_model = rest_framework.utils.model_meta.apps.get_model
def get_model(app_label, model_name):
return None
- rest_framework.utils.model_meta.models.get_model = get_model
+ rest_framework.utils.model_meta.apps.get_model = get_model
def tearDown(self):
"""Revert monkeypatching."""
- rest_framework.utils.model_meta.models.get_model = self.get_model
+ rest_framework.utils.model_meta.apps.get_model = self.get_model
def test_blows_up_if_model_does_not_resolve(self):
with self.assertRaises(ImproperlyConfigured):
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 206e882a7..acaaf5743 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -48,7 +48,7 @@ class TestUniquenessValidation(TestCase):
data = {'username': 'existing'}
serializer = UniquenessSerializer(data=data)
assert not serializer.is_valid()
- assert serializer.errors == {'username': ['This field must be unique.']}
+ assert serializer.errors == {'username': ['UniquenessModel with this username already exists.']}
def test_is_unique(self):
data = {'username': 'other'}
diff --git a/tests/test_views.py b/tests/test_views.py
index cbe1939eb..05c499481 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -74,21 +74,6 @@ class ClassBasedViewIntegrationTests(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
- def test_400_parse_error_tunneled_content(self):
- content = 'f00bar'
- content_type = 'application/json'
- form_data = {
- api_settings.FORM_CONTENT_OVERRIDE: content,
- api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type
- }
- request = factory.post('/', form_data)
- response = self.view(request)
- expected = {
- 'detail': JSON_ERROR
- }
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(sanitise_json_error(response.data), expected)
-
class FunctionBasedViewIntegrationTests(TestCase):
def setUp(self):
@@ -103,21 +88,6 @@ class FunctionBasedViewIntegrationTests(TestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
- def test_400_parse_error_tunneled_content(self):
- content = 'f00bar'
- content_type = 'application/json'
- form_data = {
- api_settings.FORM_CONTENT_OVERRIDE: content,
- api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type
- }
- request = factory.post('/', form_data)
- response = self.view(request)
- expected = {
- 'detail': JSON_ERROR
- }
- self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
- self.assertEqual(sanitise_json_error(response.data), expected)
-
class TestCustomExceptionHandler(TestCase):
def setUp(self):
diff --git a/tox.ini b/tox.ini
index ef505248b..0ef64cc49 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,30 +4,57 @@ addopts=--tb=short
[tox]
envlist =
py27-{lint,docs},
- {py26,py27,py32,py33,py34}-django{15,16},
- {py27,py32,py33,py34}-django{17,18,master}
+ {py27,py32,py33,py34}-django{17,18},
+ {py27,py34,py35}-django{19}
[testenv]
+basepython =
+ py27: python2.7
+ py32: python3.2
+ py33: python3.3
+ py34: python3.4
+ py35: python3.5
+
commands = ./runtests.py --fast {posargs} --coverage
setenv =
PYTHONDONTWRITEBYTECODE=1
deps =
- django15: Django==1.5.6 # Should track minimum supported
- django16: Django==1.6.3 # Should track minimum supported
- django17: Django==1.7.10 # Should track maximum supported
- django18: Django==1.8.4 # Should track maximum supported
- djangomaster: https://github.com/django/django/archive/master.tar.gz
- -rrequirements/requirements-testing.txt
- -rrequirements/requirements-optionals.txt
+ django17: Django==1.7.10 # Should track maximum supported
+ django18: Django==1.8.4 # Should track maximum supported
+ django19: https://www.djangoproject.com/download/1.9a1/tarball/
+ -rrequirements/requirements-testing.txt
+ -rrequirements/requirements-optionals.txt
[testenv:py27-lint]
commands = ./runtests.py --lintonly
deps =
- -rrequirements/requirements-codestyle.txt
- -rrequirements/requirements-testing.txt
+ -rrequirements/requirements-codestyle.txt
+ -rrequirements/requirements-testing.txt
[testenv:py27-docs]
commands = mkdocs build
deps =
-rrequirements/requirements-testing.txt
-rrequirements/requirements-documentation.txt
+
+# Specify explicitly to exclude Django Guardian against Django 1.9
+[testenv:py27-django19]
+deps =
+ https://www.djangoproject.com/download/1.9a1/tarball/
+ -rrequirements/requirements-testing.txt
+ markdown==2.5.2
+ django-filter==0.10.0
+
+[testenv:py34-django19]
+deps =
+ https://www.djangoproject.com/download/1.9a1/tarball/
+ -rrequirements/requirements-testing.txt
+ markdown==2.5.2
+ django-filter==0.10.0
+
+[testenv:py35-django19]
+deps =
+ https://www.djangoproject.com/download/1.9a1/tarball/
+ -rrequirements/requirements-testing.txt
+ markdown==2.5.2
+ django-filter==0.10.0