carltongibson/django-filter

Similar Filters with different lookup expressions

Closed this issue · 5 comments

Quite often I declare filters inside a FilterSet class that are similar in nature, but just use different different lookup expressions, such as:

class MyFilterSet(FilterSet):
   filter1 = CharFilter(label="Filter 1", lookup_expr="exact", method=...)
   filter2 = CharFilter(label="Filter 2", lookup_expr="icontains", method=...)

If this filter would be part of a django model, I can declare it in the Meta class, such as:

class Meta:
   fields = {"filter1": ["exact", "icontains"]}

Ideally I would like to also make it possible to add a list of lookup expressions to the declared filters, to avoid redefining similar filters. Is this something that is possible somehow, or is this not a good idea?

Check out LookupChoiceFilter.

price = django_filters.LookupChoiceFilter(
    field_class=forms.DecimalField,
    lookup_choices=[
        ('exact', 'Equals'),
        ('gt', 'Greater than'),
        ('lt', 'Less than'),
    ]
)

Check out LookupChoiceFilter.

price = django_filters.LookupChoiceFilter(
    field_class=forms.DecimalField,
    lookup_choices=[
        ('exact', 'Equals'),
        ('gt', 'Greater than'),
        ('lt', 'Less than'),
    ]
)

Looks like exactly what I want. Thanks a lot for the quick answer.

@carltongibson Just as a small Feedback: It turned out that it wasn't exactly what I needed. But I used the same idea to create a wrapper to essentially duplicate the filters with different lookup_expr set. As I wanted the exact same behavior as the filter declaration from cls.Meta.

Thanks for your help regardless

@christopher-wittlinger No problem! If you fancy you could paste your solution for others following.

@carltongibson Thanks for following up! Here's what I came up with:

Custom Filter Implementation

I created a new filter to handle multiple lookup expressions:

from django_filters import Filter

class MultipleLookupFilter(Filter):
    def __init__(self, field_class, lookup_expr, **kwargs):
        self.field_class = field_class
        self.lookup_expr = lookup_expr
        self.kwargs = kwargs

    def get_filters(self, field_name) -> dict[str, Filter]:
        filters = {}
        for lookup_expr in self.lookup_expr:
            filters[f"{field_name}__{lookup_expr}"] = self.field_class(
                lookup_expr=lookup_expr,
                **self.kwargs,
            )
        return filters

Meta Class Modification

I then modified the get_declared_filters class method in the Meta class to integrate the new filter:

@classmethod
def get_declared_filters(cls, bases, attrs):
    filters = super().get_declared_filters(bases, attrs)

    # Collect and process MultipleLookupFilters
    multi_filters: list[tuple[str, MultipleLookupFilter]] = [
        (filter_name, attrs.pop(filter_name))
        for filter_name, obj in list(attrs.items())
        if isinstance(obj, MultipleLookupFilter)
    ]

    for field_name, filter_obj in multi_filters:
        filters |= filter_obj.get_filters(field_name)

    return filters

Example Usage

Here’s how I use the new filter:

activity_heat = MultipleLookupFilter(
    field_class=filters.NumberFilter,
    lookup_expr=["gte", "lte"],
    label=_("Activity Heat"),
    precision=2,
    field_name="activity_heat",
)

This setup works perfectly for my use case. Ideally, it could be extended to support multiple methods and additional functionality if needed.