Last active
June 10, 2016 08:41
-
-
Save vdboor/c2d87672132f21f4bb7e5d62e659da2d to your computer and use it in GitHub Desktop.
Django list filters - provide a way to have filtering in lists. The behavior is completely form-driven, making it possible to write custom `filter_FIELDNAME()` methods in the form if needed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django import forms | |
from django.db.models import QuerySet, Q | |
class ListFilterForm(forms.Form): | |
""" | |
Base form for lists to add a search feature. | |
Tips: | |
* Make sure fields are not required. | |
* Use :class:`~django.forms.MultipleChoiceField` for checkboxes. | |
* Use :class:`~apps.base.forms.widgets.FilterSelectMultiple` for checkboxes as dropdowns. | |
""" | |
query = forms.CharField(required=False, widget=forms.HiddenInput) | |
#: Define how form fields map to queryset fields. | |
#: Any field that is not mentioned here, should be handled manually in :meth:`apply_filters` | |
queryset_fields = None | |
def __init__(self, *args, **kwargs): | |
self.search_fields = kwargs.pop('search_fields', ()) | |
super(ListFilterForm, self).__init__(*args, **kwargs) | |
def _is_undefined(self, form_field, value): | |
# Allow explicit False for NullBooleanSelect, or empty QuerySet. | |
if value is None: | |
return True | |
elif isinstance(value, (list, tuple)): | |
return not value | |
elif isinstance(value, QuerySet): | |
# forms.ModelMultipleChoiceField() returns qs.none() when the value is not defined. | |
# Treat that as an None value. Don't evaluate the queryset. | |
# When a valid queryset is given, make sure it's used as subquery only. | |
return value.query.is_empty() | |
else: | |
return False | |
def get_queryset_fields(self): | |
""" | |
Return which fields are filtered by the form. | |
By default, any field that is declared in the form, | |
will be used as a filter, unless there is a custom ``filter_`` method for it. | |
:return: A dict of ``{'form field name': 'model field name'}`` | |
""" | |
# This only contains statically defined fields, | |
# because the other mixins add fields dynamically during init. | |
if self.queryset_fields is None: | |
return dict((f, f) for f in self.fields.keys()) | |
else: | |
return self.queryset_fields | |
def apply_filters(self, queryset): | |
""" | |
Apply the filters for all selected fields. Features: | |
* AND filter support | |
* IN filter support | |
* Find :samp:`filter_{field-name}()` methods to apply filters. | |
* Moved logic to the form, so advanced filters are easy to add. | |
""" | |
filter_args = [] | |
filter_kwargs = {} | |
qs_filters = self.get_queryset_fields() | |
qs_filters['query'] = 'query' # ensure that's always there. | |
for form_field, model_field in qs_filters.iteritems(): | |
value = self.cleaned_data.get(form_field) | |
if self._is_undefined(form_field, value): | |
continue | |
# Allow a custom filter | |
filter_method = getattr(self, 'filter_{0}'.format(form_field), None) | |
if filter_method is not None: | |
# Apply the filter directly. | |
queryset = filter_method(queryset) | |
assert isinstance(queryset, QuerySet), "{0}() broke the queryset".format(filter_method.__name__) | |
continue | |
# Standard handling, depending on the value type. | |
if isinstance(value, Q): | |
# Add to args | |
filter_args.append(value) | |
elif isinstance(value, (list, tuple, QuerySet)): | |
# Add to kwargs, but with 'in' | |
filter_kwargs["{0}__in".format(model_field)] = value | |
else: | |
# Add to kwargs. | |
filter_kwargs[model_field] = value | |
return queryset.filter(*filter_args, **filter_kwargs) | |
def filter_query(self, queryset): | |
""" | |
Update the queryset with the results from the 'query' text field. | |
This method is called via the general apply_filters code. | |
""" | |
query = self.cleaned_data['query'] | |
if not query: | |
return queryset | |
query_words = query.split() | |
filters = None | |
for f in self.search_fields: | |
word_filters = None | |
for word in query_words: | |
q = Q(**{f + '__icontains': word}) | |
word_filters = word_filters & q if word_filters else q | |
filters = filters | word_filters if filters else word_filters | |
if filters is not None: | |
queryset = queryset.filter(filters) | |
return queryset |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import logging | |
from django.http import QueryDict | |
from django.utils.functional import cached_property | |
from django.views.generic.edit import FormMixin | |
from apps.utils.list_filters.forms import ListFilterForm | |
logger = logging.getLogger(__name__) | |
class ListFilterMixin(FormMixin): | |
""" | |
Extension to make sure lists can be properly filtered with a search box, | |
""" | |
#: The form used for filtering the list | |
form_class = ListFilterForm | |
#: The fields where the "query" field will seek through | |
search_fields = () | |
def get_form_kwargs(self): | |
kwargs = super(ListFilterMixin, self).get_form_kwargs() | |
if self.request.method == 'GET' and self.request.GET: | |
kwargs['data'] = self.request.GET | |
else: | |
kwargs['data'] = QueryDict() | |
kwargs['search_fields'] = self.get_search_fields() | |
return kwargs | |
@cached_property | |
def form(self): | |
# Since there is no standard "setup()" step in views, | |
# turn this into an "initialize on demand" property. | |
form_class = self.get_form_class() | |
return self.get_form(form_class) | |
def get_base_queryset(self): | |
"""Helper to get ListView default queryset.""" | |
return super(ListFilterMixin, self).get_queryset() | |
def get_queryset(self): | |
""" | |
Returns the filtered queryset. | |
""" | |
queryset = self.get_base_queryset() | |
form = self.form | |
if form.is_valid(): | |
queryset = form.apply_filters(queryset) | |
else: | |
logger.debug("Invalid form data for list: %s", form.errors.as_data()) | |
return queryset | |
def get_context_data(self, **kwargs): | |
context = super(ListFilterMixin, self).get_context_data(**kwargs) | |
context['form'] = self.form | |
return context | |
def get_search_fields(self): | |
# Allow to override per request. | |
return self.search_fields |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment