philipn/django-rest-framework-filters

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.