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