aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xdocs/api-guide/authentication.md3
-rw-r--r--docs/topics/contributing.md8
-rw-r--r--rest_framework/routers.py5
-rw-r--r--rest_framework/serializers.py6
-rw-r--r--rest_framework/test.py46
-rw-r--r--tests/test_testing.py47
-rw-r--r--tox.ini2
7 files changed, 110 insertions, 7 deletions
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index b355012e..3a5156fd 100755
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -168,12 +168,13 @@ The `curl` command line tool may be useful for testing token authenticated APIs.
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
+ from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
- @receiver(post_save, sender=get_user_model())
+ @receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md
index 3400bc8f..4fafb1b1 100644
--- a/docs/topics/contributing.md
+++ b/docs/topics/contributing.md
@@ -210,7 +210,9 @@ We recommend the [`django-reusable-app`][django-reusable-app] template as a good
## Linking to your package
-Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation.
+Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation. You can add your package under **Third party packages** of the API Guide section that best applies, like [Authentication][authentication] or [Permissions][permissions]. You can also link your package under the [Third Party Resources][third-party-resources] section.
+
+We also suggest adding it to the [REST Framework][rest-framework-grid] grid on Django Packages.
[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html
[code-of-conduct]: https://www.djangoproject.com/conduct/
@@ -225,3 +227,7 @@ Once your package is decently documented and available on PyPI open a pull reque
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
[mou]: http://mouapp.com/
[django-reusable-app]: https://github.com/dabapps/django-reusable-app
+[authentication]: ../api-guide/authentication.md
+[permissions]: ../api-guide/permissions.md
+[third-party-resources]: third-party-resources.md
+[rest-framework-grid]: https://www.djangopackages.com/grids/g/django-rest-framework/
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index f2d06211..169e6e8b 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -20,6 +20,7 @@ from collections import namedtuple
from django.conf.urls import patterns, url
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import NoReverseMatch
+from django.utils.datastructures import SortedDict
from rest_framework import views
from rest_framework.response import Response
from rest_framework.reverse import reverse
@@ -277,7 +278,7 @@ class DefaultRouter(SimpleRouter):
"""
Return a view to use as the API root.
"""
- api_root_dict = {}
+ api_root_dict = SortedDict()
list_name = self.routes[0].name
for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename)
@@ -286,7 +287,7 @@ class DefaultRouter(SimpleRouter):
_ignore_model_permissions = True
def get(self, request, *args, **kwargs):
- ret = {}
+ ret = SortedDict()
for key, url_name in api_root_dict.items():
try:
ret[key] = reverse(
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 340c6be0..7d85894f 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -849,7 +849,7 @@ class ModelSerializer(Serializer):
}
if model_field:
- kwargs['required'] = not(model_field.null or model_field.blank)
+ kwargs['required'] = not(model_field.null or model_field.blank) and model_field.editable
if model_field.help_text is not None:
kwargs['help_text'] = model_field.help_text
if model_field.verbose_name is not None:
@@ -872,7 +872,7 @@ class ModelSerializer(Serializer):
"""
kwargs = {}
- if model_field.null or model_field.blank:
+ if model_field.null or model_field.blank and model_field.editable:
kwargs['required'] = False
if isinstance(model_field, models.AutoField) or not model_field.editable:
@@ -1130,7 +1130,7 @@ class HyperlinkedModelSerializer(ModelSerializer):
}
if model_field:
- kwargs['required'] = not(model_field.null or model_field.blank)
+ kwargs['required'] = not(model_field.null or model_field.blank) and model_field.editable
if model_field.help_text is not None:
kwargs['help_text'] = model_field.help_text
if model_field.verbose_name is not None:
diff --git a/rest_framework/test.py b/rest_framework/test.py
index 9b40353a..74d2c868 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -156,6 +156,52 @@ class APIClient(APIRequestFactory, DjangoClient):
kwargs.update(self._credentials)
return super(APIClient, self).request(**kwargs)
+ def get(self, path, data=None, follow=False, **extra):
+ response = super(APIClient, self).get(path, data=data, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def post(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).post(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def put(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).put(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def patch(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).patch(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def delete(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).delete(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def options(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).options(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
def logout(self):
self._credentials = {}
return super(APIClient, self).logout()
diff --git a/tests/test_testing.py b/tests/test_testing.py
index 9c472026..9fd5966e 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -5,6 +5,7 @@ from django.conf.urls import patterns, url
from io import BytesIO
from django.contrib.auth.models import User
+from django.shortcuts import redirect
from django.test import TestCase
from rest_framework.decorators import api_view
from rest_framework.response import Response
@@ -28,10 +29,16 @@ def session_view(request):
})
+@api_view(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'])
+def redirect_view(request):
+ return redirect('/view/')
+
+
urlpatterns = patterns(
'',
url(r'^view/$', view),
url(r'^session-view/$', session_view),
+ url(r'^redirect-view/$', redirect_view),
)
@@ -111,6 +118,46 @@ class TestAPITestClient(TestCase):
response = self.client.get('/view/')
self.assertEqual(response.data['auth'], b'')
+ def test_follow_redirect(self):
+ """
+ Follow redirect by setting follow argument.
+ """
+ response = self.client.get('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.get('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.post('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.put('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.put('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.patch('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.patch('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.delete('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.delete('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.options('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.options('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
class TestAPIRequestFactory(TestCase):
def test_csrf_exempt_by_default(self):
diff --git a/tox.ini b/tox.ini
index d40a7079..b3f53cce 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,6 +9,8 @@ envlist =
[testenv]
commands = ./runtests.py --fast
+setenv =
+ PYTHONDONTWRITEBYTECODE=1
[testenv:flake8]
basepython = python2.7