Dica 16 - Filtros com django-filter
Versões usadas no vídeo: Django 2.2.13, django-filter 2.3.0, Python 3.8 e Bootstrap 4.0.
Importante: nas versões antigas desta página as tags de template apareciam com uma \ no meio ({%). Se copiar código de lá, remova a \.
Na Dica 15 fizemos um filtro na mão: pegamos os valores do request.GET e montamos o filter() na view. Funciona, mas cada campo novo é mais código para escrever. O django-filter é uma biblioteca que facilita bastante o uso de filtros na aplicação: você declara quais campos quer filtrar e ele gera o formulário e o queryset filtrado.
Neste tutorial vamos criar uma página "Artigos com filtros" que busca artigos pelo título e pelo sub-título, com o termo em qualquer parte do texto e sem diferenciar maiúsculas de minúsculas.
Pré-requisitos
O projeto das dicas anteriores (repositório dicas-de-django), com o app core dentro de myproject, o base.html com Bootstrap 4 da Dica 14 e o modelo Article:
# myproject/core/models.py
class Article(models.Model):
id = HashidAutoField(primary_key=True)
title = models.CharField('título', max_length=200)
subtitle = models.CharField('sub-título', max_length=200)
slug = AutoSlugField(populate_from='title')
category = models.ForeignKey(
'Category',
related_name='categories',
verbose_name='categoria',
on_delete=models.SET_NULL,
null=True,
blank=True
)
published_date = models.DateTimeField(
'criado em',
auto_now_add=True,
auto_now=False
)
class Meta:
ordering = ('title',)
verbose_name = 'artigo'
verbose_name_plural = 'artigos'
def __str__(self):
return self.title
Os artigos cadastrados no vídeo são "Dicas de Python", "Dicas de Python 2", "Django Admin", "Django Autoslug", "Django Boilerplate" e "Django extensions" (o sub-título de cada um é igual ao título).
Instalação
Instale o django-filter
pip install django-filter
No vídeo, a versão instalada foi a 2.3.0 (com Django 2.2.13). Para fixar essa versão no requirements.txt:
django-filter==2.3.0
Acrescente-o ao INSTALLED_APPS. Repare que o nome do pacote no pip é django-filter, mas o nome do app é django_filters, no plural e com underline:
# myproject/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'debug_toolbar',
'django_extensions',
'daterange_filter',
'django_filters',
'myproject.core'
]
O FilterSet
Na pasta do app, crie o arquivo filters.py:
cd myproject/core
touch filters.py
# myproject/core/filters.py
import django_filters
from .models import Article
class ArticleFilter(django_filters.FilterSet):
title = django_filters.CharFilter(lookup_expr='icontains')
subtitle = django_filters.CharFilter(lookup_expr='icontains')
class Meta:
model = Article
fields = ('title', 'subtitle')
- Um
FilterSetfunciona parecido com umModelForm: noMetadizemos o modelo e os campos que podem ser filtrados. - Por padrão, cada campo do
Meta.fieldsé filtrado com o lookupexact(igualdade). Como queremos buscar uma parte do texto, declaramos os campos explicitamente comoCharFiltercomlookup_expr='icontains'. Algumas opções de lookup sãoexact,iexact,containseicontains; oisignifica que não diferencia maiúsculas de minúsculas, entãodjangoencontra "Django Admin".
A view
Para não misturar com a view de artigos da Dica 15, criamos uma view nova, article_filter_list:
# myproject/core/views.py
from dateutil.parser import parse
from datetime import timedelta
from django.shortcuts import render
from .models import Article
from .filters import ArticleFilter
...
def article_filter_list(request):
template_name = 'core/article_filters_list.html'
object_list = Article.objects.all()
article_list = ArticleFilter(request.GET, queryset=object_list)
context = {
'object_list': object_list,
'filter': article_list
}
return render(request, template_name, context)
O ArticleFilter recebe os dados do request.GET (o que foi digitado no formulário) e o queryset inicial. É ele que vai para o template com o nome filter.
Atenção ao nome do template: no vídeo a view foi escrita primeiro com 'core/article_filter_list.html', mas o arquivo criado se chama article_filters_list.html, e o resultado foi:
TemplateDoesNotExist at /articles/filter/
core/article_filter_list.html
O nome na view tem que ser exatamente o nome do arquivo.
A rota e o menu
# myproject/core/urls.py
from django.urls import path
from myproject.core import views as v
app_name = 'core'
urlpatterns = [
path('', v.index, name='index'),
path('persons/', v.person_list, name='person_list'),
path('articles/', v.article_list, name='article_list'),
path('articles/filter/', v.article_filter_list, name='article_filter_list'),
]
No nav.html, acrescente um item no menu, logo depois de "Artigos":
<!-- myproject/core/templates/nav.html -->
<li class="nav-item">
<a class="nav-link" href="{% url 'core:article_list' %}">Artigos</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'core:article_filter_list' %}">Artigos com filtros</a>
</li>
O template
Crie article_filters_list.html (no vídeo ele começou como uma cópia do article_list.html, e o formulário de datas foi removido):
<!-- myproject/core/templates/core/article_filters_list.html -->
{% extends "base.html" %}
{% block content %}
<div class="row">
<div class="col-md-4">
<h1>Lista de Artigos</h1>
</div>
</div>
<div class="row">
<div class="col-md-4">
<form method="GET">
{{ filter.form.as_p }}
<input type="submit" />
</form>
</div>
<div class="col-md-8">
<table class="table">
<thead>
<tr>
<th>Título</th>
<th>Sub-título</th>
<th>Data de publicação</th>
</tr>
</thead>
<tbody>
{% for obj in filter.qs %}
<tr>
<td>{{ obj.title }}</td>
<td>{{ obj.subtitle }}</td>
<td>{{ obj.published_date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock content %}
Os dois pontos do django-filter no template:
{{ filter.form.as_p }}: oFilterSetgera um formulário com um campo para cada filtro, cada um num parágrafo. O rótulo vem doverbose_namedo modelo mais o lookup, e o HTML gerado fica assim:
<p><label for="id_title">Título contains:</label> <input type="text" name="title" id="id_title"></p>
<p><label for="id_subtitle">Sub-título contains:</label> <input type="text" name="subtitle" id="id_subtitle"></p>
{% for obj in filter.qs %}: o.qsé o queryset já filtrado. É preciso iterar sobre ofilter.qs, e não sobre oobject_list, que continua com todos os artigos.
O formulário usa method="GET", então os filtros vão na URL, por exemplo /articles/filter/?title=python&subtitle=2.
Testando
Rode o servidor e acesse "Artigos com filtros" no menu (http://localhost:8000/articles/filter/). O resultado, rodado com Django 2.2 e django-filter 2.3.0:
| Título contém | Sub-título contém | Resultado |
|---|---|---|
| (vazio) | (vazio) | os 6 artigos |
django |
(vazio) | Django Admin, Django Autoslug, Django Boilerplate, Django extensions |
python |
(vazio) | Dicas de Python, Dicas de Python 2 |
python |
2 |
Dicas de Python 2 |
Quando os dois campos são preenchidos, os filtros são combinados com "e": o título tem que conter python e o sub-título tem que conter 2.
O formulário gerado não tem as classes do Bootstrap, então fica com uma aparência simples; o foco aqui é o funcionamento.
Usando o filtro na view existente
Outra forma, que era a do texto original desta dica, é usar o ArticleFilter direto na view article_list e mandar o filtro para o template com os dois nomes:
# myproject/core/views.py
from .filters import ArticleFilter
def article_list(request):
template_name = 'core/article_list.html'
object_list = Article.objects.all()
article_filter = ArticleFilter(request.GET, queryset=object_list)
...
context = {
'object_list': article_filter,
'filter': article_filter
}
return render(request, template_name, context)
O template é o mesmo mostrado acima ({{ filter.form.as_p }} e {% for obj in filter.qs %}).