aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml3
-rw-r--r--README.md2
-rwxr-xr-xdocs/api-guide/authentication.md2
-rw-r--r--docs/api-guide/fields.md4
-rw-r--r--docs/api-guide/viewsets.md5
-rw-r--r--docs/index.md2
-rw-r--r--docs/topics/credits.md4
-rw-r--r--docs/topics/release-notes.md7
-rw-r--r--optionals.txt2
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/decorators.py6
-rw-r--r--rest_framework/fields.py5
-rw-r--r--rest_framework/relations.py16
-rw-r--r--rest_framework/request.py33
-rw-r--r--rest_framework/routers.py10
-rwxr-xr-xrest_framework/runtests/runtests.py7
-rw-r--r--rest_framework/serializers.py18
-rw-r--r--rest_framework/tests/test_authentication.py39
-rw-r--r--rest_framework/tests/test_fields.py49
-rw-r--r--rest_framework/tests/test_routers.py17
-rw-r--r--rest_framework/tests/test_serializer.py28
-rw-r--r--rest_framework/tests/test_validation.py22
-rw-r--r--rest_framework/tests/tests.py6
-rw-r--r--tox.ini32
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=.
diff --git a/README.md b/README.md
index 94996c39..12ed09f9 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/tox.ini b/tox.ini
index d62359a5..27db9a8a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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