Question

I want to filter multiple fields with multiple queries like this:

api/listings/?subburb=Subburb1, Subburb2&property_type=House,Apartment,Townhouse,Farm .. etc

Are there any built in ways, I looked at django-filters but it seems limited, and I think I would have to do this manually in my api view, but its getting messy, filtering on filters on filters

Was it helpful?

Solution

filtering on filters on filters is not messy it is called chained filters.

And chain filters are necessary because sometime there is going to be property_type some time not:

if property_type:
    qs = qs.filter(property_type=property_type)

If you are thinking there is going to be multiple queries then not, it will still executed in one query because queryset are lazy.

Alternatively you can build a dict and pass it just one time:

d = {'property_type:': property_type, 'subburb': subburb}
qs = MyModel.objects.filter(**d)

OTHER TIPS

Complex filters are not out of the box supported by DRF or even by django-filter plugin. For simple cases you can define your own get_queryset method

This is straight from the documentation

def get_queryset(self):
    queryset = Purchase.objects.all()
    username = self.request.query_params.get('username', None)
    if username is not None:
        queryset = queryset.filter(purchaser__username=username)
    return queryset

However this can quickly become messy if you are supported multiple filters and even some of them complex.

The solution is to define a custom filterBackend class and a ViewSet Mixin. This mixins tells the viewset how to understand a typical filter backend and this backend can understand very complex filters all defined explicitly, including rules when those filters should be applied.

A sample filter backend is like this (I have defined three different filters on different query parameters in the increasing order of complexity:

class SomeFiltersBackend(FiltersBackendBase):
    """
    Filter backend class to compliment GenericFilterMixin from utils/mixin.
    """
    mapping = {'owner': 'filter_by_owner',
               'catness': 'filter_by_catness',
               'context': 'filter_by_context'}
    def rule(self):
        return resolve(self.request.path_info).url_name == 'pet-owners-list'

Straight forward filter on ORM lookups.

    def filter_by_catness(self, value):
        """
        A simple filter to display owners of pets with high catness, canines excuse. 
        """
        catness = self.request.query_params.get('catness')
        return Q(owner__pet__catness__gt=catness)

    def filter_by_owner(self, value):
        if value == 'me':
            return Q(owner=self.request.user.profile)
        elif value.isdigit():
            try:
                profile = PetOwnerProfile.objects.get(user__id=value)
            except PetOwnerProfile.DoesNotExist:
                raise ValidationError('Owner does not exist')
            return Q(owner=profile)
        else:
            raise ValidationError('Wrong filter applied with owner')

More complex filters :

def filter_by_context(self, value):
    """
    value = {"context_type" : "context_id or context_ids separated by comma"}
    """
    import json
    try:
        context = json.loads(value)
    except json.JSONDecodeError as e:
        raise ValidationError(e)

    context_type, context_ids = context.items()
    context_ids = [int(i) for i in context_ids]
    if context_type == 'default':
        ids = context_ids
    else:
        ids = Context.get_ids_by_unsupported_contexts(context_type, context_ids)
    else:
        raise ValidationError('Wrong context type found')
    return Q(context_id__in=ids)

To understand fully how this works, you can read up my detailed blogpost : http://iank.it/pluggable-filters-for-django-rest-framework/

All the code is there in a Gist as well : https://gist.github.com/ankitml/fc8f4cf30ff40e19eae6

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top