Reputation: 4924
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
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
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
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
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