From dcc2236722ee4abde8a0a08e0d1983b98fd70d07 Mon Sep 17 00:00:00 2001 From: Nik Date: Sun, 14 Aug 2016 20:01:58 +0300 Subject: [PATCH 1/4] Test to illustrate issue #4391 --- tests/test_schemas.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 81b796c35..7f86857a8 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -39,7 +39,7 @@ class ExampleViewSet(ModelViewSet): filter_backends = [filters.OrderingFilter] serializer_class = ExampleSerializer - @detail_route(methods=['post'], serializer_class=AnotherSerializer) + @detail_route(methods=['put', 'post'], serializer_class=AnotherSerializer) def custom_action(self, request, pk): return super(ExampleSerializer, self).retrieve(self, request) @@ -188,6 +188,37 @@ class TestRouterGeneratedSchema(TestCase): ) self.assertEqual(response.data, expected) + def test_multiple_http_methods_for_detail_route(self): + client = APIClient() + client.force_authenticate(MockUser()) + response = client.get('/', HTTP_ACCEPT='application/vnd.coreapi+json') + put_action = ('custom_action', + coreapi.Link( + url='/example/{pk}/custom_action/', + action='put', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('c', required=True, location='form'), + coreapi.Field('d', required=False, location='form'), + ] + )) + post_action = ('custom_action', + coreapi.Link( + url='/example/{pk}/custom_action/', + action='post', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('c', required=True, location='form'), + coreapi.Field('d', required=False, location='form'), + ] + )) + + self.assertIn(put_action, response.data['example'].items()) + self.assertIn(post_action, response.data['example'].items()) + + @unittest.skipUnless(coreapi, 'coreapi is not installed') class TestSchemaGenerator(TestCase): From 963cf9575371cef9807dcde01079cca043715081 Mon Sep 17 00:00:00 2001 From: Nik Date: Wed, 24 Aug 2016 18:47:50 +0300 Subject: [PATCH 2/4] Workaround for multiple http methods for action --- rest_framework/schemas.py | 16 +++++++++++++--- tests/test_schemas.py | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 1b899450f..b0b11797d 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -101,16 +101,26 @@ class SchemaGenerator(object): if not links: return None + # looks for overlapped actions for multiple http methods per action case + overlapped = {} + for category, action, link in links: + key = (category, action) + overlapped[key] = key in overlapped + # Generate the schema content structure, eg: # {'users': {'list': Link()}} content = {} for category, action, link in links: + # add suffix for overlapped actions + if overlapped[(category, action)]: + action = '%s_%s' % (action, link.action) if category is None: content[action] = link - elif category in content: - content[category][action] = link else: - content[category] = {action: link} + if category not in content: + content[category] = {} + + content[category][action] = link # Return the schema document. return coreapi.Document(title=self.title, content=content, url=self.url) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index ae9ea7ae3..e8a8cdb6b 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -145,7 +145,7 @@ class TestRouterGeneratedSchema(TestCase): coreapi.Field('pk', required=True, location='path') ] ), - 'custom_action': coreapi.Link( + 'custom_action_post': coreapi.Link( url='/example/{pk}/custom_action/', action='post', encoding='application/json', @@ -155,6 +155,16 @@ class TestRouterGeneratedSchema(TestCase): coreapi.Field('d', required=False, location='form'), ] ), + 'custom_action_put': coreapi.Link( + url='/example/{pk}/custom_action/', + action='put', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('c', required=True, location='form'), + coreapi.Field('d', required=False, location='form'), + ] + ), 'custom_list_action': coreapi.Link( url='/example/custom_list_action/', action='get' @@ -195,7 +205,7 @@ class TestRouterGeneratedSchema(TestCase): client = APIClient() client.force_authenticate(MockUser()) response = client.get('/', HTTP_ACCEPT='application/vnd.coreapi+json') - put_action = ('custom_action', + put_action = ('custom_action_put', coreapi.Link( url='/example/{pk}/custom_action/', action='put', @@ -206,7 +216,7 @@ class TestRouterGeneratedSchema(TestCase): coreapi.Field('d', required=False, location='form'), ] )) - post_action = ('custom_action', + post_action = ('custom_action_post', coreapi.Link( url='/example/{pk}/custom_action/', action='post', From 7b7ef950044589d205c6a936a11545c6799387d4 Mon Sep 17 00:00:00 2001 From: Nik Date: Wed, 24 Aug 2016 19:09:44 +0300 Subject: [PATCH 3/4] Code style --- tests/test_schemas.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index e8a8cdb6b..8ce5a41e6 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -217,22 +217,21 @@ class TestRouterGeneratedSchema(TestCase): ] )) post_action = ('custom_action_post', - coreapi.Link( - url='/example/{pk}/custom_action/', - action='post', - encoding='application/json', - fields=[ - coreapi.Field('pk', required=True, location='path'), - coreapi.Field('c', required=True, location='form'), - coreapi.Field('d', required=False, location='form'), - ] - )) + coreapi.Link( + url='/example/{pk}/custom_action/', + action='post', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('c', required=True, location='form'), + coreapi.Field('d', required=False, location='form'), + ] + )) self.assertIn(put_action, response.data['example'].items()) self.assertIn(post_action, response.data['example'].items()) - @unittest.skipUnless(coreapi, 'coreapi is not installed') class TestSchemaGenerator(TestCase): def test_view(self): From 2d87d70d09fa4ded470f4e9e87821a79173d709b Mon Sep 17 00:00:00 2001 From: Nik Date: Thu, 1 Sep 2016 19:06:06 +0300 Subject: [PATCH 4/4] Schema tests: - add similar action test - decouple links check from overall request --- tests/test_schemas.py | 250 ++++++++++++++++++++++++------------------ 1 file changed, 143 insertions(+), 107 deletions(-) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 8ce5a41e6..a013dd40c 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -11,7 +11,7 @@ from rest_framework.routers import DefaultRouter from rest_framework.schemas import SchemaGenerator from rest_framework.test import APIClient from rest_framework.views import APIView -from rest_framework.viewsets import ModelViewSet +from rest_framework.viewsets import GenericViewSet, ModelViewSet class MockUser(object): @@ -19,6 +19,11 @@ class MockUser(object): return True +class MockCoreapiObject(object): + def __eq__(self, value): + return True + + class ExamplePagination(pagination.PageNumberPagination): page_size = 100 @@ -43,11 +48,11 @@ class ExampleViewSet(ModelViewSet): @detail_route(methods=['put', 'post'], serializer_class=AnotherSerializer) def custom_action(self, request, pk): - return super(ExampleSerializer, self).retrieve(self, request) + pass @list_route() def custom_list_action(self, request): - return super(ExampleViewSet, self).list(self, request) + pass def get_serializer(self, *args, **kwargs): assert self.request @@ -55,6 +60,22 @@ class ExampleViewSet(ModelViewSet): return super(ExampleViewSet, self).get_serializer(*args, **kwargs) +class ExampleViewSet1(GenericViewSet): + serializer_class = ExampleSerializer + + @detail_route(methods=['post']) + def custom_action(self, request, pk): + pass + + +class ExampleViewSet2(GenericViewSet): + serializer_class = ExampleSerializer + + @detail_route(methods=['post']) + def custom_action(self, request, pk): + pass + + class ExampleView(APIView): permission_classes = [permissions.IsAuthenticatedOrReadOnly] @@ -70,10 +91,18 @@ router.register('example', ExampleViewSet, base_name='example') urlpatterns = [ url(r'^', include(router.urls)) ] + urlpatterns2 = [ url(r'^example-view/$', ExampleView.as_view(), name='example-view') ] +router = DefaultRouter(schema_title='Example API' if coreapi else None) +router.register('example1', ExampleViewSet1, base_name='example') +router.register('example2', ExampleViewSet2, base_name='example') +urlpatterns3 = [ + url(r'^', include(router.urls)) +] + @unittest.skipUnless(coreapi, 'coreapi is not installed') @override_settings(ROOT_URLCONF='tests.test_schemas') @@ -119,121 +148,128 @@ class TestRouterGeneratedSchema(TestCase): expected = coreapi.Document( url='', title='Example API', - content={ - 'example': { - 'list': coreapi.Link( - url='/example/', - action='get', - fields=[ - coreapi.Field('page', required=False, location='query'), - coreapi.Field('ordering', required=False, location='query') - ] - ), - 'create': coreapi.Link( - url='/example/', - action='post', - encoding='application/json', - fields=[ - coreapi.Field('a', required=True, location='form', description='A field description'), - coreapi.Field('b', required=False, location='form') - ] - ), - 'retrieve': coreapi.Link( - url='/example/{pk}/', - action='get', - fields=[ - coreapi.Field('pk', required=True, location='path') - ] - ), - 'custom_action_post': coreapi.Link( - url='/example/{pk}/custom_action/', - action='post', - encoding='application/json', - fields=[ - coreapi.Field('pk', required=True, location='path'), - coreapi.Field('c', required=True, location='form'), - coreapi.Field('d', required=False, location='form'), - ] - ), - 'custom_action_put': coreapi.Link( - url='/example/{pk}/custom_action/', - action='put', - encoding='application/json', - fields=[ - coreapi.Field('pk', required=True, location='path'), - coreapi.Field('c', required=True, location='form'), - coreapi.Field('d', required=False, location='form'), - ] - ), - 'custom_list_action': coreapi.Link( - url='/example/custom_list_action/', - action='get' - ), - 'update': coreapi.Link( - url='/example/{pk}/', - action='put', - encoding='application/json', - fields=[ - coreapi.Field('pk', required=True, location='path'), - coreapi.Field('a', required=True, location='form', description='A field description'), - coreapi.Field('b', required=False, location='form') - ] - ), - 'partial_update': coreapi.Link( - url='/example/{pk}/', - action='patch', - encoding='application/json', - fields=[ - coreapi.Field('pk', required=True, location='path'), - coreapi.Field('a', required=False, location='form', description='A field description'), - coreapi.Field('b', required=False, location='form') - ] - ), - 'destroy': coreapi.Link( - url='/example/{pk}/', - action='delete', - fields=[ - coreapi.Field('pk', required=True, location='path') - ] - ) - } - } + content={'example': MockCoreapiObject()} ) self.assertEqual(response.data, expected) - def test_multiple_http_methods_for_detail_route(self): + def test_links(self): client = APIClient() client.force_authenticate(MockUser()) response = client.get('/', HTTP_ACCEPT='application/vnd.coreapi+json') - put_action = ('custom_action_put', - coreapi.Link( - url='/example/{pk}/custom_action/', - action='put', - encoding='application/json', - fields=[ - coreapi.Field('pk', required=True, location='path'), - coreapi.Field('c', required=True, location='form'), - coreapi.Field('d', required=False, location='form'), - ] - )) - post_action = ('custom_action_post', - coreapi.Link( - url='/example/{pk}/custom_action/', - action='post', - encoding='application/json', - fields=[ - coreapi.Field('pk', required=True, location='path'), - coreapi.Field('c', required=True, location='form'), - coreapi.Field('d', required=False, location='form'), - ] - )) + self.assertEqual(response.status_code, 200) + expected_links = [ + coreapi.Link( # list + url='/example/', + action='get', + fields=[ + coreapi.Field('page', required=False, location='query'), + coreapi.Field('ordering', required=False, location='query') + ] + ), + coreapi.Link( # create + url='/example/', + action='post', + encoding='application/json', + fields=[ + coreapi.Field('a', required=True, location='form', description='A field description'), + coreapi.Field('b', required=False, location='form') + ] + ), + coreapi.Link( # retrieve + url='/example/{pk}/', + action='get', + fields=[ + coreapi.Field('pk', required=True, location='path') + ] + ), + coreapi.Link( # custom_action post + url='/example/{pk}/custom_action/', + action='post', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('c', required=True, location='form'), + coreapi.Field('d', required=False, location='form'), + ] + ), + coreapi.Link( # custom_action put + url='/example/{pk}/custom_action/', + action='put', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('c', required=True, location='form'), + coreapi.Field('d', required=False, location='form'), + ] + ), + coreapi.Link( # custom_list_action + url='/example/custom_list_action/', + action='get' + ), + coreapi.Link( # update + url='/example/{pk}/', + action='put', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('a', required=True, location='form', description='A field description'), + coreapi.Field('b', required=False, location='form') + ] + ), + coreapi.Link( # partial_update + url='/example/{pk}/', + action='patch', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('a', required=False, location='form', description='A field description'), + coreapi.Field('b', required=False, location='form') + ] + ), + coreapi.Link( # destroy + url='/example/{pk}/', + action='delete', + fields=[ + coreapi.Field('pk', required=True, location='path') + ] + ), + ] - self.assertIn(put_action, response.data['example'].items()) - self.assertIn(post_action, response.data['example'].items()) + response_links = response.data['example'].links.values() + for link in expected_links: + self.assertIn(link, response_links) @unittest.skipUnless(coreapi, 'coreapi is not installed') class TestSchemaGenerator(TestCase): + def test_similar_actions(self): + schema_generator = SchemaGenerator(title='Test View', patterns=urlpatterns3) + schema = schema_generator.get_schema() + self.assertIn('example1', schema) + self.assertIn('example2', schema) + custom_action_1 = coreapi.Link( + url='/example1/{pk}/custom_action/', + action='post', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('a', required=True, location='form', description='A field description'), + coreapi.Field('b', required=False, location='form') + ] + ) + custom_action_2 = coreapi.Link( + url='/example2/{pk}/custom_action/', + action='post', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('a', required=True, location='form', description='A field description'), + coreapi.Field('b', required=False, location='form') + ] + ) + self.assertIn(custom_action_1, schema['example1'].links.values()) + self.assertIn(custom_action_2, schema['example2'].links.values()) + def test_view(self): schema_generator = SchemaGenerator(title='Test View', patterns=urlpatterns2) schema = schema_generator.get_schema()