DRF 3.7 autogen 文档中的条件 FilterSets:我可以为路由添加查询参数过滤器吗(但仅适用于某些 HTTP 动词)

conditional FilterSets in DRF 3.7 autogen docs: can I add a queryparam filter for a route (but only for certain HTTP verbs)

(DRF v3.7, django-filters v1.1.0)

嗨!我有一个有效的 FilterSet,它可以让我通过查询参数过滤我的结果,例如http://localhost:9000/mymodel?name=FooOnly

这工作得很好。

class MyNameFilter(FilterSet):
    name = CharFilter(field_name='name', help_text='Filter by name')

    class Meta:
        model = MyModel
        fields = ('name',)


class MyModel(...):
    ...
    filter_backends = (DjangoFilterBackend,)
    filter_class = MyNameFilter

但是当我为我的 API 呈现内置自动生成的文档时,我看到这个查询参数记录在我的路由中的所有方法中,例如GETPUTPATCH

我只打算通过此查询参数过滤其中一些 HTTP 动词,因为它对其他动词没有意义,例如PUT

有没有一种好方法可以让我的 FilterSet 以这种方式成为条件?以路由方法为条件。

我尝试在路由器级别应用此逻辑(误入歧途的想法)。同样在 ViewSet 级别——但是没有 get_filter_class 重写方法,例如get_serializer_class.

感谢您的帮助。

您将在 DjangoFilterBackend 中获得 get_filter_class。您需要创建一个新的 FilterBackend 来覆盖 filter_queryset 方法。

class GETFilterBackend(DjangoFilterBackend):

    def filter_queryset(self, request, queryset, view):
        if request.method == 'GET':
            return super().filter_queryset(request, queryset, view)
        return queryset


class MyModel(...):
    ...
    filter_backends = (GETFilterBackend,)
    filter_class = MyNameFilter

在 django-filters Google 群组论坛上 Carlton G. 的帮助下解决了这个问题(谢谢 Carlton)。

我的解决方案是提高一个级别并拦截来自 AutoSchema 检查的 CoreAPI 模式,但在它进入自动生成的文档之前。

在这个拦截点,我覆盖 _allows_filters 以仅应用于我感兴趣的 HTTP 动词。 (尽管以 _ 为前缀,因此旨在作为私有方法而不是为了覆盖,该方法的注释明确鼓励这样做。Introduced in v3.7: Initially "private" (i.e. with leading underscore) to allow changes based on user experience.

我的代码如下:

from rest_framework.schemas import AutoSchema


# see https://www.django-rest-framework.org/api-guide/schemas/#autoschema
#     and https://www.django-rest-framework.org/api-guide/filtering/

class LimitedFilteringViewSchema(AutoSchema):
    # Initially copied from lib/python2.7/site-packages/rest_framework/schemas/inspectors.py:352,
    # then modified to restrict our filtering by query-parameters to only certain view
    # actions or HTTP verbs
    def _allows_filters(self, path, method):
        if getattr(self.view, 'filter_backends', None) is None:
            return False

        if hasattr(self.view, 'action'):
            return self.view.action in ["list"]  # original code:  ["list", "retrieve", "update", "partial_update", "destroy"]

        return method.lower() in ["get"]  # original code:  ["get", "put", "patch", "delete"]

然后,在我的 APIView 级别:

class MyViewSchema(LimitedFilteringViewSchema):

    # note to Whosebug:  this was some additional schema repair work I 
    # needed to do, again adding logic conditional on the HTTP verb.  
    # Not related to the original question posted here, but hopefully relevant
    # all the same.

    def get_serializer_fields(self, path, method):
        fields = super(MyViewSchema, self).get_serializer_fields(path, method)

        # The 'name' parameter is set in MyModelListItemSerializer as not being required.
        # However, when creating an access-code-pool, it must be required -- and in DRF v3.7, there's
        # no clean way of encoding this conditional logic, short of what you see here:
        #
        # We override the AutoSchema inspection class, so we can intercept the CoreAPI Fields it generated,
        # on their way out but before they make their way into the auto-generated api docs.
        #
        # CoreAPI Fields are named tuples, hence the poor man's copy constructor below.

        if path == u'/v1/domains/{domain_name}/access-code-pools' and method == 'POST':
            # find the index of our 'name' field in our fields list
            i = next((i for i, f in enumerate(fields) if (lambda f: f.name == 'name')(f)), -1)
            if i >= 0:
                name_field = fields[i]
                fields[i] = Field(name=name_field.name, location=name_field.location,
                                  schema=name_field.schema, description=name_field.description,
                                  type=name_field.type, example=name_field.example,
                                  required=True)  # all this inspection, just to set this here boolean.
        return fields


class MyNameFilter(FilterSet):
    name = CharFilter(field_name='name', help_text='Filter returned access code pools by name')

    class Meta:
        model = MyModel
        fields = ('name',)


class MyAPIView(...)

    schema = MyViewSchema()
    filter_backends = (DjangoFilterBackend,)
    filter_class = MyNameFilter