Brian C
Brian C

Reputation: 1437

Filter by URL Kwargs while using Django FilterSets

I have an endpoint that can follow this format:

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

Model C has a FK to B, which has a FK to A. I should only ever see C's that correspond to the same A and B at one time. In the above example, we should...

  1. filter C by those with an FK to B id = 5 and A id = 2
  2. also filter C by the field 'word' that contains hello.

I know how to use the filter_queryset() method to accomplish #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"])

I also know how to use a Filterset class to filter by fields on ModelC to accomplish #2

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

But I can only get one or the other to work. If I add filterset_class = ModelCFilterSet to ModelCViewSet then it no longer does #1, and without it, it does not do #2.

How do I accomplish both? Ideally I want all of this in the ModelCFilterSet

Note - As hinted by the use of GeoFilterSet I will (later on) be using DRF to add a GIS query, this is just a simplified example. So I think that restricts me to using FilterSet classes in some manner.

Upvotes: 0

Views: 768

Answers (2)

Brian C
Brian C

Reputation: 1437

Figured it out. So creating a filter_queryset() method overwrites the one that is in the GenericAPIView class (which I already knew).

However - that overwritten class is also responsible for using the FilterSet class that I defined. So by overwriting it, I also "broke" the FilterSet.

Solution was adding a super() to call the original class before the one I wrote:

    def filter_queryset(self, queryset):
        queryset = super().filter_queryset(queryset)
        return queryset.filter(asset=self.kwargs["asset"], asset__program=self.kwargs["program"])

Upvotes: 1

ansakoy
ansakoy

Reputation: 101

I'm not sure this would be of help in your situation, but I often use nested urls in DRF in a way that would be convenient to perform the task. I use a library called drf-nested-routers that does part of the job, namely keeps track of the relations by the provided ids. Let me show an example:

# 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')
...

In this case I can make a query like www.example.com/ModelA/2/ModelB/ that will only return the entries of ModelB that point to the object of ModelA with id 2. Likewise, www.example.com/ModelA/2/ModelB/5 will return only the corresponding object of ModelB in case it is related to ModelA-id2. A further level for ModelC would act correspondingly.

Sticking to the example, by now we have filtered the entries of ModelB related to a particular object of ModelA, that is we have received the relevant queryset. Next, we have to search for a particular subset within this queryset, and here's where FilterSet comes in play. The easiest way to customise its behaviour is by writing specific methods.

# 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)

In fact, you don't even have to use a method here; the way you pasted would work as well (word = CharFilter(field_name='word', lookup_expr='icontains')), I just wanted to point out that there is such an option too.

The filter starts its job with the queryset that has already been processed by the viewset and now it will just narrow down our sample using the given parameter.

I haven't tried this with a three-level nested URL, only checked on the example of two levels, but I think the third level should act in the same way.

Upvotes: 1

Related Questions