Source code for djcrud.views.filter
import functools
import django_filters
from django import forms
from django.db import models
from django.utils.translation import gettext as _
[docs]
class FilterMixin:
"""django-filter FilterSet for explicit model field filters on list views.
Attributes:
filter_fields (list[str] | None): Model field names passed to the
dynamic FilterSet ``Meta.fields``. ``None`` means no field filters.
filter_form_class (type | None): When set on a subclass, replaces the
composed filter form entirely (see :class:`~djcrud.views.list.ListView`).
filter_target (str): Unpoly ``up-target`` for the filter form.
"""
filter_fields = None
filter_form_class = None
filter_target = "[up-list]"
@property
def filter_submit_label(self):
"""Label for the filter form submit button."""
return _("Apply")
@functools.cached_property
def filterset_class(self):
return self.get_filterset_class()
@functools.cached_property
def filterset(self):
if not self.filter_fields:
return None
filterset = self.filterset_class(**self.get_filterset_kwargs())
self._sync_filter_widgets(filterset)
self._prune_empty_fk_choices(filterset)
return filterset
def _sync_filter_widgets(self, filterset):
"""Use model ``formfield()`` widgets (e.g. djhacker/DAL) on filter fields."""
for name, form_field in filterset.form.fields.items():
try:
model_field = self.model._meta.get_field(name)
except Exception:
continue
if not isinstance(
model_field,
(models.ForeignKey, models.OneToOneField, models.ManyToManyField),
):
continue
patched = model_field.formfield()
form_field.widget = patched.widget
if getattr(patched, "queryset", None) is not None:
form_field.queryset = patched.queryset
def get_filterset_kwargs(self):
return {
"data": self.request.GET.copy(),
"request": self.request,
"queryset": super(FilterMixin, self).get_queryset(),
}
def get_filterset_meta_filter_overrides(self):
return {
models.CharField: {
"filter_class": django_filters.CharFilter,
"extra": lambda f: {
"lookup_expr": "icontains",
},
},
}
def get_filterset_form_class(self):
return forms.Form
def get_filterset_meta_attributes(self):
return dict(
model=self.model,
fields=self.filter_fields,
filter_overrides=self.get_filterset_meta_filter_overrides(),
form=self.get_filterset_form_class(),
)
def get_filterset_meta_class(self):
return type("Meta", (), self.get_filterset_meta_attributes())
def get_filterset_extra_class_attributes(self):
extra = {}
for field_name in self.filter_fields:
try:
field = self.model._meta.get_field(field_name)
except Exception:
continue
choices = getattr(field, "choices", None)
if choices is None:
continue
extra[field_name] = django_filters.MultipleChoiceFilter(
choices=choices,
)
return extra
def get_filterset_class_attributes(self):
attrs = dict(Meta=self.get_filterset_meta_class())
attrs.update(self.get_filterset_extra_class_attributes())
return attrs
def get_filterset_class(self):
return type(
f"{self.model.__name__}FilterSet",
(django_filters.FilterSet,),
self.get_filterset_class_attributes(),
)
def _prune_empty_fk_choices(self, filterset):
"""Hide FK choices that would return no rows (crudlfap pattern)."""
for name, field in filterset.form.fields.items():
try:
mf = self.model._meta.get_field(name)
except Exception:
continue
if not isinstance(mf, models.ForeignKey):
continue
field.queryset = field.queryset.annotate(
c=models.Count(mf.related_query_name()),
).filter(c__gt=0)
@property
def filter_attributes(self):
"""HTML attributes for the filter ``<form>`` tag."""
return {
"up-submit": True,
"up-target": self.filter_target,
"up-history": True,
}