Search code examples
pythondjangodjango-filterdrf-queryset

Pass additional attribute to django-filter


I'm using django-filter together with DRF. I have a favourite-model, which is linked to several other models through a GenericRelation. To filter for entries which have a favourite-flag, I've created a custom FavouriteFilter, which I add to the respective model. I would like to query for the content_type_id of the respective model in order to limit the results from Favourite. However, I don't know how I can pass down the model to the filter-method in the FavouriteFilter.

Here's a code snippet to illustrate the issue:

class ProjectFilter(BaseFilter):

    favourite_only = FavouriteFilter()


class FavouriteFilter(django_filters.BooleanFilter):
    """
    A custom filter which returns a users favourites of an element
    """

    def __init__(self, *args, **kwargs):
        # gettext_lazy breaks the OpenAPI generation => use gettext instead
        kwargs['label'] = gettext("My favourites")
        super(FavouriteFilter, self).__init__(*args, **kwargs)

    def filter(self, qs, value):
        if value == True:
            user = get_current_user()
            content_type = ContentType.objects.get_for_model(<model>)
            return qs.filter(pk__in=Favourite.objects
                             .filter(owner_id=user)
                             .filter(content_type_id=content_type)
                             .values_list('object_id', flat=True)
                             )
        else:
            return qs

In this example, the <model>-attribute is missing. How can I pass down this information from Project to the filter?


Solution

  • Keyword arguments can be passed down to the filter, but they need to be removed from the kwarg-dict before the super()-method is called. Otherwise they get passed on to the superclass, the superclass's __init__()-method doesn't know the keyword and a TypeError is thrown:

    TypeError: __init__() got an unexpected keyword argument 'model'
    

    In the example above, the superclass is django_filters.BooleanFilter respectively django_filters.Filter.

    Using the dict.pop()-method, the keyword is removed from the kwargs-dictionary and at the same time we can save it for further use. Since content_type never changes after initialization, it can already be set in __init__().

    Here's a working example of the code above, where Project is the django-model I want to pass down to the filter:

    class ProjectFilter(BaseFilter):
    
        favourite_only = FavouriteFilter(model=Project)
    
    
    class FavouriteFilter(django_filters.BooleanFilter):
        """
        A custom filter which returns a users favourites of an element
        """
    
        def __init__(self, *args, **kwargs):
            # gettext_lazy breaks the OpenAPI generation => use gettext instead
            kwargs['label'] = gettext("My favourites")
            model = kwargs.pop('model')
            self.content_type = ContentType.objects.get_for_model(model)
            super(FavouriteFilter, self).__init__(*args, **kwargs)
    
        def filter(self, qs, value):
            if value == True:
                user = get_current_user()
                return qs.filter(pk__in=Favourite.objects
                                 .filter(owner_id=user)
                                 .filter(content_type_id=self.content_type)
                                 .values_list('object_id', flat=True)
                                 )
            else:
                return qs
    

    For my specific use-case, where I'm looking for the model that is using the filter, the model is available through the queryset as qs.model. The code-snippet looks like this:

    class ProjectFilter(BaseFilter):
    
        favourite_only = FavouriteFilter()
    
    
    class FavouriteFilter(django_filters.BooleanFilter):
        """
        A custom filter which returns a users favourites of an element
        """
    
        def __init__(self, *args, **kwargs):
            # gettext_lazy breaks the OpenAPI generation => use gettext instead
            kwargs['label'] = gettext("My favourites")
            super(FavouriteFilter, self).__init__(*args, **kwargs)
    
        def filter(self, qs, value):
            if value == True:
                user = get_current_user()
                content_type = ContentType.objects.get_for_model(qs.model)
                return qs.filter(pk__in=Favourite.objects
                                 .filter(owner_id=user)
                                 .filter(content_type_id=content_type)
                                 .values_list('object_id', flat=True)
                                 )
            else:
                return qs