aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml3
-rw-r--r--README.md2
-rw-r--r--docs/api-guide/fields.md4
-rw-r--r--docs/api-guide/filtering.md2
-rw-r--r--docs/api-guide/viewsets.md5
-rw-r--r--docs/index.md2
-rw-r--r--docs/topics/credits.md2
-rw-r--r--docs/topics/release-notes.md9
-rw-r--r--docs/tutorial/1-serialization.md2
-rw-r--r--docs/tutorial/quickstart.md2
-rw-r--r--rest_framework/decorators.py6
-rw-r--r--rest_framework/fields.py5
-rw-r--r--rest_framework/relations.py38
-rw-r--r--rest_framework/routers.py10
-rwxr-xr-xrest_framework/runtests/runtests.py7
-rw-r--r--rest_framework/serializers.py13
-rw-r--r--rest_framework/tests/models.py2
-rw-r--r--rest_framework/tests/test_authentication.py4
-rw-r--r--rest_framework/tests/test_fields.py49
-rw-r--r--rest_framework/tests/test_routers.py84
-rw-r--r--rest_framework/tests/test_serializer.py28
-rw-r--r--rest_framework/tests/test_testcases.py66
-rw-r--r--rest_framework/tests/tests.py6
-rw-r--r--tox.ini32
24 files changed, 246 insertions, 137 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/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/filtering.md b/docs/api-guide/filtering.md
index 4242f40d..05c997a3 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -231,7 +231,7 @@ Multiple orderings may also be specified:
If an `ordering` attribute is set on the view, this will be used as the default ordering.
-Typicaly you'd instead control this by setting `order_by` on the initial queryset, but using the `ordering` parameter on the view allows you to specify the ordering in a way that it can then be passed automatically as context to a rendered template. This makes it possible to automatically render column headers differently if they are being used to order the results.
+Typically you'd instead control this by setting `order_by` on the initial queryset, but using the `ordering` parameter on the view allows you to specify the ordering in a way that it can then be passed automatically as context to a rendered template. This makes it possible to automatically render column headers differently if they are being used to order the results.
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
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..2552af19 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -137,6 +137,7 @@ The following people have helped make REST framework great.
* Michal Dvořák - [mikee2185]
* Markus Törnqvist - [mjtorn]
* Pascal Borreli - [pborreli]
+* Alex Burgel - [aburgel]
Many thanks to everyone who's contributed to the project.
@@ -310,3 +311,4 @@ 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
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index fb14cf5f..005538ae 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -40,6 +40,15 @@ You can determine your currently installed version using `pip freeze`:
## 2.3.x series
+### Master
+
+* Added `get_url` hook to `HyperlinkedIdentityField`.
+* Serializer field `default` argument may be a callable.
+* `@action` decorator now accepts a `methods` argument.
+* 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)
+
### 2.3.4
**Date**: 24th May 2013
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index 3382deea..bbb9b73c 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -146,6 +146,8 @@ The first thing we need to get started on our Web API is provide a way of serial
The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
+Notice that we can also use various attributes that would typically be used on form fields, such as `widget=widgets.Testarea`. These can be used to control how the serializer should render when displayed as an HTML form. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
+
We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
## Working with Serializers
diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md
index a80e31c0..f15e75c0 100644
--- a/docs/tutorial/quickstart.md
+++ b/docs/tutorial/quickstart.md
@@ -91,7 +91,7 @@ We can easily break these down into individual views if we need to, but using vi
## URLs
-Okay, now let's wire up the API URLs. On to `quickstart/urls.py`...
+Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
from django.conf.urls import patterns, url, include
from rest_framework import routers
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 41707efc..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,13 +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
- self.format = kwargs.pop('format', None)
+ try:
+ self.view_name = kwargs.pop('view_name')
+ except KeyError:
+ msg = "HyperlinkedIdentityField requires 'view_name' argument"
+ raise ValueError(msg)
- self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
+ self.format = kwargs.pop('format', None)
+ lookup_field = kwargs.pop('lookup_field', None)
+ self.lookup_field = lookup_field or self.lookup_field
# These are pending deprecation
if 'pk_url_kwarg' in kwargs:
@@ -517,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 "
@@ -537,6 +538,25 @@ class HyperlinkedIdentityField(Field):
if format and self.format and self.format != format:
format = self.format
+ # Return the hyperlink, or error if incorrectly configured.
+ try:
+ return self.get_url(obj, view_name, request, format)
+ except NoReverseMatch:
+ msg = (
+ 'Could not resolve URL for hyperlinked relationship using '
+ 'view name "%s". You may have failed to include the related '
+ 'model in your API, or incorrectly configured the '
+ '`lookup_field` attribute on this field.'
+ )
+ raise Exception(msg % view_name)
+
+ def get_url(self, obj, view_name, request, format):
+ """
+ Given an object, return the URL that hyperlinks to the object.
+
+ May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
+ attributes are not configured to correctly match the URL conf.
+ """
lookup_field = getattr(obj, self.lookup_field)
kwargs = {self.lookup_field: lookup_field}
try:
@@ -562,7 +582,7 @@ class HyperlinkedIdentityField(Field):
except NoReverseMatch:
pass
- raise Exception('Could not resolve URL for field using view name "%s"' % view_name)
+ raise NoReverseMatch()
### Old-style many classes for backwards compat
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 e7b8fdc0..11ead02e 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -904,13 +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)
+
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/models.py b/rest_framework/tests/models.py
index abf50a2d..e2d4eacd 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -162,8 +162,8 @@ class NullableOneToOneSource(RESTFrameworkModel):
target = models.OneToOneField(OneToOneTarget, null=True, blank=True,
related_name='nullable_source')
+
# Serializer used to test BasicModel
class BasicModelSerializer(serializers.ModelSerializer):
class Meta:
model = BasicModel
-
diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py
index 90e1f5c4..05e9fbc3 100644
--- a/rest_framework/tests/test_authentication.py
+++ b/rest_framework/tests/test_authentication.py
@@ -48,7 +48,7 @@ urlpatterns = patterns('',
(r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])),
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
(r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])),
- (r'^oauth-with-scope/$', MockView.as_view(authentication_classes=[OAuthAuthentication],
+ (r'^oauth-with-scope/$', MockView.as_view(authentication_classes=[OAuthAuthentication],
permission_classes=[permissions.TokenHasReadWriteScope]))
)
@@ -56,7 +56,7 @@ if oauth2_provider is not None:
urlpatterns += patterns('',
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])),
- url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication],
+ url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication],
permission_classes=[permissions.TokenHasReadWriteScope])),
)
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 4e4765cb..10d3cc25 100644
--- a/rest_framework/tests/test_routers.py
+++ b/rest_framework/tests/test_routers.py
@@ -1,15 +1,17 @@
from __future__ import unicode_literals
+from django.db import models
from django.test import TestCase
from django.test.client import RequestFactory
-from rest_framework import status
-from rest_framework.response import Response
-from rest_framework import viewsets
+from rest_framework import serializers, viewsets
+from rest_framework.compat import include, patterns, url
from rest_framework.decorators import link, action
+from rest_framework.response import Response
from rest_framework.routers import SimpleRouter
-import copy
factory = RequestFactory()
+urlpatterns = patterns('',)
+
class BasicViewSet(viewsets.ViewSet):
def list(self, request, *args, **kwargs):
@@ -23,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'})
@@ -40,16 +46,76 @@ 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):
+ uuid = models.CharField(max_length=20)
+ text = models.CharField(max_length=200)
+
+
+class TestCustomLookupFields(TestCase):
+ """
+ Ensure that custom lookup fields are correctly routed.
+ """
+ urls = 'rest_framework.tests.test_routers'
+
+ def setUp(self):
+ class NoteSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = RouterTestModel
+ lookup_field = 'uuid'
+ fields = ('url', 'uuid', 'text')
+
+ class NoteViewSet(viewsets.ModelViewSet):
+ queryset = RouterTestModel.objects.all()
+ serializer_class = NoteSerializer
+ lookup_field = 'uuid'
+
+ RouterTestModel.objects.create(uuid='123', text='foo bar')
+
+ self.router = SimpleRouter()
+ self.router.register(r'notes', NoteViewSet)
+
+ from rest_framework.tests import test_routers
+ urls = getattr(test_routers, 'urlpatterns')
+ urls += patterns('',
+ url(r'^', include(self.router.urls)),
+ )
+
+ def test_custom_lookup_field_route(self):
+ detail_route = self.router.urls[-1]
+ detail_url_pattern = detail_route.regex.pattern
+ self.assertIn('<uuid>', detail_url_pattern)
+
+ def test_retrieve_lookup_field_list_view(self):
+ response = self.client.get('/notes/')
+ self.assertEquals(response.data,
+ [{
+ "url": "http://testserver/notes/123/",
+ "uuid": "123", "text": "foo bar"
+ }]
+ )
+ def test_retrieve_lookup_field_detail_view(self):
+ response = self.client.get('/notes/123/')
+ self.assertEquals(response.data,
+ {
+ "url": "http://testserver/notes/123/",
+ "uuid": "123", "text": "foo bar"
+ }
+ )
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_testcases.py b/rest_framework/tests/test_testcases.py
deleted file mode 100644
index f8c2579e..00000000
--- a/rest_framework/tests/test_testcases.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# http://djangosnippets.org/snippets/1011/
-from __future__ import unicode_literals
-from django.conf import settings
-from django.core.management import call_command
-from django.db.models import loading
-from django.test import TestCase
-
-NO_SETTING = ('!', None)
-
-
-class TestSettingsManager(object):
- """
- A class which can modify some Django settings temporarily for a
- test and then revert them to their original values later.
-
- Automatically handles resyncing the DB if INSTALLED_APPS is
- modified.
-
- """
- def __init__(self):
- self._original_settings = {}
-
- def set(self, **kwargs):
- for k, v in kwargs.iteritems():
- self._original_settings.setdefault(k, getattr(settings, k,
- NO_SETTING))
- setattr(settings, k, v)
- if 'INSTALLED_APPS' in kwargs:
- self.syncdb()
-
- def syncdb(self):
- loading.cache.loaded = False
- call_command('syncdb', verbosity=0)
-
- def revert(self):
- for k, v in self._original_settings.iteritems():
- if v == NO_SETTING:
- delattr(settings, k)
- else:
- setattr(settings, k, v)
- if 'INSTALLED_APPS' in self._original_settings:
- self.syncdb()
- self._original_settings = {}
-
-
-class SettingsTestCase(TestCase):
- """
- A subclass of the Django TestCase with a settings_manager
- attribute which is an instance of TestSettingsManager.
-
- Comes with a tearDown() method that calls
- self.settings_manager.revert().
-
- """
- def __init__(self, *args, **kwargs):
- super(SettingsTestCase, self).__init__(*args, **kwargs)
- self.settings_manager = TestSettingsManager()
-
- def tearDown(self):
- self.settings_manager.revert()
-
-
-class TestModelsTestCase(SettingsTestCase):
- def setUp(self, *args, **kwargs):
- installed_apps = tuple(settings.INSTALLED_APPS) + ('rest_framework.tests',)
- self.settings_manager.set(INSTALLED_APPS=installed_apps)
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