mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-04 12:30:11 +03:00
Merge branch 'master' into htmlr
This commit is contained in:
commit
752cf7b41d
|
@ -21,6 +21,7 @@ env:
|
||||||
- TOX_ENV=py27-djangomaster
|
- TOX_ENV=py27-djangomaster
|
||||||
- TOX_ENV=py34-djangomaster
|
- TOX_ENV=py34-djangomaster
|
||||||
- TOX_ENV=py35-djangomaster
|
- TOX_ENV=py35-djangomaster
|
||||||
|
- TOX_ENV=py36-djangomaster
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
|
@ -28,10 +29,11 @@ matrix:
|
||||||
- env: TOX_ENV=py27-djangomaster
|
- env: TOX_ENV=py27-djangomaster
|
||||||
- env: TOX_ENV=py34-djangomaster
|
- env: TOX_ENV=py34-djangomaster
|
||||||
- env: TOX_ENV=py35-djangomaster
|
- env: TOX_ENV=py35-djangomaster
|
||||||
|
- env: TOX_ENV=py36-djangomaster
|
||||||
|
|
||||||
install:
|
install:
|
||||||
# Virtualenv < 14 is required to keep the Python 3.2 builds running.
|
# Virtualenv < 14 is required to keep the Python 3.2 builds running.
|
||||||
- pip install tox "virtualenv<14"
|
- pip install tox virtualenv
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- tox -e $TOX_ENV
|
- tox -e $TOX_ENV
|
||||||
|
|
|
@ -113,7 +113,7 @@ The following third party packages provide additional metadata implementations.
|
||||||
[drf-schema-adapter][drf-schema-adapter] is a set of tools that makes it easier to provide schema information to frontend frameworks and libraries. It provides a metadata mixin as well as 2 metadata classes and several adapters suitable to generate [json-schema][json-schema] as well as schema information readable by various libraries.
|
[drf-schema-adapter][drf-schema-adapter] is a set of tools that makes it easier to provide schema information to frontend frameworks and libraries. It provides a metadata mixin as well as 2 metadata classes and several adapters suitable to generate [json-schema][json-schema] as well as schema information readable by various libraries.
|
||||||
|
|
||||||
You can also write your own adapter to work with your specific frontend.
|
You can also write your own adapter to work with your specific frontend.
|
||||||
If you whish to do so, it also provides an exporter that can export those schema information to json files.
|
If you wish to do so, it also provides an exporter that can export those schema information to json files.
|
||||||
|
|
||||||
[cite]: http://tools.ietf.org/html/rfc7231#section-4.3.7
|
[cite]: http://tools.ietf.org/html/rfc7231#section-4.3.7
|
||||||
[no-options]: https://www.mnot.net/blog/2012/10/29/NO_OPTIONS
|
[no-options]: https://www.mnot.net/blog/2012/10/29/NO_OPTIONS
|
||||||
|
|
|
@ -259,7 +259,7 @@ The [REST Condition][rest-condition] package is another extension for building c
|
||||||
|
|
||||||
## DRY Rest Permissions
|
## DRY Rest Permissions
|
||||||
|
|
||||||
The [DRY Rest Permissions][dry-rest-permissions] package provides the ability to define different permissions for individual default and custom actions. This package is made for apps with permissions that are derived from relationships defined in the app's data model. It also supports permission checks being returned to a client app through the API's serializer. Additionally it supports adding permissions to the default and custom list actions to restrict the data they retrive per user.
|
The [DRY Rest Permissions][dry-rest-permissions] package provides the ability to define different permissions for individual default and custom actions. This package is made for apps with permissions that are derived from relationships defined in the app's data model. It also supports permission checks being returned to a client app through the API's serializer. Additionally it supports adding permissions to the default and custom list actions to restrict the data they retrieve per user.
|
||||||
|
|
||||||
## Django Rest Framework Roles
|
## Django Rest Framework Roles
|
||||||
|
|
||||||
|
|
|
@ -241,7 +241,7 @@ Default:
|
||||||
If set, this maps the `'pk'` identifier in the URL conf onto the actual field
|
If set, this maps the `'pk'` identifier in the URL conf onto the actual field
|
||||||
name when generating a schema path parameter. Typically this will be `'id'`.
|
name when generating a schema path parameter. Typically this will be `'id'`.
|
||||||
This gives a more suitable representation as "primary key" is an implementation
|
This gives a more suitable representation as "primary key" is an implementation
|
||||||
detail, wheras "identifier" is a more general concept.
|
detail, whereas "identifier" is a more general concept.
|
||||||
|
|
||||||
Default: `True`
|
Default: `True`
|
||||||
|
|
||||||
|
|
|
@ -153,7 +153,7 @@ For more information, see the documentation on [customizing field mappings][cust
|
||||||
|
|
||||||
We've now moved a number of packages out of the core of REST framework, and into separately installable packages. If you're currently using these you don't need to worry, you simply need to `pip install` the new packages, and change any import paths.
|
We've now moved a number of packages out of the core of REST framework, and into separately installable packages. If you're currently using these you don't need to worry, you simply need to `pip install` the new packages, and change any import paths.
|
||||||
|
|
||||||
We're making this change in order to help distribute the maintainance workload, and keep better focus of the core essentials of the framework.
|
We're making this change in order to help distribute the maintenance workload, and keep better focus of the core essentials of the framework.
|
||||||
|
|
||||||
The change also means we can be more flexible with which external packages we recommend. For example, the excellently maintained [Django OAuth toolkit](https://github.com/evonove/django-oauth-toolkit) has now been promoted as our recommended option for integrating OAuth support.
|
The change also means we can be more flexible with which external packages we recommend. For example, the excellently maintained [Django OAuth toolkit](https://github.com/evonove/django-oauth-toolkit) has now been promoted as our recommended option for integrating OAuth support.
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ Name | Support | PyPI pa
|
||||||
---------------------------------|-------------------------------------|--------------------------------
|
---------------------------------|-------------------------------------|--------------------------------
|
||||||
[Core JSON][core-json] | Schema generation & client support. | Built-in support in `coreapi`.
|
[Core JSON][core-json] | Schema generation & client support. | Built-in support in `coreapi`.
|
||||||
[Swagger / OpenAPI][swagger] | Schema generation & client support. | The `openapi-codec` package.
|
[Swagger / OpenAPI][swagger] | Schema generation & client support. | The `openapi-codec` package.
|
||||||
[JSON Hyper-Schema][hyperschema] | Currrently client support only. | The `hyperschema-codec` package.
|
[JSON Hyper-Schema][hyperschema] | Currently client support only. | The `hyperschema-codec` package.
|
||||||
[API Blueprint][api-blueprint] | Not yet available. | Not yet available.
|
[API Blueprint][api-blueprint] | Not yet available. | Not yet available.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
@ -217,7 +217,7 @@ credentials, headers and bookmarks:
|
||||||
|
|
||||||
# Python client library
|
# Python client library
|
||||||
|
|
||||||
The `coreapi` Python package allows you to programatically interact with any
|
The `coreapi` Python package allows you to programmatically interact with any
|
||||||
API that exposes a supported schema format.
|
API that exposes a supported schema format.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
|
@ -102,7 +102,7 @@ Our gold sponsors include companies large and small. Many thanks for their signi
|
||||||
|
|
||||||
### Silver sponsors
|
### Silver sponsors
|
||||||
|
|
||||||
The serious financial contribution that our silver sponsors have made is very much appreciated. I'd like to say a particular thank you to individuals who have choosen to privately support the project at this level.
|
The serious financial contribution that our silver sponsors have made is very much appreciated. I'd like to say a particular thank you to individuals who have chosen to privately support the project at this level.
|
||||||
|
|
||||||
<ul class="sponsor silver">
|
<ul class="sponsor silver">
|
||||||
<li><a href="http://www.imtapps.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-imt_computer_services.png);">IMT Computer Services</a></li>
|
<li><a href="http://www.imtapps.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-imt_computer_services.png);">IMT Computer Services</a></li>
|
||||||
|
|
|
@ -228,7 +228,7 @@ You can determine your currently installed version using `pip freeze`:
|
||||||
* Fixed use of deprecated Query.aggregates. ([#4003][gh4003])
|
* Fixed use of deprecated Query.aggregates. ([#4003][gh4003])
|
||||||
* Fix blank lines around docstrings. ([#4002][gh4002])
|
* Fix blank lines around docstrings. ([#4002][gh4002])
|
||||||
* Fixed admin pagination when limit is 0. ([#3990][gh3990])
|
* Fixed admin pagination when limit is 0. ([#3990][gh3990])
|
||||||
* OrderingFilter adjustements. ([#3983][gh3983])
|
* OrderingFilter adjustments. ([#3983][gh3983])
|
||||||
* Non-required serializer related fields. ([#3976][gh3976])
|
* Non-required serializer related fields. ([#3976][gh3976])
|
||||||
* Using safer calling way of "@api_view" in tutorial. ([#3971][gh3971])
|
* Using safer calling way of "@api_view" in tutorial. ([#3971][gh3971])
|
||||||
* ListSerializer doesn't handle unique_together constraints. ([#3970][gh3970])
|
* ListSerializer doesn't handle unique_together constraints. ([#3970][gh3970])
|
||||||
|
|
|
@ -62,7 +62,7 @@ So far, so good. It looks pretty similar to the previous case, but we've got be
|
||||||
|
|
||||||
That's looking good. Again, it's still pretty similar to the function based view right now.
|
That's looking good. Again, it's still pretty similar to the function based view right now.
|
||||||
|
|
||||||
We'll also need to refactor our `urls.py` slightly now we're using class-based views.
|
We'll also need to refactor our `urls.py` slightly now that we're using class-based views.
|
||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from rest_framework.urlpatterns import format_suffix_patterns
|
from rest_framework.urlpatterns import format_suffix_patterns
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# PyTest for running the tests.
|
# PyTest for running the tests.
|
||||||
pytest==2.9.1
|
pytest==3.0.5
|
||||||
pytest-django==2.9.1
|
pytest-django==3.1.2
|
||||||
pytest-cov==1.8.1
|
pytest-cov==2.4.0
|
||||||
|
|
|
@ -149,7 +149,7 @@ def to_choices_dict(choices):
|
||||||
# choices = [('Category', ((1, 'First'), (2, 'Second'))), (3, 'Third')]
|
# choices = [('Category', ((1, 'First'), (2, 'Second'))), (3, 'Third')]
|
||||||
ret = OrderedDict()
|
ret = OrderedDict()
|
||||||
for choice in choices:
|
for choice in choices:
|
||||||
if (not isinstance(choice, (list, tuple))):
|
if not isinstance(choice, (list, tuple)):
|
||||||
# single choice
|
# single choice
|
||||||
ret[choice] = choice
|
ret[choice] = choice
|
||||||
else:
|
else:
|
||||||
|
@ -1614,7 +1614,7 @@ class JSONField(Field):
|
||||||
def get_value(self, dictionary):
|
def get_value(self, dictionary):
|
||||||
if html.is_html_input(dictionary) and self.field_name in dictionary:
|
if html.is_html_input(dictionary) and self.field_name in dictionary:
|
||||||
# When HTML form input is used, mark up the input
|
# When HTML form input is used, mark up the input
|
||||||
# as being a JSON string, rather than a JSON primative.
|
# as being a JSON string, rather than a JSON primitive.
|
||||||
class JSONString(six.text_type):
|
class JSONString(six.text_type):
|
||||||
def __new__(self, value):
|
def __new__(self, value):
|
||||||
ret = six.text_type.__new__(self, value)
|
ret = six.text_type.__new__(self, value)
|
||||||
|
|
|
@ -131,7 +131,7 @@ def _reverse_ordering(ordering_tuple):
|
||||||
ordering and return a new tuple, eg. `('created', '-uuid')`.
|
ordering and return a new tuple, eg. `('created', '-uuid')`.
|
||||||
"""
|
"""
|
||||||
def invert(x):
|
def invert(x):
|
||||||
return x[1:] if (x.startswith('-')) else '-' + x
|
return x[1:] if x.startswith('-') else '-' + x
|
||||||
|
|
||||||
return tuple([invert(item) for item in ordering_tuple])
|
return tuple([invert(item) for item in ordering_tuple])
|
||||||
|
|
||||||
|
|
|
@ -503,7 +503,7 @@ class ManyRelatedField(Field):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
relationship = get_attribute(instance, self.source_attrs)
|
relationship = get_attribute(instance, self.source_attrs)
|
||||||
return relationship.all() if (hasattr(relationship, 'all')) else relationship
|
return relationship.all() if hasattr(relationship, 'all') else relationship
|
||||||
|
|
||||||
def to_representation(self, iterable):
|
def to_representation(self, iterable):
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -228,7 +228,7 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
|
||||||
|
|
||||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||||
renderer_context = renderer_context or {}
|
renderer_context = renderer_context or {}
|
||||||
response = renderer_context['response']
|
response = renderer_context.get('response')
|
||||||
|
|
||||||
if response and response.exception:
|
if response and response.exception:
|
||||||
request = renderer_context['request']
|
request = renderer_context['request']
|
||||||
|
@ -540,7 +540,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
# If possible, serialize the initial content for the generic form
|
# If possible, serialize the initial content for the generic form
|
||||||
default_parser = view.parser_classes[0]
|
default_parser = view.parser_classes[0]
|
||||||
renderer_class = getattr(default_parser, 'renderer_class', None)
|
renderer_class = getattr(default_parser, 'renderer_class', None)
|
||||||
if (hasattr(view, 'get_serializer') and renderer_class):
|
if hasattr(view, 'get_serializer') and renderer_class:
|
||||||
# View has a serializer defined and parser class has a
|
# View has a serializer defined and parser class has a
|
||||||
# corresponding renderer that can be used to render the data.
|
# corresponding renderer that can be used to render the data.
|
||||||
|
|
||||||
|
@ -598,7 +598,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
||||||
paginator = getattr(view, 'paginator', None)
|
paginator = getattr(view, 'paginator', None)
|
||||||
if isinstance(data, list):
|
if isinstance(data, list):
|
||||||
pass
|
pass
|
||||||
elif (paginator is not None and data is not None):
|
elif paginator is not None and data is not None:
|
||||||
try:
|
try:
|
||||||
paginator.get_results(data)
|
paginator.get_results(data)
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
|
@ -738,7 +738,7 @@ class AdminRenderer(BrowsableAPIRenderer):
|
||||||
ret = template_render(template, context, request=renderer_context['request'])
|
ret = template_render(template, context, request=renderer_context['request'])
|
||||||
|
|
||||||
# Creation and deletion should use redirects in the admin style.
|
# Creation and deletion should use redirects in the admin style.
|
||||||
if (response.status_code == status.HTTP_201_CREATED) and ('Location' in response):
|
if response.status_code == status.HTTP_201_CREATED and 'Location' in response:
|
||||||
response.status_code = status.HTTP_303_SEE_OTHER
|
response.status_code = status.HTTP_303_SEE_OTHER
|
||||||
response['Location'] = request.build_absolute_uri()
|
response['Location'] = request.build_absolute_uri()
|
||||||
ret = ''
|
ret = ''
|
||||||
|
@ -764,7 +764,7 @@ class AdminRenderer(BrowsableAPIRenderer):
|
||||||
)
|
)
|
||||||
|
|
||||||
paginator = getattr(context['view'], 'paginator', None)
|
paginator = getattr(context['view'], 'paginator', None)
|
||||||
if (paginator is not None and data is not None):
|
if paginator is not None and data is not None:
|
||||||
try:
|
try:
|
||||||
results = paginator.get_results(data)
|
results = paginator.get_results(data)
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
|
|
|
@ -152,7 +152,7 @@ class Request(object):
|
||||||
|
|
||||||
force_user = getattr(request, '_force_auth_user', None)
|
force_user = getattr(request, '_force_auth_user', None)
|
||||||
force_token = getattr(request, '_force_auth_token', None)
|
force_token = getattr(request, '_force_auth_token', None)
|
||||||
if (force_user is not None or force_token is not None):
|
if force_user is not None or force_token is not None:
|
||||||
forced_auth = ForcedAuthentication(force_user, force_token)
|
forced_auth = ForcedAuthentication(force_user, force_token)
|
||||||
self.authenticators = (forced_auth,)
|
self.authenticators = (forced_auth,)
|
||||||
|
|
||||||
|
|
|
@ -1177,7 +1177,7 @@ class ModelSerializer(Serializer):
|
||||||
|
|
||||||
if postgres_fields and isinstance(model_field, postgres_fields.ArrayField):
|
if postgres_fields and isinstance(model_field, postgres_fields.ArrayField):
|
||||||
# Populate the `child` argument on `ListField` instances generated
|
# Populate the `child` argument on `ListField` instances generated
|
||||||
# for the PostgrSQL specfic `ArrayField`.
|
# for the PostgreSQL specific `ArrayField`.
|
||||||
child_model_field = model_field.base_field
|
child_model_field = model_field.base_field
|
||||||
child_field_class, child_field_kwargs = self.build_standard_field(
|
child_field_class, child_field_kwargs = self.build_standard_field(
|
||||||
'child', child_model_field
|
'child', child_model_field
|
||||||
|
|
|
@ -117,7 +117,7 @@ class NamespaceVersioning(BaseVersioning):
|
||||||
|
|
||||||
def determine_version(self, request, *args, **kwargs):
|
def determine_version(self, request, *args, **kwargs):
|
||||||
resolver_match = getattr(request, 'resolver_match', None)
|
resolver_match = getattr(request, 'resolver_match', None)
|
||||||
if (resolver_match is None or not resolver_match.namespace):
|
if resolver_match is None or not resolver_match.namespace:
|
||||||
return self.default_version
|
return self.default_version
|
||||||
|
|
||||||
# Allow for possibly nested namespaces.
|
# Allow for possibly nested namespaces.
|
||||||
|
|
|
@ -2,6 +2,7 @@ from datetime import date, datetime, timedelta, tzinfo
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from rest_framework.compat import coreapi
|
from rest_framework.compat import coreapi
|
||||||
|
@ -57,7 +58,7 @@ class JSONEncoderTests(TestCase):
|
||||||
|
|
||||||
current_time = datetime.now().time()
|
current_time = datetime.now().time()
|
||||||
current_time = current_time.replace(tzinfo=UTC())
|
current_time = current_time.replace(tzinfo=UTC())
|
||||||
with self.assertRaises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
self.encoder.default(current_time)
|
self.encoder.default(current_time)
|
||||||
|
|
||||||
def test_encode_date(self):
|
def test_encode_date(self):
|
||||||
|
@ -85,8 +86,8 @@ class JSONEncoderTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests encoding a coreapi objects raises proper error
|
Tests encoding a coreapi objects raises proper error
|
||||||
"""
|
"""
|
||||||
with self.assertRaises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
self.encoder.default(coreapi.Document())
|
self.encoder.default(coreapi.Document())
|
||||||
|
|
||||||
with self.assertRaises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
self.encoder.default(coreapi.Error())
|
self.encoder.default(coreapi.Error())
|
||||||
|
|
|
@ -268,7 +268,7 @@ class TestSimpleMetadataFieldInfo(TestCase):
|
||||||
def test_null_boolean_field_info_type(self):
|
def test_null_boolean_field_info_type(self):
|
||||||
options = metadata.SimpleMetadata()
|
options = metadata.SimpleMetadata()
|
||||||
field_info = options.get_field_info(serializers.NullBooleanField())
|
field_info = options.get_field_info(serializers.NullBooleanField())
|
||||||
self.assertEqual(field_info['type'], 'boolean')
|
assert field_info['type'] == 'boolean'
|
||||||
|
|
||||||
def test_related_field_choices(self):
|
def test_related_field_choices(self):
|
||||||
options = metadata.SimpleMetadata()
|
options = metadata.SimpleMetadata()
|
||||||
|
@ -277,7 +277,7 @@ class TestSimpleMetadataFieldInfo(TestCase):
|
||||||
field_info = options.get_field_info(
|
field_info = options.get_field_info(
|
||||||
serializers.RelatedField(queryset=BasicModel.objects.all())
|
serializers.RelatedField(queryset=BasicModel.objects.all())
|
||||||
)
|
)
|
||||||
self.assertNotIn('choices', field_info)
|
assert 'choices' not in field_info
|
||||||
|
|
||||||
|
|
||||||
class TestModelSerializerMetadata(TestCase):
|
class TestModelSerializerMetadata(TestCase):
|
||||||
|
|
|
@ -35,7 +35,7 @@ class TestFormParser(TestCase):
|
||||||
stream = StringIO(self.string)
|
stream = StringIO(self.string)
|
||||||
data = parser.parse(stream)
|
data = parser.parse(stream)
|
||||||
|
|
||||||
self.assertEqual(Form(data).is_valid(), True)
|
assert Form(data).is_valid() is True
|
||||||
|
|
||||||
|
|
||||||
class TestFileUploadParser(TestCase):
|
class TestFileUploadParser(TestCase):
|
||||||
|
@ -62,7 +62,7 @@ class TestFileUploadParser(TestCase):
|
||||||
self.stream.seek(0)
|
self.stream.seek(0)
|
||||||
data_and_files = parser.parse(self.stream, None, self.parser_context)
|
data_and_files = parser.parse(self.stream, None, self.parser_context)
|
||||||
file_obj = data_and_files.files['file']
|
file_obj = data_and_files.files['file']
|
||||||
self.assertEqual(file_obj._size, 14)
|
assert file_obj._size == 14
|
||||||
|
|
||||||
def test_parse_missing_filename(self):
|
def test_parse_missing_filename(self):
|
||||||
"""
|
"""
|
||||||
|
@ -108,22 +108,22 @@ class TestFileUploadParser(TestCase):
|
||||||
def test_get_filename(self):
|
def test_get_filename(self):
|
||||||
parser = FileUploadParser()
|
parser = FileUploadParser()
|
||||||
filename = parser.get_filename(self.stream, None, self.parser_context)
|
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||||
self.assertEqual(filename, 'file.txt')
|
assert filename == 'file.txt'
|
||||||
|
|
||||||
def test_get_encoded_filename(self):
|
def test_get_encoded_filename(self):
|
||||||
parser = FileUploadParser()
|
parser = FileUploadParser()
|
||||||
|
|
||||||
self.__replace_content_disposition('inline; filename*=utf-8\'\'ÀĥƦ.txt')
|
self.__replace_content_disposition('inline; filename*=utf-8\'\'ÀĥƦ.txt')
|
||||||
filename = parser.get_filename(self.stream, None, self.parser_context)
|
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||||
self.assertEqual(filename, 'ÀĥƦ.txt')
|
assert filename == 'ÀĥƦ.txt'
|
||||||
|
|
||||||
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'\'ÀĥƦ.txt')
|
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'\'ÀĥƦ.txt')
|
||||||
filename = parser.get_filename(self.stream, None, self.parser_context)
|
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||||
self.assertEqual(filename, 'ÀĥƦ.txt')
|
assert filename == 'ÀĥƦ.txt'
|
||||||
|
|
||||||
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'en-us\'ÀĥƦ.txt')
|
self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'en-us\'ÀĥƦ.txt')
|
||||||
filename = parser.get_filename(self.stream, None, self.parser_context)
|
filename = parser.get_filename(self.stream, None, self.parser_context)
|
||||||
self.assertEqual(filename, 'ÀĥƦ.txt')
|
assert filename == 'ÀĥƦ.txt'
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -5,9 +5,11 @@ import json
|
||||||
import re
|
import re
|
||||||
from collections import MutableMapping, OrderedDict
|
from collections import MutableMapping, OrderedDict
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.http.request import HttpRequest
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils.safestring import SafeText
|
from django.utils.safestring import SafeText
|
||||||
|
@ -15,8 +17,10 @@ from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework import permissions, serializers, status
|
from rest_framework import permissions, serializers, status
|
||||||
from rest_framework.renderers import (
|
from rest_framework.renderers import (
|
||||||
BaseRenderer, BrowsableAPIRenderer, HTMLFormRenderer, JSONRenderer
|
AdminRenderer, BaseRenderer, BrowsableAPIRenderer,
|
||||||
|
HTMLFormRenderer, JSONRenderer, StaticHTMLRenderer
|
||||||
)
|
)
|
||||||
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
|
@ -269,6 +273,18 @@ def strip_trailing_whitespace(content):
|
||||||
return re.sub(' +\n', '\n', content)
|
return re.sub(' +\n', '\n', content)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRendererTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests BaseRenderer
|
||||||
|
"""
|
||||||
|
def test_render_raise_error(self):
|
||||||
|
"""
|
||||||
|
BaseRenderer.render should raise NotImplementedError
|
||||||
|
"""
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
BaseRenderer().render('test')
|
||||||
|
|
||||||
|
|
||||||
class JSONRendererTests(TestCase):
|
class JSONRendererTests(TestCase):
|
||||||
"""
|
"""
|
||||||
Tests specific to the JSON Renderer
|
Tests specific to the JSON Renderer
|
||||||
|
@ -568,3 +584,67 @@ class TestMultipleChoiceFieldHTMLFormRenderer(TestCase):
|
||||||
result)
|
result)
|
||||||
self.assertInHTML('<option value="1">Option1</option>', result)
|
self.assertInHTML('<option value="1">Option1</option>', result)
|
||||||
self.assertInHTML('<option value="2">Option2</option>', result)
|
self.assertInHTML('<option value="2">Option2</option>', result)
|
||||||
|
|
||||||
|
|
||||||
|
class StaticHTMLRendererTests(TestCase):
|
||||||
|
"""
|
||||||
|
Tests specific for Static HTML Renderer
|
||||||
|
"""
|
||||||
|
def setUp(self):
|
||||||
|
self.renderer = StaticHTMLRenderer()
|
||||||
|
|
||||||
|
def test_static_renderer(self):
|
||||||
|
data = '<html><body>text</body></html>'
|
||||||
|
result = self.renderer.render(data)
|
||||||
|
assert result == data
|
||||||
|
|
||||||
|
def test_static_renderer_with_exception(self):
|
||||||
|
context = {
|
||||||
|
'response': Response(status=500, exception=True),
|
||||||
|
'request': Request(HttpRequest())
|
||||||
|
}
|
||||||
|
result = self.renderer.render({}, renderer_context=context)
|
||||||
|
assert result == '500 Internal Server Error'
|
||||||
|
|
||||||
|
|
||||||
|
class BrowsableAPIRendererTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.renderer = BrowsableAPIRenderer()
|
||||||
|
|
||||||
|
def test_get_description_returns_empty_string_for_401_and_403_statuses(self):
|
||||||
|
assert self.renderer.get_description({}, status_code=401) == ''
|
||||||
|
assert self.renderer.get_description({}, status_code=403) == ''
|
||||||
|
|
||||||
|
def test_get_filter_form_returns_none_if_data_is_not_list_instance(self):
|
||||||
|
class DummyView(object):
|
||||||
|
get_queryset = None
|
||||||
|
filter_backends = None
|
||||||
|
|
||||||
|
result = self.renderer.get_filter_form(data='not list',
|
||||||
|
view=DummyView(), request={})
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
class AdminRendererTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.renderer = AdminRenderer()
|
||||||
|
|
||||||
|
def test_render_when_resource_created(self):
|
||||||
|
class DummyView(APIView):
|
||||||
|
renderer_classes = (AdminRenderer, )
|
||||||
|
request = Request(HttpRequest())
|
||||||
|
request.build_absolute_uri = lambda: 'http://example.com'
|
||||||
|
response = Response(status=201, headers={'Location': '/test'})
|
||||||
|
context = {
|
||||||
|
'view': DummyView(),
|
||||||
|
'request': request,
|
||||||
|
'response': response
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.renderer.render(data={'test': 'test'},
|
||||||
|
renderer_context=context)
|
||||||
|
assert result == ''
|
||||||
|
assert response.status_code == status.HTTP_303_SEE_OTHER
|
||||||
|
assert response['Location'] == 'http://example.com'
|
||||||
|
|
|
@ -44,9 +44,9 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
serializer = self.BookSerializer(data=data, many=True)
|
serializer = self.BookSerializer(data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), True)
|
assert serializer.is_valid() is True
|
||||||
self.assertEqual(serializer.validated_data, data)
|
assert serializer.validated_data == data
|
||||||
self.assertEqual(serializer.errors, [])
|
assert serializer.errors == []
|
||||||
|
|
||||||
def test_bulk_create_errors(self):
|
def test_bulk_create_errors(self):
|
||||||
"""
|
"""
|
||||||
|
@ -75,9 +75,9 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
serializer = self.BookSerializer(data=data, many=True)
|
serializer = self.BookSerializer(data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), False)
|
assert serializer.is_valid() is False
|
||||||
self.assertEqual(serializer.errors, expected_errors)
|
assert serializer.errors == expected_errors
|
||||||
self.assertEqual(serializer.validated_data, [])
|
assert serializer.validated_data == []
|
||||||
|
|
||||||
def test_invalid_list_datatype(self):
|
def test_invalid_list_datatype(self):
|
||||||
"""
|
"""
|
||||||
|
@ -85,7 +85,7 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
"""
|
"""
|
||||||
data = ['foo', 'bar', 'baz']
|
data = ['foo', 'bar', 'baz']
|
||||||
serializer = self.BookSerializer(data=data, many=True)
|
serializer = self.BookSerializer(data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), False)
|
assert serializer.is_valid() is False
|
||||||
|
|
||||||
text_type_string = six.text_type.__name__
|
text_type_string = six.text_type.__name__
|
||||||
message = 'Invalid data. Expected a dictionary, but got %s.' % text_type_string
|
message = 'Invalid data. Expected a dictionary, but got %s.' % text_type_string
|
||||||
|
@ -95,7 +95,7 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
{'non_field_errors': [message]}
|
{'non_field_errors': [message]}
|
||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(serializer.errors, expected_errors)
|
assert serializer.errors == expected_errors
|
||||||
|
|
||||||
def test_invalid_single_datatype(self):
|
def test_invalid_single_datatype(self):
|
||||||
"""
|
"""
|
||||||
|
@ -103,11 +103,11 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
"""
|
"""
|
||||||
data = 123
|
data = 123
|
||||||
serializer = self.BookSerializer(data=data, many=True)
|
serializer = self.BookSerializer(data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), False)
|
assert serializer.is_valid() is False
|
||||||
|
|
||||||
expected_errors = {'non_field_errors': ['Expected a list of items but got type "int".']}
|
expected_errors = {'non_field_errors': ['Expected a list of items but got type "int".']}
|
||||||
|
|
||||||
self.assertEqual(serializer.errors, expected_errors)
|
assert serializer.errors == expected_errors
|
||||||
|
|
||||||
def test_invalid_single_object(self):
|
def test_invalid_single_object(self):
|
||||||
"""
|
"""
|
||||||
|
@ -120,8 +120,8 @@ class BulkCreateSerializerTests(TestCase):
|
||||||
'author': 'Tom Wolfe'
|
'author': 'Tom Wolfe'
|
||||||
}
|
}
|
||||||
serializer = self.BookSerializer(data=data, many=True)
|
serializer = self.BookSerializer(data=data, many=True)
|
||||||
self.assertEqual(serializer.is_valid(), False)
|
assert serializer.is_valid() is False
|
||||||
|
|
||||||
expected_errors = {'non_field_errors': ['Expected a list of items but got type "dict".']}
|
expected_errors = {'non_field_errors': ['Expected a list of items but got type "dict".']}
|
||||||
|
|
||||||
self.assertEqual(serializer.errors, expected_errors)
|
assert serializer.errors == expected_errors
|
||||||
|
|
|
@ -3,15 +3,20 @@ Tests for the throttling implementations in the permissions module.
|
||||||
"""
|
"""
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import pytest
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
from django.http import HttpRequest
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.settings import api_settings
|
from rest_framework.settings import api_settings
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory, force_authenticate
|
||||||
from rest_framework.throttling import (
|
from rest_framework.throttling import (
|
||||||
BaseThrottle, ScopedRateThrottle, UserRateThrottle
|
AnonRateThrottle, BaseThrottle, ScopedRateThrottle, SimpleRateThrottle,
|
||||||
|
UserRateThrottle
|
||||||
)
|
)
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
@ -189,6 +194,8 @@ class ScopedRateThrottleTests(TestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self.throttle = ScopedRateThrottle()
|
||||||
|
|
||||||
class XYScopedRateThrottle(ScopedRateThrottle):
|
class XYScopedRateThrottle(ScopedRateThrottle):
|
||||||
TIMER_SECONDS = 0
|
TIMER_SECONDS = 0
|
||||||
THROTTLE_RATES = {'x': '3/min', 'y': '1/min'}
|
THROTTLE_RATES = {'x': '3/min', 'y': '1/min'}
|
||||||
|
@ -288,6 +295,18 @@ class ScopedRateThrottleTests(TestCase):
|
||||||
response = self.unscoped_view(request)
|
response = self.unscoped_view(request)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
def test_get_cache_key_returns_correct_key_if_user_is_authenticated(self):
|
||||||
|
class DummyView(object):
|
||||||
|
throttle_scope = 'user'
|
||||||
|
|
||||||
|
request = Request(HttpRequest())
|
||||||
|
user = User.objects.create(username='test')
|
||||||
|
force_authenticate(request, user)
|
||||||
|
request.user = user
|
||||||
|
self.throttle.allow_request(request, DummyView())
|
||||||
|
cache_key = self.throttle.get_cache_key(request, view=DummyView())
|
||||||
|
assert cache_key == 'throttle_user_%s' % user.pk
|
||||||
|
|
||||||
|
|
||||||
class XffTestingBase(TestCase):
|
class XffTestingBase(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -354,3 +373,79 @@ class XffUniqueMachinesTest(XffTestingBase):
|
||||||
self.view(self.request)
|
self.view(self.request)
|
||||||
self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 7.7.7.7, 2.2.2.2'
|
self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 7.7.7.7, 2.2.2.2'
|
||||||
assert self.view(self.request).status_code == 200
|
assert self.view(self.request).status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
class BaseThrottleTests(TestCase):
|
||||||
|
|
||||||
|
def test_allow_request_raises_not_implemented_error(self):
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
BaseThrottle().allow_request(request={}, view={})
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleRateThrottleTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
SimpleRateThrottle.scope = 'anon'
|
||||||
|
|
||||||
|
def test_get_rate_raises_error_if_scope_is_missing(self):
|
||||||
|
throttle = SimpleRateThrottle()
|
||||||
|
with pytest.raises(ImproperlyConfigured):
|
||||||
|
throttle.scope = None
|
||||||
|
throttle.get_rate()
|
||||||
|
|
||||||
|
def test_throttle_raises_error_if_rate_is_missing(self):
|
||||||
|
SimpleRateThrottle.scope = 'invalid scope'
|
||||||
|
with pytest.raises(ImproperlyConfigured):
|
||||||
|
SimpleRateThrottle()
|
||||||
|
|
||||||
|
def test_parse_rate_returns_tuple_with_none_if_rate_not_provided(self):
|
||||||
|
rate = SimpleRateThrottle().parse_rate(None)
|
||||||
|
assert rate == (None, None)
|
||||||
|
|
||||||
|
def test_allow_request_returns_true_if_rate_is_none(self):
|
||||||
|
assert SimpleRateThrottle().allow_request(request={}, view={}) is True
|
||||||
|
|
||||||
|
def test_get_cache_key_raises_not_implemented_error(self):
|
||||||
|
with pytest.raises(NotImplementedError):
|
||||||
|
SimpleRateThrottle().get_cache_key({}, {})
|
||||||
|
|
||||||
|
def test_allow_request_returns_true_if_key_is_none(self):
|
||||||
|
throttle = SimpleRateThrottle()
|
||||||
|
throttle.rate = 'some rate'
|
||||||
|
throttle.get_cache_key = lambda *args: None
|
||||||
|
assert throttle.allow_request(request={}, view={}) is True
|
||||||
|
|
||||||
|
def test_wait_returns_correct_waiting_time_without_history(self):
|
||||||
|
throttle = SimpleRateThrottle()
|
||||||
|
throttle.num_requests = 1
|
||||||
|
throttle.duration = 60
|
||||||
|
throttle.history = []
|
||||||
|
waiting_time = throttle.wait()
|
||||||
|
assert isinstance(waiting_time, float)
|
||||||
|
assert waiting_time == 30.0
|
||||||
|
|
||||||
|
def test_wait_returns_none_if_there_are_no_available_requests(self):
|
||||||
|
throttle = SimpleRateThrottle()
|
||||||
|
throttle.num_requests = 1
|
||||||
|
throttle.duration = 60
|
||||||
|
throttle.now = throttle.timer()
|
||||||
|
throttle.history = [throttle.timer() for _ in range(3)]
|
||||||
|
assert throttle.wait() is None
|
||||||
|
|
||||||
|
|
||||||
|
class AnonRateThrottleTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.throttle = AnonRateThrottle()
|
||||||
|
|
||||||
|
def test_authenticated_user_not_affected(self):
|
||||||
|
request = Request(HttpRequest())
|
||||||
|
user = User.objects.create(username='test')
|
||||||
|
force_authenticate(request, user)
|
||||||
|
request.user = user
|
||||||
|
assert self.throttle.get_cache_key(request, view={}) is None
|
||||||
|
|
||||||
|
def test_get_cache_key_returns_correct_value(self):
|
||||||
|
request = Request(HttpRequest())
|
||||||
|
cache_key = self.throttle.get_cache_key(request, view={})
|
||||||
|
assert cache_key == 'throttle_anon_None'
|
||||||
|
|
3
tox.ini
3
tox.ini
|
@ -7,7 +7,7 @@ envlist =
|
||||||
{py27,py33,py34,py35}-django18,
|
{py27,py33,py34,py35}-django18,
|
||||||
{py27,py34,py35}-django19,
|
{py27,py34,py35}-django19,
|
||||||
{py27,py34,py35}-django110,
|
{py27,py34,py35}-django110,
|
||||||
{py27,py34,py35}-django{master}
|
{py27,py34,py35,py36}-django{master}
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands = ./runtests.py --fast {posargs} --coverage -rw
|
commands = ./runtests.py --fast {posargs} --coverage -rw
|
||||||
|
@ -22,6 +22,7 @@ deps =
|
||||||
-rrequirements/requirements-testing.txt
|
-rrequirements/requirements-testing.txt
|
||||||
-rrequirements/requirements-optionals.txt
|
-rrequirements/requirements-optionals.txt
|
||||||
basepython =
|
basepython =
|
||||||
|
py36: python3.6
|
||||||
py35: python3.5
|
py35: python3.5
|
||||||
py34: python3.4
|
py34: python3.4
|
||||||
py33: python3.3
|
py33: python3.3
|
||||||
|
|
Loading…
Reference in New Issue
Block a user