diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index 62f14087f..55f6664e0 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -127,7 +127,7 @@ REST framework also allows you to work with regular function based views. It pr ## @api_view() -**Signature:** `@api_view(http_method_names=['GET'])` +**Signature:** `@api_view(http_method_names=['GET'], exclude_from_schema=False)` The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data: @@ -139,7 +139,7 @@ The core of this functionality is the `api_view` decorator, which takes a list o This view will use the default renderers, parsers, authentication classes etc specified in the [settings]. -By default only `GET` methods will be accepted. Other methods will respond with "405 Method Not Allowed". To alter this behavior, specify which methods the view allows, like so: +By default only `GET` methods will be accepted. Other methods will respond with "405 Method Not Allowed". To alter this behaviour, specify which methods the view allows, like so: @api_view(['GET', 'POST']) def hello_world(request): @@ -147,6 +147,13 @@ By default only `GET` methods will be accepted. Other methods will respond with return Response({"message": "Got some data!", "data": request.data}) return Response({"message": "Hello, world!"}) +You can also mark an API view as being omitted from any [auto-generated schema][schemas], +using the `exclude_from_schema` argument.: + + @api_view(['GET'], exclude_from_schema=True) + def api_docs(request): + ... + ## API policy decorators To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `@api_view` decorator. For example, to create a view that uses a [throttle][throttling] to ensure it can only be called once per day by a particular user, use the `@throttle_classes` decorator, passing a list of throttle classes: @@ -178,3 +185,4 @@ Each of these decorators takes a single argument which must be a list or tuple o [cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html [settings]: settings.md [throttling]: throttling.md +[schemas]: schemas.md diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 554e5236c..bf9b32aaa 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -15,7 +15,7 @@ from django.utils import six from rest_framework.views import APIView -def api_view(http_method_names=None): +def api_view(http_method_names=None, exclude_from_schema=False): """ Decorator that converts a function-based view into an APIView subclass. Takes a list of allowed methods for the view as an argument. @@ -72,6 +72,7 @@ def api_view(http_method_names=None): WrappedAPIView.permission_classes = getattr(func, 'permission_classes', APIView.permission_classes) + WrappedAPIView.exclude_from_schema = exclude_from_schema return WrappedAPIView.as_view() return decorator diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 859b60460..9ef8d11f0 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -311,6 +311,7 @@ class DefaultRouter(SimpleRouter): class APISchemaView(views.APIView): _ignore_model_permissions = True + exclude_from_schema = True renderer_classes = schema_renderers def get(self, request, *args, **kwargs): @@ -332,6 +333,7 @@ class DefaultRouter(SimpleRouter): class APIRootView(views.APIView): _ignore_model_permissions = True + exclude_from_schema = True def get(self, request, *args, **kwargs): # Return a plain {"name": "hyperlink"} response. diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 0928e7661..c3b36cd1e 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -56,6 +56,22 @@ def is_custom_action(action): ]) +def is_list_view(path, method, view): + """ + Return True if the given path/method appears to represent a list view. + """ + if hasattr(view, 'action'): + # Viewsets have an explicitly defined action, which we can inspect. + return view.action == 'list' + + if method.lower() != 'get': + return False + path_components = path.strip('/').split('/') + if path_components and '{' in path_components[-1]: + return False + return True + + def endpoint_ordering(endpoint): path, method, callback = endpoint method_priority = { @@ -136,9 +152,6 @@ class EndpointInspector(object): if path.endswith('.{format}') or path.endswith('.{format}/'): return False # Ignore .json style URLs. - if path == '/': - return False # Ignore the root endpoint. - return True def get_allowed_methods(self, callback): @@ -201,7 +214,7 @@ class SchemaGenerator(object): links = OrderedDict() for path, method, callback in self.endpoints: view = self.create_view(callback, method, request) - if not self.has_view_permissions(view): + if not self.should_include_view(path, method, view): continue link = self.get_link(path, method, view) keys = self.get_keys(path, method, view) @@ -235,10 +248,13 @@ class SchemaGenerator(object): return view - def has_view_permissions(self, view): + def should_include_view(self, path, method, view): """ Return `True` if the incoming request has the correct view permissions. """ + if getattr(view, 'exclude_from_schema', False): + return False + if view.request is None: return True @@ -248,20 +264,6 @@ class SchemaGenerator(object): return False return True - def is_list_endpoint(self, path, method, view): - """ - Return True if the given path/method appears to represent a list endpoint. - """ - if hasattr(view, 'action'): - return view.action == 'list' - - if method.lower() != 'get': - return False - path_components = path.strip('/').split('/') - if path_components and '{' in path_components[-1]: - return False - return True - # Methods for generating each individual `Link` instance... def get_link(self, path, method, view): @@ -359,7 +361,7 @@ class SchemaGenerator(object): return fields def get_pagination_fields(self, path, method, view): - if not self.is_list_endpoint(path, method, view): + if not is_list_view(path, method, view): return [] if not getattr(view, 'pagination_class', None): @@ -369,7 +371,7 @@ class SchemaGenerator(object): return as_query_fields(paginator.get_fields(view)) def get_filter_fields(self, path, method, view): - if not self.is_list_endpoint(path, method, view): + if not is_list_view(path, method, view): return [] if not getattr(view, 'filter_backends', None): @@ -402,7 +404,7 @@ class SchemaGenerator(object): action = view.action else: # Views have no associated action, so we determine one from the method. - if self.is_list_endpoint(path, method, view): + if is_list_view(path, method, view): action = 'list' else: action = self.default_mapping[method.lower()] diff --git a/rest_framework/views.py b/rest_framework/views.py index 23d48962c..bb9aba72c 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -110,6 +110,9 @@ class APIView(View): # Allow dependency injection of other settings to make testing easier. settings = api_settings + # Mark the view as being included or excluded from schema generation. + exclude_from_schema = False + @classmethod def as_view(cls, **initkwargs): """