Фильтр по URL Kwargs при использовании Django FilterSets
У меня есть конечная точка, которая может следовать такому формату:
www.example.com/ModelA/2/ModelB/5/ModelC?word=hello
Модель C имеет FK к B, которая имеет FK к A. Я должен видеть только те C, которые соответствуют одним и тем же A и B одновременно. В приведенном выше примере мы должны...
- отфильтруйте C по тем, у кого FK на B id = 5 и A id = 2 .
- также отфильтруйте 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"])