Ability to handle all lookups for a non-field filter
hemache opened this issue · 3 comments
Hello guys,
I need to define a filter (full_name
) that will lookup multiple fields ('first_name', 'middle_name', 'last_name'
) at once based on a supplied lookup_expr
.
for example, /api/users/?full_name__startswith=test
will result queryset Q(first_name__startswith='test') | Q(middle_name__startswith='test') | Q(last_name__startswith='test')
is there a way to achieve this?
FULL_NAME_FIELD_NAMES = ['first_name', 'middle_name', 'last_name']
class UserFilter(filters.FilterSet):
# lookup FULL_NAME_FIELD_NAMES
full_name = filters.CharFilter(method='filter_full_name', lookups=filters.ALL_LOOKUPS)
class Meta:
model = Expert
fields = {
'email': ['exact', 'iexact', 'contains', 'icontains', 'startswith', 'istartswith', 'endswith', 'iendswith'],
'full_name': filters.ALL_LOOKUPS,
}
def filter_full_name(self, qs, name, value):
lookups = Q()
for field_name in FULL_NAME_FIELD_NAMES:
# how to access `lookup_expr` inside filter method?
lookups |= Q(**{
LOOKUP_SEP.join([field_name, lookup_expr]): value
})
return qs.filter(lookups)
What versions of django-filter and djangorestframework-filters are you using? For example, passing a list of lookups to a single filter is no longer supported - you would need to use the LookupChoiceFilter
.
What versions of django-filter and djangorestframework-filters are you using?
@rpkilby
I'm stuck with Python2.7
and Django1.11
so I'm using
django-filter==1.1.0
djangorestframework-filters==0.10.2.post0
First, note that these two things produce two different results
full_name = filters.CharFilter(method='filter_full_name', lookup_expr=filters.ALL_LOOKUPS)
class Meta:
fields = {
'full_name': filters.ALL_LOOKUPS,
}
The former creates a single filter that expects expects both a value and a lookup expression. e.g., your querystring would be (at least in v1.1.0) ?full_name_0=text&full_name_1=contains
. Note that this only supports one lookup at time - there is no way to combine both gte
and lte
lookups, although that wouldn't really make sense in this case.
The latter actually generates separate filters for each lookup. You would end up separate with full_name__contains
& full_name__icontains
filters. In this case, the lookup is part of the parameter name, and is not supplied as part of a second value.
Now, to your actual question... custom filter methods are only available to declared filters, not those generated with Meta.fields
. However, you have two options, and it's a matter of what kind of API you want.
If you just want a single filter, you can use the list form of lookup_expr
, as in the first example. However, the querystring will always look like ?full_name_0=text&full_name_1=icontains
class UserFilter(filters.FilterSet):
full_name = filters.CharFilter(method='filter_full_name', lookups=filters.ALL_LOOKUPS)
def filter_full_name(self, qs, name, value):
# value is actually a Lookup namedtuple.
# https://github.com/carltongibson/django-filter/blob/1.1.0/django_filters/fields.py#L76-L81
value, lookup_expr = value.value, value.lookup_type
lookup = Q()
for field_name in ['first_name', 'middle_name', 'last_name']:
lookup |= Q(**{LOOKUP_SEP.join([field_name, lookup_expr]): value})
return qs.filter(lookups)
The second option is to generate unique filters per lookup, where the lookup is embedded in the parameter name. e.g., ?full_name__icontains=text
. The problem is that in this case, the lookup_expr
is not provided to the filter method. However, method also accepts callables, and you can use a partial to bind the lookup_expr
to the function.
from functools import partial
def filter_full_name(qs, name, value, lookup_expr):
# value is just the validated value, not a Lookup namedtuple.
lookup = Q()
for field_name in ['first_name', 'middle_name', 'last_name']:
lookup |= Q(**{LOOKUP_SEP.join([field_name, lookup_expr]): value})
return qs.filter(lookups)
class UserFilter(filters.FilterSet):
full_name = filters.CharFilter(method=partial(filter_full_name, lookup_expr='exact'))
full_name__contains = filters.CharFilter(method=partial(filter_full_name, lookup_expr='contains'))
full_name__icontains = filters.CharFilter(method=partial(filter_full_name, lookup_expr='icontains'))
...
Happy to answer further questions, but am closing as this isn't an actual bug report.