MarkD
MarkD

Reputation: 4944

django-rest-framework- Filtering using 'or' on multiple values from one url parameter

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?

Upvotes: 5

Views: 2684

Answers (2)

Josh Correia
Josh Correia

Reputation: 4346

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.

Upvotes: 1

Gabriel Muj
Gabriel Muj

Reputation: 3805

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

Upvotes: 6

Related Questions