From f772d720930152908bb99f752583840817fd46f0 Mon Sep 17 00:00:00 2001 From: Tommy Beadle Date: Fri, 7 Apr 2017 11:41:06 -0400 Subject: [PATCH] Allow @detail_route and @list_route methods to have docstring sections. If a method decorated as a @detail_route or @list_route has a docstring that includes sections named after the action for the http methods that are supported for that route, then they will be used for the description in the schema associated with that http method for the view. --- docs/topics/documenting-your-api.md | 14 ++++++++++ rest_framework/schemas.py | 36 ++++++++++++++----------- tests/test_schemas.py | 42 ++++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md index 9a87c17c1..d73a13ab5 100644 --- a/docs/topics/documenting-your-api.md +++ b/docs/topics/documenting-your-api.md @@ -69,6 +69,20 @@ When using viewsets, you should use the relevant action names as delimiters. Create a new user instance. """ +When using the `@detail_route` or `@list_route` decorators, the docstring for +the decorated method may use the `action:` style delimiters. + + class UserViewSet(viewsets.ModelViewSet): + @detail_route(methods=('get', 'post')) + def groups(self, request, pk=None): + """ + retrieve: + Return the groups for the given user. + + create: + Add the user to a new group. + """ + --- ## Third party packages diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index ec6d7f3c3..3ebda4c0e 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -8,7 +8,7 @@ from django.core.exceptions import PermissionDenied from django.db import models from django.http import Http404 from django.utils import six -from django.utils.encoding import force_text, smart_text +from django.utils.encoding import force_text from django.utils.translation import ugettext_lazy as _ from rest_framework import exceptions, renderers, serializers @@ -19,7 +19,6 @@ from rest_framework.compat import ( from rest_framework.request import clone_request from rest_framework.response import Response from rest_framework.settings import api_settings -from rest_framework.utils import formatting from rest_framework.utils.model_meta import _get_pk from rest_framework.views import APIView @@ -467,31 +466,36 @@ class SchemaGenerator(object): This will be based on the method docstring if one exists, or else the class docstring. """ - method_name = getattr(view, 'action', method.lower()) - method_docstring = getattr(view, method_name, None).__doc__ - if method_docstring: + method = method.lower() + method_name = getattr(view, 'action', method) + view_method = getattr(view, method_name, None) + description = None + if view_method: # An explicit docstring on the method or action. - return formatting.dedent(smart_text(method_docstring)) - - description = view.get_view_description() + description = api_settings.VIEW_DESCRIPTION_FUNCTION(view_method) + if not description: + description = view.get_view_description() lines = [line.strip() for line in description.splitlines()] current_section = '' sections = {'': ''} for line in lines: if header_regex.match(line): - current_section, seperator, lead = line.partition(':') + current_section, separator, lead = line.partition(':') sections[current_section] = lead.strip() else: sections[current_section] += '\n' + line - header = getattr(view, 'action', method.lower()) - if header in sections: - return sections[header].strip() - if header in self.coerce_method_names: - if self.coerce_method_names[header] in sections: - return sections[self.coerce_method_names[header]].strip() - return sections[''].strip() + section_name = '' + for attempt in (method_name, self.default_mapping.get(method, method)): + if attempt in sections: + section_name = attempt + break + if self.coerce_method_names.get(attempt) in sections: + section_name = self.coerce_method_names[attempt] + break + + return sections[section_name].strip() def get_encoding(self, path, method, view): """ diff --git a/tests/test_schemas.py b/tests/test_schemas.py index f75370170..999d703df 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -55,7 +55,19 @@ class ExampleViewSet(ModelViewSet): """ A description of custom action. """ - return super(ExampleSerializer, self).retrieve(self, request) + return super(ExampleViewSet, self).retrieve(self, request) + + @detail_route(methods=['get', 'post'], serializer_class=EmptySerializer) + def custom_action2(self, request, pk): + """ + read: A description for getting items. + + create: A description for creating items. + """ + if request.method == 'GET': + return super(ExampleViewSet, self).retrieve(self, request) + else: + return super(ExampleViewSet, self).create(self, request) @list_route() def custom_list_action(self, request): @@ -105,6 +117,16 @@ class TestRouterGeneratedSchema(TestCase): coreapi.Field('ordering', required=False, location='query', schema=coreschema.String(title='Ordering', description='Which field to use when ordering the results.')) ] ), + 'custom_action2': { + 'read': coreapi.Link( + url='/example/{id}/custom_action2/', + action='get', + description='A description for getting items.', + fields=[ + coreapi.Field('id', required=True, location='path', schema=coreschema.String()), + ], + ), + }, 'custom_list_action': coreapi.Link( url='/example/custom_list_action/', action='get' @@ -173,6 +195,24 @@ class TestRouterGeneratedSchema(TestCase): coreapi.Field('d', required=False, location='form', schema=coreschema.String(title='D')), ] ), + 'custom_action2': { + 'read': coreapi.Link( + url='/example/{id}/custom_action2/', + action='get', + description='A description for getting items.', + fields=[ + coreapi.Field('id', required=True, location='path', schema=coreschema.String()), + ], + ), + 'create': coreapi.Link( + url='/example/{id}/custom_action2/', + action='post', + description='A description for creating items.', + fields=[ + coreapi.Field('id', required=True, location='path', schema=coreschema.String()), + ], + ), + }, 'custom_list_action': coreapi.Link( url='/example/custom_list_action/', action='get'