Since filtering by non-field attributes such as property inevitably converts the QuerySet to list (or similar), I like to postpone it and do the filtering on object_list in get_context_data method. To keep the filtering logic inside the filterset class, I use a simple trick. I've defined a decorator
def attr_filter(func):
def wrapper(self, queryset, name, value, force=False, *args, **kwargs):
if force:
return func(self, queryset, name, value, *args, **kwargs)
else:
return queryset
return wrapper
which is used on django-filter non-field filtering methods. Thanks to this decorator, the filtering basically does nothing (or skips) the non-field filtering methods (because of force=False default value).
Next, I defined a Mixin to be used in the view class.
class FilterByAttrsMixin:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
filtered_list = self.filter_qs_by_attributes(self.object_list, self.filterset)
context.update({
'object_list': filtered_list,
})
return context
def filter_qs_by_attributes(self, queryset, filterset_instance):
if hasattr(filterset_instance.form, 'cleaned_data'):
for field_name in filter_instance.filters:
method_name = f'attr_filter_{field_name}'
if hasattr(filterset_instance, method_name):
value = filterset_instance.form.cleaned_data[field_name]
if value:
queryset = getattr(filterset_instance, filter_method_name)(queryset, field_name, value, force=True)
return queryset
It basically just returns to your filterset and runs all methods called attr_filter_<field_name>, this time with force=True.
In summary, you need to:
- Inherit the
FilterByAttrsMixin in your view class
- call your filtering method
attr_filter_<field_name>
- use
attr_filter decorator on the filtering method
Simple example (given that I have model called MyModel with property called is_static that I want to filter by:
model:
class MyModel(models.Model):
...
@property
def is_static(self):
...
view:
class MyFilterView(FilterByAttrsMixin, django_filters.views.FilterView):
...
filterset_class = MyFiltersetClass
...
filter:
class MyFiltersetClass(django_filters.FilterSet):
is_static = django_filters.BooleanFilter(
method='attr_filter_is_static',
)
class Meta:
model = MyModel
fields = [...]
@attr_filter
def attr_filter_is_static(self, queryset, name, value):
return [instance for instance in queryset if instance.is_static]