diff options
| -rw-r--r-- | .travis.yml | 3 | ||||
| -rw-r--r-- | README.md | 2 | ||||
| -rwxr-xr-x | docs/api-guide/authentication.md | 2 | ||||
| -rw-r--r-- | docs/api-guide/fields.md | 4 | ||||
| -rw-r--r-- | docs/api-guide/viewsets.md | 5 | ||||
| -rw-r--r-- | docs/index.md | 2 | ||||
| -rw-r--r-- | docs/topics/credits.md | 4 | ||||
| -rw-r--r-- | docs/topics/release-notes.md | 7 | ||||
| -rw-r--r-- | optionals.txt | 2 | ||||
| -rw-r--r-- | rest_framework/__init__.py | 2 | ||||
| -rw-r--r-- | rest_framework/decorators.py | 6 | ||||
| -rw-r--r-- | rest_framework/fields.py | 5 | ||||
| -rw-r--r-- | rest_framework/relations.py | 16 | ||||
| -rw-r--r-- | rest_framework/request.py | 33 | ||||
| -rw-r--r-- | rest_framework/routers.py | 10 | ||||
| -rwxr-xr-x | rest_framework/runtests/runtests.py | 7 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 18 | ||||
| -rw-r--r-- | rest_framework/tests/test_authentication.py | 39 | ||||
| -rw-r--r-- | rest_framework/tests/test_fields.py | 49 | ||||
| -rw-r--r-- | rest_framework/tests/test_routers.py | 17 | ||||
| -rw-r--r-- | rest_framework/tests/test_serializer.py | 28 | ||||
| -rw-r--r-- | rest_framework/tests/test_validation.py | 22 | ||||
| -rw-r--r-- | rest_framework/tests/tests.py | 6 | ||||
| -rw-r--r-- | tox.ini | 32 |
24 files changed, 240 insertions, 81 deletions
diff --git a/.travis.yml b/.travis.yml index 3a7c2d7a..6a453241 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ python: - "3.3" env: + - DJANGO="https://www.djangoproject.com/download/1.6a1/tarball/" - DJANGO="django==1.5.1 --use-mirrors" - DJANGO="django==1.4.5 --use-mirrors" - DJANGO="django==1.3.7 --use-mirrors" @@ -16,7 +17,7 @@ install: - pip install defusedxml==0.3 - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.3 --use-mirrors; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi" - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi" - export PYTHONPATH=. @@ -27,7 +27,7 @@ There is a live example API for testing purposes, [available here][sandbox]. # Requirements * Python (2.6.5+, 2.7, 3.2, 3.3) -* Django (1.3, 1.4, 1.5) +* Django (1.3, 1.4, 1.5, 1.6) # Installation diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 6888ac4e..09491f02 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -333,7 +333,7 @@ The following example will authenticate any incoming request as the user given b try: user = User.objects.get(username=username) except User.DoesNotExist: - raise authenticate.AuthenticationFailed('No such user') + raise exceptions.AuthenticationFailed('No such user') return (user, None) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 9820cb40..c7db32ed 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -41,7 +41,9 @@ Defaults to `True`. ### `default` -If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all. +If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all. + +May be set to a function or other callable, in which case the value will be evaluated each time it is used. ### `validators` diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 79364626..2783da98 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -126,6 +126,11 @@ The `@action` and `@link` decorators can additionally take extra arguments that def set_password(self, request, pk=None): ... +The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example: + + @action(methods=['POST', 'DELETE']) + def unset_password(self, request, pk=None): + ... --- # API Reference diff --git a/docs/index.md b/docs/index.md index 78259a00..d944ddd4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,7 +32,7 @@ There is a live example API for testing purposes, [available here][sandbox]. REST framework requires the following: * Python (2.6.5+, 2.7, 3.2, 3.3) -* Django (1.3, 1.4, 1.5) +* Django (1.3, 1.4, 1.5, 1.6) The following packages are optional: diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 1271fe45..bbe209c7 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -137,6 +137,8 @@ The following people have helped make REST framework great. * Michal Dvořák - [mikee2185] * Markus Törnqvist - [mjtorn] * Pascal Borreli - [pborreli] +* Alex Burgel - [aburgel] +* David Medina - [copitux] Many thanks to everyone who's contributed to the project. @@ -310,3 +312,5 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [mikee2185]: https://github.com/mikee2185 [mjtorn]: https://github.com/mjtorn [pborreli]: https://github.com/pborreli +[aburgel]: https://github.com/aburgel +[copitux]: https://github.com/copitux diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 77cd4f9e..9ac51f42 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,9 +40,14 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series -### Master +### 2.3.5 + +**Date**: 3rd June 2013 * Added `get_url` hook to `HyperlinkedIdentityField`. +* Serializer field `default` argument may be a callable. +* `@action` decorator now accepts a `methods` argument. +* Bugfix: `request.user` should be still be accessible in renderer context if authentication fails. * Bugfix: The `lookup_field` option on `HyperlinkedIdentityField` should apply by default to the url field on the serializer. * Bugfix: `HyperlinkedIdentityField` should continue to support `pk_url_kwarg`, `slug_url_kwarg`, `slug_field`, in a pending deprecation state. * Bugfix: Ensure we always return 404 instead of 500 if a lookup field cannot be converted to the correct lookup type. (Eg non-numeric `AutoInteger` pk lookup) diff --git a/optionals.txt b/optionals.txt index 1853f74b..4ebfceab 100644 --- a/optionals.txt +++ b/optionals.txt @@ -4,4 +4,4 @@ defusedxml>=0.3 django-filter>=0.5.4 django-oauth-plus>=2.0 oauth2>=1.5.211 -django-oauth2-provider>=0.2.3 +django-oauth2-provider>=0.2.4 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 59046733..0a210186 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.3.4' +__version__ = '2.3.5' VERSION = __version__ # synonym diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 25bbbb17..c69756a4 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -112,18 +112,18 @@ def link(**kwargs): Used to mark a method on a ViewSet that should be routed for GET requests. """ def decorator(func): - func.bind_to_method = 'get' + func.bind_to_methods = ['get'] func.kwargs = kwargs return func return decorator -def action(**kwargs): +def action(methods=['post'], **kwargs): """ Used to mark a method on a ViewSet that should be routed for POST requests. """ def decorator(func): - func.bind_to_method = 'post' + func.bind_to_methods = methods func.kwargs = kwargs return func return decorator diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1534eeca..535aa2ac 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -295,7 +295,10 @@ class WritableField(Field): except KeyError: if self.default is not None and not self.partial: # Note: partial updates shouldn't set defaults - native = self.default + if is_simple_callable(self.default): + native = self.default() + else: + native = self.default else: if self.required: raise ValidationError(self.error_messages['required']) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 4ecf795c..e3675b51 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -72,7 +72,6 @@ class RelatedField(WritableField): else: # Reverse self.queryset = manager.field.rel.to._default_manager.all() except Exception: - raise msg = ('Serializer related fields must include a `queryset`' + ' argument or set `read_only=True') raise Exception(msg) @@ -488,14 +487,15 @@ class HyperlinkedIdentityField(Field): slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden def __init__(self, *args, **kwargs): - # TODO: Make view_name mandatory, and have the - # HyperlinkedModelSerializer set it on-the-fly - self.view_name = kwargs.pop('view_name', None) - # Optionally the format of the target hyperlink may be specified + 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) - if lookup_field is not None: - self.lookup_field = lookup_field + self.lookup_field = lookup_field or self.lookup_field # These are pending deprecation if 'pk_url_kwarg' in kwargs: @@ -518,7 +518,7 @@ class HyperlinkedIdentityField(Field): 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 or self.parent.opts.view_name + view_name = self.view_name if request is None: warnings.warn("Using `HyperlinkedIdentityField` without including the " diff --git a/rest_framework/request.py b/rest_framework/request.py index a434659c..0d88ebc7 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -173,7 +173,7 @@ class Request(object): by the authentication classes provided to the request. """ if not hasattr(self, '_user'): - self._authenticator, self._user, self._auth = self._authenticate() + self._authenticate() return self._user @user.setter @@ -192,7 +192,7 @@ class Request(object): request, such as an authentication token. """ if not hasattr(self, '_auth'): - self._authenticator, self._user, self._auth = self._authenticate() + self._authenticate() return self._auth @auth.setter @@ -210,7 +210,7 @@ class Request(object): to authenticate the request, or `None`. """ if not hasattr(self, '_authenticator'): - self._authenticator, self._user, self._auth = self._authenticate() + self._authenticate() return self._authenticator def _load_data_and_files(self): @@ -330,11 +330,18 @@ class Request(object): Returns a three-tuple of (authenticator, user, authtoken). """ for authenticator in self.authenticators: - user_auth_tuple = authenticator.authenticate(self) + try: + user_auth_tuple = authenticator.authenticate(self) + except exceptions.APIException: + self._not_authenticated() + raise + if not user_auth_tuple is None: - user, auth = user_auth_tuple - return (authenticator, user, auth) - return self._not_authenticated() + self._authenticator = authenticator + self._user, self._auth = user_auth_tuple + return + + self._not_authenticated() def _not_authenticated(self): """ @@ -343,17 +350,17 @@ class Request(object): By default this will be (None, AnonymousUser, None). """ + self._authenticator = None + if api_settings.UNAUTHENTICATED_USER: - user = api_settings.UNAUTHENTICATED_USER() + self._user = api_settings.UNAUTHENTICATED_USER() else: - user = None + self._user = None if api_settings.UNAUTHENTICATED_TOKEN: - auth = api_settings.UNAUTHENTICATED_TOKEN() + self._auth = api_settings.UNAUTHENTICATED_TOKEN() else: - auth = None - - return (None, user, auth) + self._auth = None def __getattr__(self, attr): """ diff --git a/rest_framework/routers.py b/rest_framework/routers.py index dba104c3..6c5fd004 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -131,20 +131,20 @@ class SimpleRouter(BaseRouter): dynamic_routes = [] for methodname in dir(viewset): attr = getattr(viewset, methodname) - httpmethod = getattr(attr, 'bind_to_method', None) - if httpmethod: - dynamic_routes.append((httpmethod, methodname)) + httpmethods = getattr(attr, 'bind_to_methods', None) + if httpmethods: + dynamic_routes.append((httpmethods, methodname)) ret = [] for route in self.routes: if route.mapping == {'{httpmethod}': '{methodname}'}: # Dynamic routes (@link or @action decorator) - for httpmethod, methodname in dynamic_routes: + for httpmethods, methodname in dynamic_routes: initkwargs = route.initkwargs.copy() initkwargs.update(getattr(viewset, methodname).kwargs) ret.append(Route( url=replace_methodname(route.url, methodname), - mapping={httpmethod: methodname}, + mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), name=replace_methodname(route.name, methodname), initkwargs=initkwargs, )) diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py index 4a333fb3..da36d23f 100755 --- a/rest_framework/runtests/runtests.py +++ b/rest_framework/runtests/runtests.py @@ -10,6 +10,7 @@ import sys sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings' +import django from django.conf import settings from django.test.utils import get_runner @@ -35,7 +36,11 @@ def main(): else: print(usage()) sys.exit(1) - failures = test_runner.run_tests(['tests' + test_case]) + test_module_name = 'rest_framework.tests' + if django.VERSION[0] == 1 and django.VERSION[1] < 6: + test_module_name = 'tests' + + failures = test_runner.run_tests([test_module_name + test_case]) sys.exit(failures) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4dde0d7c..11ead02e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -315,7 +315,8 @@ class BaseSerializer(WritableField): self._errors = {} if data is not None or files is not None: attrs = self.restore_fields(data, files) - attrs = self.perform_validation(attrs) + if attrs is not None: + attrs = self.perform_validation(attrs) else: self._errors['non_field_errors'] = ['No input provided'] @@ -903,15 +904,24 @@ class HyperlinkedModelSerializer(ModelSerializer): _default_view_name = '%(model_name)s-detail' _hyperlink_field_class = HyperlinkedRelatedField - url = HyperlinkedIdentityField() + # Just a placeholder to ensure 'url' is the first field + # The field itself is actually created on initialization, + # when the view_name and lookup_field arguments are available. + url = Field() def __init__(self, *args, **kwargs): super(HyperlinkedModelSerializer, self).__init__(*args, **kwargs) - lookup_field = self.opts.lookup_field - self.fields['url'] = HyperlinkedIdentityField(lookup_field=lookup_field) + if self.opts.view_name is None: self.opts.view_name = self._get_default_view_name(self.opts.model) + url_field = HyperlinkedIdentityField( + view_name=self.opts.view_name, + lookup_field=self.opts.lookup_field + ) + url_field.initialize(self, 'url') + self.fields['url'] = url_field + def _get_default_view_name(self, model): """ Return the view name to use if 'view_name' is not specified in 'Meta' diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index 05e9fbc3..d46ac079 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -6,6 +6,8 @@ from django.utils import unittest from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions from rest_framework import permissions +from rest_framework import renderers +from rest_framework.response import Response from rest_framework import status from rest_framework.authentication import ( BaseAuthentication, @@ -553,3 +555,40 @@ class OAuth2Tests(TestCase): auth = self._create_authorization_header(token=read_write_access_token.token) response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) + + +class FailingAuthAccessedInRenderer(TestCase): + def setUp(self): + class AuthAccessingRenderer(renderers.BaseRenderer): + media_type = 'text/plain' + format = 'txt' + + def render(self, data, media_type=None, renderer_context=None): + request = renderer_context['request'] + if request.user.is_authenticated(): + return b'authenticated' + return b'not authenticated' + + class FailingAuth(BaseAuthentication): + def authenticate(self, request): + raise exceptions.AuthenticationFailed('authentication failed') + + class ExampleView(APIView): + authentication_classes = (FailingAuth,) + renderer_classes = (AuthAccessingRenderer,) + + def get(self, request): + return Response({'foo': 'bar'}) + + self.view = ExampleView.as_view() + + def test_failing_auth_accessed_in_renderer(self): + """ + When authentication fails the renderer should still be able to access + `request.user` without raising an exception. Particularly relevant + to HTML responses that might reasonably access `request.user`. + """ + request = factory.get('/') + response = self.view(request) + content = response.render().content + self.assertEqual(content, b'not authenticated') diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index bff4400b..de371001 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -11,8 +11,6 @@ from django.db import models from django.test import TestCase from django.utils.datastructures import SortedDict from rest_framework import serializers -from rest_framework.fields import Field, CharField -from rest_framework.serializers import Serializer from rest_framework.tests.models import RESTFrameworkModel @@ -590,7 +588,7 @@ class DecimalFieldTest(TestCase): """ Make sure the serializer works correctly """ - class DecimalSerializer(Serializer): + class DecimalSerializer(serializers.Serializer): decimal_field = serializers.DecimalField(max_value=9010, min_value=9000, max_digits=6, @@ -608,7 +606,7 @@ class DecimalFieldTest(TestCase): """ Make sure max_value violations raises ValidationError """ - class DecimalSerializer(Serializer): + class DecimalSerializer(serializers.Serializer): decimal_field = serializers.DecimalField(max_value=100) s = DecimalSerializer(data={'decimal_field': '123'}) @@ -620,7 +618,7 @@ class DecimalFieldTest(TestCase): """ Make sure min_value violations raises ValidationError """ - class DecimalSerializer(Serializer): + class DecimalSerializer(serializers.Serializer): decimal_field = serializers.DecimalField(min_value=100) s = DecimalSerializer(data={'decimal_field': '99'}) @@ -632,7 +630,7 @@ class DecimalFieldTest(TestCase): """ Make sure max_digits violations raises ValidationError """ - class DecimalSerializer(Serializer): + class DecimalSerializer(serializers.Serializer): decimal_field = serializers.DecimalField(max_digits=5) s = DecimalSerializer(data={'decimal_field': '123.456'}) @@ -644,7 +642,7 @@ class DecimalFieldTest(TestCase): """ Make sure max_decimal_places violations raises ValidationError """ - class DecimalSerializer(Serializer): + class DecimalSerializer(serializers.Serializer): decimal_field = serializers.DecimalField(decimal_places=3) s = DecimalSerializer(data={'decimal_field': '123.4567'}) @@ -656,7 +654,7 @@ class DecimalFieldTest(TestCase): """ Make sure max_whole_digits violations raises ValidationError """ - class DecimalSerializer(Serializer): + class DecimalSerializer(serializers.Serializer): decimal_field = serializers.DecimalField(max_digits=4, decimal_places=3) s = DecimalSerializer(data={'decimal_field': '12345.6'}) @@ -837,11 +835,11 @@ class URLFieldTests(TestCase): class FieldMetadata(TestCase): def setUp(self): - self.required_field = Field() + self.required_field = serializers.Field() self.required_field.label = uuid4().hex self.required_field.required = True - self.optional_field = Field() + self.optional_field = serializers.Field() self.optional_field.label = uuid4().hex self.optional_field.required = False @@ -856,24 +854,15 @@ class FieldMetadata(TestCase): self.assertEqual(field.metadata()['label'], field.label) -class MetadataSerializer(Serializer): - field1 = CharField(3, required=True) - field2 = CharField(10, required=False) - - -class MetadataSerializerTestCase(TestCase): +class FieldCallableDefault(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) + 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.assertEquals(into, {'field': 'foo bar'}) diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py index fc3a87e9..10d3cc25 100644 --- a/rest_framework/tests/test_routers.py +++ b/rest_framework/tests/test_routers.py @@ -25,6 +25,10 @@ class BasicViewSet(viewsets.ViewSet): def action2(self, request, *args, **kwargs): return Response({'method': 'action2'}) + @action(methods=['post', 'delete']) + def action3(self, request, *args, **kwargs): + return Response({'method': 'action2'}) + @link() def link1(self, request, *args, **kwargs): return Response({'method': 'link1'}) @@ -42,17 +46,20 @@ class TestSimpleRouter(TestCase): routes = self.router.get_routes(BasicViewSet) decorator_routes = routes[2:] # Make sure all these endpoints exist and none have been clobbered - for i, endpoint in enumerate(['action1', 'action2', 'link1', 'link2']): + for i, endpoint in enumerate(['action1', 'action2', 'action3', 'link1', 'link2']): route = decorator_routes[i] # check url listing self.assertEqual(route.url, '^{{prefix}}/{{lookup}}/{0}/$'.format(endpoint)) # check method to function mapping - if endpoint.startswith('action'): - method_map = 'post' + if endpoint == 'action3': + methods_map = ['post', 'delete'] + elif endpoint.startswith('action'): + methods_map = ['post'] else: - method_map = 'get' - self.assertEqual(route.mapping[method_map], endpoint) + methods_map = ['get'] + for method in methods_map: + self.assertEqual(route.mapping[method], endpoint) class RouterTestModel(models.Model): diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index f2c31872..6cc913c5 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1528,3 +1528,31 @@ class DefaultValuesOnAutogeneratedFieldsTests(TestCase): def test_url_field(self): self.field_test('url_field') + + +class MetadataSerializer(serializers.Serializer): + field1 = serializers.CharField(3, required=True) + field2 = serializers.CharField(10, required=False) + + +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) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index cbdd6515..a6ec0e99 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -63,3 +63,25 @@ class TestPreSaveValidationExclusions(TestCase): # does not have `blank=True`, so this serializer should not validate. serializer = ShouldValidateModelSerializer(data={'renamed': ''}) self.assertEqual(serializer.is_valid(), False) + + +class ValidationSerializer(serializers.Serializer): + foo = serializers.CharField() + + def validate_foo(self, attrs, source): + raise serializers.ValidationError("foo invalid") + + def validate(self, attrs): + raise serializers.ValidationError("serializer invalid") + + +class TestAvoidValidation(TestCase): + """ + If serializer was initialized with invalid data (None or non dict-like), it + should avoid validation layer (validate_<field> and validate methods) + """ + def test_serializer_errors_has_only_invalid_data_error(self): + serializer = ValidationSerializer(data='invalid data') + self.assertFalse(serializer.is_valid()) + self.assertDictEqual(serializer.errors, + {'non_field_errors': ['Invalid data']}) diff --git a/rest_framework/tests/tests.py b/rest_framework/tests/tests.py index 08f88e11..554ebd1a 100644 --- a/rest_framework/tests/tests.py +++ b/rest_framework/tests/tests.py @@ -4,11 +4,13 @@ runner to pick up the tests. Yowzers. """ from __future__ import unicode_literals import os +import django modules = [filename.rsplit('.', 1)[0] for filename in os.listdir(os.path.dirname(__file__)) if filename.endswith('.py') and not filename.startswith('_')] __test__ = dict() -for module in modules: - exec("from rest_framework.tests.%s import *" % module) +if django.VERSION < (1, 6): + for module in modules: + exec("from rest_framework.tests.%s import *" % module) @@ -1,10 +1,40 @@ [tox] downloadcache = {toxworkdir}/cache/ -envlist = py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3 +envlist = py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3 [testenv] commands = {envpython} rest_framework/runtests/runtests.py +[testenv:py3.3-django1.6] +basepython = python3.3 +deps = https://www.djangoproject.com/download/1.6a1/tarball/ + django-filter==0.6a1 + defusedxml==0.3 + +[testenv:py3.2-django1.6] +basepython = python3.2 +deps = https://www.djangoproject.com/download/1.6a1/tarball/ + django-filter==0.6a1 + defusedxml==0.3 + +[testenv:py2.7-django1.6] +basepython = python2.7 +deps = https://www.djangoproject.com/download/1.6a1/tarball/ + django-filter==0.6a1 + defusedxml==0.3 + django-oauth-plus==2.0 + oauth2==1.5.211 + django-oauth2-provider==0.2.3 + +[testenv:py2.6-django1.6] +basepython = python2.6 +deps = https://www.djangoproject.com/download/1.6a1/tarball/ + django-filter==0.6a1 + defusedxml==0.3 + django-oauth-plus==2.0 + oauth2==1.5.211 + django-oauth2-provider==0.2.3 + [testenv:py3.3-django1.5] basepython = python3.3 deps = django==1.5 |
