diff --git a/README.md b/README.md
index e1e252609..155f8dead 100644
--- a/README.md
+++ b/README.md
@@ -24,9 +24,10 @@ The initial aim is to provide a single full-time position on REST framework.
+
-*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), and [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf).*
+*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), and [Machinalis](http://www.machinalis.com/#services).*
---
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 8d880b037..bf3a31eb7 100644
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -148,7 +148,7 @@ For clients to authenticate, the token key should be included in the `Authorizat
If successfully authenticated, `TokenAuthentication` provides the following credentials.
* `request.user` will be a Django `User` instance.
-* `request.auth` will be a `rest_framework.authtoken.models.BasicToken` instance.
+* `request.auth` will be a `rest_framework.authtoken.models.Token` instance.
Unauthenticated responses that are denied permission will result in an `HTTP 401 Unauthorized` response with an appropriate WWW-Authenticate header. For example:
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index d7dc30ce1..5fea8d7e0 100644
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -330,7 +330,8 @@ For example, if you need to lookup objects based on multiple fields in the URL c
queryset = self.filter_queryset(queryset) # Apply any filter backends
filter = {}
for field in self.lookup_fields:
- filter[field] = self.kwargs[field]
+ if self.kwargs[field]: # Ignore empty fields.
+ filter[field] = self.kwargs[field]
return get_object_or_404(queryset, **filter) # Lookup the object
You can then simply apply this mixin to a view or viewset anytime you need to apply the custom behavior.
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index 70fab448c..5772d940a 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -442,7 +442,7 @@ Declaring a `ModelSerializer` looks like this:
By default, all the model fields on the class will be mapped to a corresponding serializer fields.
-Any relationships such as foreign keys on the model will be mapped to `PrimaryKeyRelatedField`. Reverse relationships are not included by default unless explicitly included as described below.
+Any relationships such as foreign keys on the model will be mapped to `PrimaryKeyRelatedField`. Reverse relationships are not included by default unless explicitly included as specified in the [serializer relations][relations] documentation.
#### Inspecting a `ModelSerializer`
diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md
index 51d2beef1..da4d5f725 100644
--- a/docs/api-guide/throttling.md
+++ b/docs/api-guide/throttling.md
@@ -188,7 +188,7 @@ The following is an example of a rate throttle, that will randomly throttle 1 in
class RandomRateThrottle(throttling.BaseThrottle):
def allow_request(self, request, view):
- return random.randint(1, 10) == 1
+ return random.randint(1, 10) != 1
[cite]: https://dev.twitter.com/docs/error-codes-responses
[permissions]: permissions.md
diff --git a/docs/img/premium/machinalis-readme.png b/docs/img/premium/machinalis-readme.png
new file mode 100644
index 000000000..4bdb020c2
Binary files /dev/null and b/docs/img/premium/machinalis-readme.png differ
diff --git a/docs/img/premium/rover-readme.png b/docs/img/premium/rover-readme.png
index aeef1de4f..c9865f2a9 100644
Binary files a/docs/img/premium/rover-readme.png and b/docs/img/premium/rover-readme.png differ
diff --git a/docs/img/premium/sentry-readme.png b/docs/img/premium/sentry-readme.png
index 0b8a80c49..1e869f3b1 100644
Binary files a/docs/img/premium/sentry-readme.png and b/docs/img/premium/sentry-readme.png differ
diff --git a/docs/img/premium/stream-readme.png b/docs/img/premium/stream-readme.png
index a04009d7f..955c11429 100644
Binary files a/docs/img/premium/stream-readme.png and b/docs/img/premium/stream-readme.png differ
diff --git a/docs/index.md b/docs/index.md
index 88276e678..3019467ed 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -74,10 +74,11 @@ The initial aim is to provide a single full-time position on REST framework.
Rover.com
Sentry
Stream
+ Machinalis
-*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), and [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf).*
+*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), and [Machinalis](http://www.machinalis.com/#services).*
---
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 24728a252..6ef6cb83a 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -40,6 +40,27 @@ You can determine your currently installed version using `pip freeze`:
## 3.4.x series
+### 3.4.7
+
+**Date**: [21st September 2016][3.4.7-milestone]
+
+* Fallback behavior for request parsing when request.POST already accessed. ([#3951][gh3951], [#4500][gh4500])
+* Fix regression of `RegexField`. ([#4489][gh4489], [#4490][gh4490], [#2617][gh2617])
+* Missing comma in `admin.html` causing CSRF error. ([#4472][gh4472], [#4473][gh4473])
+* Fix response rendering with empty context. ([#4495][gh4495])
+* Fix indentation regression in API listing. ([#4493][gh4493])
+* Fixed an issue where the incorrect value is set to `ResolverMatch.func_name` of api_view decorated view. ([#4465][gh4465], [#4462][gh4462])
+* Fix `APIClient.get()` when path contains unicode arguments ([#4458][gh4458])
+
+### 3.4.6
+
+**Date**: [23rd August 2016][3.4.6-milestone]
+
+* Fix malformed Javascript in browsable API. ([#4435][gh4435])
+* Skip HiddenField from Schema fields. ([#4425][gh4425], [#4429][gh4429])
+* Improve Create to show the original exception traceback. ([#3508][gh3508])
+* Fix `AdminRenderer` display of PK only related fields. ([#4419][gh4419], [#4423][gh4423])
+
### 3.4.5
**Date**: [19th August 2016][3.4.5-milestone]
@@ -573,6 +594,11 @@ For older release notes, [please see the version 2.x documentation][old-release-
[3.4.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.3+Release%22
[3.4.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.4+Release%22
[3.4.5-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.5+Release%22
+<<<<<<< HEAD
+=======
+[3.4.6-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.6+Release%22
+[3.4.7-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.7+Release%22
+>>>>>>> master
[gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013
@@ -1090,3 +1116,27 @@ For older release notes, [please see the version 2.x documentation][old-release-
[gh4403]: https://github.com/tomchristie/django-rest-framework/issues/4403
[gh4404]: https://github.com/tomchristie/django-rest-framework/issues/4404
[gh4412]: https://github.com/tomchristie/django-rest-framework/issues/4412
+
+
+
+[gh4435]: https://github.com/tomchristie/django-rest-framework/issues/4435
+[gh4425]: https://github.com/tomchristie/django-rest-framework/issues/4425
+[gh4429]: https://github.com/tomchristie/django-rest-framework/issues/4429
+[gh3508]: https://github.com/tomchristie/django-rest-framework/issues/3508
+[gh4419]: https://github.com/tomchristie/django-rest-framework/issues/4419
+[gh4423]: https://github.com/tomchristie/django-rest-framework/issues/4423
+
+
+
+[gh3951]: https://github.com/tomchristie/django-rest-framework/issues/3951
+[gh4500]: https://github.com/tomchristie/django-rest-framework/issues/4500
+[gh4489]: https://github.com/tomchristie/django-rest-framework/issues/4489
+[gh4490]: https://github.com/tomchristie/django-rest-framework/issues/4490
+[gh2617]: https://github.com/tomchristie/django-rest-framework/issues/2617
+[gh4472]: https://github.com/tomchristie/django-rest-framework/issues/4472
+[gh4473]: https://github.com/tomchristie/django-rest-framework/issues/4473
+[gh4495]: https://github.com/tomchristie/django-rest-framework/issues/4495
+[gh4493]: https://github.com/tomchristie/django-rest-framework/issues/4493
+[gh4465]: https://github.com/tomchristie/django-rest-framework/issues/4465
+[gh4462]: https://github.com/tomchristie/django-rest-framework/issues/4462
+[gh4458]: https://github.com/tomchristie/django-rest-framework/issues/4458
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 3f8736c25..68e96703f 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -8,7 +8,7 @@ ______ _____ _____ _____ __
"""
__title__ = 'Django REST framework'
-__version__ = '3.4.5'
+__version__ = '3.5.0'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2016 Tom Christie'
diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py
index 1b21e643b..554e5236c 100644
--- a/rest_framework/decorators.py
+++ b/rest_framework/decorators.py
@@ -55,6 +55,7 @@ def api_view(http_method_names=None):
setattr(WrappedAPIView, method.lower(), handler)
WrappedAPIView.__name__ = func.__name__
+ WrappedAPIView.__module__ = func.__module__
WrappedAPIView.renderer_classes = getattr(func, 'renderer_classes',
APIView.renderer_classes)
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index f76e4e801..917a151e5 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -252,6 +252,8 @@ class SkipField(Exception):
pass
+REGEX_TYPE = type(re.compile(''))
+
NOT_READ_ONLY_WRITE_ONLY = 'May not set both `read_only` and `write_only`'
NOT_READ_ONLY_REQUIRED = 'May not set both `read_only` and `required`'
NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`'
@@ -581,16 +583,17 @@ class Field(object):
When cloning fields we instantiate using the arguments it was
originally created with, rather than copying the complete state.
"""
- args = copy.deepcopy(self._args)
- kwargs = dict(self._kwargs)
- # Bit ugly, but we need to special case 'validators' as Django's
- # RegexValidator does not support deepcopy.
- # We treat validator callables as immutable objects.
+ # Treat regexes and validators as immutable.
# See https://github.com/tomchristie/django-rest-framework/issues/1954
- validators = kwargs.pop('validators', None)
- kwargs = copy.deepcopy(kwargs)
- if validators is not None:
- kwargs['validators'] = validators
+ # and https://github.com/tomchristie/django-rest-framework/pull/4489
+ args = [
+ copy.deepcopy(item) if not isinstance(item, REGEX_TYPE) else item
+ for item in self._args
+ ]
+ kwargs = {
+ key: (copy.deepcopy(value) if (key not in ('validators', 'regex')) else value)
+ for key, value in self._kwargs.items()
+ }
return self.__class__(*args, **kwargs)
def __repr__(self):
diff --git a/rest_framework/request.py b/rest_framework/request.py
index 355cccad7..0a827728a 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -15,6 +15,7 @@ import sys
from django.conf import settings
from django.http import QueryDict
from django.http.multipartparser import parse_header
+from django.http.request import RawPostDataException
from django.utils import six
from django.utils.datastructures import MultiValueDict
@@ -263,10 +264,20 @@ class Request(object):
if content_length == 0:
self._stream = None
- elif hasattr(self._request, 'read'):
+ elif not self._request._read_started:
self._stream = self._request
else:
- self._stream = six.BytesIO(self.raw_post_data)
+ self._stream = six.BytesIO(self.body)
+
+ def _supports_form_parsing(self):
+ """
+ Return True if this requests supports parsing form data.
+ """
+ form_media = (
+ 'application/x-www-form-urlencoded',
+ 'multipart/form-data'
+ )
+ return any([parser.media_type in form_media for parser in self.parsers])
def _parse(self):
"""
@@ -274,8 +285,18 @@ class Request(object):
May raise an `UnsupportedMediaType`, or `ParseError` exception.
"""
- stream = self.stream
media_type = self.content_type
+ try:
+ stream = self.stream
+ except RawPostDataException:
+ if not hasattr(self._request, '_post'):
+ raise
+ # If request.POST has been accessed in middleware, and a method='POST'
+ # request was made with 'multipart/form-data', then the request stream
+ # will already have been exhausted.
+ if self._supports_form_parsing():
+ return (self._request.POST, self._request.FILES)
+ stream = None
if stream is None or media_type is None:
empty_data = QueryDict('', encoding=self._request._encoding)
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 4b863cb99..cb0f290ce 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -56,7 +56,7 @@ class Response(SimpleTemplateResponse):
assert renderer, ".accepted_renderer not set on Response"
assert accepted_media_type, ".accepted_media_type not set on Response"
- assert context, ".renderer_context not set on Response"
+ assert context is not None, ".renderer_context not set on Response"
context['response'] = self
media_type = renderer.media_type
diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py
index c9834c64d..1b899450f 100644
--- a/rest_framework/schemas.py
+++ b/rest_framework/schemas.py
@@ -296,8 +296,9 @@ class SchemaGenerator(object):
fields = []
for field in serializer.fields.values():
- if field.read_only:
+ if field.read_only or isinstance(field, serializers.HiddenField):
continue
+
required = field.required and method != 'PATCH'
description = force_text(field.help_text) if field.help_text else ''
field = coreapi.Field(
diff --git a/rest_framework/templates/rest_framework/admin.html b/rest_framework/templates/rest_framework/admin.html
index eb2b8f1c7..de011cd09 100644
--- a/rest_framework/templates/rest_framework/admin.html
+++ b/rest_framework/templates/rest_framework/admin.html
@@ -232,7 +232,7 @@
{% block script %}
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index 989a086ea..5df23b767 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -150,10 +150,10 @@
-
HTTP {{ response.status_code }} {{ response.status_text }}{% autoescape off %}
- {% for key, val in response_headers.items %}{{ key }}: {{ val|break_long_headers|urlize_quoted_links }}
- {% endfor %}
- {{ content|urlize_quoted_links }}
{% endautoescape %}
+
HTTP {{ response.status_code }} {{ response.status_text }}{% autoescape off %}{% for key, val in response_headers.items %}
+{{ key }}: {{ val|break_long_headers|urlize_quoted_links }}{% endfor %}
+
+{{ content|urlize_quoted_links }}
{% endautoescape %}
@@ -263,7 +263,7 @@
{% block script %}
diff --git a/rest_framework/test.py b/rest_framework/test.py
index b8e486b21..1b3ad80c2 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -195,10 +195,13 @@ class APIRequestFactory(DjangoRequestFactory):
r = {
'QUERY_STRING': urlencode(data or {}, doseq=True),
}
- # Fix to support old behavior where you have the arguments in the url
- # See #1461
if not data and '?' in path:
- r['QUERY_STRING'] = path.split('?')[1]
+ # Fix to support old behavior where you have the arguments in the
+ # url. See #1461.
+ query_string = force_bytes(path.split('?')[1])
+ if six.PY3:
+ query_string = query_string.decode('iso-8859-1')
+ r['QUERY_STRING'] = query_string
r.update(extra)
return self.generic('GET', path, **r)
diff --git a/tests/test_fields.py b/tests/test_fields.py
index f1a588c27..4a4b741c5 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1,5 +1,6 @@
import datetime
import os
+import re
import uuid
from decimal import Decimal
@@ -590,6 +591,20 @@ class TestRegexField(FieldValues):
field = serializers.RegexField(regex='[a-z][0-9]')
+class TestiCompiledRegexField(FieldValues):
+ """
+ Valid and invalid values for `RegexField`.
+ """
+ valid_inputs = {
+ 'a9': 'a9',
+ }
+ invalid_inputs = {
+ 'A9': ["This value does not match the required pattern."]
+ }
+ outputs = {}
+ field = serializers.RegexField(regex=re.compile('[a-z][0-9]'))
+
+
class TestSlugField(FieldValues):
"""
Valid and invalid values for `SlugField`.
diff --git a/tests/test_parsers.py b/tests/test_parsers.py
index f3af6817f..5052e2e53 100644
--- a/tests/test_parsers.py
+++ b/tests/test_parsers.py
@@ -7,11 +7,16 @@ from django import forms
from django.core.files.uploadhandler import (
MemoryFileUploadHandler, TemporaryFileUploadHandler
)
+from django.http.request import RawPostDataException
from django.test import TestCase
from django.utils.six.moves import StringIO
from rest_framework.exceptions import ParseError
-from rest_framework.parsers import FileUploadParser, FormParser
+from rest_framework.parsers import (
+ FileUploadParser, FormParser, JSONParser, MultiPartParser
+)
+from rest_framework.request import Request
+from rest_framework.test import APIRequestFactory
class Form(forms.Form):
@@ -122,3 +127,39 @@ class TestFileUploadParser(TestCase):
def __replace_content_disposition(self, disposition):
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition
+
+
+class TestPOSTAccessed(TestCase):
+ def setUp(self):
+ self.factory = APIRequestFactory()
+
+ def test_post_accessed_in_post_method(self):
+ django_request = self.factory.post('/', {'foo': 'bar'})
+ request = Request(django_request, parsers=[FormParser(), MultiPartParser()])
+ django_request.POST
+ assert request.POST == {'foo': ['bar']}
+ assert request.data == {'foo': ['bar']}
+
+ def test_post_accessed_in_post_method_with_json_parser(self):
+ django_request = self.factory.post('/', {'foo': 'bar'})
+ request = Request(django_request, parsers=[JSONParser()])
+ django_request.POST
+ assert request.POST == {}
+ assert request.data == {}
+
+ def test_post_accessed_in_put_method(self):
+ django_request = self.factory.put('/', {'foo': 'bar'})
+ request = Request(django_request, parsers=[FormParser(), MultiPartParser()])
+ django_request.POST
+ assert request.POST == {'foo': ['bar']}
+ assert request.data == {'foo': ['bar']}
+
+ def test_request_read_before_parsing(self):
+ django_request = self.factory.put('/', {'foo': 'bar'})
+ request = Request(django_request, parsers=[FormParser(), MultiPartParser()])
+ django_request.read()
+ with pytest.raises(RawPostDataException):
+ request.POST
+ with pytest.raises(RawPostDataException):
+ request.POST
+ request.data
diff --git a/tests/test_schemas.py b/tests/test_schemas.py
index c866e09be..197e62eb0 100644
--- a/tests/test_schemas.py
+++ b/tests/test_schemas.py
@@ -26,6 +26,8 @@ class ExamplePagination(pagination.PageNumberPagination):
class ExampleSerializer(serializers.Serializer):
a = serializers.CharField(required=True, help_text='A field description')
b = serializers.CharField(required=False)
+ read_only = serializers.CharField(read_only=True)
+ hidden = serializers.HiddenField(default='hello')
class AnotherSerializer(serializers.Serializer):
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index 4e9080909..bd9ef9500 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -2,6 +2,7 @@
from __future__ import unicode_literals
import pickle
+import re
import pytest
@@ -337,3 +338,16 @@ class TestDefaultInclusions:
assert serializer.is_valid()
assert serializer.validated_data == {'integer': 456}
assert serializer.errors == {}
+
+
+class TestSerializerValidationWithCompiledRegexField:
+ def setup(self):
+ class ExampleSerializer(serializers.Serializer):
+ name = serializers.RegexField(re.compile(r'\d'), required=True)
+ self.Serializer = ExampleSerializer
+
+ def test_validation_success(self):
+ serializer = self.Serializer(data={'name': '2'})
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'name': '2'}
+ assert serializer.errors == {}
diff --git a/tests/test_testing.py b/tests/test_testing.py
index 3adcc55f8..6683ae6ed 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -245,3 +245,10 @@ class TestAPIRequestFactory(TestCase):
self.assertEqual(dict(request.GET), {'demo': ['test']})
request = factory.get('/view/', {'demo': 'test'})
self.assertEqual(dict(request.GET), {'demo': ['test']})
+
+ def test_request_factory_url_arguments_with_unicode(self):
+ factory = APIRequestFactory()
+ request = factory.get('/view/?demo=testé')
+ self.assertEqual(dict(request.GET), {'demo': ['testé']})
+ request = factory.get('/view/', {'demo': 'testé'})
+ self.assertEqual(dict(request.GET), {'demo': ['testé']})