debian-django-filter/docs/usage.txt

389 lines
13 KiB
Plaintext
Raw Normal View History

2009-01-30 20:15:57 +01:00
Using django-filter
===================
2009-01-30 20:15:57 +01:00
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
2014-09-29 20:20:07 +02:00
our users filter which products they see on a list page.
The model
---------
Let's start with our model::
2009-01-30 20:15:57 +01:00
from django.db import models
2009-01-30 20:15:57 +01:00
class Product(models.Model):
name = models.CharField(max_length=255)
price = models.DecimalField()
description = models.TextField()
release_date = models.DateField()
manufacturer = models.ForeignKey(Manufacturer)
2009-01-30 20:15:57 +01:00
The filter
----------
We have a number of fields and we want to let our users filter based on the
2009-01-30 20:15:57 +01:00
price or the release_date. We create a ``FilterSet`` for this::
import django_filters
class ProductFilter(django_filters.FilterSet):
2016-02-19 18:24:29 +01:00
name = django_filters.CharFilter(lookup_expr='iexact')
2009-01-30 20:15:57 +01:00
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
2016-02-19 18:24:29 +01:00
declarative syntax.
2016-02-19 18:24:29 +01:00
Declaring filters
~~~~~~~~~~~~~~~~~
The declarative syntax provides you with the most flexibility when creating
filters, however it is fairly verbose. We'll use the below example to outline
the :ref:`core filter arguments <core-arguments>` on a ``FilterSet``::
class ProductFilter(django_filters.FilterSet):
2016-02-19 18:24:29 +01:00
price = django_filters.NumberFilter()
price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt')
price__lt = django_filters.NumberFilter(name='price', lookup_expr='lt')
release_year = django_filters.NumberFilter(name='release_date', lookup_expr='year')
release_year__gt = django_filters.NumberFilter(name='release_date', lookup_expr='year__gt')
release_year__lt = django_filters.NumberFilter(name='release_date', lookup_expr='year__lt')
manufacturer__name = django_filters.CharFilter(lookup_expr='icontains')
class Meta:
model = Product
2016-02-19 18:24:29 +01:00
There are two main arguments for filters:
- ``name``: The name of the model field to filter on. You can traverse
"relationship paths" using Django's ``__`` syntax to filter fields on a
related model. ex, ``manufacturer__name``.
- ``lookup_expr``: The `field lookup`_ to use when filtering. Django's ``__``
syntax can again be used in order to support lookup transforms.
ex, ``year__gte``.
.. _`field lookup`: https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups
2009-01-30 20:15:57 +01:00
2016-02-19 18:24:29 +01:00
Together, the field ``name`` and ``lookup_expr`` represent a complete Django
lookup expression. A detailed explanation of lookup expressions is provided in
Django's `lookup reference`_. django-filter supports expressions containing
both transforms and a final lookup for version 1.9 of Django and above.
For Django version 1.8, transformed expressions are not supported.
.. _`lookup reference`: https://docs.djangoproject.com/en/dev/ref/models/lookups/#module-django.db.models.lookups
2016-04-04 08:44:51 +02:00
Common declarative problems
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Below are some of the common problem that occur when declaring filters. It is
recommended that you do read this as it provides a more complete understanding
on how filters work.
Filter ``name`` and ``lookup_expr`` not configured
""""""""""""""""""""""""""""""""""""""""""""""""""
While ``name`` and ``lookup_expr`` are optional, it is recommended that you specify
2016-02-19 18:24:29 +01:00
them. By default, if ``name`` is not specified, the filter's name on the
filterset class will be used. Additionally, ``lookup_expr`` defaults to
``exact``. The following is an example of a misconfigured price filter::
class ProductFilter(django_filters.FilterSet):
price__gt = django_filters.NumberFilter()
The filter instance will have a field name of ``price__gt`` and an ``exact``
lookup type. Under the hood, this will incorrectly be resolved as::
Produce.objects.filter(price__gt__exact=value)
2016-04-04 09:01:39 +02:00
The above will most likely generate a ``FieldError``. The correct configuration
2016-04-04 08:44:51 +02:00
would be::
class ProductFilter(django_filters.FilterSet):
price__gt = django_filters.NumberFilter(name='price', lookup_expr='gt')
Missing ``lookup_expr`` for text search filters
"""""""""""""""""""""""""""""""""""""""""""""""
It's quite common to forget to set the lookup expression for :code:`CharField`
and :code:`TextField` and wonder why a search for "foo" does not return results
for "foobar". This is because the default lookup type is ``exact``, but you
probably want to perform an ``icontains`` lookup.
2016-02-19 18:24:29 +01:00
2016-04-04 08:44:51 +02:00
Filter and lookup expression mismatch (in, range, isnull)
"""""""""""""""""""""""""""""""""""""""""""""""""""""""""
It's not always appropriate to directly match a filter to its model field's
2016-04-04 09:01:39 +02:00
type, as some lookups expect different types of values. This is a commonly
found issue with ``in``, ``range``, and ``isnull`` lookups. Let's look
2016-04-04 08:44:51 +02:00
at the following product model::
class Product(models.Model):
category = models.ForeignKey(Category, null=True)
Given that ``category`` is optional, it's reasonable to want to enable a search
for uncategorized products. The following is an incorrectly configured
``isnull`` filter::
class ProductFilter(django_filters.FilterSet):
uncategorized = django_filters.NumberFilter(name='category', lookup_expr='isnull')
So what's the issue? While the underlying column type for ``category`` is an
integer, ``isnull`` lookups expect a boolean value. A ``NumberFilter`` however
only validates numbers. Filters are not `'expression aware'` and won't change
behavior based on their ``lookup_expr``. You should use filters that match the
data type of the lookup expression `instead` of the data type underlying model
field. The following would correctly allow you to search for both uncategorized
products and products for a set of categories::
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
pass
class ProductFilter(django_filters.FilterSet):
categories = NumberInFilter(name='category', lookup_expr='in')
uncategorized = django_filters.BooleanFilter(name='category', lookup_expr='isnull')
2016-04-04 09:01:39 +02:00
More info on constructing IN and RANGE csv :ref:`filters <base-in-filter>`.
2016-02-19 18:24:29 +01:00
2016-02-19 18:24:29 +01:00
Generating filters with Meta.fields
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The FilterSet Meta class provides a ``fields`` attribute that can be used for
easily specifying multiple filters without significant code duplication. The
base syntax supports a list of multiple field names::
import django_filters
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = ['price', 'release_date']
The above generates 'exact' lookups for both the 'price' and 'release_date'
fields.
Additionally, a dictionary can be used to specify multiple lookup expressions
for each field::
import django_filters
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = {
'price': ['lt', 'gt'],
'release_date': ['exact', 'year__gt'],
}
The above would generate 'price__lt', 'price__gt', 'release_date', and
'release_date__year__gt' filters.
.. note::
The filter lookup type 'exact' is an implicit default and therefore never
added to a filter name. In the above example, the release date's exact
filter is 'release_date', not 'release_date__exact'.
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']
Overriding default filters
""""""""""""""""""""""""""
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_expr': 'icontains',
},
},
models.BooleanField: {
'filter_class': django_filters.BooleanFilter,
'extra': lambda f: {
'widget': 'forms.CheckboxInput',
},
},
}
class Meta:
model = Product
fields = {
'name': ['exact'],
'release_date': ['isnull'],
}
2016-02-19 19:05:16 +01:00
Custom filtering with MethodFilter
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want fine control over each individual filter attribute, you can use
the ``MethodFilter`` filter.
By passing in the name of a custom defined filter function as an ``action``,
the filter attribute gets linked to the custom filter function.
Here is an example of overriding the filter function of the
filter attribute ``username``
::
class F(django_filters.FilterSet):
username = MethodFilter(action='my_custom_filter')
class Meta:
model = User
fields = ['username']
def my_custom_filter(self, queryset, value):
return queryset.filter(
username=value
)
The filter function can also be defined outside of the filter class scope.
Though you would need to pass in the actual function value, not it's name.
::
def my_custom_filter(queryset, value):
return queryset.filter(
username=value
)
class F(django_filters.FilterSet):
# Notice: In this case, action accepts a func, not a string
2015-10-19 18:01:00 +02:00
username = MethodFilter(action=my_custom_filter)
class Meta:
model = User
fields = ['username']
Lastly, when using a ``MethodFilter``, there is no need to define an action.
You may simply do the following and ``filter_username`` will be auto-detected
and used. ::
class F(FilterSet):
username = MethodFilter()
class Meta:
model = User
fields = ['username']
def filter_username(self, queryset, value):
return queryset.filter(
username__contains='ke'
)
2015-09-18 16:16:12 +02:00
Under the hood, if ``action`` is not defined, ``django_filter``
searches for a class method with a name that follows the pattern
``filter_{{ATTRIBUTE_NAME}}``. For example, if the attribute name is
``email``, then the filter class will be scanned for the filter function
``filter_email``. If no action is provided, and no filter class
function is found, then the filter attribute will be left unfiltered.
The view
--------
2009-01-30 20:15:57 +01:00
Now we need to write a view::
2009-01-30 20:15:57 +01:00
def product_list(request):
f = ProductFilter(request.GET, queryset=Product.objects.all())
return render(request, 'my_app/template.html', {'filter': f})
2009-01-30 20:15:57 +01:00
If a queryset argument isn't provided then all the items in the default manager
of the model will be used.
2014-09-29 20:20:07 +02:00
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
2013-06-04 14:01:38 +02:00
The URL conf
------------
We need a URL pattern to call the view::
url(r'^list$', views.product_list)
The template
------------
2009-01-30 20:15:57 +01:00
And lastly we need a template::
2009-01-30 20:15:57 +01:00
{% extends "base.html" %}
2009-01-30 20:15:57 +01:00
{% block content %}
<form action="" method="get">
{{ filter.form.as_p }}
2009-05-23 18:51:50 +02:00
<input type="submit" />
</form>
2016-06-30 07:57:54 +02:00
{% for obj in filter.qs %}
2009-05-23 18:51:50 +02:00
{{ obj.name }} - ${{ obj.price }}<br />
2009-01-30 20:15:57 +01:00
{% endfor %}
{% endblock %}
And that's all there is to it! The ``form`` attribute contains a normal
2016-06-30 07:57:54 +02:00
Django form, and when we iterate over the ``FilterSet.qs`` we get the objects in
2009-01-30 20:15:57 +01:00
the resulting queryset.
2009-01-30 20:36:21 +01:00
Generic view & configuration
-----------------------------
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 url
from django_filters.views import FilterView
from myapp.models import Product
2014-09-29 20:20:07 +02:00
urlpatterns = [
url(r'^list/$', FilterView.as_view(model=Product)),
]
You must provide a template at ``<app>/<model>_filter.html`` which gets the
2014-09-29 20:20:07 +02:00
context parameter ``filter``. Additionally, the context will contain
``object_list`` which holds the filtered queryset.
2013-06-04 14:01:38 +02:00
A legacy functional generic view is still included in django-filter, although
2014-09-29 20:20:07 +02:00
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 url
from django_filters.views import object_filter
from myapp.models import Product
urlpatterns = [
url(r'^list/$', object_filter, {'model': Product}),
]
The needed template and its context variables will also be the same as the
class-based view above.