aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
authorPhilip Douglas2013-07-04 05:29:42 -0700
committerPhilip Douglas2013-07-04 05:29:42 -0700
commitbf8e71c455a47a53898f8239ac7dad47e5f1d53a (patch)
tree967055f41f43a35b8d9362f47c81aad5af4036d3 /rest_framework
parentfa9f5fb8dcf6d51b2db70d4e2a991779b056d1d4 (diff)
parent5fa100245cbf71a47c7a1ea7a869d03049380130 (diff)
downloaddjango-rest-framework-bf8e71c455a47a53898f8239ac7dad47e5f1d53a.tar.bz2
Merge pull request #1 from tomchristie/master
Update from main
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/authentication.py9
-rw-r--r--rest_framework/authtoken/models.py4
-rw-r--r--rest_framework/compat.py27
-rw-r--r--rest_framework/parsers.py25
-rw-r--r--rest_framework/routers.py16
-rw-r--r--rest_framework/runtests/settings.py2
-rw-r--r--rest_framework/serializers.py5
-rw-r--r--rest_framework/tests/description.py26
-rw-r--r--rest_framework/tests/test_description.py13
-rw-r--r--rest_framework/tests/test_routers.py57
-rw-r--r--rest_framework/utils/formatting.py4
-rw-r--r--rest_framework/views.py11
13 files changed, 153 insertions, 48 deletions
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 0a210186..776618ac 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,4 +1,4 @@
-__version__ = '2.3.5'
+__version__ = '2.3.6'
VERSION = __version__ # synonym
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index f659a172..10298027 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -3,14 +3,13 @@ Provides various authentication policies.
"""
from __future__ import unicode_literals
import base64
-from datetime import datetime
from django.contrib.auth import authenticate
from django.core.exceptions import ImproperlyConfigured
from rest_framework import exceptions, HTTP_HEADER_ENCODING
from rest_framework.compat import CsrfViewMiddleware
from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
-from rest_framework.compat import oauth2_provider
+from rest_framework.compat import oauth2_provider, provider_now
from rest_framework.authtoken.models import Token
@@ -320,9 +319,9 @@ class OAuth2Authentication(BaseAuthentication):
try:
token = oauth2_provider.models.AccessToken.objects.select_related('user')
- # TODO: Change to timezone aware datetime when oauth2_provider add
- # support to it.
- token = token.get(token=access_token, expires__gt=datetime.now())
+ # provider_now switches to timezone aware datetime when
+ # the oauth2_provider version supports to it.
+ token = token.get(token=access_token, expires__gt=provider_now())
except oauth2_provider.models.AccessToken.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token')
diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py
index 52c45ad1..7601f5b7 100644
--- a/rest_framework/authtoken/models.py
+++ b/rest_framework/authtoken/models.py
@@ -1,7 +1,7 @@
import uuid
import hmac
from hashlib import sha1
-from rest_framework.compat import User
+from rest_framework.compat import AUTH_USER_MODEL
from django.conf import settings
from django.db import models
@@ -11,7 +11,7 @@ class Token(models.Model):
The default authorization token model.
"""
key = models.CharField(max_length=40, primary_key=True)
- user = models.OneToOneField(User, related_name='auth_token')
+ user = models.OneToOneField(AUTH_USER_MODEL, related_name='auth_token')
created = models.DateTimeField(auto_now_add=True)
class Meta:
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 76dc0052..cb122846 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -2,6 +2,7 @@
The `compat` module provides support for backwards compatibility with older
versions of django/python, and compatibility wrappers around optional packages.
"""
+
# flake8: noqa
from __future__ import unicode_literals
@@ -33,6 +34,12 @@ except ImportError:
from django.utils.encoding import force_unicode as force_text
+# HttpResponseBase only exists from 1.5 onwards
+try:
+ from django.http.response import HttpResponseBase
+except ImportError:
+ from django.http import HttpResponse as HttpResponseBase
+
# django-filter is optional
try:
import django_filters
@@ -77,15 +84,9 @@ def get_concrete_model(model_cls):
# Django 1.5 add support for custom auth user model
if django.VERSION >= (1, 5):
from django.conf import settings
- if hasattr(settings, 'AUTH_USER_MODEL'):
- User = settings.AUTH_USER_MODEL
- else:
- from django.contrib.auth.models import User
+ AUTH_USER_MODEL = settings.AUTH_USER_MODEL
else:
- try:
- from django.contrib.auth.models import User
- except ImportError:
- raise ImportError("User model is not to be found.")
+ AUTH_USER_MODEL = 'auth.User'
if django.VERSION >= (1, 5):
@@ -489,12 +490,22 @@ try:
from provider.oauth2 import forms as oauth2_provider_forms
from provider import scope as oauth2_provider_scope
from provider import constants as oauth2_constants
+ from provider import __version__ as provider_version
+ if provider_version in ('0.2.3', '0.2.4'):
+ # 0.2.3 and 0.2.4 are supported version that do not support
+ # timezone aware datetimes
+ import datetime
+ provider_now = datetime.datetime.now
+ else:
+ # Any other supported version does use timezone aware datetimes
+ from django.utils.timezone import now as provider_now
except ImportError:
oauth2_provider = None
oauth2_provider_models = None
oauth2_provider_forms = None
oauth2_provider_scope = None
oauth2_constants = None
+ provider_now = None
# Handle lazy strings
from django.utils.functional import Promise
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index 25be2e6a..96bfac84 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -50,10 +50,7 @@ class JSONParser(BaseParser):
def parse(self, stream, media_type=None, parser_context=None):
"""
- Returns a 2-tuple of `(data, files)`.
-
- `data` will be an object which is the parsed content of the response.
- `files` will always be `None`.
+ Parses the incoming bytestream as JSON and returns the resulting data.
"""
parser_context = parser_context or {}
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
@@ -74,10 +71,7 @@ class YAMLParser(BaseParser):
def parse(self, stream, media_type=None, parser_context=None):
"""
- Returns a 2-tuple of `(data, files)`.
-
- `data` will be an object which is the parsed content of the response.
- `files` will always be `None`.
+ Parses the incoming bytestream as YAML and returns the resulting data.
"""
assert yaml, 'YAMLParser requires pyyaml to be installed'
@@ -100,10 +94,8 @@ class FormParser(BaseParser):
def parse(self, stream, media_type=None, parser_context=None):
"""
- Returns a 2-tuple of `(data, files)`.
-
- `data` will be a :class:`QueryDict` containing all the form parameters.
- `files` will always be :const:`None`.
+ Parses the incoming bytestream as a URL encoded form,
+ and returns the resulting QueryDict.
"""
parser_context = parser_context or {}
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
@@ -120,7 +112,8 @@ class MultiPartParser(BaseParser):
def parse(self, stream, media_type=None, parser_context=None):
"""
- Returns a DataAndFiles object.
+ Parses the incoming bytestream as a multipart encoded form,
+ and returns a DataAndFiles object.
`.data` will be a `QueryDict` containing all the form parameters.
`.files` will be a `QueryDict` containing all the form files.
@@ -147,6 +140,9 @@ class XMLParser(BaseParser):
media_type = 'application/xml'
def parse(self, stream, media_type=None, parser_context=None):
+ """
+ Parses the incoming bytestream as XML and returns the resulting data.
+ """
assert etree, 'XMLParser requires defusedxml to be installed'
parser_context = parser_context or {}
@@ -216,7 +212,8 @@ class FileUploadParser(BaseParser):
def parse(self, stream, media_type=None, parser_context=None):
"""
- Returns a DataAndFiles object.
+ Treats the incoming bytestream as a raw file upload and returns
+ a `DateAndFiles` object.
`.data` will be None (we expect request body to be a file content).
`.files` will be a `QueryDict` containing one 'file' element.
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index ae64cc3b..930011d3 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -15,7 +15,9 @@ For example, you might have a `urls.py` that looks something like this:
"""
from __future__ import unicode_literals
+import itertools
from collections import namedtuple
+from django.core.exceptions import ImproperlyConfigured
from rest_framework import views
from rest_framework.compat import patterns, url
from rest_framework.response import Response
@@ -38,6 +40,13 @@ def replace_methodname(format_string, methodname):
return ret
+def flatten(list_of_lists):
+ """
+ Takes an iterable of iterables, returns a single iterable containing all items
+ """
+ return itertools.chain(*list_of_lists)
+
+
class BaseRouter(object):
def __init__(self):
self.registry = []
@@ -117,7 +126,7 @@ class SimpleRouter(BaseRouter):
if model_cls is None and queryset is not None:
model_cls = queryset.model
- assert model_cls, '`name` not argument not specified, and could ' \
+ assert model_cls, '`base_name` argument not specified, and could ' \
'not automatically determine the name from the viewset, as ' \
'it does not have a `.model` or `.queryset` attribute.'
@@ -130,12 +139,17 @@ class SimpleRouter(BaseRouter):
Returns a list of the Route namedtuple.
"""
+ known_actions = flatten([route.mapping.values() for route in self.routes])
+
# Determine any `@action` or `@link` decorated methods on the viewset
dynamic_routes = []
for methodname in dir(viewset):
attr = getattr(viewset, methodname)
httpmethods = getattr(attr, 'bind_to_methods', None)
if httpmethods:
+ if methodname in known_actions:
+ raise ImproperlyConfigured('Cannot use @action or @link decorator on '
+ 'method "%s" as it is an existing route' % methodname)
httpmethods = [method.lower() for method in httpmethods]
dynamic_routes.append((httpmethods, methodname))
diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py
index 9dd7b545..b3702d0b 100644
--- a/rest_framework/runtests/settings.py
+++ b/rest_framework/runtests/settings.py
@@ -134,6 +134,8 @@ PASSWORD_HASHERS = (
'django.contrib.auth.hashers.CryptPasswordHasher',
)
+AUTH_USER_MODEL = 'auth.User'
+
import django
if django.VERSION < (1, 3):
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 5a8fd89f..023f7ccf 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -915,7 +915,10 @@ class HyperlinkedModelSerializer(ModelSerializer):
view_name=self.opts.view_name,
lookup_field=self.opts.lookup_field
)
- fields.insert(0, 'url', url_field)
+ ret = self._dict_class()
+ ret['url'] = url_field
+ ret.update(fields)
+ fields = ret
return fields
diff --git a/rest_framework/tests/description.py b/rest_framework/tests/description.py
new file mode 100644
index 00000000..b46d7f54
--- /dev/null
+++ b/rest_framework/tests/description.py
@@ -0,0 +1,26 @@
+# -- coding: utf-8 --
+
+# Apparently there is a python 2.6 issue where docstrings of imported view classes
+# do not retain their encoding information even if a module has a proper
+# encoding declaration at the top of its source file. Therefore for tests
+# to catch unicode related errors, a mock view has to be declared in a separate
+# module.
+
+from rest_framework.views import APIView
+
+
+# test strings snatched from http://www.columbia.edu/~fdc/utf8/,
+# http://winrus.com/utf8-jap.htm and memory
+UTF8_TEST_DOCSTRING = (
+ 'zażółć gęślą jaźń'
+ 'Sîne klâwen durh die wolken sint geslagen'
+ 'Τη γλώσσα μου έδωσαν ελληνική'
+ 'யாமறிந்த மொழிகளிலே தமிழ்மொழி'
+ 'На берегу пустынных волн'
+ 'てすと'
+ 'アイウエオカキクケコサシスセソタチツテ'
+)
+
+
+class ViewWithNonASCIICharactersInDocstring(APIView):
+ __doc__ = UTF8_TEST_DOCSTRING
diff --git a/rest_framework/tests/test_description.py b/rest_framework/tests/test_description.py
index 52c1a34c..8019f5ec 100644
--- a/rest_framework/tests/test_description.py
+++ b/rest_framework/tests/test_description.py
@@ -2,8 +2,10 @@
from __future__ import unicode_literals
from django.test import TestCase
+from rest_framework.compat import apply_markdown, smart_text
from rest_framework.views import APIView
-from rest_framework.compat import apply_markdown
+from rest_framework.tests.description import ViewWithNonASCIICharactersInDocstring
+from rest_framework.tests.description import UTF8_TEST_DOCSTRING
from rest_framework.utils.formatting import get_view_name, get_view_description
# We check that docstrings get nicely un-indented.
@@ -83,11 +85,10 @@ class TestViewNamesAndDescriptions(TestCase):
Unicode in docstrings should be respected.
"""
- class MockView(APIView):
- """Проверка"""
- pass
-
- self.assertEqual(get_view_description(MockView), "Проверка")
+ self.assertEqual(
+ get_view_description(ViewWithNonASCIICharactersInDocstring),
+ smart_text(UTF8_TEST_DOCSTRING)
+ )
def test_view_description_can_be_empty(self):
"""
diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py
index 291142cf..d375f4a8 100644
--- a/rest_framework/tests/test_routers.py
+++ b/rest_framework/tests/test_routers.py
@@ -2,7 +2,8 @@ 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 serializers, viewsets
+from django.core.exceptions import ImproperlyConfigured
+from rest_framework import serializers, viewsets, permissions
from rest_framework.compat import include, patterns, url
from rest_framework.decorators import link, action
from rest_framework.response import Response
@@ -120,7 +121,7 @@ class TestCustomLookupFields(TestCase):
)
-class TestTrailingSlash(TestCase):
+class TestTrailingSlashIncluded(TestCase):
def setUp(self):
class NoteViewSet(viewsets.ModelViewSet):
model = RouterTestModel
@@ -135,7 +136,7 @@ class TestTrailingSlash(TestCase):
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
-class TestTrailingSlash(TestCase):
+class TestTrailingSlashRemoved(TestCase):
def setUp(self):
class NoteViewSet(viewsets.ModelViewSet):
model = RouterTestModel
@@ -149,6 +150,7 @@ class TestTrailingSlash(TestCase):
for idx in range(len(expected)):
self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
+
class TestNameableRoot(TestCase):
def setUp(self):
class NoteViewSet(viewsets.ModelViewSet):
@@ -162,3 +164,52 @@ class TestNameableRoot(TestCase):
expected = 'nameable-root'
self.assertEqual(expected, self.urls[0].name)
+
+class TestActionKeywordArgs(TestCase):
+ """
+ Ensure keyword arguments passed in the `@action` decorator
+ are properly handled. Refs #940.
+ """
+
+ def setUp(self):
+ class TestViewSet(viewsets.ModelViewSet):
+ permission_classes = []
+
+ @action(permission_classes=[permissions.AllowAny])
+ def custom(self, request, *args, **kwargs):
+ return Response({
+ 'permission_classes': self.permission_classes
+ })
+
+ self.router = SimpleRouter()
+ self.router.register(r'test', TestViewSet, base_name='test')
+ self.view = self.router.urls[-1].callback
+
+ def test_action_kwargs(self):
+ request = factory.post('/test/0/custom/')
+ response = self.view(request)
+ self.assertEqual(
+ response.data,
+ {'permission_classes': [permissions.AllowAny]}
+ )
+
+class TestActionAppliedToExistingRoute(TestCase):
+ """
+ Ensure `@action` decorator raises an except when applied
+ to an existing route
+ """
+
+ def test_exception_raised_when_action_applied_to_existing_route(self):
+ class TestViewSet(viewsets.ModelViewSet):
+
+ @action()
+ def retrieve(self, request, *args, **kwargs):
+ return Response({
+ 'hello': 'world'
+ })
+
+ self.router = SimpleRouter()
+ self.router.register(r'test', TestViewSet, base_name='test')
+
+ with self.assertRaises(ImproperlyConfigured):
+ self.router.urls
diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py
index ebadb3a6..4bec8387 100644
--- a/rest_framework/utils/formatting.py
+++ b/rest_framework/utils/formatting.py
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
from django.utils.html import escape
from django.utils.safestring import mark_safe
-from rest_framework.compat import apply_markdown
+from rest_framework.compat import apply_markdown, smart_text
import re
@@ -63,7 +63,7 @@ def get_view_description(cls, html=False):
Return a description for an `APIView` class or `@api_view` function.
"""
description = cls.__doc__ or ''
- description = _remove_leading_indent(description)
+ description = _remove_leading_indent(smart_text(description))
if html:
return markup_description(description)
return description
diff --git a/rest_framework/views.py b/rest_framework/views.py
index c28d2835..37bba7f0 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -4,11 +4,11 @@ Provides an APIView class that is the base of all views in REST framework.
from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied
-from django.http import Http404, HttpResponse
+from django.http import Http404
from django.utils.datastructures import SortedDict
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status, exceptions
-from rest_framework.compat import View
+from rest_framework.compat import View, HttpResponseBase
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.settings import api_settings
@@ -244,9 +244,10 @@ class APIView(View):
Returns the final response object.
"""
# Make the error obvious if a proper response is not returned
- assert isinstance(response, HttpResponse), (
- 'Expected a `Response` to be returned from the view, '
- 'but received a `%s`' % type(response)
+ assert isinstance(response, HttpResponseBase), (
+ 'Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` '
+ 'to be returned from the view, but received a `%s`'
+ % type(response)
)
if isinstance(response, Response):