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.
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'})
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.
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 %}
{% for obj in filter %}
{{ obj.name }} - ${{ obj.price }}
{% 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.``
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 ``filter_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 ``/_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.