mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 17:47:04 +03:00
7261ae653a
Closes #5528. Viewset custom actions (@detail_route etc) OPTIONS (and HEAD) methods were not being excluded from Schema Generations. This PR adds a test reproducing the reported error and adjusts `EndpointEnumerator.get_allowed_methods()` to filter ViewSet actions in the same way as other `APIView`s
954 lines
34 KiB
Python
954 lines
34 KiB
Python
import unittest
|
|
|
|
import pytest
|
|
from django.conf.urls import include, url
|
|
from django.core.exceptions import PermissionDenied
|
|
from django.http import Http404
|
|
from django.test import TestCase, override_settings
|
|
|
|
from rest_framework import (
|
|
filters, generics, pagination, permissions, serializers
|
|
)
|
|
from rest_framework.compat import coreapi, coreschema, get_regex_pattern
|
|
from rest_framework.decorators import (
|
|
api_view, detail_route, list_route, schema
|
|
)
|
|
from rest_framework.request import Request
|
|
from rest_framework.routers import DefaultRouter, SimpleRouter
|
|
from rest_framework.schemas import (
|
|
AutoSchema, ManualSchema, SchemaGenerator, get_schema_view
|
|
)
|
|
from rest_framework.schemas.generators import EndpointEnumerator
|
|
from rest_framework.schemas.utils import is_list_view
|
|
from rest_framework.test import APIClient, APIRequestFactory
|
|
from rest_framework.utils import formatting
|
|
from rest_framework.views import APIView
|
|
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
|
|
|
from .models import BasicModel
|
|
|
|
factory = APIRequestFactory()
|
|
|
|
|
|
class MockUser(object):
|
|
def is_authenticated(self):
|
|
return True
|
|
|
|
|
|
class ExamplePagination(pagination.PageNumberPagination):
|
|
page_size = 100
|
|
page_size_query_param = 'page_size'
|
|
|
|
|
|
class EmptySerializer(serializers.Serializer):
|
|
pass
|
|
|
|
|
|
class ExampleSerializer(serializers.Serializer):
|
|
a = serializers.CharField(required=True, help_text='A field description')
|
|
b = serializers.CharField(required=False)
|
|
read_only = serializers.CharField(read_only=True)
|
|
hidden = serializers.HiddenField(default='hello')
|
|
|
|
|
|
class AnotherSerializerWithListFields(serializers.Serializer):
|
|
a = serializers.ListField(child=serializers.IntegerField())
|
|
b = serializers.ListSerializer(child=serializers.CharField())
|
|
|
|
|
|
class AnotherSerializer(serializers.Serializer):
|
|
c = serializers.CharField(required=True)
|
|
d = serializers.CharField(required=False)
|
|
|
|
|
|
class ExampleViewSet(ModelViewSet):
|
|
pagination_class = ExamplePagination
|
|
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
|
filter_backends = [filters.OrderingFilter]
|
|
serializer_class = ExampleSerializer
|
|
|
|
@detail_route(methods=['post'], serializer_class=AnotherSerializer)
|
|
def custom_action(self, request, pk):
|
|
"""
|
|
A description of custom action.
|
|
"""
|
|
return super(ExampleSerializer, self).retrieve(self, request)
|
|
|
|
@detail_route(methods=['post'], serializer_class=AnotherSerializerWithListFields)
|
|
def custom_action_with_list_fields(self, request, pk):
|
|
"""
|
|
A custom action using both list field and list serializer in the serializer.
|
|
"""
|
|
return super(ExampleSerializer, self).retrieve(self, request)
|
|
|
|
@list_route()
|
|
def custom_list_action(self, request):
|
|
return super(ExampleViewSet, self).list(self, request)
|
|
|
|
@list_route(methods=['post', 'get'], serializer_class=EmptySerializer)
|
|
def custom_list_action_multiple_methods(self, request):
|
|
return super(ExampleViewSet, self).list(self, request)
|
|
|
|
def get_serializer(self, *args, **kwargs):
|
|
assert self.request
|
|
assert self.action
|
|
return super(ExampleViewSet, self).get_serializer(*args, **kwargs)
|
|
|
|
|
|
if coreapi:
|
|
schema_view = get_schema_view(title='Example API')
|
|
else:
|
|
def schema_view(request):
|
|
pass
|
|
|
|
router = DefaultRouter()
|
|
router.register('example', ExampleViewSet, base_name='example')
|
|
urlpatterns = [
|
|
url(r'^$', schema_view),
|
|
url(r'^', include(router.urls))
|
|
]
|
|
|
|
|
|
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
|
@override_settings(ROOT_URLCONF='tests.test_schemas')
|
|
class TestRouterGeneratedSchema(TestCase):
|
|
def test_anonymous_request(self):
|
|
client = APIClient()
|
|
response = client.get('/', HTTP_ACCEPT='application/coreapi+json')
|
|
assert response.status_code == 200
|
|
expected = coreapi.Document(
|
|
url='http://testserver/',
|
|
title='Example API',
|
|
content={
|
|
'example': {
|
|
'list': coreapi.Link(
|
|
url='/example/',
|
|
action='get',
|
|
fields=[
|
|
coreapi.Field('page', required=False, location='query', schema=coreschema.Integer(title='Page', description='A page number within the paginated result set.')),
|
|
coreapi.Field('page_size', required=False, location='query', schema=coreschema.Integer(title='Page size', description='Number of results to return per page.')),
|
|
coreapi.Field('ordering', required=False, location='query', schema=coreschema.String(title='Ordering', description='Which field to use when ordering the results.'))
|
|
]
|
|
),
|
|
'custom_list_action': coreapi.Link(
|
|
url='/example/custom_list_action/',
|
|
action='get'
|
|
),
|
|
'custom_list_action_multiple_methods': {
|
|
'read': coreapi.Link(
|
|
url='/example/custom_list_action_multiple_methods/',
|
|
action='get'
|
|
)
|
|
},
|
|
'read': coreapi.Link(
|
|
url='/example/{id}/',
|
|
action='get',
|
|
fields=[
|
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String()),
|
|
coreapi.Field('ordering', required=False, location='query', schema=coreschema.String(title='Ordering', description='Which field to use when ordering the results.'))
|
|
]
|
|
)
|
|
}
|
|
}
|
|
)
|
|
assert response.data == expected
|
|
|
|
def test_authenticated_request(self):
|
|
client = APIClient()
|
|
client.force_authenticate(MockUser())
|
|
response = client.get('/', HTTP_ACCEPT='application/coreapi+json')
|
|
assert response.status_code == 200
|
|
expected = coreapi.Document(
|
|
url='http://testserver/',
|
|
title='Example API',
|
|
content={
|
|
'example': {
|
|
'list': coreapi.Link(
|
|
url='/example/',
|
|
action='get',
|
|
fields=[
|
|
coreapi.Field('page', required=False, location='query', schema=coreschema.Integer(title='Page', description='A page number within the paginated result set.')),
|
|
coreapi.Field('page_size', required=False, location='query', schema=coreschema.Integer(title='Page size', description='Number of results to return per page.')),
|
|
coreapi.Field('ordering', required=False, location='query', schema=coreschema.String(title='Ordering', description='Which field to use when ordering the results.'))
|
|
]
|
|
),
|
|
'create': coreapi.Link(
|
|
url='/example/',
|
|
action='post',
|
|
encoding='application/json',
|
|
fields=[
|
|
coreapi.Field('a', required=True, location='form', schema=coreschema.String(title='A', description='A field description')),
|
|
coreapi.Field('b', required=False, location='form', schema=coreschema.String(title='B'))
|
|
]
|
|
),
|
|
'read': coreapi.Link(
|
|
url='/example/{id}/',
|
|
action='get',
|
|
fields=[
|
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String()),
|
|
coreapi.Field('ordering', required=False, location='query', schema=coreschema.String(title='Ordering', description='Which field to use when ordering the results.'))
|
|
]
|
|
),
|
|
'custom_action': coreapi.Link(
|
|
url='/example/{id}/custom_action/',
|
|
action='post',
|
|
encoding='application/json',
|
|
description='A description of custom action.',
|
|
fields=[
|
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String()),
|
|
coreapi.Field('c', required=True, location='form', schema=coreschema.String(title='C')),
|
|
coreapi.Field('d', required=False, location='form', schema=coreschema.String(title='D')),
|
|
]
|
|
),
|
|
'custom_action_with_list_fields': coreapi.Link(
|
|
url='/example/{id}/custom_action_with_list_fields/',
|
|
action='post',
|
|
encoding='application/json',
|
|
description='A custom action using both list field and list serializer in the serializer.',
|
|
fields=[
|
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String()),
|
|
coreapi.Field('a', required=True, location='form', schema=coreschema.Array(title='A', items=coreschema.Integer())),
|
|
coreapi.Field('b', required=True, location='form', schema=coreschema.Array(title='B', items=coreschema.String())),
|
|
]
|
|
),
|
|
'custom_list_action': coreapi.Link(
|
|
url='/example/custom_list_action/',
|
|
action='get'
|
|
),
|
|
'custom_list_action_multiple_methods': {
|
|
'read': coreapi.Link(
|
|
url='/example/custom_list_action_multiple_methods/',
|
|
action='get'
|
|
),
|
|
'create': coreapi.Link(
|
|
url='/example/custom_list_action_multiple_methods/',
|
|
action='post'
|
|
)
|
|
},
|
|
'update': coreapi.Link(
|
|
url='/example/{id}/',
|
|
action='put',
|
|
encoding='application/json',
|
|
fields=[
|
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String()),
|
|
coreapi.Field('a', required=True, location='form', schema=coreschema.String(title='A', description=('A field description'))),
|
|
coreapi.Field('b', required=False, location='form', schema=coreschema.String(title='B')),
|
|
coreapi.Field('ordering', required=False, location='query', schema=coreschema.String(title='Ordering', description='Which field to use when ordering the results.'))
|
|
]
|
|
),
|
|
'partial_update': coreapi.Link(
|
|
url='/example/{id}/',
|
|
action='patch',
|
|
encoding='application/json',
|
|
fields=[
|
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String()),
|
|
coreapi.Field('a', required=False, location='form', schema=coreschema.String(title='A', description='A field description')),
|
|
coreapi.Field('b', required=False, location='form', schema=coreschema.String(title='B')),
|
|
coreapi.Field('ordering', required=False, location='query', schema=coreschema.String(title='Ordering', description='Which field to use when ordering the results.'))
|
|
]
|
|
),
|
|
'delete': coreapi.Link(
|
|
url='/example/{id}/',
|
|
action='delete',
|
|
fields=[
|
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String()),
|
|
coreapi.Field('ordering', required=False, location='query', schema=coreschema.String(title='Ordering', description='Which field to use when ordering the results.'))
|
|
]
|
|
)
|
|
}
|
|
}
|
|
)
|
|
assert response.data == expected
|
|
|
|
|
|
class DenyAllUsingHttp404(permissions.BasePermission):
|
|
|
|
def has_permission(self, request, view):
|
|
raise Http404()
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
raise Http404()
|
|
|
|
|
|
class DenyAllUsingPermissionDenied(permissions.BasePermission):
|
|
|
|
def has_permission(self, request, view):
|
|
raise PermissionDenied()
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
raise PermissionDenied()
|
|
|
|
|
|
class Http404ExampleViewSet(ExampleViewSet):
|
|
permission_classes = [DenyAllUsingHttp404]
|
|
|
|
|
|
class PermissionDeniedExampleViewSet(ExampleViewSet):
|
|
permission_classes = [DenyAllUsingPermissionDenied]
|
|
|
|
|
|
class MethodLimitedViewSet(ExampleViewSet):
|
|
permission_classes = []
|
|
http_method_names = ['get', 'head', 'options']
|
|
|
|
|
|
class ExampleListView(APIView):
|
|
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
|
|
|
def get(self, *args, **kwargs):
|
|
pass
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
pass
|
|
|
|
|
|
class ExampleDetailView(APIView):
|
|
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
|
|
|
def get(self, *args, **kwargs):
|
|
pass
|
|
|
|
|
|
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
|
class TestSchemaGenerator(TestCase):
|
|
def setUp(self):
|
|
self.patterns = [
|
|
url('^example/?$', ExampleListView.as_view()),
|
|
url('^example/(?P<pk>\d+)/?$', ExampleDetailView.as_view()),
|
|
url('^example/(?P<pk>\d+)/sub/?$', ExampleDetailView.as_view()),
|
|
]
|
|
|
|
def test_schema_for_regular_views(self):
|
|
"""
|
|
Ensure that schema generation works for APIView classes.
|
|
"""
|
|
generator = SchemaGenerator(title='Example API', patterns=self.patterns)
|
|
schema = generator.get_schema()
|
|
expected = coreapi.Document(
|
|
url='',
|
|
title='Example API',
|
|
content={
|
|
'example': {
|
|
'create': coreapi.Link(
|
|
url='/example/',
|
|
action='post',
|
|
fields=[]
|
|
),
|
|
'list': coreapi.Link(
|
|
url='/example/',
|
|
action='get',
|
|
fields=[]
|
|
),
|
|
'read': coreapi.Link(
|
|
url='/example/{id}/',
|
|
action='get',
|
|
fields=[
|
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
|
]
|
|
),
|
|
'sub': {
|
|
'list': coreapi.Link(
|
|
url='/example/{id}/sub/',
|
|
action='get',
|
|
fields=[
|
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
|
]
|
|
)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
assert schema == expected
|
|
|
|
|
|
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
|
class TestSchemaGeneratorNotAtRoot(TestCase):
|
|
def setUp(self):
|
|
self.patterns = [
|
|
url('^api/v1/example/?$', ExampleListView.as_view()),
|
|
url('^api/v1/example/(?P<pk>\d+)/?$', ExampleDetailView.as_view()),
|
|
url('^api/v1/example/(?P<pk>\d+)/sub/?$', ExampleDetailView.as_view()),
|
|
]
|
|
|
|
def test_schema_for_regular_views(self):
|
|
"""
|
|
Ensure that schema generation with an API that is not at the URL
|
|
root continues to use correct structure for link keys.
|
|
"""
|
|
generator = SchemaGenerator(title='Example API', patterns=self.patterns)
|
|
schema = generator.get_schema()
|
|
expected = coreapi.Document(
|
|
url='',
|
|
title='Example API',
|
|
content={
|
|
'example': {
|
|
'create': coreapi.Link(
|
|
url='/api/v1/example/',
|
|
action='post',
|
|
fields=[]
|
|
),
|
|
'list': coreapi.Link(
|
|
url='/api/v1/example/',
|
|
action='get',
|
|
fields=[]
|
|
),
|
|
'read': coreapi.Link(
|
|
url='/api/v1/example/{id}/',
|
|
action='get',
|
|
fields=[
|
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
|
]
|
|
),
|
|
'sub': {
|
|
'list': coreapi.Link(
|
|
url='/api/v1/example/{id}/sub/',
|
|
action='get',
|
|
fields=[
|
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String())
|
|
]
|
|
)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
assert schema == expected
|
|
|
|
|
|
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
|
class TestSchemaGeneratorWithMethodLimitedViewSets(TestCase):
|
|
def setUp(self):
|
|
router = DefaultRouter()
|
|
router.register('example1', MethodLimitedViewSet, base_name='example1')
|
|
self.patterns = [
|
|
url(r'^', include(router.urls))
|
|
]
|
|
|
|
def test_schema_for_regular_views(self):
|
|
"""
|
|
Ensure that schema generation works for ViewSet classes
|
|
with method limitation by Django CBV's http_method_names attribute
|
|
"""
|
|
generator = SchemaGenerator(title='Example API', patterns=self.patterns)
|
|
request = factory.get('/example1/')
|
|
schema = generator.get_schema(Request(request))
|
|
|
|
expected = coreapi.Document(
|
|
url='http://testserver/example1/',
|
|
title='Example API',
|
|
content={
|
|
'example1': {
|
|
'list': coreapi.Link(
|
|
url='/example1/',
|
|
action='get',
|
|
fields=[
|
|
coreapi.Field('page', required=False, location='query', schema=coreschema.Integer(title='Page', description='A page number within the paginated result set.')),
|
|
coreapi.Field('page_size', required=False, location='query', schema=coreschema.Integer(title='Page size', description='Number of results to return per page.')),
|
|
coreapi.Field('ordering', required=False, location='query', schema=coreschema.String(title='Ordering', description='Which field to use when ordering the results.'))
|
|
]
|
|
),
|
|
'custom_list_action': coreapi.Link(
|
|
url='/example1/custom_list_action/',
|
|
action='get'
|
|
),
|
|
'custom_list_action_multiple_methods': {
|
|
'read': coreapi.Link(
|
|
url='/example1/custom_list_action_multiple_methods/',
|
|
action='get'
|
|
)
|
|
},
|
|
'read': coreapi.Link(
|
|
url='/example1/{id}/',
|
|
action='get',
|
|
fields=[
|
|
coreapi.Field('id', required=True, location='path', schema=coreschema.String()),
|
|
coreapi.Field('ordering', required=False, location='query', schema=coreschema.String(title='Ordering', description='Which field to use when ordering the results.'))
|
|
]
|
|
)
|
|
}
|
|
}
|
|
)
|
|
assert schema == expected
|
|
|
|
|
|
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
|
class TestSchemaGeneratorWithRestrictedViewSets(TestCase):
|
|
def setUp(self):
|
|
router = DefaultRouter()
|
|
router.register('example1', Http404ExampleViewSet, base_name='example1')
|
|
router.register('example2', PermissionDeniedExampleViewSet, base_name='example2')
|
|
self.patterns = [
|
|
url('^example/?$', ExampleListView.as_view()),
|
|
url(r'^', include(router.urls))
|
|
]
|
|
|
|
def test_schema_for_regular_views(self):
|
|
"""
|
|
Ensure that schema generation works for ViewSet classes
|
|
with permission classes raising exceptions.
|
|
"""
|
|
generator = SchemaGenerator(title='Example API', patterns=self.patterns)
|
|
request = factory.get('/')
|
|
schema = generator.get_schema(Request(request))
|
|
expected = coreapi.Document(
|
|
url='http://testserver/',
|
|
title='Example API',
|
|
content={
|
|
'example': {
|
|
'list': coreapi.Link(
|
|
url='/example/',
|
|
action='get',
|
|
fields=[]
|
|
),
|
|
},
|
|
}
|
|
)
|
|
assert schema == expected
|
|
|
|
|
|
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
|
class Test4605Regression(TestCase):
|
|
def test_4605_regression(self):
|
|
generator = SchemaGenerator()
|
|
prefix = generator.determine_path_prefix([
|
|
'/api/v1/items/',
|
|
'/auth/convert-token/'
|
|
])
|
|
assert prefix == '/'
|
|
|
|
|
|
class TestDescriptor(TestCase):
|
|
|
|
def test_apiview_schema_descriptor(self):
|
|
view = APIView()
|
|
assert hasattr(view, 'schema')
|
|
assert isinstance(view.schema, AutoSchema)
|
|
|
|
def test_get_link_requires_instance(self):
|
|
descriptor = APIView.schema # Accessed from class
|
|
with pytest.raises(AssertionError):
|
|
descriptor.get_link(None, None, None) # ???: Do the dummy arguments require a tighter assert?
|
|
|
|
def test_manual_fields(self):
|
|
|
|
class CustomView(APIView):
|
|
schema = AutoSchema(manual_fields=[
|
|
coreapi.Field(
|
|
"my_extra_field",
|
|
required=True,
|
|
location="path",
|
|
schema=coreschema.String()
|
|
),
|
|
])
|
|
|
|
view = CustomView()
|
|
link = view.schema.get_link('/a/url/{id}/', 'GET', '')
|
|
fields = link.fields
|
|
|
|
assert len(fields) == 2
|
|
assert "my_extra_field" in [f.name for f in fields]
|
|
|
|
def test_view_with_manual_schema(self):
|
|
|
|
path = '/example'
|
|
method = 'get'
|
|
base_url = None
|
|
|
|
fields = [
|
|
coreapi.Field(
|
|
"first_field",
|
|
required=True,
|
|
location="path",
|
|
schema=coreschema.String()
|
|
),
|
|
coreapi.Field(
|
|
"second_field",
|
|
required=True,
|
|
location="path",
|
|
schema=coreschema.String()
|
|
),
|
|
coreapi.Field(
|
|
"third_field",
|
|
required=True,
|
|
location="path",
|
|
schema=coreschema.String()
|
|
),
|
|
]
|
|
description = "A test endpoint"
|
|
|
|
class CustomView(APIView):
|
|
"""
|
|
ManualSchema takes list of fields for endpoint.
|
|
- Provides url and action, which are always dynamic
|
|
"""
|
|
schema = ManualSchema(fields, description)
|
|
|
|
expected = coreapi.Link(
|
|
url=path,
|
|
action=method,
|
|
fields=fields,
|
|
description=description
|
|
)
|
|
|
|
view = CustomView()
|
|
link = view.schema.get_link(path, method, base_url)
|
|
assert link == expected
|
|
|
|
|
|
def test_docstring_is_not_stripped_by_get_description():
|
|
class ExampleDocstringAPIView(APIView):
|
|
"""
|
|
=== title
|
|
|
|
* item a
|
|
* item a-a
|
|
* item a-b
|
|
* item b
|
|
|
|
- item 1
|
|
- item 2
|
|
|
|
code block begin
|
|
code
|
|
code
|
|
code
|
|
code block end
|
|
|
|
the end
|
|
"""
|
|
|
|
def get(self, *args, **kwargs):
|
|
pass
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
pass
|
|
|
|
view = ExampleDocstringAPIView()
|
|
schema = view.schema
|
|
descr = schema.get_description('example', 'get')
|
|
# the first and last character are '\n' correctly removed by get_description
|
|
assert descr == formatting.dedent(ExampleDocstringAPIView.__doc__[1:][:-1])
|
|
|
|
|
|
# Views for SchemaGenerationExclusionTests
|
|
class ExcludedAPIView(APIView):
|
|
schema = None
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
pass
|
|
|
|
|
|
@api_view(['GET'])
|
|
@schema(None)
|
|
def excluded_fbv(request):
|
|
pass
|
|
|
|
|
|
@api_view(['GET'])
|
|
def included_fbv(request):
|
|
pass
|
|
|
|
|
|
@unittest.skipUnless(coreapi, 'coreapi is not installed')
|
|
class SchemaGenerationExclusionTests(TestCase):
|
|
def setUp(self):
|
|
self.patterns = [
|
|
url('^excluded-cbv/$', ExcludedAPIView.as_view()),
|
|
url('^excluded-fbv/$', excluded_fbv),
|
|
url('^included-fbv/$', included_fbv),
|
|
]
|
|
|
|
def test_schema_generator_excludes_correctly(self):
|
|
"""Schema should not include excluded views"""
|
|
generator = SchemaGenerator(title='Exclusions', patterns=self.patterns)
|
|
schema = generator.get_schema()
|
|
expected = coreapi.Document(
|
|
url='',
|
|
title='Exclusions',
|
|
content={
|
|
'included-fbv': {
|
|
'list': coreapi.Link(url='/included-fbv/', action='get')
|
|
}
|
|
}
|
|
)
|
|
|
|
assert len(schema.data) == 1
|
|
assert 'included-fbv' in schema.data
|
|
assert schema == expected
|
|
|
|
def test_endpoint_enumerator_excludes_correctly(self):
|
|
"""It is responsibility of EndpointEnumerator to exclude views"""
|
|
inspector = EndpointEnumerator(self.patterns)
|
|
endpoints = inspector.get_api_endpoints()
|
|
|
|
assert len(endpoints) == 1
|
|
path, method, callback = endpoints[0]
|
|
assert path == '/included-fbv/'
|
|
|
|
def test_should_include_endpoint_excludes_correctly(self):
|
|
"""This is the specific method that should handle the exclusion"""
|
|
inspector = EndpointEnumerator(self.patterns)
|
|
|
|
# Not pretty. Mimics internals of EndpointEnumerator to put should_include_endpoint under test
|
|
pairs = [(inspector.get_path_from_regex(get_regex_pattern(pattern)), pattern.callback)
|
|
for pattern in self.patterns]
|
|
|
|
should_include = [
|
|
inspector.should_include_endpoint(*pair) for pair in pairs
|
|
]
|
|
|
|
expected = [False, False, True]
|
|
|
|
assert should_include == expected
|
|
|
|
def test_deprecations(self):
|
|
with pytest.warns(PendingDeprecationWarning) as record:
|
|
@api_view(["GET"], exclude_from_schema=True)
|
|
def view(request):
|
|
pass
|
|
|
|
assert len(record) == 1
|
|
assert str(record[0].message) == (
|
|
"The `exclude_from_schema` argument to `api_view` is pending "
|
|
"deprecation. Use the `schema` decorator instead, passing `None`."
|
|
)
|
|
|
|
class OldFashionedExcludedView(APIView):
|
|
exclude_from_schema = True
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
pass
|
|
|
|
patterns = [
|
|
url('^excluded-old-fashioned/$', OldFashionedExcludedView.as_view()),
|
|
]
|
|
|
|
inspector = EndpointEnumerator(patterns)
|
|
with pytest.warns(PendingDeprecationWarning) as record:
|
|
inspector.get_api_endpoints()
|
|
|
|
assert len(record) == 1
|
|
assert str(record[0].message) == (
|
|
"The `OldFashionedExcludedView.exclude_from_schema` attribute is "
|
|
"pending deprecation. Set `schema = None` instead."
|
|
)
|
|
|
|
|
|
@api_view(["GET"])
|
|
def simple_fbv(request):
|
|
pass
|
|
|
|
|
|
class BasicModelSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = BasicModel
|
|
fields = "__all__"
|
|
|
|
|
|
class NamingCollisionView(generics.RetrieveUpdateDestroyAPIView):
|
|
queryset = BasicModel.objects.all()
|
|
serializer_class = BasicModelSerializer
|
|
|
|
|
|
class BasicNamingCollisionView(generics.RetrieveAPIView):
|
|
queryset = BasicModel.objects.all()
|
|
|
|
|
|
class NamingCollisionViewSet(GenericViewSet):
|
|
"""
|
|
Example via: https://stackoverflow.com/questions/43778668/django-rest-framwork-occured-typeerror-link-object-does-not-support-item-ass/
|
|
"""
|
|
permision_class = ()
|
|
|
|
@list_route()
|
|
def detail(self, request):
|
|
return {}
|
|
|
|
@list_route(url_path='detail/export')
|
|
def detail_export(self, request):
|
|
return {}
|
|
|
|
|
|
naming_collisions_router = SimpleRouter()
|
|
naming_collisions_router.register(r'collision', NamingCollisionViewSet, base_name="collision")
|
|
|
|
|
|
class TestURLNamingCollisions(TestCase):
|
|
"""
|
|
Ref: https://github.com/encode/django-rest-framework/issues/4704
|
|
"""
|
|
def test_manually_routing_nested_routes(self):
|
|
patterns = [
|
|
url(r'^test', simple_fbv),
|
|
url(r'^test/list/', simple_fbv),
|
|
]
|
|
|
|
generator = SchemaGenerator(title='Naming Colisions', patterns=patterns)
|
|
schema = generator.get_schema()
|
|
|
|
expected = coreapi.Document(
|
|
url='',
|
|
title='Naming Colisions',
|
|
content={
|
|
'test': {
|
|
'list': {
|
|
'list': coreapi.Link(url='/test/list/', action='get')
|
|
},
|
|
'list_0': coreapi.Link(url='/test', action='get')
|
|
}
|
|
}
|
|
)
|
|
|
|
assert expected == schema
|
|
|
|
def _verify_cbv_links(self, loc, url, methods=None, suffixes=None):
|
|
if methods is None:
|
|
methods = ('read', 'update', 'partial_update', 'delete')
|
|
if suffixes is None:
|
|
suffixes = (None for m in methods)
|
|
|
|
for method, suffix in zip(methods, suffixes):
|
|
if suffix is not None:
|
|
key = '{}_{}'.format(method, suffix)
|
|
else:
|
|
key = method
|
|
assert loc[key].url == url
|
|
|
|
def test_manually_routing_generic_view(self):
|
|
patterns = [
|
|
url(r'^test', NamingCollisionView.as_view()),
|
|
url(r'^test/retrieve/', NamingCollisionView.as_view()),
|
|
url(r'^test/update/', NamingCollisionView.as_view()),
|
|
|
|
# Fails with method names:
|
|
url(r'^test/get/', NamingCollisionView.as_view()),
|
|
url(r'^test/put/', NamingCollisionView.as_view()),
|
|
url(r'^test/delete/', NamingCollisionView.as_view()),
|
|
]
|
|
|
|
generator = SchemaGenerator(title='Naming Colisions', patterns=patterns)
|
|
|
|
schema = generator.get_schema()
|
|
|
|
self._verify_cbv_links(schema['test']['delete'], '/test/delete/')
|
|
self._verify_cbv_links(schema['test']['put'], '/test/put/')
|
|
self._verify_cbv_links(schema['test']['get'], '/test/get/')
|
|
self._verify_cbv_links(schema['test']['update'], '/test/update/')
|
|
self._verify_cbv_links(schema['test']['retrieve'], '/test/retrieve/')
|
|
self._verify_cbv_links(schema['test'], '/test', suffixes=(None, '0', None, '0'))
|
|
|
|
def test_from_router(self):
|
|
patterns = [
|
|
url(r'from-router', include(naming_collisions_router.urls)),
|
|
]
|
|
|
|
generator = SchemaGenerator(title='Naming Colisions', patterns=patterns)
|
|
schema = generator.get_schema()
|
|
desc = schema['detail_0'].description # not important here
|
|
|
|
expected = coreapi.Document(
|
|
url='',
|
|
title='Naming Colisions',
|
|
content={
|
|
'detail': {
|
|
'detail_export': coreapi.Link(
|
|
url='/from-routercollision/detail/export/',
|
|
action='get',
|
|
description=desc)
|
|
},
|
|
'detail_0': coreapi.Link(
|
|
url='/from-routercollision/detail/',
|
|
action='get',
|
|
description=desc
|
|
)
|
|
}
|
|
)
|
|
|
|
assert schema == expected
|
|
|
|
def test_url_under_same_key_not_replaced(self):
|
|
patterns = [
|
|
url(r'example/(?P<pk>\d+)/$', BasicNamingCollisionView.as_view()),
|
|
url(r'example/(?P<slug>\w+)/$', BasicNamingCollisionView.as_view()),
|
|
]
|
|
|
|
generator = SchemaGenerator(title='Naming Colisions', patterns=patterns)
|
|
schema = generator.get_schema()
|
|
|
|
assert schema['example']['read'].url == '/example/{id}/'
|
|
assert schema['example']['read_0'].url == '/example/{slug}/'
|
|
|
|
def test_url_under_same_key_not_replaced_another(self):
|
|
|
|
patterns = [
|
|
url(r'^test/list/', simple_fbv),
|
|
url(r'^test/(?P<pk>\d+)/list/', simple_fbv),
|
|
]
|
|
|
|
generator = SchemaGenerator(title='Naming Colisions', patterns=patterns)
|
|
schema = generator.get_schema()
|
|
|
|
assert schema['test']['list']['list'].url == '/test/list/'
|
|
assert schema['test']['list']['list_0'].url == '/test/{id}/list/'
|
|
|
|
|
|
def test_is_list_view_recognises_retrieve_view_subclasses():
|
|
class TestView(generics.RetrieveAPIView):
|
|
pass
|
|
|
|
path = '/looks/like/a/list/view/'
|
|
method = 'get'
|
|
view = TestView()
|
|
|
|
is_list = is_list_view(path, method, view)
|
|
assert not is_list, "RetrieveAPIView subclasses should not be classified as list views."
|
|
|
|
|
|
def test_head_and_options_methods_are_excluded():
|
|
"""
|
|
Regression test for #5528
|
|
https://github.com/encode/django-rest-framework/issues/5528
|
|
|
|
Viewset OPTIONS actions were not being correctly excluded
|
|
|
|
Initial cases here shown to be working as expected.
|
|
"""
|
|
|
|
@api_view(['options', 'get'])
|
|
def fbv(request):
|
|
pass
|
|
|
|
inspector = EndpointEnumerator()
|
|
|
|
path = '/a/path/'
|
|
callback = fbv
|
|
|
|
assert inspector.should_include_endpoint(path, callback)
|
|
assert inspector.get_allowed_methods(callback) == ["GET"]
|
|
|
|
class AnAPIView(APIView):
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
pass
|
|
|
|
def options(self, request, *args, **kwargs):
|
|
pass
|
|
|
|
callback = AnAPIView.as_view()
|
|
|
|
assert inspector.should_include_endpoint(path, callback)
|
|
assert inspector.get_allowed_methods(callback) == ["GET"]
|
|
|
|
class AViewSet(ModelViewSet):
|
|
|
|
@detail_route(methods=['options', 'get'])
|
|
def custom_action(self, request, pk):
|
|
pass
|
|
|
|
callback = AViewSet.as_view({
|
|
"options": "custom_action",
|
|
"get": "custom_action"
|
|
})
|
|
|
|
assert inspector.should_include_endpoint(path, callback)
|
|
assert inspector.get_allowed_methods(callback) == ["GET"]
|