Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Anna 2017-07-18 02:12:52 +02:00
commit dfcf284047
29 changed files with 270 additions and 62 deletions

View File

@ -330,7 +330,7 @@ The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is a
## JSON Web Token Authentication ## JSON Web Token Authentication
JSON Web Token is a fairly new standard which can be used for token-based authentication. Unlike the built-in TokenAuthentication scheme, JWT Authentication doesn't need to use a database to validate a token. [Blimp][blimp] maintains the [djangorestframework-jwt][djangorestframework-jwt] package which provides a JWT Authentication class as well as a mechanism for clients to obtain a JWT given the username and password. JSON Web Token is a fairly new standard which can be used for token-based authentication. Unlike the built-in TokenAuthentication scheme, JWT Authentication doesn't need to use a database to validate a token. [Blimp][blimp] maintains the [djangorestframework-jwt][djangorestframework-jwt] package which provides a JWT Authentication class as well as a mechanism for clients to obtain a JWT given the username and password. An alternative package for JWT authentication is [djangorestframework-simplejwt][djangorestframework-simplejwt] which provides different features as well as a pluggable token blacklist app.
## Hawk HTTP Authentication ## Hawk HTTP Authentication
@ -388,6 +388,7 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/integrations.md# [doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/integrations.md#
[blimp]: https://github.com/GetBlimp [blimp]: https://github.com/GetBlimp
[djangorestframework-jwt]: https://github.com/GetBlimp/django-rest-framework-jwt [djangorestframework-jwt]: https://github.com/GetBlimp/django-rest-framework-jwt
[djangorestframework-simplejwt]: https://github.com/davesque/django-rest-framework-simplejwt
[etoccalino]: https://github.com/etoccalino/ [etoccalino]: https://github.com/etoccalino/
[djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature [djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature
[amazon-http-signature]: http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html [amazon-http-signature]: http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html

View File

@ -242,29 +242,6 @@ We'd then need to setup the custom class in our configuration:
Note that if you care about how the ordering of keys is displayed in responses in the browsable API you might choose to use an `OrderedDict` when constructing the body of paginated responses, but this is optional. Note that if you care about how the ordering of keys is displayed in responses in the browsable API you might choose to use an `OrderedDict` when constructing the body of paginated responses, but this is optional.
## Header based pagination
Let's modify the built-in `PageNumberPagination` style, so that instead of include the pagination links in the body of the response, we'll instead include a `Link` header, in a [similar style to the GitHub API][github-link-pagination].
class LinkHeaderPagination(pagination.PageNumberPagination):
def get_paginated_response(self, data):
next_url = self.get_next_link()
previous_url = self.get_previous_link()
if next_url is not None and previous_url is not None:
link = '<{next_url}>; rel="next", <{previous_url}>; rel="prev"'
elif next_url is not None:
link = '<{next_url}>; rel="next"'
elif previous_url is not None:
link = '<{previous_url}>; rel="prev"'
else:
link = ''
link = link.format(next_url=next_url, previous_url=previous_url)
headers = {'Link': link} if link else {}
return Response(data, headers=headers)
## Using your custom pagination class ## Using your custom pagination class
To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting: To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting:
@ -328,10 +305,15 @@ The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin`
The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagination` class which allows to choose pagination class with a query parameter. The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagination` class which allows to choose pagination class with a query parameter.
## link-header-pagination
The [`django-rest-framework-link-header-pagination` package][drf-link-header-pagination] includes a `LinkHeaderPagination` class which provides pagination via an HTTP `Link` header as desribed in [Github's developer documentation](github-link-pagination).
[cite]: https://docs.djangoproject.com/en/stable/topics/pagination/ [cite]: https://docs.djangoproject.com/en/stable/topics/pagination/
[github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/ [github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/
[link-header]: ../img/link-header-pagination.png [link-header]: ../img/link-header-pagination.png
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/ [drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin [paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
[drf-proxy-pagination]: https://github.com/tuffnatty/drf-proxy-pagination [drf-proxy-pagination]: https://github.com/tuffnatty/drf-proxy-pagination
[drf-link-header-pagination]: https://github.com/tbeadle/django-rest-framework-link-header-pagination
[disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api [disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api

View File

@ -185,6 +185,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
* [django-oauth-toolkit][django-oauth-toolkit] - Provides OAuth 2.0 support. * [django-oauth-toolkit][django-oauth-toolkit] - Provides OAuth 2.0 support.
* [doac][doac] - Provides OAuth 2.0 support. * [doac][doac] - Provides OAuth 2.0 support.
* [djangorestframework-jwt][djangorestframework-jwt] - Provides JSON Web Token Authentication support. * [djangorestframework-jwt][djangorestframework-jwt] - Provides JSON Web Token Authentication support.
* [djangorestframework-simplejwt][djangorestframework-simplejwt] - An alternative package that provides JSON Web Token Authentication support.
* [hawkrest][hawkrest] - Provides Hawk HTTP Authorization. * [hawkrest][hawkrest] - Provides Hawk HTTP Authorization.
* [djangorestframework-httpsignature][djangorestframework-httpsignature] - Provides an easy to use HTTP Signature Authentication mechanism. * [djangorestframework-httpsignature][djangorestframework-httpsignature] - Provides an easy to use HTTP Signature Authentication mechanism.
* [djoser][djoser] - Provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. * [djoser][djoser] - Provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation.
@ -284,6 +285,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[django-oauth-toolkit]: https://github.com/evonove/django-oauth-toolkit [django-oauth-toolkit]: https://github.com/evonove/django-oauth-toolkit
[doac]: https://github.com/Rediker-Software/doac [doac]: https://github.com/Rediker-Software/doac
[djangorestframework-jwt]: https://github.com/GetBlimp/django-rest-framework-jwt [djangorestframework-jwt]: https://github.com/GetBlimp/django-rest-framework-jwt
[djangorestframework-simplejwt]: https://github.com/davesque/django-rest-framework-simplejwt
[hawkrest]: https://github.com/kumar303/hawkrest [hawkrest]: https://github.com/kumar303/hawkrest
[djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature [djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature
[djoser]: https://github.com/sunscrapers/djoser [djoser]: https://github.com/sunscrapers/djoser

View File

@ -0,0 +1,45 @@
from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError
from rest_framework.authtoken.models import Token
UserModel = get_user_model()
class Command(BaseCommand):
help = 'Create DRF Token for a given user'
def create_user_token(self, username, reset_token):
user = UserModel._default_manager.get_by_natural_key(username)
if reset_token:
Token.objects.filter(user=user).delete()
token = Token.objects.get_or_create(user=user)
return token[0]
def add_arguments(self, parser):
parser.add_argument('username', type=str, nargs='+')
parser.add_argument(
'-r',
'--reset',
action='store_true',
dest='reset_token',
default=False,
help='Reset existing User token and create a new one',
)
def handle(self, *args, **options):
username = options['username']
reset_token = options['reset_token']
try:
token = self.create_user_token(username, reset_token)
except UserModel.DoesNotExist:
raise CommandError(
'Cannot create the Token: user {0} does not exist'.format(
username)
)
self.stdout.write(
'Generated token {0} for user {1}'.format(token.key, username))

View File

@ -791,13 +791,17 @@ class RegexField(CharField):
class SlugField(CharField): class SlugField(CharField):
default_error_messages = { default_error_messages = {
'invalid': _('Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.') 'invalid': _('Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.'),
'invalid_unicode': _('Enter a valid "slug" consisting of Unicode letters, numbers, underscores, or hyphens.')
} }
def __init__(self, **kwargs): def __init__(self, allow_unicode=False, **kwargs):
super(SlugField, self).__init__(**kwargs) super(SlugField, self).__init__(**kwargs)
slug_regex = re.compile(r'^[-a-zA-Z0-9_]+$') self.allow_unicode = allow_unicode
validator = RegexValidator(slug_regex, message=self.error_messages['invalid']) if self.allow_unicode:
validator = RegexValidator(re.compile(r'^[-\w]+\Z', re.UNICODE), message=self.error_messages['invalid_unicode'])
else:
validator = RegexValidator(re.compile(r'^[-a-zA-Z0-9_]+$'), message=self.error_messages['invalid'])
self.validators.append(validator) self.validators.append(validator)
@ -1133,18 +1137,16 @@ class DateTimeField(Field):
if input_format.lower() == ISO_8601: if input_format.lower() == ISO_8601:
try: try:
parsed = parse_datetime(value) parsed = parse_datetime(value)
except (ValueError, TypeError):
pass
else:
if parsed is not None: if parsed is not None:
return self.enforce_timezone(parsed) return self.enforce_timezone(parsed)
except (ValueError, TypeError):
pass
else: else:
try: try:
parsed = self.datetime_parser(value, input_format) parsed = self.datetime_parser(value, input_format)
return self.enforce_timezone(parsed)
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
else:
return self.enforce_timezone(parsed)
humanized_format = humanize_datetime.datetime_formats(input_formats) humanized_format = humanize_datetime.datetime_formats(input_formats)
self.fail('invalid', format=humanized_format) self.fail('invalid', format=humanized_format)

View File

@ -140,12 +140,14 @@ class SearchFilter(BaseFilterBackend):
] ]
base = queryset base = queryset
conditions = []
for search_term in search_terms: for search_term in search_terms:
queries = [ queries = [
models.Q(**{orm_lookup: search_term}) models.Q(**{orm_lookup: search_term})
for orm_lookup in orm_lookups for orm_lookup in orm_lookups
] ]
queryset = queryset.filter(reduce(operator.or_, queries)) conditions.append(reduce(operator.or_, queries))
queryset = queryset.filter(reduce(operator.and_, conditions))
if self.must_call_distinct(queryset, search_fields): if self.must_call_distinct(queryset, search_fields):
# Filtering against a many-to-many field requires us to # Filtering against a many-to-many field requires us to

View File

@ -31,7 +31,7 @@ def _positive_int(integer_string, strict=False, cutoff=None):
if ret < 0 or (ret == 0 and strict): if ret < 0 or (ret == 0 and strict):
raise ValueError() raise ValueError()
if cutoff: if cutoff:
ret = min(ret, cutoff) return min(ret, cutoff)
return ret return ret
@ -95,7 +95,7 @@ def _get_displayed_page_numbers(current, final):
# Now sort the page numbers and drop anything outside the limits. # Now sort the page numbers and drop anything outside the limits.
included = [ included = [
idx for idx in sorted(list(included)) idx for idx in sorted(list(included))
if idx > 0 and idx <= final if 0 < idx <= final
] ]
# Finally insert any `...` breaks # Finally insert any `...` breaks

View File

@ -556,7 +556,10 @@ class BrowsableAPIRenderer(BaseRenderer):
accepted = self.accepted_media_type accepted = self.accepted_media_type
context = self.renderer_context.copy() context = self.renderer_context.copy()
context['indent'] = 4 context['indent'] = 4
content = renderer.render(serializer.data, accepted, context) data = {k: v for (k, v) in serializer.data.items()
if not isinstance(serializer.fields[k],
serializers.HiddenField)}
content = renderer.render(data, accepted, context)
else: else:
content = None content = None

View File

@ -250,6 +250,10 @@ class Request(object):
else: else:
self._full_data = self._data self._full_data = self._data
# copy files refs to the underlying request so that closable
# objects are handled appropriately.
self._request._files = self._files
def _load_stream(self): def _load_stream(self):
""" """
Return the content body of the request, as a stream. Return the content body of the request, as a stream.

View File

@ -9,23 +9,23 @@ from __future__ import unicode_literals
def is_informational(code): def is_informational(code):
return code >= 100 and code <= 199 return 100 <= code <= 199
def is_success(code): def is_success(code):
return code >= 200 and code <= 299 return 200 <= code <= 299
def is_redirect(code): def is_redirect(code):
return code >= 300 and code <= 399 return 300 <= code <= 399
def is_client_error(code): def is_client_error(code):
return code >= 400 and code <= 499 return 400 <= code <= 499
def is_server_error(code): def is_server_error(code):
return code >= 500 and code <= 599 return 500 <= code <= 599
HTTP_100_CONTINUE = 100 HTTP_100_CONTINUE = 100

View File

@ -13,7 +13,7 @@
{% if 'javascript' in langs %}{% include "rest_framework/docs/langs/javascript-intro.html" %}{% endif %} {% if 'javascript' in langs %}{% include "rest_framework/docs/langs/javascript-intro.html" %}{% endif %}
</div> </div>
</div> </div>
{% if document.data %}
{% for section_key, section in document.data|items %} {% for section_key, section in document.data|items %}
{% if section_key %} {% if section_key %}
<h2 id="{{ section_key }}" class="coredocs-section-title">{{ section_key }} <a href="#{{ section_key }}"><i class="fa fa-link" aria-hidden="true"></i> <h2 id="{{ section_key }}" class="coredocs-section-title">{{ section_key }} <a href="#{{ section_key }}"><i class="fa fa-link" aria-hidden="true"></i>
@ -28,3 +28,4 @@
{% for link_key, link in document.links|items %} {% for link_key, link in document.links|items %}
{% include "rest_framework/docs/link.html" %} {% include "rest_framework/docs/link.html" %}
{% endfor %} {% endfor %}
{% endif %}

View File

@ -5,16 +5,18 @@
<i class="fa fa-bars fa-2x toggle-btn" data-toggle="collapse" data-target="#menu-content"></i> <i class="fa fa-bars fa-2x toggle-btn" data-toggle="collapse" data-target="#menu-content"></i>
<div class="menu-list"> <div class="menu-list">
<ul id="menu-content" class="menu-content collapse out"> <ul id="menu-content" class="menu-content collapse out">
{% if document.data %}
{% for section_key, section in document.data|items %} {% for section_key, section in document.data|items %}
<li data-toggle="collapse" data-target="#{{ section_key }}-dropdown" class="collapsed"> <li data-toggle="collapse" data-target="#{{ section_key }}-dropdown" class="collapsed">
<a><i class="fa fa-dot-circle-o fa-lg"></i> {% if section_key %}{{ section_key }}{% else %}API Endpoints{% endif %} <span class="arrow"></span></a> <a><i class="fa fa-dot-circle-o fa-lg"></i> {% if section_key %}{{ section_key }}{% else %}API Endpoints{% endif %} <span class="arrow"></span></a>
</li>
<ul class="sub-menu {% if section_key %}collapse{% endif %}" id="{{ section_key }}-dropdown"> <ul class="sub-menu {% if section_key %}collapse{% endif %}" id="{{ section_key }}-dropdown">
{% for link_key, link in section.links|items %} {% for link_key, link in section.links|items %}
<li><a href="#{{ section_key }}-{{ link_key }}">{{ link.title|default:link_key }}</a></li> <li><a href="#{{ section_key }}-{{ link_key }}">{{ link.title|default:link_key }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</li>
{% endfor %} {% endfor %}
{% endif %}
</ul> </ul>
<ul class="menu-list menu-list-bottom"> <ul class="menu-list menu-list-bottom">

View File

@ -52,8 +52,8 @@ def camelcase_to_spaces(content):
Translate 'CamelCaseNames' to 'Camel Case Names'. Translate 'CamelCaseNames' to 'Camel Case Names'.
Used when generating names from view classes. Used when generating names from view classes.
""" """
camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))' camelcase_boundary = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))'
content = re.sub(camelcase_boundry, ' \\1', content).strip() content = re.sub(camelcase_boundary, ' \\1', content).strip()
return ' '.join(content.split('_')).title() return ' '.join(content.split('_')).title()

View File

@ -82,6 +82,10 @@ class ViewSetMixin(object):
if hasattr(self, 'get') and not hasattr(self, 'head'): if hasattr(self, 'get') and not hasattr(self, 'head'):
self.head = self.get self.head = self.get
self.request = request
self.args = args
self.kwargs = kwargs
# And continue as usual # And continue as usual
return self.dispatch(request, *args, **kwargs) return self.dispatch(request, *args, **kwargs)

View File

@ -275,13 +275,13 @@ class APIClientTests(APITestCase):
client = CoreAPIClient() client = CoreAPIClient()
schema = client.get('http://api.example.com/') schema = client.get('http://api.example.com/')
temp = tempfile.NamedTemporaryFile() with tempfile.NamedTemporaryFile() as temp:
temp.write(b'example file content') temp.write(b'example file content')
temp.flush() temp.flush()
temp.seek(0)
with open(temp.name, 'rb') as upload: name = os.path.basename(temp.name)
name = os.path.basename(upload.name) data = client.action(schema, ['encoding', 'multipart'], params={'example': temp})
data = client.action(schema, ['encoding', 'multipart'], params={'example': upload})
expected = { expected = {
'method': 'POST', 'method': 'POST',
@ -407,13 +407,13 @@ class APIClientTests(APITestCase):
client = CoreAPIClient() client = CoreAPIClient()
schema = client.get('http://api.example.com/') schema = client.get('http://api.example.com/')
temp = tempfile.NamedTemporaryFile() with tempfile.NamedTemporaryFile(delete=False) as temp:
temp.write(b'example file content') temp.write(b'example file content')
temp.flush() temp.flush()
temp.seek(0)
with open(temp.name, 'rb') as upload: name = os.path.basename(temp.name)
name = os.path.basename(upload.name) data = client.action(schema, ['encoding', 'raw_upload'], params={'example': temp})
data = client.action(schema, ['encoding', 'raw_upload'], params={'example': upload})
expected = { expected = {
'method': 'POST', 'method': 'POST',

View File

@ -45,6 +45,7 @@ class NonAtomicAPIExceptionView(APIView):
BasicModel.objects.all() BasicModel.objects.all()
raise Http404 raise Http404
urlpatterns = ( urlpatterns = (
url(r'^$', NonAtomicAPIExceptionView.as_view()), url(r'^$', NonAtomicAPIExceptionView.as_view()),
) )

View File

@ -4,6 +4,8 @@ from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from rest_framework.authtoken.admin import TokenAdmin from rest_framework.authtoken.admin import TokenAdmin
from rest_framework.authtoken.management.commands.drf_create_token import \
Command as AuthTokenCommand
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from rest_framework.authtoken.serializers import AuthTokenSerializer from rest_framework.authtoken.serializers import AuthTokenSerializer
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
@ -33,3 +35,36 @@ class AuthTokenTests(TestCase):
self.user.set_password(data['password']) self.user.set_password(data['password'])
self.user.save() self.user.save()
assert AuthTokenSerializer(data=data).is_valid() assert AuthTokenSerializer(data=data).is_valid()
class AuthTokenCommandTests(TestCase):
def setUp(self):
self.site = site
self.user = User.objects.create_user(username='test_user')
def test_command_create_user_token(self):
token = AuthTokenCommand().create_user_token(self.user.username, False)
assert token is not None
token_saved = Token.objects.first()
assert token.key == token_saved.key
def test_command_create_user_token_invalid_user(self):
with pytest.raises(User.DoesNotExist):
AuthTokenCommand().create_user_token('not_existing_user', False)
def test_command_reset_user_token(self):
AuthTokenCommand().create_user_token(self.user.username, False)
first_token_key = Token.objects.first().key
AuthTokenCommand().create_user_token(self.user.username, True)
second_token_key = Token.objects.first().key
assert first_token_key != second_token_key
def test_command_do_not_reset_user_token(self):
AuthTokenCommand().create_user_token(self.user.username, False)
first_token_key = Token.objects.first().key
AuthTokenCommand().create_user_token(self.user.username, False)
second_token_key = Token.objects.first().key
assert first_token_key == second_token_key

View File

@ -5,6 +5,7 @@ import unittest
import uuid import uuid
from decimal import Decimal from decimal import Decimal
import django
import pytest import pytest
from django.http import QueryDict from django.http import QueryDict
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
@ -704,6 +705,17 @@ class TestSlugField(FieldValues):
outputs = {} outputs = {}
field = serializers.SlugField() field = serializers.SlugField()
def test_allow_unicode_true(self):
field = serializers.SlugField(allow_unicode=True)
validation_error = False
try:
field.run_validation(u'slug-99-\u0420')
except serializers.ValidationError:
validation_error = True
assert not validation_error
class TestURLField(FieldValues): class TestURLField(FieldValues):
""" """
@ -1135,12 +1147,11 @@ class TestDateTimeField(FieldValues):
'2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc), '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc), datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc), datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc),
# Django 1.4 does not support timezone string parsing.
'2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=utc)
} }
invalid_inputs = { invalid_inputs = {
'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'], 'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],
'2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'], '2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],
'2018-08-16 22:00-24:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],
datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'], datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
} }
outputs = { outputs = {
@ -1154,6 +1165,11 @@ class TestDateTimeField(FieldValues):
field = serializers.DateTimeField(default_timezone=utc) field = serializers.DateTimeField(default_timezone=utc)
if django.VERSION[:2] <= (1, 8):
# Doesn't raise an error on earlier versions of Django
TestDateTimeField.invalid_inputs.pop('2018-08-16 22:00-24:00')
class TestCustomInputFormatDateTimeField(FieldValues): class TestCustomInputFormatDateTimeField(FieldValues):
""" """
Valid and invalid values for `DateTimeField` with a custom input format. Valid and invalid values for `DateTimeField` with a custom input format.

View File

@ -5,6 +5,7 @@ import unittest
import warnings import warnings
from decimal import Decimal from decimal import Decimal
import django
import pytest import pytest
from django.conf.urls import url from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
@ -645,6 +646,51 @@ class SearchFilterM2MTests(TestCase):
) )
class Blog(models.Model):
name = models.CharField(max_length=20)
class Entry(models.Model):
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
headline = models.CharField(max_length=120)
pub_date = models.DateField(null=True)
class BlogSerializer(serializers.ModelSerializer):
class Meta:
model = Blog
fields = '__all__'
class SearchFilterToManyTests(TestCase):
@classmethod
def setUpTestData(cls):
b1 = Blog.objects.create(name='Blog 1')
b2 = Blog.objects.create(name='Blog 2')
# Multiple entries on Lennon published in 1979 - distinct should deduplicate
Entry.objects.create(blog=b1, headline='Something about Lennon', pub_date=datetime.date(1979, 1, 1))
Entry.objects.create(blog=b1, headline='Another thing about Lennon', pub_date=datetime.date(1979, 6, 1))
# Entry on Lennon *and* a separate entry in 1979 - should not match
Entry.objects.create(blog=b2, headline='Something unrelated', pub_date=datetime.date(1979, 1, 1))
Entry.objects.create(blog=b2, headline='Retrospective on Lennon', pub_date=datetime.date(1990, 6, 1))
@unittest.skipIf(django.VERSION < (1, 9), "Django 1.8 does not support transforms")
def test_multiple_filter_conditions(self):
class SearchListView(generics.ListAPIView):
queryset = Blog.objects.all()
serializer_class = BlogSerializer
filter_backends = (filters.SearchFilter,)
search_fields = ('=name', 'entry__headline', '=entry__pub_date__year')
view = SearchListView.as_view()
request = factory.get('/', {'search': 'Lennon,1979'})
response = view(request)
assert len(response.data) == 1
class OrderingFilterModel(models.Model): class OrderingFilterModel(models.Model):
title = models.CharField(max_length=20, verbose_name='verbose title') title = models.CharField(max_length=20, verbose_name='verbose title')
text = models.CharField(max_length=100) text = models.CharField(max_length=100)

View File

@ -246,6 +246,7 @@ class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [ViewObjectPermissions] permission_classes = [ViewObjectPermissions]
object_permissions_view = ObjectPermissionInstanceView.as_view() object_permissions_view = ObjectPermissionInstanceView.as_view()
@ -255,6 +256,7 @@ class ObjectPermissionListView(generics.ListAPIView):
authentication_classes = [authentication.BasicAuthentication] authentication_classes = [authentication.BasicAuthentication]
permission_classes = [ViewObjectPermissions] permission_classes = [ViewObjectPermissions]
object_permissions_list_view = ObjectPermissionListView.as_view() object_permissions_list_view = ObjectPermissionListView.as_view()
@ -443,6 +445,7 @@ class DeniedObjectView(PermissionInstanceView):
class DeniedObjectViewWithDetail(PermissionInstanceView): class DeniedObjectViewWithDetail(PermissionInstanceView):
permission_classes = (BasicObjectPermWithDetail,) permission_classes = (BasicObjectPermWithDetail,)
denied_view = DeniedView.as_view() denied_view = DeniedView.as_view()
denied_view_with_detail = DeniedViewWithDetail.as_view() denied_view_with_detail = DeniedViewWithDetail.as_view()

View File

@ -108,6 +108,7 @@ class HTMLView1(APIView):
def get(self, request, **kwargs): def get(self, request, **kwargs):
return Response('text') return Response('text')
urlpatterns = [ urlpatterns = [
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])), url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])), url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),

View File

@ -3,6 +3,9 @@ Tests for content parsing, and form-overloaded content parsing.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import os.path
import tempfile
from django.conf.urls import url from django.conf.urls import url
from django.contrib.auth import authenticate, login, logout from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -120,11 +123,39 @@ class MockView(APIView):
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class FileUploadView(APIView):
def post(self, request):
filenames = [file.temporary_file_path() for file in request.FILES.values()]
for filename in filenames:
assert os.path.exists(filename)
return Response(status=status.HTTP_200_OK, data=filenames)
urlpatterns = [ urlpatterns = [
url(r'^$', MockView.as_view()), url(r'^$', MockView.as_view()),
url(r'^upload/$', FileUploadView.as_view())
] ]
@override_settings(
ROOT_URLCONF='tests.test_request',
FILE_UPLOAD_HANDLERS=['django.core.files.uploadhandler.TemporaryFileUploadHandler'])
class FileUploadTests(TestCase):
def test_fileuploads_closed_at_request_end(self):
with tempfile.NamedTemporaryFile() as f:
response = self.client.post('/upload/', {'file': f})
# sanity check that file was processed
assert len(response.data) == 1
for file in response.data:
assert not os.path.exists(file)
@override_settings(ROOT_URLCONF='tests.test_request') @override_settings(ROOT_URLCONF='tests.test_request')
class TestContentParsingWithAuthentication(TestCase): class TestContentParsingWithAuthentication(TestCase):
def setUp(self): def setUp(self):

View File

@ -32,6 +32,7 @@ class MockJsonRenderer(BaseRenderer):
class MockTextMediaRenderer(BaseRenderer): class MockTextMediaRenderer(BaseRenderer):
media_type = 'text/html' media_type = 'text/html'
DUMMYSTATUS = status.HTTP_200_OK DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent' DUMMYCONTENT = 'dummycontent'

View File

@ -13,6 +13,7 @@ factory = APIRequestFactory()
def null_view(request): def null_view(request):
pass pass
urlpatterns = [ urlpatterns = [
url(r'^view$', null_view, name='view'), url(r'^view$', null_view, name='view'),
] ]

View File

@ -70,6 +70,7 @@ class ExampleViewSet(ModelViewSet):
assert self.action assert self.action
return super(ExampleViewSet, self).get_serializer(*args, **kwargs) return super(ExampleViewSet, self).get_serializer(*args, **kwargs)
if coreapi: if coreapi:
schema_view = get_schema_view(title='Example API') schema_view = get_schema_view(title='Example API')
else: else:

View File

@ -243,6 +243,7 @@ class RegexSerializer(serializers.Serializer):
validators=[RegexValidator(regex=re.compile('^[0-9]{4,6}$'), validators=[RegexValidator(regex=re.compile('^[0-9]{4,6}$'),
message='A PIN is 4-6 digits')]) message='A PIN is 4-6 digits')])
expected_repr = """ expected_repr = """
RegexSerializer(): RegexSerializer():
pin = CharField(validators=[<django.core.validators.RegexValidator object>]) pin = CharField(validators=[<django.core.validators.RegexValidator object>])

View File

@ -13,6 +13,15 @@ class BasicViewSet(GenericViewSet):
return Response({'ACTION': 'LIST'}) return Response({'ACTION': 'LIST'})
class InstanceViewSet(GenericViewSet):
def dispatch(self, request, *args, **kwargs):
return self.dummy(request, *args, **kwargs)
def dummy(self, request, *args, **kwargs):
return Response({'view': self})
class InitializeViewSetsTestCase(TestCase): class InitializeViewSetsTestCase(TestCase):
def test_initialize_view_set_with_actions(self): def test_initialize_view_set_with_actions(self):
request = factory.get('/', '', content_type='application/json') request = factory.get('/', '', content_type='application/json')
@ -42,3 +51,17 @@ class InitializeViewSetsTestCase(TestCase):
"For example `.as_view({'get': 'list'})`") "For example `.as_view({'get': 'list'})`")
else: else:
self.fail("actions must not be empty.") self.fail("actions must not be empty.")
def test_args_kwargs_request_action_map_on_self(self):
"""
Test a view only has args, kwargs, request, action_map
once `as_view` has been called.
"""
bare_view = InstanceViewSet()
view = InstanceViewSet.as_view(actions={
'get': 'dummy',
})(factory.get('/')).data['view']
for attribute in ('args', 'kwargs', 'request', 'action_map'):
self.assertNotIn(attribute, dir(bare_view))
self.assertIn(attribute, dir(view))