Django REST Framework - Filtering

2019-01-28 13:12发布

问题:

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

回答1:

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)


回答2:

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