From 99aa001f1098061af58ce0b740eb61b42f4f1f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20ROCHER?= Date: Thu, 28 Jul 2016 15:16:38 +0200 Subject: [PATCH 1/5] Use the help_text attribute of serializer fields in the schema doc --- rest_framework/schemas.py | 11 ++++++++--- tests/test_schemas.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 41dc82da1..2fb9c7eb5 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -4,6 +4,7 @@ from django.conf import settings from django.contrib.admindocs.views import simplify_regex from django.core.urlresolvers import RegexURLPattern, RegexURLResolver from django.utils import six +from django.utils.encoding import force_text from rest_framework import exceptions, serializers from rest_framework.compat import coreapi, uritemplate, urlparse @@ -258,8 +259,6 @@ class SchemaGenerator(object): if not hasattr(view, 'get_serializer_class'): return [] - fields = [] - serializer_class = view.get_serializer_class() serializer = serializer_class() @@ -269,11 +268,17 @@ class SchemaGenerator(object): if not isinstance(serializer, serializers.Serializer): return [] + fields = [] for field in serializer.fields.values(): if field.read_only: continue required = field.required and method != 'PATCH' - field = coreapi.Field(name=field.source, location='form', required=required) + field = coreapi.Field( + name=field.source, + location='form', + required=required, + description=force_text(field.help_text), + ) fields.append(field) return fields diff --git a/tests/test_schemas.py b/tests/test_schemas.py index a32b8a117..a2c723a8b 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -23,8 +23,8 @@ class ExamplePagination(pagination.PageNumberPagination): class ExampleSerializer(serializers.Serializer): - a = serializers.CharField(required=True) - b = serializers.CharField(required=False) + a = serializers.CharField(required=True, help_text='About a') + b = serializers.CharField(required=False, help_text='About b') class ExampleViewSet(ModelViewSet): @@ -109,8 +109,8 @@ class TestRouterGeneratedSchema(TestCase): action='post', encoding='application/json', fields=[ - coreapi.Field('a', required=True, location='form'), - coreapi.Field('b', required=False, location='form') + coreapi.Field('a', required=True, location='form', description='About a'), + coreapi.Field('b', required=False, location='form', description='About b') ] ), 'retrieve': coreapi.Link( @@ -126,8 +126,8 @@ class TestRouterGeneratedSchema(TestCase): encoding='application/json', fields=[ coreapi.Field('pk', required=True, location='path'), - coreapi.Field('a', required=True, location='form'), - coreapi.Field('b', required=False, location='form') + coreapi.Field('a', required=True, location='form', description='About a'), + coreapi.Field('b', required=False, location='form', description='About b') ] ), 'partial_update': coreapi.Link( @@ -136,8 +136,8 @@ class TestRouterGeneratedSchema(TestCase): encoding='application/json', fields=[ coreapi.Field('pk', required=True, location='path'), - coreapi.Field('a', required=False, location='form'), - coreapi.Field('b', required=False, location='form') + coreapi.Field('a', required=False, location='form', description='About a'), + coreapi.Field('b', required=False, location='form', description='About b') ] ), 'destroy': coreapi.Link( From 38b953bb7e1b412a4a4bb258d9ad3ce0d3ebf239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20ROCHER?= Date: Thu, 28 Jul 2016 15:39:49 +0200 Subject: [PATCH 2/5] Export the docstring of the method in the Link description For viewset, it exports the corresponding action (list, retrieve, update, etc.). For simple APIView, it returns the http handler (get, post, put, etc.). --- rest_framework/schemas.py | 10 +++++++++- tests/test_schemas.py | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 2fb9c7eb5..d249ac4e1 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -6,7 +6,7 @@ from django.core.urlresolvers import RegexURLPattern, RegexURLResolver from django.utils import six from django.utils.encoding import force_text -from rest_framework import exceptions, serializers +from rest_framework import exceptions, serializers, viewsets from rest_framework.compat import coreapi, uritemplate, urlparse from rest_framework.request import clone_request from rest_framework.views import APIView @@ -207,10 +207,18 @@ class SchemaGenerator(object): else: encoding = None + if isinstance(view, viewsets.GenericViewSet): + actions = getattr(callback, 'actions', self.default_mapping) + action = actions[method.lower()] + view_fn = getattr(callback.cls, action, None) + else: + view_fn = getattr(callback.cls, method.lower(), None) + return coreapi.Link( url=urlparse.urljoin(self.url, path), action=method.lower(), encoding=encoding, + description=view_fn.__doc__ if view_fn else '', fields=fields ) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index a2c723a8b..f5b49abcd 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -38,6 +38,7 @@ class ExampleView(APIView): permission_classes = [permissions.IsAuthenticatedOrReadOnly] def get(self, request, *args, **kwargs): + """get documentation""" return Response() def post(self, request, *args, **kwargs): @@ -171,6 +172,7 @@ class TestSchemaGenerator(TestCase): 'read': coreapi.Link( url='/example-view/', action='get', + description='get documentation', fields=[] ) } From 7f3c4188b96a5be93fbb130de8d576403e858120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20ROCHER?= Date: Thu, 28 Jul 2016 15:43:27 +0200 Subject: [PATCH 3/5] Prevent lower api to erase higher namespace Before, an API plugged to a namespace (for instance /account/{pk}/) would erase API plugged to the same namespace with additional URL segments: (for instance /account/{user_id}/book/{pk}/) --- rest_framework/schemas.py | 10 ++--- tests/test_schemas.py | 94 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 7 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index d249ac4e1..5ce2e9e70 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -176,18 +176,16 @@ class SchemaGenerator(object): Return a tuple of strings, indicating the identity to use for a given endpoint. eg. ('users', 'list'). """ - category = None + category = [] for item in path.strip('/').split('/'): if '{' in item: - break - category = item + continue + category.append(item) actions = getattr(callback, 'actions', self.default_mapping) action = actions[method.lower()] - if category: - return (category, action) - return (action,) + return (' '.join(category), action) # Methods for generating each individual `Link` instance... diff --git a/tests/test_schemas.py b/tests/test_schemas.py index f5b49abcd..2a15e0e64 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -6,7 +6,7 @@ from django.test import TestCase, override_settings from rest_framework import filters, pagination, permissions, serializers from rest_framework.compat import coreapi from rest_framework.response import Response -from rest_framework.routers import DefaultRouter +from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework.schemas import SchemaGenerator from rest_framework.test import APIClient from rest_framework.views import APIView @@ -55,6 +55,14 @@ urlpatterns2 = [ ] +router = SimpleRouter() +router.register('example', ExampleViewSet, base_name='example') +urlpatterns3 = [ + url(r'^', include(router.urls)), + url(r'^(?P\w+)/example-view/$', ExampleView.as_view(), name='example-view') +] + + @unittest.skipUnless(coreapi, 'coreapi is not installed') @override_settings(ROOT_URLCONF='tests.test_schemas') class TestRouterGeneratedSchema(TestCase): @@ -179,3 +187,87 @@ class TestSchemaGenerator(TestCase): } ) self.assertEquals(schema, expected) + + +@unittest.skipUnless(coreapi, 'coreapi is not installed') +class TestSchemaAndSubSchemaGenerator(TestCase): + def test_view(self): + schema_generator = SchemaGenerator(title='Test View', patterns=urlpatterns3) + schema = schema_generator.get_schema() + expected = coreapi.Document( + url='', + title='Test View', + 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='About a'), + coreapi.Field('b', required=False, location='form', description='About b') + ] + ), + 'retrieve': coreapi.Link( + url='/example/{pk}/', + action='get', + fields=[ + coreapi.Field('pk', required=True, location='path') + ] + ), + '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='About a'), + coreapi.Field('b', required=False, location='form', description='About b') + ] + ), + '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='About a'), + coreapi.Field('b', required=False, location='form', description='About b') + ] + ), + 'destroy': coreapi.Link( + url='/example/{pk}/', + action='delete', + fields=[ + coreapi.Field('pk', required=True, location='path') + ] + ) + }, + 'example-view': { + 'create': coreapi.Link( + url='/{example_id}/example-view/', + action='post', + fields=[ + coreapi.Field('example_id', required=True, location='path') + ] + ), + 'read': coreapi.Link( + url='/{example_id}/example-view/', + action='get', + description='get documentation', + fields=[ + coreapi.Field('example_id', required=True, location='path') + ] + ) + }, + } + ) + self.assertEquals(schema, expected) From 29afba67e1e17e6c7a13c11f584f727af4aa1a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20ROCHER?= Date: Thu, 28 Jul 2016 15:46:35 +0200 Subject: [PATCH 4/5] Captitalize the name of the APIs --- rest_framework/schemas.py | 2 +- tests/test_schemas.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 5ce2e9e70..cd2c39258 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -180,7 +180,7 @@ class SchemaGenerator(object): for item in path.strip('/').split('/'): if '{' in item: continue - category.append(item) + category.append(item.capitalize()) actions = getattr(callback, 'actions', self.default_mapping) action = actions[method.lower()] diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 2a15e0e64..8284c71c4 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -74,7 +74,7 @@ class TestRouterGeneratedSchema(TestCase): url='', title='Example API', content={ - 'example': { + 'Example': { 'list': coreapi.Link( url='/example/', action='get', @@ -104,7 +104,7 @@ class TestRouterGeneratedSchema(TestCase): url='', title='Example API', content={ - 'example': { + 'Example': { 'list': coreapi.Link( url='/example/', action='get', @@ -171,7 +171,7 @@ class TestSchemaGenerator(TestCase): url='', title='Test View', content={ - 'example-view': { + 'Example-view': { 'create': coreapi.Link( url='/example-view/', action='post', @@ -198,7 +198,7 @@ class TestSchemaAndSubSchemaGenerator(TestCase): url='', title='Test View', content={ - 'example': { + 'Example': { 'list': coreapi.Link( url='/example/', action='get', @@ -251,7 +251,7 @@ class TestSchemaAndSubSchemaGenerator(TestCase): ] ) }, - 'example-view': { + 'Example-view': { 'create': coreapi.Link( url='/{example_id}/example-view/', action='post', From 78aba78f0250ff4b0afa6ec45555c8f37771ca77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20ROCHER?= Date: Thu, 28 Jul 2016 15:50:31 +0200 Subject: [PATCH 5/5] Add a class owner dict to describe path parameters --- rest_framework/schemas.py | 8 +++++++- tests/test_schemas.py | 7 +++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index cd2c39258..a2e54347f 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -246,10 +246,16 @@ class SchemaGenerator(object): Return a list of `coreapi.Field` instances corresponding to any templated path variables. """ + path_descriptions = getattr(view, 'path_fields_descriptions', {}) + fields = [] for variable in uritemplate.variables(path): - field = coreapi.Field(name=variable, location='path', required=True) + field = coreapi.Field(name=variable, + location='path', + required=True, + description=path_descriptions.get(variable, ''), + ) fields.append(field) return fields diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 8284c71c4..155ad1ff1 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -36,6 +36,9 @@ class ExampleViewSet(ModelViewSet): class ExampleView(APIView): permission_classes = [permissions.IsAuthenticatedOrReadOnly] + path_fields_descriptions = { + 'example_id': 'Description of example_id path parameter', + } def get(self, request, *args, **kwargs): """get documentation""" @@ -256,7 +259,7 @@ class TestSchemaAndSubSchemaGenerator(TestCase): url='/{example_id}/example-view/', action='post', fields=[ - coreapi.Field('example_id', required=True, location='path') + coreapi.Field('example_id', required=True, location='path', description='Description of example_id path parameter') ] ), 'read': coreapi.Link( @@ -264,7 +267,7 @@ class TestSchemaAndSubSchemaGenerator(TestCase): action='get', description='get documentation', fields=[ - coreapi.Field('example_id', required=True, location='path') + coreapi.Field('example_id', required=True, location='path', description='Description of example_id path parameter') ] ) },