Merge branch 'master' into localizedfloatfield

This commit is contained in:
kgeorgy 2017-07-11 12:34:40 +02:00 committed by GitHub
commit 5094299826
24 changed files with 208 additions and 30 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

@ -329,7 +329,7 @@ For further enquires please contact <a href=mailto:funding@django-rest-framework
## Accountability ## Accountability
In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](http://www.encode.io/reports/may-2017) and regularly include financial reports and cost breakdowns. In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](http://www.encode.io/reports/june-2017) and regularly include financial reports and cost breakdowns.
<!-- Begin MailChimp Signup Form --> <!-- Begin MailChimp Signup Form -->
<link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css"> <link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css">

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

@ -793,13 +793,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)
@ -1148,18 +1152,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

@ -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

@ -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

@ -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):
""" """
@ -1165,12 +1177,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 = {
@ -1184,6 +1195,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

@ -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))