299 lines
9.8 KiB
Plaintext
299 lines
9.8 KiB
Plaintext
Using django-filter
|
|
===================
|
|
|
|
Django-filter provides a simple way to filter down a queryset based on
|
|
parameters a user provides. Say we have a ``Product`` model and we want to let
|
|
our users filter which products they see on a list page.
|
|
|
|
The model
|
|
---------
|
|
|
|
Let's start with our model::
|
|
|
|
from django.db import models
|
|
|
|
class Product(models.Model):
|
|
name = models.CharField(max_length=255)
|
|
price = models.DecimalField()
|
|
description = models.TextField()
|
|
release_date = models.DateField()
|
|
manufacturer = models.ForeignKey(Manufacturer)
|
|
|
|
The filter
|
|
----------
|
|
|
|
We have a number of fields and we want to let our users filter based on the
|
|
price or the release_date. We create a ``FilterSet`` for this::
|
|
|
|
import django_filters
|
|
|
|
class ProductFilter(django_filters.FilterSet):
|
|
class Meta:
|
|
model = Product
|
|
fields = ['price', 'release_date']
|
|
|
|
|
|
As you can see this uses a very similar API to Django's ``ModelForm``. Just
|
|
like with a ``ModelForm`` we can also override filters, or add new ones using a
|
|
declarative syntax::
|
|
|
|
import django_filters
|
|
|
|
class ProductFilter(django_filters.FilterSet):
|
|
price = django_filters.NumberFilter(lookup_type='lt')
|
|
class Meta:
|
|
model = Product
|
|
fields = ['price', 'release_date']
|
|
|
|
Filters take a ``lookup_type`` argument which specifies what lookup type to
|
|
use with `Django's ORM`_. So here when a user entered a price it would show all
|
|
Products with a price less than that.
|
|
|
|
.. _`Django's ORM`: https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups
|
|
|
|
**It's quite common to forget to set lookup type for `CharField`s/`TextField`s
|
|
and wonder why search for "foo" doesn't return result for "foobar". It's because
|
|
default lookup type is exact text, but you probably want `icontains` lookup
|
|
field.**
|
|
|
|
The FilterSet Meta class fields can additionally be set using a Dictionary to
|
|
specify multiple ``lookup_type`` filters without significant code duplication::
|
|
|
|
import django_filters
|
|
|
|
class ProductFilter(django_filters.FilterSet):
|
|
class Meta:
|
|
model = Product
|
|
fields = {'price': ['lt', 'gt'],
|
|
'release_date': ['exact'],
|
|
}
|
|
|
|
The above would generate 'price__lt', 'price__gt' and 'release_date' filters.
|
|
The filter lookup type keyword 'exact' is the default and therefore never added
|
|
to a filter name.
|
|
|
|
Items in the ``fields`` sequence in the ``Meta`` class may include
|
|
"relationship paths" using Django's ``__`` syntax to filter on fields on a
|
|
related model::
|
|
|
|
class ProductFilter(django_filters.FilterSet):
|
|
class Meta:
|
|
model = Product
|
|
fields = ['manufacturer__country']
|
|
|
|
Filters also take any arbitrary keyword arguments which get passed onto the
|
|
``django.forms.Field`` initializer. These extra keyword arguments get stored
|
|
in ``Filter.extra``, so it's possible to override the initializer of a
|
|
``FilterSet`` to add extra ones::
|
|
|
|
class ProductFilter(django_filters.FilterSet):
|
|
class Meta:
|
|
model = Product
|
|
fields = ['manufacturer']
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(ProductFilter, self).__init__(*args, **kwargs)
|
|
self.filters['manufacturer'].extra.update(
|
|
{'empty_label': 'All Manufacturers'})
|
|
|
|
Like ``django.contrib.admin.ModelAdmin``, it is possible to override
|
|
default filters for all the models fields of the same kind using
|
|
``filter_overrides``::
|
|
|
|
class ProductFilter(django_filters.FilterSet):
|
|
filter_overrides = {
|
|
models.CharField: {
|
|
'filter_class': django_filters.CharFilter,
|
|
'extra': lambda f: {
|
|
'lookup_type': 'icontains',
|
|
}
|
|
}
|
|
}
|
|
|
|
class Meta:
|
|
model = Product
|
|
fields = ['name']
|
|
|
|
|
|
The view
|
|
--------
|
|
|
|
Now we need to write a view::
|
|
|
|
def product_list(request):
|
|
f = ProductFilter(request.GET, queryset=Product.objects.all())
|
|
return render_to_response('my_app/template.html', {'filter': f})
|
|
|
|
If a queryset argument isn't provided then all the items in the default manager
|
|
of the model will be used.
|
|
|
|
If you want to access the filtered objects in your views, for example if you
|
|
want to paginate them, you can do that. They are in f.qs
|
|
|
|
The URL conf
|
|
------------
|
|
|
|
We need a URL pattern to call the view::
|
|
|
|
url(r'^list$', views.product_list)
|
|
|
|
The template
|
|
------------
|
|
|
|
And lastly we need a template::
|
|
|
|
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<form action="" method="get">
|
|
{{ filter.form.as_p }}
|
|
<input type="submit" />
|
|
</form>
|
|
{% for obj in filter %}
|
|
{{ obj.name }} - ${{ obj.price }}<br />
|
|
{% endfor %}
|
|
{% endblock %}
|
|
|
|
And that's all there is to it! The ``form`` attribute contains a normal
|
|
Django form, and when we iterate over the ``FilterSet`` we get the objects in
|
|
the resulting queryset.
|
|
|
|
Other Meta options
|
|
------------------
|
|
|
|
Ordering using ``order_by``
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
You can allow the user to control ordering by providing the
|
|
``order_by`` argument in the Filter's Meta class. ``order_by`` can be either a
|
|
``list`` or ``tuple`` of field names, in which case those are the options, or
|
|
it can be a ``bool`` which, if True, indicates that all fields that
|
|
the user can filter on can also be sorted on. An example or ordering using a list::
|
|
|
|
import django_filters
|
|
|
|
class ProductFilter(django_filters.FilterSet):
|
|
price = django_filters.NumberFilter(lookup_type='lt')
|
|
class Meta:
|
|
model = Product
|
|
fields = ['price', 'release_date']
|
|
order_by = ['price']
|
|
|
|
If you want to control the display of items in ``order_by``, you can set it to
|
|
a list or tuple of 2-tuples in the format ``(field_name, display_name)``.
|
|
This lets you override the displayed names for your ordering fields::
|
|
|
|
order_by = (
|
|
('name', 'Company Name'),
|
|
('average_rating', 'Stars'),
|
|
)
|
|
|
|
Note that the default query parameter name used for ordering is ``o``. You
|
|
can override this by setting an ``order_by_field`` attribute on the
|
|
``FilterSet`` class to the string value you would like to use.
|
|
|
|
Custom Forms using ``form``
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The inner ``Meta`` class also takes an optional ``form`` argument. This is a
|
|
form class from which ``FilterSet.form`` will subclass. This works similar to
|
|
the ``form`` option on a ``ModelAdmin.``
|
|
|
|
Group fields with ``together``
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The inner ``Meta`` class also takes an optional ``together`` argument. This
|
|
is a list of lists, each containing field names. For convenience can be a
|
|
single list/tuple when dealing with a single set of fields. Fields within a
|
|
field set must either be all or none present in the request for
|
|
``FilterSet.form`` to be valid.
|
|
|
|
import django_filters
|
|
|
|
class ProductFilter(django_filters.FilterSet):
|
|
class Meta:
|
|
model = Product
|
|
fields = ['price', 'release_date', 'rating']
|
|
together = ['rating', 'price']
|
|
|
|
|
|
Non-Meta options
|
|
----------------
|
|
|
|
Note that these options do not go in the Meta class, they are specified directly
|
|
in your FilterSet class.
|
|
|
|
``strict``
|
|
~~~~~~~~~~
|
|
|
|
The ``strict`` option controls whether results are returned when an invalid
|
|
value is specified by the user for any filter field. By default, ``strict`` is
|
|
set to ``True`` meaning that an empty queryset is returned if any field contains
|
|
an invalid value. You can loosen this behavior by setting ``strict`` to
|
|
``False`` which will effectively ignore a filter field if its value is invalid.
|
|
|
|
Overriding ``FilterSet`` methods
|
|
--------------------------------
|
|
|
|
``get_ordering_field()``
|
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
If you want to use a custom widget, or in any other way override the ordering
|
|
field you can override the ``get_ordering_field()`` method on a ``FilterSet``.
|
|
This method just needs to return a Form Field.
|
|
|
|
Ordering on multiple fields, or other complex orderings can be achieved by
|
|
overriding the ``Filterset.get_order_by()`` method. This is passed the selected
|
|
``order_by`` value, and is expected to return an iterable of values to pass to
|
|
``QuerySet.order_by``. For example, to sort a ``User`` table by last name, then
|
|
first name::
|
|
|
|
class UserFilter(django_filters.FilterSet):
|
|
class Meta:
|
|
order_by = (
|
|
('username', 'Username'),
|
|
('last_name', 'Last Name')
|
|
)
|
|
|
|
def get_order_by(self, order_value):
|
|
if order_value == 'last_name':
|
|
return ['last_name', 'first_name']
|
|
return super(UserFilter, self).get_order_by(order_value)
|
|
|
|
Generic View
|
|
------------
|
|
|
|
In addition to the above usage there is also a class-based generic view
|
|
included in django-filter, which lives at ``django_filters.views.FilterView``.
|
|
You must provide either a ``model`` or ``filterset_class`` argument, similar to
|
|
``ListView`` in Django itself::
|
|
|
|
# urls.py
|
|
from django.conf.urls import patterns, url
|
|
from django_filters.views import FilterView
|
|
from myapp.models import Product
|
|
|
|
urlpatterns = patterns('',
|
|
(r'^list/$', FilterView.as_view(model=Product)),
|
|
)
|
|
|
|
You must provide a template at ``<app>/<model>_filter.html`` which gets the
|
|
context parameter ``filter``. Additionally, the context will contain
|
|
``object_list`` which holds the filtered queryset.
|
|
|
|
A legacy functional generic view is still included in django-filter, although
|
|
its use is deprecated. It can be found at
|
|
``django_filters.views.object_filter``. You must provide the same arguments
|
|
to it as the class based view::
|
|
|
|
# urls.py
|
|
from django.conf.urls import patterns, url
|
|
from myapp.models import Product
|
|
|
|
urlpatterns = patterns('',
|
|
(r'^list/$', 'django_filters.views.object_filter', {'model': Product}),
|
|
)
|
|
|
|
The needed template and its context variables will also be the same as the
|
|
class-based view above.
|