Repository: encode/django-rest-framework

Issue:
SchemaGenerator.get_filter_fields is inconsistent with GenericAPIView.get_object
Hello devs,

In [SchemaGenerator.get_filter_fields,](https://github.com/encode/django-rest-framework/blob/master/rest_framework/schemas.py#L616) there's a default path for non-list actions that returns an empty list.

I believe this is inconsistent with the behavior of [GenericAPIView.get_object](https://github.com/encode/django-rest-framework/blob/master/rest_framework/generics.py#L85) which will use filter_queryset.
The [related documentation](http://www.django-rest-framework.org/api-guide/filtering/#filtering-and-object-lookups) tends to confirm that this behavior is legitimate : 

> Note that if a filter backend is configured for a view, then as well as being used to filter list views, it will also be used to filter the querysets used for returning a single object.
>
> For instance, given the previous example, and a product with an id of 4675, the following URL would either return the corresponding object, or return a 404 response, depending on if the filtering conditions were met by the given product instance:

The schema generated by SchemaGenerator will not reflect that filter fields are available for object-lookup actions, this is especially annoying when filter fields expect a mandatory parameter (filtering objects against a required client_id parameter for example).

## Steps to reproduce
- create a filter subclass which overrides `get_schema_fields` and return a non empty list of `coreapi.Fields`
- create a `ModelViewSet` subclass with the above filter in `filter_backends`
- generate a schema including this view

## Expected behavior
- The generated schema should define the filter's schema fields for object-lookup actions

## Actual behavior
- It doesn't.


Discussion:
Yep. This seems correct (there is an inconsistency). 

Resolving patch:
diff --git a/rest_framework/schemas/inspectors.py b/rest_framework/schemas/inspectors.py
--- a/rest_framework/schemas/inspectors.py
+++ b/rest_framework/schemas/inspectors.py
@@ -337,18 +337,33 @@ def get_pagination_fields(self, path, method):
         paginator = view.pagination_class()
         return paginator.get_schema_fields(view)
 
-    def get_filter_fields(self, path, method):
-        view = self.view
+    def _allows_filters(self, path, method):
+        """
+        Determine whether to include filter Fields in schema.
 
-        if not is_list_view(path, method, view):
-            return []
+        Default implementation looks for ModelViewSet or GenericAPIView
+        actions/methods that cause filtering on the default implementation.
+
+        Override to adjust behaviour for your view.
 
-        if not getattr(view, 'filter_backends', None):
+        Note: Introduced in v3.7: Initially "private" (i.e. with leading underscore)
+            to allow changes based on user experience.
+        """
+        if getattr(self.view, 'filter_backends', None) is None:
+            return False
+
+        if hasattr(self.view, 'action'):
+            return self.view.action in ["list", "retrieve", "update", "partial_update", "destroy"]
+
+        return method.lower in ["get", "put", "patch", "delete"]
+
+    def get_filter_fields(self, path, method):
+        if not self._allows_filters(path, method):
             return []
 
         fields = []
-        for filter_backend in view.filter_backends:
-            fields += filter_backend().get_schema_fields(view)
+        for filter_backend in self.view.filter_backends:
+            fields += filter_backend().get_schema_fields(self.view)
         return fields
 
     def get_encoding(self, path, method):


Test patch:
diff --git a/tests/test_schemas.py b/tests/test_schemas.py
--- a/tests/test_schemas.py
+++ b/tests/test_schemas.py
@@ -139,7 +139,8 @@ def test_anonymous_request(self):
                         url='/example/{id}/',
                         action='get',
                         fields=[
-                            coreapi.Field('id', required=True, location='path', schema=coreschema.String())
+                            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.'))
                         ]
                     )
                 }
@@ -179,7 +180,8 @@ def test_authenticated_request(self):
                         url='/example/{id}/',
                         action='get',
                         fields=[
-                            coreapi.Field('id', required=True, location='path', schema=coreschema.String())
+                            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(
@@ -225,7 +227,8 @@ def test_authenticated_request(self):
                         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('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(
@@ -235,14 +238,16 @@ def test_authenticated_request(self):
                         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('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('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.'))
                         ]
                     )
                 }
@@ -450,7 +455,8 @@ def test_schema_for_regular_views(self):
                         url='/example1/{id}/',
                         action='get',
                         fields=[
-                            coreapi.Field('id', required=True, location='path', schema=coreschema.String())
+                            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.'))
                         ]
                     )
                 }