2025, Sep 29 19:16
Как выполнить кастомный фильтр последним в django-filter
Как в django-filter запускать кастомный фильтр последним: BooleanFilter без метода и логика в FilterSet.qs. Пример с with_parents и учетом поля archived_flag.
Когда пользовательскому фильтру в django-filter нужно выполниться после всех стандартных полей, обычный хук на методе не выручит. Типичный пример — переключатель include-ancestors, который следует оценивать только после того, как известна базовая выборка результатов, например, после применения ограничения is_archived. Ниже показано, как организовать FilterSet так, чтобы шаг с предками выполнялся последним — и только по запросу.
Постановка задачи
Цель — отложить логику include_ancestors на самый конец цепочки фильтрации, поскольку она зависит от того, какие записи остались после применения всех прочих фильтров.
import django_filters
class NodeFilters(django_filters.FilterSet):
    model = CatalogItem
    fields = {
        "archived_flag": ("exact",),
    }
    with_parents = django_filters.BooleanFilter(method="with_parents_hook")
    def with_parents_hook(self, queryset, name, value):
        pass
Вопрос в том, как обеспечить, чтобы фильтр with_parents запускался после archived_flag (и любых других полевых фильтров), чтобы работать уже с конечным набором результатов.
Почему так происходит
Фильтры, реализованные через метод, участвуют в том же конвейере фильтрации, что и любые другие поля. Если шаг с предками должен учитывать уже отфильтрованный результат, его не стоит исполнять как обычный полевой фильтр внутри этого конвейера. Вместо этого его нужно применять после того, как сформирован базовый queryset.
Решение
Объявите переключатель как обычный BooleanFilter без метода. Затем переопределите свойство qs: сначала получите полностью отфильтрованный queryset, и только потом, если переключатель установлен, примените логику предков к финальному queryset.
import django_filters
class NodeFilters(django_filters.FilterSet):
    with_parents = django_filters.BooleanFilter(required=False)
    class Meta:
        model = CatalogItem
        fields = {"archived_flag": ["exact"]}
    @property
    def qs(self):
        base_qs = super().qs
        if self.form.cleaned_data.get("with_parents"):
            base_qs = self._expand_with_parents(base_qs)
        return base_qs
    def _expand_with_parents(self, queryset):
        pass
Таким образом, стандартные фильтры выполняются первыми через super().qs. И лишь затем, если параметр with_parents в отправленных данных имеет истинное значение, результат дополняется вызовом _expand_with_parents.
Почему это важно
Когда смысл фильтра зависит от исхода других фильтров, решающим становится порядок. Применяя шаг с предками последним, мы гарантируем, что он работает с корректной, финальной выборкой — именно это нужно, когда включение зависит от того, какие дочерние элементы пережили предыдущие ограничения.
Итоги
Используйте неметодный BooleanFilter для переключателя и перенесите условную логику в свойство qs. Сначала получите полностью отфильтрованный queryset из super().qs, затем добавьте расширение по предкам только по запросу. Такой подход делает фильтрацию предсказуемой и позволяет заключительному шагу опираться на реальный итоговый набор.