Merge master

This commit is contained in:
Tom Christie 2016-09-29 09:58:06 +01:00
commit c2cec78bd4
25 changed files with 192 additions and 31 deletions

View File

@ -24,9 +24,10 @@ The initial aim is to provide a single full-time position on REST framework.
<a href="http://jobs.rover.com/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/rover-readme.png"/></a>
<a href="https://getsentry.com/welcome/"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/sentry-readme.png"/></a>
<a href="https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/stream-readme.png"/></a>
<a href="http://www.machinalis.com/#services"><img src="https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/docs/img/premium/machinalis-readme.png"/></a>
</p>
*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).*
---

View File

@ -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:

View File

@ -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.

View File

@ -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`

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -74,10 +74,11 @@ The initial aim is to provide a single full-time position on REST framework.
<li><a href="http://jobs.rover.com/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rover_130x130.png)">Rover.com</a></li>
<li><a href="https://getsentry.com/welcome/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/sentry130.png)">Sentry</a></li>
<li><a href="https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
<li><a href="http://www.machinalis.com/#services" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/Machinalis130.png)">Machinalis</a></li>
</ul>
<div style="clear: both; padding-bottom: 20px;"></div>
*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).*
---

View File

@ -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
<!-- 3.0.1 -->
[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
<!-- 3.4.6 -->
[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
<!-- 3.4.7 -->
[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

View File

@ -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'

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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(

View File

@ -232,7 +232,7 @@
{% block script %}
<script>
window.drf = {
csrfHeaderName: "{{ csrf_header_name|default:'X-CSRFToken' }}"
csrfHeaderName: "{{ csrf_header_name|default:'X-CSRFToken' }}",
csrfCookieName: "{{ csrf_cookie_name|default:'csrftoken' }}"
};
</script>

View File

@ -150,10 +150,10 @@
</div>
<div class="response-info">
<pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
{% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
{% endfor %}
</span>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
<pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}{% for key, val in response_headers.items %}
<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>{% endfor %}
</span>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
</div>
</div>
@ -263,7 +263,7 @@
{% block script %}
<script>
window.drf = {
csrfHeaderName: "{{ csrf_header_name|default:'X-CSRFToken' }}"
csrfHeaderName: "{{ csrf_header_name|default:'X-CSRFToken' }}",
csrfCookieName: "{{ csrf_cookie_name|default:'csrftoken' }}"
};
</script>

View File

@ -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)

View File

@ -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`.

View File

@ -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

View File

@ -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):

View File

@ -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 == {}

View File

@ -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é']})