Фильтр по URL Kwargs при использовании Django FilterSets

У меня есть конечная точка, которая может следовать такому формату:

www.example.com/ModelA/2/ModelB/5/ModelC?word=hello

Модель C имеет FK к B, которая имеет FK к A. Я должен видеть только те C, которые соответствуют одним и тем же A и B одновременно. В приведенном выше примере мы должны...

  1. отфильтруйте C по тем, у кого FK на B id = 5 и A id = 2
  2. .
  3. также отфильтруйте C по полю 'word', которое содержит hello.

Я знаю, как использовать метод filter_queryset() для достижения #1:

class BaseModelCViewSet(GenericViewSet):
    queryset = ModelC.objects.all()

class ModelCViewSet(BaseModelCViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin):

    def filter_queryset(self, queryset):
        return queryset.filter(ModelB=self.kwargs["ModelB"], ModelB__ModelA=self.kwargs["ModelA"])

Я также знаю, как использовать класс Filterset для фильтрации по полям в ModelC для достижения #2

class ModelCFilterSet(GeoFilterSet):
    word = CharFilter(field_name='word', lookup_expr='icontains')

Но я могу заставить работать только одно или другое. Если я добавляю filterset_class = ModelCFilterSet к ModelCViewSet, то он больше не делает #1, а без него он не делает #2.

Как мне добиться того и другого? В идеале я хочу, чтобы все это было в ModelCFilterSet

Примечание - Как намекает использование GeoFilterSet я буду (позже) использовать DRF для добавления GIS запроса, это просто упрощенный пример. Поэтому я думаю, что это ограничивает меня в использовании классов FilterSet каким-либо образом.

Я не уверен, что это поможет в вашей ситуации, но я часто использую вложенные урлы в DRF таким образом, чтобы было удобно выполнять задачу. Я использую библиотеку под названием drf-nested-routers, которая делает часть работы, а именно отслеживает отношения по предоставленным идентификаторам. Позвольте мне показать пример:

# views.py

from rest_framework import exceptions, viewsets

class ModelBViewSet(viewsets.ModelViewSet):
    # This is a viewset for the nested part that depends on ModelA
    queryset = ModelB.objects.order_by('id').select_related('model_a_fk_field')
    serializer_class = ModelBSerializer
    filterset_class = ModelBFilterSet  # more about it below

    def get_queryset(self, *args, **kwargs):
        model_a_entry_id = self.kwargs.get('model_a_pk')
        model_a_entry = ModelA.objects.filter(id=model_a_entry_id).first()
        if not model_a_entry:
            raise exceptions.NotFound("MAYDAY")
        return self.queryset.filter(model_c_fk_field=model_a_entry)


class ModelAViewSet(viewsets.ModelViewSet):
    queryset = ModelA.objects.order_by('id')
    serializer_class = ModelASerializer


# urls.py

from rest_framework_nested import routers

router = routers.SimpleRouter()
router.register('model-a', ModelAViewSet, basename='model_a')
model_a_router = routers.NestedSimpleRouter(router, 'model-a', lookup='model_a')
model_a_router.register('model-b', ModelBViewSet, basename='model_b')
...

В этом случае я могу сделать запрос типа www.example.com/ModelA/2/ModelB/, который вернет только те записи ModelB, которые указывают на объект ModelA с id 2. Аналогично, www.example.com/ModelA/2/ModelB/5 вернет только соответствующий объект ModelB, если он связан с ModelA-id2. Следующий уровень для МоделиС будет действовать соответствующим образом.

Продолжая пример, к настоящему моменту мы отфильтровали записи ModelB, относящиеся к определенному объекту ModelA, то есть получили соответствующий набор запросов. Далее нам нужно найти определенное подмножество в этом наборе запросов, и вот здесь в игру вступает FilterSet. Самый простой способ настроить его поведение - это написать специальные методы.

# filters.py

import django_filters

class ModelBFilterSet(django_filters.FilterSet):
    word = django_filters.CharFilter(
        method="get_word",
    )

    def get_word(self, queryset, name, value):
        return queryset.filter(word__icontains=value)

На самом деле, здесь даже не обязательно использовать метод; способ, который вы вставили, тоже подойдет (word = CharFilter(field_name='word', lookup_expr='icontains')), я просто хотел указать, что такая возможность тоже есть.

Фильтр начинает свою работу с набором запросов, который уже был обработан набором представлений, и теперь он просто сузит нашу выборку с помощью заданного параметра.

Я не пробовал это с трехуровневым вложенным URL, проверил только на примере двух уровней, но думаю, что третий уровень должен действовать таким же образом.

Разобрался. Значит, создание метода filter_queryset() перезаписывает тот, который есть в классе GenericAPIView (что я уже знал).

Однако - этот перезаписанный класс также отвечает за использование класса FilterSet, который я определил. Поэтому, переписав его, я также "сломал" FilterSet.

Решением было добавление super() для вызова оригинального класса перед тем, который я написал:

    def filter_queryset(self, queryset):
        queryset = super().filter_queryset(queryset)
        return queryset.filter(asset=self.kwargs["asset"], asset__program=self.kwargs["program"])
Вернуться на верх