5

I have a tagging system in place for a model that my API exposes. The models look something like this:

class TaggableModel(models.Model):
    name = models.CharField(max_length=255)
    tags = models.ManyToManyField(Tag, related_name="taggable_models")

class Tag(models.Model):
    tag = models.CharField(max_length=32)

I've then set up a serializer and view that look like:

class TaggableModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = TaggableModel
        fields = ('id', 'name', 'tags',)
        read_only_fields = ('id',)

class TaggableModelViewSet(viewsets.ModelViewSet):
    queryset = TaggableModel.objects.all()
    serializer_class = TaggableModelSerializer
    permission_classes = (AllowAny,)
    filter_backend = [DjangoFilterBackend]
    filterset_fields = ['tags']

If I want to grab all TaggableModels that have tag ids 1, 2, or 3, I can do so via:

https://my-api-domain/api/taggable-models?tags=1&tags=2&tags=3

Is there a way to split on a delimiter, so I can pass it all as one parameter? e.g.:

https://my-api-domain/api/taggable-models?tags=1,2,3

It looks like I can write my own custom DjangoFilterBackend filters, but I am a bit unsure as to where to start. Or perhaps there is an easier way to accomplish this?

2 Answers 2

6

Sure you can do this by having custom filterset class with specific field 'widget' (that's how it is called in django-filters)

Here's a sample you can try:

# filters.py

from django_filters.rest_framework import FilterSet, filters
from django_filters.widgets import CSVWidget

from .your_models import Tag, TaggableModel

class TaggableModelFilterSet(FilterSet):
    tags = filters.ModelMultipleChoiceFilter(
        queryset=Tag.objects.all(), widget=CSVWidget,
        help_text=_("A list of ids, comma separated, identifying tags"),
        method='filter_tags'
    )

    class Meta:
        model = TaggableModel
        fields = ['tags']

    def filter_tags(self, queryset, name, value):
        if value:
            queryset = queryset.filter(tags__in=value)
        return queryset
# views.py

class TaggableModelViewSet(viewsets.ModelViewSet):
    queryset = TaggableModel.objects.all()
    serializer_class = TaggableModelSerializer
    permission_classes = (AllowAny,)
    filter_backends = [DjangoFilterBackend]
    filter_class = TaggableModelFilterSet
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for this, it isn't well documented. For someone looking to just convert their existing field: the key settings that make this work are: specifying ModelMultipleChoiceFilter, Including the queryset for the object and setting the widget.
1

There is an even simpler way to achieve this using the django-filter package. Deep within the django-filter documentation, it mentions that you can use "a dictionary of field names mapped to a list of lookups".

Your code would be updated like so:

# views.py

from django_filters.rest_framework import DjangoFilterBackend

class TaggableModelViewSet(viewsets.ModelViewSet):
    queryset = TaggableModel.objects.all()
    serializer_class = TaggableModelSerializer
    permission_classes = (AllowAny,)
    filter_backend = [DjangoFilterBackend]
    filterset_fields = {
        'tags': ["in", "exact"] # note the 'in' field
    }

Now in the URL you would add __in to the filter before supplying your list of parameters and it would work as you expect:

https://my-api-domain/api/taggable-models?tags__in=1,2,3

The django-filter documentation on what lookup filters are available is quite poor, but the in lookup filter is mentioned in the Django documentation itself.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.