nutship
nutship

Reputation: 4924

Altering django-filter default behaviour

This is a django-filter app specific guestion.

Has anyone tried to introduce conditions for the filters to query according to the condition?

Let me give an example:

Suppose we have a Product model. It can be filtered according to its name and price.

The default django-filter behaviour is that, as we use more filters and chain them together, they filter data using AND statements (it narrows the search).

I'd like to change this behaviour and add a ChoiceFilter, say with two options: AND as well as OR. From this point, the filter should work according to what a user have selected.

Eg. if a user query for products with name__startswith="Juice" OR price__lte=10.00, it should list all the products with names starting with Juice as well as products with price below 10.00.

Django-filter docs say that the filter can take an argument:

action

An optional callable that tells the filter how to handle the queryset. It recieves a 
QuerySet and the value to filter on and should return a Queryset that is filtered 
appropriately.

which seems to be what I am looking for, but the docs lacks any further explanation. Suggestions please?

@EDIT:

This is views.py:

def product_list(request):
    f = ProductFilter(request.GET, queryset=Product.objects.all())
    return render_to_response('my_app/template.html', {'filter': f})

Upvotes: 2

Views: 3735

Answers (4)

Anshik
Anshik

Reputation: 711




class FileFilterSet(django_filters.FilterSet):
    class Meta:
        model = File
        fields = ['project']

    def __init__(self, *args, **kwargs):
        super(FileFilterSet, self).__init__(*args, **kwargs)

        for name, field in self.filters.items():
            if isinstance(field, ModelChoiceFilter):
                field.extra['empty_label'] = None
                field.extra['initial'] = Project.objects.get(pk=2)
                # field.extra['queryset'] = Project.objects.filter(pk=2)


class FileFilter(FilterView):
    model = File
    template_name = 'files_list.html'
    filterset_class = FileFilterSet

Upvotes: 1

mariodev
mariodev

Reputation: 15484

action won't cut it. This callback is used for particular filter field and only has access to that field's value.

The cleanest way would be to create multi-widget filter field, similar to RangeField. Check out the source.

So instead two date fields you use name, price and the logic type [AND|OR] as fields, this way you have access to all these values at once to use in custom queryset.

EDIT 1:

This is a little gist I wrote to show how to query two fields with selected operator. https://gist.github.com/mariodev/6689472

Usage:

class ProductFilter(django_filters.FilterSet):
    nameprice = NamePriceFilter()

    class Meta:
        model = Product
        fields = ['nameprice']

It's actually not very flexible in terms of re-usage, but certainly can be re-factored to make it useful.

Upvotes: 2

Vladimir Sidorenko
Vladimir Sidorenko

Reputation: 4265

In order to make filters work with OR, you should make a subclass of FilterSet and override qs from Tim's answer like this:

@property
def qs(self):
    qs = self.queryset.none()
    for filter_ in self.filters():
        qs |= filter_.filter(self.queryset.all())

I haven't tested this, but I think you got the idea. QuerySets support bitwise operations, so you can easily combine results of two filters with OR.

Upvotes: 2

Tim Heap
Tim Heap

Reputation: 1711

Because of the way the final queryset is constructed, making each filter be ORed together is difficult. Essentially, the code works like this:

FilterSet, filterset.py line 253:

@property
def qs(self):
    qs = self.queryset.all()
    for filter_ in self.filters():
        qs = filter_.filter(qs)

Filters, filters.py line 253:

def filter(self, qs):
    return qs.filter(name=self.value)

Each filter can decide how to apply itself to the incoming queryset, and all filters, as currently implemented, filter the incoming queryset using AND. You could make a new set of filters that OR themselves to the incoming queryset, but there is no way of overriding the behaviour from the FilterSet side.

Upvotes: 4

Related Questions