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="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).*
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
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="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).*
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 == {}
|
||||
|
|
|
@ -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é']})
|
||||
|
|