Merge master
|
@ -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="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://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="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>
|
</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).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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.
|
If successfully authenticated, `TokenAuthentication` provides the following credentials.
|
||||||
|
|
||||||
* `request.user` will be a Django `User` instance.
|
* `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:
|
Unauthenticated responses that are denied permission will result in an `HTTP 401 Unauthorized` response with an appropriate WWW-Authenticate header. For example:
|
||||||
|
|
||||||
|
|
|
@ -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
|
queryset = self.filter_queryset(queryset) # Apply any filter backends
|
||||||
filter = {}
|
filter = {}
|
||||||
for field in self.lookup_fields:
|
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
|
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.
|
You can then simply apply this mixin to a view or viewset anytime you need to apply the custom behavior.
|
||||||
|
|
|
@ -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.
|
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`
|
#### Inspecting a `ModelSerializer`
|
||||||
|
|
||||||
|
|
|
@ -188,7 +188,7 @@ The following is an example of a rate throttle, that will randomly throttle 1 in
|
||||||
|
|
||||||
class RandomRateThrottle(throttling.BaseThrottle):
|
class RandomRateThrottle(throttling.BaseThrottle):
|
||||||
def allow_request(self, request, view):
|
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
|
[cite]: https://dev.twitter.com/docs/error-codes-responses
|
||||||
[permissions]: permissions.md
|
[permissions]: permissions.md
|
||||||
|
|
BIN
docs/img/premium/machinalis-readme.png
Normal file
After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
|
@ -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="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://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="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>
|
</ul>
|
||||||
<div style="clear: both; padding-bottom: 20px;"></div>
|
<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).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,27 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
|
|
||||||
## 3.4.x series
|
## 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
|
### 3.4.5
|
||||||
|
|
||||||
**Date**: [19th August 2016][3.4.5-milestone]
|
**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.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.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
|
[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 -->
|
<!-- 3.0.1 -->
|
||||||
[gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013
|
[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
|
[gh4403]: https://github.com/tomchristie/django-rest-framework/issues/4403
|
||||||
[gh4404]: https://github.com/tomchristie/django-rest-framework/issues/4404
|
[gh4404]: https://github.com/tomchristie/django-rest-framework/issues/4404
|
||||||
[gh4412]: https://github.com/tomchristie/django-rest-framework/issues/4412
|
[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
|
||||||
|
|
|
@ -8,7 +8,7 @@ ______ _____ _____ _____ __
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__title__ = 'Django REST framework'
|
__title__ = 'Django REST framework'
|
||||||
__version__ = '3.4.5'
|
__version__ = '3.5.0'
|
||||||
__author__ = 'Tom Christie'
|
__author__ = 'Tom Christie'
|
||||||
__license__ = 'BSD 2-Clause'
|
__license__ = 'BSD 2-Clause'
|
||||||
__copyright__ = 'Copyright 2011-2016 Tom Christie'
|
__copyright__ = 'Copyright 2011-2016 Tom Christie'
|
||||||
|
|
|
@ -55,6 +55,7 @@ def api_view(http_method_names=None):
|
||||||
setattr(WrappedAPIView, method.lower(), handler)
|
setattr(WrappedAPIView, method.lower(), handler)
|
||||||
|
|
||||||
WrappedAPIView.__name__ = func.__name__
|
WrappedAPIView.__name__ = func.__name__
|
||||||
|
WrappedAPIView.__module__ = func.__module__
|
||||||
|
|
||||||
WrappedAPIView.renderer_classes = getattr(func, 'renderer_classes',
|
WrappedAPIView.renderer_classes = getattr(func, 'renderer_classes',
|
||||||
APIView.renderer_classes)
|
APIView.renderer_classes)
|
||||||
|
|
|
@ -252,6 +252,8 @@ class SkipField(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
REGEX_TYPE = type(re.compile(''))
|
||||||
|
|
||||||
NOT_READ_ONLY_WRITE_ONLY = 'May not set both `read_only` and `write_only`'
|
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_READ_ONLY_REQUIRED = 'May not set both `read_only` and `required`'
|
||||||
NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`'
|
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
|
When cloning fields we instantiate using the arguments it was
|
||||||
originally created with, rather than copying the complete state.
|
originally created with, rather than copying the complete state.
|
||||||
"""
|
"""
|
||||||
args = copy.deepcopy(self._args)
|
# Treat regexes and validators as immutable.
|
||||||
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.
|
|
||||||
# See https://github.com/tomchristie/django-rest-framework/issues/1954
|
# See https://github.com/tomchristie/django-rest-framework/issues/1954
|
||||||
validators = kwargs.pop('validators', None)
|
# and https://github.com/tomchristie/django-rest-framework/pull/4489
|
||||||
kwargs = copy.deepcopy(kwargs)
|
args = [
|
||||||
if validators is not None:
|
copy.deepcopy(item) if not isinstance(item, REGEX_TYPE) else item
|
||||||
kwargs['validators'] = validators
|
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)
|
return self.__class__(*args, **kwargs)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|
|
@ -15,6 +15,7 @@ import sys
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import QueryDict
|
from django.http import QueryDict
|
||||||
from django.http.multipartparser import parse_header
|
from django.http.multipartparser import parse_header
|
||||||
|
from django.http.request import RawPostDataException
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.datastructures import MultiValueDict
|
from django.utils.datastructures import MultiValueDict
|
||||||
|
|
||||||
|
@ -263,10 +264,20 @@ class Request(object):
|
||||||
|
|
||||||
if content_length == 0:
|
if content_length == 0:
|
||||||
self._stream = None
|
self._stream = None
|
||||||
elif hasattr(self._request, 'read'):
|
elif not self._request._read_started:
|
||||||
self._stream = self._request
|
self._stream = self._request
|
||||||
else:
|
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):
|
def _parse(self):
|
||||||
"""
|
"""
|
||||||
|
@ -274,8 +285,18 @@ class Request(object):
|
||||||
|
|
||||||
May raise an `UnsupportedMediaType`, or `ParseError` exception.
|
May raise an `UnsupportedMediaType`, or `ParseError` exception.
|
||||||
"""
|
"""
|
||||||
stream = self.stream
|
|
||||||
media_type = self.content_type
|
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:
|
if stream is None or media_type is None:
|
||||||
empty_data = QueryDict('', encoding=self._request._encoding)
|
empty_data = QueryDict('', encoding=self._request._encoding)
|
||||||
|
|
|
@ -56,7 +56,7 @@ class Response(SimpleTemplateResponse):
|
||||||
|
|
||||||
assert renderer, ".accepted_renderer not set on Response"
|
assert renderer, ".accepted_renderer not set on Response"
|
||||||
assert accepted_media_type, ".accepted_media_type 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
|
context['response'] = self
|
||||||
|
|
||||||
media_type = renderer.media_type
|
media_type = renderer.media_type
|
||||||
|
|
|
@ -296,8 +296,9 @@ class SchemaGenerator(object):
|
||||||
|
|
||||||
fields = []
|
fields = []
|
||||||
for field in serializer.fields.values():
|
for field in serializer.fields.values():
|
||||||
if field.read_only:
|
if field.read_only or isinstance(field, serializers.HiddenField):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
required = field.required and method != 'PATCH'
|
required = field.required and method != 'PATCH'
|
||||||
description = force_text(field.help_text) if field.help_text else ''
|
description = force_text(field.help_text) if field.help_text else ''
|
||||||
field = coreapi.Field(
|
field = coreapi.Field(
|
||||||
|
|
|
@ -232,7 +232,7 @@
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script>
|
<script>
|
||||||
window.drf = {
|
window.drf = {
|
||||||
csrfHeaderName: "{{ csrf_header_name|default:'X-CSRFToken' }}"
|
csrfHeaderName: "{{ csrf_header_name|default:'X-CSRFToken' }}",
|
||||||
csrfCookieName: "{{ csrf_cookie_name|default:'csrftoken' }}"
|
csrfCookieName: "{{ csrf_cookie_name|default:'csrftoken' }}"
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -150,10 +150,10 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="response-info">
|
<div class="response-info">
|
||||||
<pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
|
<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 %}
|
||||||
{% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
|
<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
</span>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
|
</span>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -263,7 +263,7 @@
|
||||||
{% block script %}
|
{% block script %}
|
||||||
<script>
|
<script>
|
||||||
window.drf = {
|
window.drf = {
|
||||||
csrfHeaderName: "{{ csrf_header_name|default:'X-CSRFToken' }}"
|
csrfHeaderName: "{{ csrf_header_name|default:'X-CSRFToken' }}",
|
||||||
csrfCookieName: "{{ csrf_cookie_name|default:'csrftoken' }}"
|
csrfCookieName: "{{ csrf_cookie_name|default:'csrftoken' }}"
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -195,10 +195,13 @@ class APIRequestFactory(DjangoRequestFactory):
|
||||||
r = {
|
r = {
|
||||||
'QUERY_STRING': urlencode(data or {}, doseq=True),
|
'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:
|
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)
|
r.update(extra)
|
||||||
return self.generic('GET', path, **r)
|
return self.generic('GET', path, **r)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
@ -590,6 +591,20 @@ class TestRegexField(FieldValues):
|
||||||
field = serializers.RegexField(regex='[a-z][0-9]')
|
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):
|
class TestSlugField(FieldValues):
|
||||||
"""
|
"""
|
||||||
Valid and invalid values for `SlugField`.
|
Valid and invalid values for `SlugField`.
|
||||||
|
|
|
@ -7,11 +7,16 @@ from django import forms
|
||||||
from django.core.files.uploadhandler import (
|
from django.core.files.uploadhandler import (
|
||||||
MemoryFileUploadHandler, TemporaryFileUploadHandler
|
MemoryFileUploadHandler, TemporaryFileUploadHandler
|
||||||
)
|
)
|
||||||
|
from django.http.request import RawPostDataException
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils.six.moves import StringIO
|
from django.utils.six.moves import StringIO
|
||||||
|
|
||||||
from rest_framework.exceptions import ParseError
|
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):
|
class Form(forms.Form):
|
||||||
|
@ -122,3 +127,39 @@ class TestFileUploadParser(TestCase):
|
||||||
|
|
||||||
def __replace_content_disposition(self, disposition):
|
def __replace_content_disposition(self, disposition):
|
||||||
self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = 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
|
||||||
|
|
|
@ -26,6 +26,8 @@ class ExamplePagination(pagination.PageNumberPagination):
|
||||||
class ExampleSerializer(serializers.Serializer):
|
class ExampleSerializer(serializers.Serializer):
|
||||||
a = serializers.CharField(required=True, help_text='A field description')
|
a = serializers.CharField(required=True, help_text='A field description')
|
||||||
b = serializers.CharField(required=False)
|
b = serializers.CharField(required=False)
|
||||||
|
read_only = serializers.CharField(read_only=True)
|
||||||
|
hidden = serializers.HiddenField(default='hello')
|
||||||
|
|
||||||
|
|
||||||
class AnotherSerializer(serializers.Serializer):
|
class AnotherSerializer(serializers.Serializer):
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import pickle
|
import pickle
|
||||||
|
import re
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -337,3 +338,16 @@ class TestDefaultInclusions:
|
||||||
assert serializer.is_valid()
|
assert serializer.is_valid()
|
||||||
assert serializer.validated_data == {'integer': 456}
|
assert serializer.validated_data == {'integer': 456}
|
||||||
assert serializer.errors == {}
|
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 == {}
|
||||||
|
|
|
@ -245,3 +245,10 @@ class TestAPIRequestFactory(TestCase):
|
||||||
self.assertEqual(dict(request.GET), {'demo': ['test']})
|
self.assertEqual(dict(request.GET), {'demo': ['test']})
|
||||||
request = factory.get('/view/', {'demo': 'test'})
|
request = factory.get('/view/', {'demo': 'test'})
|
||||||
self.assertEqual(dict(request.GET), {'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é']})
|
||||||
|
|