Фильтры Django: настраиваемые методы фильтрации с различными выражениями поиска

Я использую фильтры Django вместе с Django Rest Framework для создания фильтров для представлений API. У них хороший синтаксис, который я показываю в фильтре поля age ниже, определенном в метаклассе fields = {'age': ['exact', 'gte', 'lte', 'lt', 'gt', 'in']}, который позволяет создавать все эти фильтры больше или равно, меньше или равно, меньше, больше и т. д. для поля все однажды...

Однако с настраиваемым полем, таким как artworks_count, которое я также показываю ниже, кажется, что мне нужно добавить разные методы для каждого выражения поиска, что довольно повторяется. Мне просто интересно, нет ли лучшего способа сделать это? (возможно, с использованием чего-то другого, кроме NumberFilter?), который будет принимать lookup_expression в качестве аргумента и позволит мне создать единственный метод, используя его

class UserFilter(django_rest_filters.FilterSet):


    artworks_count__gte = django_rest_filters.NumberFilter(method="filter_artworks_count__gte")

    class Meta:
        model = User
        fields = {'age': ['exact', 'gte', 'lte', 'lt', 'gt', 'in']}

    def filter_artwork_count__gte(self, qs, name, value):
        return qs.annotate(art_count=Count('artworks')).filter(art_count__gte=value)

person fpghost    schedule 12.08.2020    source источник


Ответы (1)


Вы можете создать свой собственный базовый фильтр, от которого вы можете наследовать и определять свои фильтры поля модели.

from django_filters import (
    BaseInFilter,
    BaseRangeFilter,
    BooleanFilter,
    FilterSet,
    NumberFilter,
)


### first, create filterset class attributes

def create_custom_method_filters(filter_name, base_filter_class, lookups, func):
    custom_method_name = f'filter_{filter_name}'

    def custom_method(self, queryset, name, value):
        return func(queryset, name, value)
    boolean_lookups = ('isnull', 'isempty')
    in_lookups = ('in',)
    range_lookups = ('range',)
    d = dict(
        **{
            f'{filter_name}__{lookup}' if lookup != 'exact' else filter_name: base_filter_class(method=custom_method_name)
            for lookup in lookups
            if lookup not in boolean_lookups + in_lookups + range_lookups
        },
        **{
            f'{filter_name}__{lookup}': BooleanFilter(method=custom_method_name)
            for lookup in lookups
            if lookup in boolean_lookups
        },
        **{
            f'{filter_name}__{lookup}': type('InFilter', (BaseInFilter, base_filter_class), {})(method=custom_method_name)
            for lookup in lookups
            if lookup in in_lookups
        },
        **{
            f'{filter_name}__{lookup}': type('RangeFilter', (BaseRangeFilter, base_filter_class), {})(method=custom_method_name)
            for lookup in lookups
            if lookup in range_lookups
        },
        **{
            custom_method_name: custom_method,
        },
    )
    return d

### second, create your base filter for the specific filter

UserFilterBase = type(
    'UserFilterBase',
    (FilterSet,),
    create_custom_method_filters(
        'artworks_count',
        NumberFilter,
        ['exact', 'in', 'gt', 'gt', 'lt', 'lte', 'range'],
        lambda queryset, name, value: queryset.annotate(artworks_count=Count('artworks')).filter(**{name: value}),
    ),
)

### Then, inherit from that base class and use the filter meta as usual.


class UserFilter(UserFilterBase):
    class Meta:
        model = User
        fields = {'age': ['exact', 'gte', 'lte', 'lt', 'gt', 'in']}

В Python 3.9+ вы можете заменить уродливую конструкцию dict(**{…}, **{…}) гораздо более красивой конструкцией {…} | {…}.

person Cyrille Pontvieux    schedule 06.11.2020