又到开学季,小编我希望大家在新的学年继续努力,学有所成。Django-filter是小编我最喜欢的几个Django第三方安装包之一,在日常开发项目中经常用到,尤其当碰到用户需要对搜索结果中做进一步筛选的场景时。Django-filter允许用户根据自定义字段过滤从数据库查询得到的queryset,进一步筛选出用户想要的查询结果。这样做避免了对数据库的再次查询,大大提升了效率。小编我就今天带你看看如何安装和使用django-filter,并重点讲解如何美化django-filter并给其添加分页功能。该文值得收藏指数99,建议先加入微信收藏再阅读,以备后用。
实战内容
我们将开发一个文章搜索页面,用户可以根据标题关键词搜索文章得到文章列表。如果用户对显示搜索结果不满意,可以进一步根据文章的发表日期和文章类别对搜索结果进行进一步筛选。筛选结果以表格和分页显示。我们将借助django-filter实现。
本项目模型很简单(models.py内容如下所示),包含Article和Category两个模型,其中Article与Category是单对多的关系。模型还是基于之前大江狗实战案例Django实战专题: 开发专业博客(1)之内容管理后台开发。
#models.py
class Article(models.Model):"""文章模型""" STATUS_CHOICES = ( ('d', '草稿'), ('p', '发表'), ) title = models.CharField('标题', max_length=200, db_index=True) slug = models.SlugField('slug', max_length=60, blank=True) body = models.TextField('正文') pub_date = models.DateTimeField('发布时间', null=True) status = models.CharField('文章状态', max_length=1, choices=STATUS_CHOICES, default='p')
category = models.ForeignKey('Category', verbose_name='分类',
on_delete=models.CASCADE, blank=False, null=False)
def __str__(self):return self.titleclass Category(models.Model):"""文章分类""" name = models.CharField('分类名', max_length=30, unique=True)
def __str__(self):
return self.name
django-filter的安装
首先使用如下pip命令安装django-filter。
$ pip install django-filter
然后将 'django_filters'
加入INSTALLED_APPS
.
INSTALLED_APPS = [...'django_filters',]
快速开始
使用django-filter有点类似使用django的forms类。我们先创建filters.py并添加如下代码。其作用是自定义一个ArticleFilter类,其中Meta选项指定通过Article模型的title字段来进行查询筛选。
#filters.py
import django_filtersclass ArticleFilter(django_filters.FilterSet):class Meta: model = Article fields = ['title',]
现在我们需要编写视图views.py, 使用ArticleFilter类。由于filter是django自带的关键词,不能用作变量名,这里我们将ArticleFilter类的实例命名为f。每个f其实包含两个属性,f.form生成筛选表单,f.qs包含筛选结果集。
#views.py
from .models import Article,
from .filters import ArticleFilter# Create your views here.def article_search(request): f = ArticleFilter(request.GET, queryset=Article.objects.filter(status='p')) context = {'filter': f, }return render(request, 'blog/article_search.html', context)
然后在urls.py里添加如下代码,建立urls与视图之间的关系。当用户访问/articles/filter/,调用article_search的视图函数。
from . import views# namespaceapp_name = 'blog'urlpatterns = [path('articles/filter/', views.article_search, name='article_search'),]
最后我们编写我们的模板,用于显示查询表单filter.form和查询结果filter.qs。
#blog/article_search.html
{% extends "blog/base.html" %}
搜索文章{% block content %}action="" method="get">{{ filter.form.as_p }}type="submit" />{% for obj in filter.qs %}{{ obj.title }}, 类别: {{ obj.category.name }}{% endfor %}{% endblock %}
显示结果如下所示:
但是当我们输入关键词"python"时,你会发现链接中多了?title=python,然而筛选结果却一条都没有,这是为什么呢?
这是因为我们创建Filter类时,不仅要指定筛选字段,而且要需要指定该字段的匹配查询方式(lookup_expr)。如果不指定,Filter类默认都是使用"exact"精确匹配的,这显然不是我们想要的。这里一个查询结果都没有是因为没有一篇文章的标题能100%匹配python。在这里使用"icontains"匹配更合适。
精确定义你的Filter
我们现在修改下filters.py里的ArticleFilter类,添加其它两个个字段(类别和发布日期)并指定对每个字段的匹配查询方式。
class ArticleFilter(django_filters.FilterSet):class Meta: model = Article fields = {'title': ['icontains'],'category__name': ['icontains'],'pub_date': ['year__gt'],}
你会发现我们现在多了两个查询字段,你现在可以按1个或多个条件筛选结果啦,是不是很帅?
不过结果展现形式非常的不美观,主要有如下几个问题:
- 搜索文章更习惯以q作为搜索的参数名,而不是title__icontains
- 选择文章分类居然要输入分类名字,更好方式是以下拉菜单的形式显示
- 所有表单的label都是中英混杂,比如标题contains, 我们要改成"标题包含"
- 筛选表单没有美化,无对齐,无排版,很难看。
- 搜索结果结果展示也比较难看,且没有分页。
优化你的Filter类
对于1, 2和3我们可以继续优化ArticleFilter类,如下所示。对于4, 5, 6我们需要从视图和模板上动手修改,稍后会提到。在优化ArticleFilter类时,我们指定了q为title字段的参数名,category字段使用了ModelChoiceFilter(下拉菜单), 日期使用了NumberFilter。所有字段都添加了label,避免了中英混杂的情况。
class ArticleFilter(django_filters.FilterSet):
q = django_filters.CharFilter(field_name='title',
lookup_expr='icontains', label="关键词")
category = django_filters.ModelChoiceFilter(field_name='category', queryset=Category.objects.all(),)
pub_date__gte = django_filters.NumberFilter(field_name='pub_date',
lookup_expr='year__gte', label="发表年份>=")
class Meta: model = Article fields = {}
现在是不是好多了? 有下拉菜单,无中英混杂。
优化视图和模板
我们现在修改视图加入分页内容,并调整显示模板, 以表格形式显示筛选结果。
#views.py
from .filters import ArticleFilterfrom django.core.paginator import Paginator# Create your views here.def article_search(request): f = ArticleFilter(request.GET, queryset=Article.objects.filter(status='p')) paginator = Paginator(f.qs, 3) page = request.GET.get('page') page_obj = paginator.get_page(page)
context = {'page_obj': page_obj, 'paginator': paginator,
'is_paginated': True, 'filter': f, }
return render(request, 'blog/article_search.html', context)
#blog/article_search.html
{% extends "blog/base.html" %}{% load widget_tweaks %}{% load blog_extras %}{% block content %}
搜索筛选文章:共 {{ filter.qs | length }}个结果。{# 注释: page_obj不要改。Article可以改成自己对象 #}
class="well">action="" role="search" method="get">
class="input-group col-md-12">{% with form=filter.form %} {% for field in form.visible_fields %}
class="form-group col-sm-4 col-md-4">{{ field.label_tag }} {% render_field field %}
{% endfor %}class="input-group-btn">class="btn btn-info form-control" type="submit" value="submit">筛选
{% endwith %}
{% if is_paginated %}
class="table table-striped">标题类别发布日期查看修改删除
{% for article in page_obj %}{{ article.title }}{{ article.category.name }}{{ article.pub_date | date:"Y-m-d" }} href="{% url 'blog:article_detail' article.id article.slug %}">class="glyphicon glyphicon-eye-open">href="{% url 'blog:article_update' article.id article.slug %}">class="glyphicon glyphicon-wrench">href="{% url 'blog:article_delete' article.id article.slug %}">class="glyphicon glyphicon-trash">
{% endfor %}{% else %}There are no articles yet.{% endif %}{% if is_paginated %}
class="pagination">{% if page_obj.has_previous %}class="page-item">class="page-link" href="?{% param_replace page=page_obj.previous_page_number %}">«{% else %}class="page-item disabled">class="page-link">«{% endif %} {% for i in paginator.page_range %} {% if page_obj.number == i %}class="page-item active">class="page-link"> {{ i }} class="sr-only">(current){% else %}class="page-item">class="page-link" href="?{% param_replace page=i %}">{{ i }}{% endif %} {% endfor %} {% if page_obj.has_next %}class="page-item">class="page-link" href="?{% param_replace page=page_obj.next_page_number %}">»{% else %}class="page-item disabled">class="page-link">»{% endif %}{% endif %}{% endblock %}
调整过后的展示效果如下。此处是不是应该有点掌声?
注意到下面两个标签了吗?模板的美化使用到render_field和param_replace两个自定义的模板标签。其中render_field需要先安装django-widget-tweaks即可使用,主要用于给表单的字段添加css类名。param_replace是一个自定义的模板标签,其作用是拼接过滤参数(如?q=python)和页码参数(page=1),提供完整的urls。分页样式采用了Bootstrap的分页样式,来源于Django代码分享: 可以重用的Bootstrap 4分页模板。
{% load widget_tweaks %}{% load blog_extras %}
关于如何自定义模板标签,见Django基础(16): 模板标签(tags)的分类及如何自定义模板标签。param_replace代码如下。
@register.simple_tag(takes_context=True)def param_replace(context, **kwargs):"""
Return encoded URL parameters
"""d = context['request'].GET.copy()for k, v in kwargs.items():
d[k] = vfor k in [k for k, v in d.items() if not v]:del d[k]return d.urlencode()
小结
- Django-filter是个很有用的第三方包,当你希望让用户通过多个条件搜索或筛选查询结果时,使用django-filter可以完美帮你实现这个功能。
- 定义你的filter类时,不仅需要定义查询字段名,还要定义字段的查询匹配方式(lookup_expr)以及字段的显示标签(label)。
- 使用自定义的filter可以像使用django的forms类一样。每个filter类包含里filter.form筛选表单和filter.qs查询结果集。
- Django-filter与分页连用时,别忘了使用自定义模板标签param_replace拼接URLs(该解决方案来自overstackflow,非原创)。