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 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
@ -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#
[blimp]: https://github.com/GetBlimp
[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/
[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

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.
## 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
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.
## 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/
[github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/
[link-header]: ../img/link-header-pagination.png
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
[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

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.
* [doac][doac] - Provides OAuth 2.0 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.
* [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.
@ -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
[doac]: https://github.com/Rediker-Software/doac
[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
[djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature
[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):
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)
slug_regex = re.compile(r'^[-a-zA-Z0-9_]+$')
validator = RegexValidator(slug_regex, message=self.error_messages['invalid'])
self.allow_unicode = allow_unicode
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)
@ -1133,18 +1137,16 @@ class DateTimeField(Field):
if input_format.lower() == ISO_8601:
try:
parsed = parse_datetime(value)
except (ValueError, TypeError):
pass
else:
if parsed is not None:
return self.enforce_timezone(parsed)
except (ValueError, TypeError):
pass
else:
try:
parsed = self.datetime_parser(value, input_format)
return self.enforce_timezone(parsed)
except (ValueError, TypeError):
pass
else:
return self.enforce_timezone(parsed)
humanized_format = humanize_datetime.datetime_formats(input_formats)
self.fail('invalid', format=humanized_format)

View File

@ -140,12 +140,14 @@ class SearchFilter(BaseFilterBackend):
]
base = queryset
conditions = []
for search_term in search_terms:
queries = [
models.Q(**{orm_lookup: search_term})
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):
# 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):
raise ValueError()
if cutoff:
ret = min(ret, cutoff)
return min(ret, cutoff)
return ret
@ -95,7 +95,7 @@ def _get_displayed_page_numbers(current, final):
# Now sort the page numbers and drop anything outside the limits.
included = [
idx for idx in sorted(list(included))
if idx > 0 and idx <= final
if 0 < idx <= final
]
# Finally insert any `...` breaks

View File

@ -556,7 +556,10 @@ class BrowsableAPIRenderer(BaseRenderer):
accepted = self.accepted_media_type
context = self.renderer_context.copy()
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:
content = None

View File

@ -250,6 +250,10 @@ class Request(object):
else:
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):
"""
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):
return code >= 100 and code <= 199
return 100 <= code <= 199
def is_success(code):
return code >= 200 and code <= 299
return 200 <= code <= 299
def is_redirect(code):
return code >= 300 and code <= 399
return 300 <= code <= 399
def is_client_error(code):
return code >= 400 and code <= 499
return 400 <= code <= 499
def is_server_error(code):
return code >= 500 and code <= 599
return 500 <= code <= 599
HTTP_100_CONTINUE = 100

View File

@ -13,7 +13,7 @@
{% if 'javascript' in langs %}{% include "rest_framework/docs/langs/javascript-intro.html" %}{% endif %}
</div>
</div>
{% if document.data %}
{% for section_key, section in document.data|items %}
{% 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>
@ -28,3 +28,4 @@
{% for link_key, link in document.links|items %}
{% include "rest_framework/docs/link.html" %}
{% 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>
<div class="menu-list">
<ul id="menu-content" class="menu-content collapse out">
{% if document.data %}
{% for section_key, section in document.data|items %}
<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>
</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 %}
<li><a href="#{{ section_key }}-{{ link_key }}">{{ link.title|default:link_key }}</a></li>
{% endfor %}
</ul>
</ul>
</li>
{% endfor %}
{% endif %}
</ul>
<ul class="menu-list menu-list-bottom">

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,8 @@ from django.contrib.auth.models import User
from django.test import TestCase
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.serializers import AuthTokenSerializer
from rest_framework.exceptions import ValidationError
@ -33,3 +35,36 @@ class AuthTokenTests(TestCase):
self.user.set_password(data['password'])
self.user.save()
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
from decimal import Decimal
import django
import pytest
from django.http import QueryDict
from django.test import TestCase, override_settings
@ -704,6 +705,17 @@ class TestSlugField(FieldValues):
outputs = {}
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):
"""
@ -1135,12 +1147,11 @@ class TestDateTimeField(FieldValues):
'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, 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 = {
'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].'],
'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.'],
}
outputs = {
@ -1154,6 +1165,11 @@ class TestDateTimeField(FieldValues):
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):
"""
Valid and invalid values for `DateTimeField` with a custom input format.

View File

@ -5,6 +5,7 @@ import unittest
import warnings
from decimal import Decimal
import django
import pytest
from django.conf.urls import url
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):
title = models.CharField(max_length=20, verbose_name='verbose title')
text = models.CharField(max_length=100)

View File

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

View File

@ -108,6 +108,7 @@ class HTMLView1(APIView):
def get(self, request, **kwargs):
return Response('text')
urlpatterns = [
url(r'^.*\.(?P<format>.+)$', 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
import os.path
import tempfile
from django.conf.urls import url
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User
@ -120,11 +123,39 @@ class MockView(APIView):
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 = [
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')
class TestContentParsingWithAuthentication(TestCase):
def setUp(self):

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,15 @@ class BasicViewSet(GenericViewSet):
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):
def test_initialize_view_set_with_actions(self):
request = factory.get('/', '', content_type='application/json')
@ -42,3 +51,17 @@ class InitializeViewSetsTestCase(TestCase):
"For example `.as_view({'get': 'list'})`")
else:
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))