سؤال

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})
هل كانت مفيدة؟

المحلول 2

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.

نصائح أخرى

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.

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.




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

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top