Combine several filters into one filter() with django-filters
-
27-02-2021 - |
Вопрос
I'm using django-filter app. There is however one problem I do not know how to solve. It's almost exactly the same thing as is described in django documentation:
https://docs.djangoproject.com/en/1.2/topics/db/queries/#spanning-multi-valued-relationships
I want to make a query where I select all Blogs that has an entry with both "Lennon" in headline and was published in 2008, eg.:
Blog.objects.filter(entry__headline__contains='Lennon',
entry__pub_date__year=2008)
Not to select Blogs that has an entry with "Lennon" in headline and another entry (possibly the same) that was published in 2008:
Blog.objects.filter(entry__headline__contains='Lennon').filter(
entry__pub_date__year=2008)
However, if I set up Filter such that there are two fields (nevermind __contains x __exact, just an example):
class BlogFilter(django_filters.FilterSet):
entry__headline = django_filters.CharFilter()
entry__pub_date = django_filters.CharFilter()
class Meta:
model = Blog
fields = ['entry__headline', 'entry__pub_date', ]
django-filter will generete the latter:
Blog.objects.filter(entry__headline__exact='Lennon').filter(
entry__pub_date__exact=2008)
Is there a way to combine both filters into a single filter field?
Решение
Well, I came with a solution. It is not possible to do using the regular django-filters, so I extended it a bit. Could've been improved, this is quick-n-dirty solution.
1st added a custom "grouped" field to django_filters.Filter and a filter_grouped method (almost copy of filter method)
class Filter(object):
def __init__(self, name=None, label=None, widget=None, action=None,
lookup_type='exact', required=False, grouped=False, **kwargs):
(...)
self.grouped = grouped
def filter_grouped(self, qs, value):
if isinstance(value, (list, tuple)):
lookup = str(value[1])
if not lookup:
lookup = 'exact' # we fallback to exact if no choice for lookup is provided
value = value[0]
else:
lookup = self.lookup_type
if value:
return {'%s__%s' % (self.name, lookup): value}
return {}
the only difference is that instead of creating a filter on query set, it returns a dictionary.
2nd updated BaseFilterSet qs method/property:
class BaseFilterSet(object):
(...)
@property
def qs(self):
if not hasattr(self, '_qs'):
qs = self.queryset.all()
grouped_dict = {}
for name, filter_ in self.filters.iteritems():
try:
if self.is_bound:
data = self.form[name].data
else:
data = self.form.initial.get(name, self.form[name].field.initial)
val = self.form.fields[name].clean(data)
if filter_.grouped:
grouped_dict.update(filter_.filter_grouped(qs, val))
else:
qs = filter_.filter(qs, val)
except forms.ValidationError:
pass
if grouped_dict:
qs = qs.filter(**grouped_dict)
(...)
return self._qs
The trick is to store all "grouped" filters in a dictionary and then use them all as a single filter.
The filter will look something like this then:
class BlogFilter(django_filters.FilterSet):
entry__headline = django_filters.CharFilter(grouped=True)
entry__pub_date = django_filters.CharFilter(grouped=True)
class Meta:
model = Blog
fields = ['entry__headline', 'entry__pub_date', ]