本文完整目录请见 Django 3网页开发指南 - 第4版
本章包含如下内容:
- 使用CRUDL函数创建应用
- 保存模型实例的作者
- 上传图片
- 通过自定义模型创建表单布局
- 通过django-crispy-forms创建表单布局
- 处理formsets
- 过滤对象列表
- 管理分页列表
- 编写基于类的视图
- 添加Open Graph和Twitter Card数据
- 添加schema.org用词
- 生成PDF文档
- 通过Haystack和Whoosh实现多语言搜索
- 通过Elasticsearch DSL实现多语言搜索
过滤对象列表
在网页开发中,除带表单的视图外,对象列表视图和详情视图也很常见。列表视图可以简单列举已排序的对象,如,按字母排序或创建日期排序;但庞大的数据量对用户不友好。为实现最好的可用性和便捷性,应当能够通过所有可用分类对内容进行过滤。本节中,我们将学习用于对过分类号过滤列表视图的模式。
我们会创建一个ideas的列表视图,可通过作者、分类或评分进行过滤。应用了Bootstrap 4之后效果类似下面这样:
准备工作
这个过滤示例,我们将使用Idea模型关联作者和分类进行过滤。也可以通过评分进行过滤,即带选项的PositiveIntegerField。我们使用的是前面小节中所创建的ideas应用。
如何实现...
按照如下步骤完成本小节的学习:
- 创建IdeaFilterForm进行可用分类的过滤:
# myproject/apps/ideas/forms.py
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.db import models
from django.contrib.auth import get_user_model
from myproject.apps.categories.models import Category
from .models import RATING_CHOICES
User = get_user_model()
class IdeaFilterForm(forms.Form):
author = forms.ModelChoiceField(
label=_("Author"),
required=False,
queryset=User.objects.annotate(
idea_count=models.Count("authored_ideas")
).filter(idea_count__gt=0),
)
category = forms.ModelChoiceField(
label=_("Category"),
required=False,
queryset=Category.objects.annotate(
idea_count=models.Count("category_ideas")
).filter(idea_count__gt=0),
)
rating = forms.ChoiceField(
label=_("Rating"), required=False, choices=RATING_CHOICES
)
- 创建idea_list视图来列举过滤后的ideas:
# myproject/apps/ideas/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.conf import settings
from .forms import IdeaFilterForm
from .models import Idea, RATING_CHOICES
PAGE_SIZE = getattr(settings, "PAGE_SIZE", 24)
def idea_list(request):
qs = Idea.objects.order_by("title")
form = IdeaFilterForm(data=request.GET)
facets = {
"selected": {},
"categories": {
"authors": form.fields["author"].queryset,
"categories": form.fields["category"].queryset,
"ratings": RATING_CHOICES,
},
}
if form.is_valid():
filters = (
# query parameter, filter parameter
("author", "author"),
("category", "categories"),
("rating", "rating"),
)
qs = filter_facets(facets, qs, form, filters)
context = {"form": form, "facets": facets, "object_list": qs}
return render(request, "ideas/idea_list.html", context)
- 在同一个文件中添加帮助函数filter_facets():
def filter_facets(facets, qs, form, filters):
for query_param, filter_param in filters:
value = form.cleaned_data[query_param]
if value:
selected_value = value
if query_param == "rating":
rating = int(value)
selected_value = (rating, dict(RATING_CHOICES)[rating])
facets["selected"][query_param] = selected_value
filter_args = {filter_param: value}
qs = qs.filter(**filter_args).distinct()
return qs
- 如未创建,请创建base.html模板。可以参照第4章 模板和JavaScript中的编排base.html模板一节的示例。
- 创建idea_list.html 模型并添加如下内容:
{# ideas/idea_list.html #}
{% extends "base.html" %}
{% load i18n utility_tags %}
{% block sidebar %}
{% include "ideas/includes/filters.html" %}
{% endblock %}
{% block main %}
{% trans "Ideas" %}
{% if object_list %}
{% for idea in object_list %}
{{ idea.translated_title }}
{% endfor %}
{% else %}
{% trans "There are no ideas yet." %}
{% endif %}
{% trans "Add idea" %}
{% endblock %}
- 然后对过滤器创建模板。这个模板使用 {% modify_query %}模板标签来生成过滤器的URL,参见第5章 自定义模板过滤器和标签中的创建模板标签来修改请求查询参数一节:
{# ideas/includes/filters.html #}
{% load i18n utility_tags %}
{% with title=_('Author') selected=facets.selected.author %}
{% include "misc/includes/filter_heading.html" with title=title %}
{% include "misc/includes/filter_all.html" with param="author" %}
{% for cat in facets.categories.authors %}
{{ cat }}
{% endfor %}
{% endwith %}
{% with title=_('Category') selected=facets.selected.category %}
{% include "misc/includes/filter_heading.html" with title=title %}
{% include "misc/includes/filter_all.html" with param="category" %}
{% for cat in facets.categories.categories %}
{{ cat }}
{% endfor %}
{% endwith %}
{% with title=_('Rating') selected=facets.selected.rating %}
{% include "misc/includes/filter_heading.html" with title=title %}
{% include "misc/includes/filter_all.html" with param="rating" %}
{% for r_val, r_display in facets.categories.ratings %}
{{ r_display }}
{% endfor %}
{% endwith %}
- 每个分类在边栏中遵循通用的模式,因此我们可以创建并包含带有通用部分的模板。首先有过滤头部,对应文件为 misc/includes/filter_heading.html,如下:
{# misc/includes/filter_heading.html #}
{% load i18n %}
- 然后每个过滤器会包含重置该分类过滤器的链接,这里位于misc/includes/filter_all.html。这个模板还使用{% modify_query %}模板标签,参见第5章 自定义模板过滤器和标签中的创建模板标签来修改请求查询参数一节:
{# misc/includes/filter_all.html #}
{% load i18n utility_tags %}
{% trans "All" %}
- 这个idea列表需要添加到ideas应用的URL中:
# myproject/apps/ideas/urls.py
from django.urls import path
from .views import idea_list
urlpatterns = [
path("", idea_list, name="idea_list"),
# other paths...
]
实现原理...
我们使用facets字典,传递给模板上文来知道有哪些过滤器以及选中了哪些过滤器。再深入一点,facets字典包含两块:categories字典和selected字典。categories字典包含查询集或所有分类的选项。selected字典包含当前每个分类的选中值。在IdeaFilterForm中,我们确保只有至少包含一个idea的分类和作者被列举出来。
在视图中,我们检查表单中的查询参数是否有效,然后根据所选中分类过滤对象的查询集。此外,我们为facets字典设置所选中的值,这个字典会传递给模板。
在模板中,对facets字典中的每个分类,我们列举出所有分类并当前选中的分类为活跃状态。如果未选中所给定分类,我们默认标记All为活跃值。
相关内容
- 管理分页列表一节
- 编写基于类的视图一节
- 第4章 模板和JavaScript中安排base.html模板一节
- 第5章 自定义模板过滤器和标签中的创建模板标签来修改请求查询参数一节
管理分页列表
如果动态地修改了对象列表或其总数超过24等数,很有可能会需要通过分页来提供更好的用户体验。分页不是查询全部集合,而是在数据集中取出对应一页数量的指定个数据项。我们还提供链接来让用户访问组成完整数据的其它页面。Django有一个管理分页数据的类,本节中我们就来学习如何使用。
准备工作
我们使用过滤对象列表一节中ideas应用的模型、表单和视图。
如何实现...
按照如下步骤来向ideas的列表视图添加分页功能:
- 在views.py文件中导入Django分页类。我们将在过滤代码后对idea_list视图添加分页管理。同时,还会通过向object_list键分配page来微调上下文字典:
# myproject/apps/ideas/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.conf import settings
from django.core.paginator import (EmptyPage, PageNotAnInteger, Paginator)
from .forms import IdeaFilterForm
from .models import Idea, RATING_CHOICES
PAGE_SIZE = getattr(settings, "PAGE_SIZE", 24)
def idea_list(request):
qs = Idea.objects.order_by("title")
form = IdeaFilterForm(data=request.GET)
facets = {
"selected": {},
"categories": {
"authors": form.fields["author"].queryset,
"categories": form.fields["category"].queryset,
"ratings": RATING_CHOICES,
},
}
if form.is_valid():
filters = (
# query parameter, filter parameter
("author", "author"),
("category", "categories"),
("rating", "rating"),
)
qs = filter_facets(facets, qs, form, filters)
paginator = Paginator(qs, PAGE_SIZE)
page_number = request.GET.get("page")
try:
page = paginator.page(page_number)
except PageNotAnInteger:
# If page is not an integer, show first page.
page = paginator.page(1)
except EmptyPage:
# If page is out of range, show last existing page.
page = paginator.page(paginator.num_pages)
context = {
"form": form,
"facets": facets,
"object_list": page,
}
return render(request, "ideas/idea_list.html", context)
- 修改idea_list.html模板如下:
{# ideas/idea_list.html #}
{% extends "base.html" %}
{% load i18n utility_tags %}
{% block sidebar %}
{% include "ideas/includes/filters.html" %}
{% endblock %}
{% block main %}
{% trans "Ideas" %}
{% if object_list %}
{% for idea in object_list %}
{{ idea.translated_title }}
{% endfor %}
{% include "misc/includes/pagination.html" %}
{% else %}
{% trans "There are no ideas yet." %}
{% endif %}
{% trans "Add idea" %}
{% endblock %}
- 创建分类组件模板:
{# misc/includes/pagination.html #}
{% load i18n utility_tags %}
{% if object_list.has_other_pages %}
{% endif %}
实现原理...
在浏览器中查看结果时,会看到类似下面这样的分页控制器:
这是如何实现的呢?在过滤好查询集合后,我们会创建一个paginator对象,传递查询集合及每页希望显示的最大条数,本例中为24。然后,我们会通过查询参数page读取当前页数。下一步是通过paginator获取当前页对象。如果页数不是整型,则获取第一页。如果页数超出可显示页面数,获取最后一页。page对象中有上图所示分页组件所需的方法和属性。同时,page类似查询集合,可以进行遍历并取出页面中的部分项。
模板中所示的代码片断用Bootstrap 4前端框架创建一个分页组件。仅在页面多于当前页时才会显示分页控件。里面有上一页、下一页,以及组件中所有页码的列表。当前页码进行了高亮显示。我们使用{% modify_query %}来将链接生成URL,这部分在稍后的第5章 自定义模板过滤器和标签中的创建模板标签来修改请求查询参数一节中会进行详细讲解。
相关内容
- 过滤对象列表一节
- 编写基于类的视图一节
- 第5章 自定义模板过滤器和标签中的创建模板标签来修改请求查询参数一节
编写基于类的视图
Django视图是可接收请求并返回响应的可调用函数。除了函数形式的视图外,Django还提供了一种通过类定义视图的方式。这种方式在想要创建可复用的模块化视图或合并通用mixin的视图时非常有用。本小节中,我们会将前面的函数视图idea_list化为类视图IdeaListView。
准备工作
创建类似前面过滤对象列表和管理分页列表小节中的模型、表单和模板。
如何实现...
按照如下步骤来完成本小节的内容:
- 我们的类视图IdeaListView继承Django的View类并重载get()方法:
# myproject/apps/ideas/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.conf import settings
from django.core.paginator import (EmptyPage, PageNotAnInteger, Paginator)
from django.views.generic import View
from .forms import IdeaFilterForm
from .models import Idea, RATING_CHOICES
PAGE_SIZE = getattr(settings, "PAGE_SIZE", 24)
class IdeaListView(View):
form_class = IdeaFilterForm
template_name = "ideas/idea_list.html"
def get(self, request, *args, **kwargs):
form = self.form_class(data=request.GET)
qs, facets = self.get_queryset_and_facets(form)
page = self.get_page(request, qs)
context = {"form": form, "facets": facets, "object_list": page}
return render(request, self.template_name, context)
def get_queryset_and_facets(self, form):
qs = Idea.objects.order_by("title")
facets = {
"selected": {},
"categories": {
"authors": form.fields["author"].queryset,
"categories": form.fields["category"].queryset,
"ratings": RATING_CHOICES,
},
}
if form.is_valid():
filters = (
# query parameter, filter parameter
("author", "author"),
("category", "categories"),
("rating", "rating"),
)
qs = self.filter_facets(facets, qs, form, filters)
return qs, facets
@staticmethod
def filter_facets(facets, qs, form, filters):
for query_param, filter_param in filters:
value = form.cleaned_data[query_param]
if value:
selected_value = value
if query_param == "rating":
rating = int(value)
selected_value = (rating, dict(RATING_CHOICES)[rating])
facets["selected"][query_param] = selected_value
filter_args = {filter_param: value}
qs = qs.filter(**filter_args).distinct()
return qs
def get_page(self, request, qs):
paginator = Paginator(qs, PAGE_SIZE)
page_number = request.GET.get("page")
try:
page = paginator.page(page_number)
except PageNotAnInteger:
page = paginator.page(1)
except EmptyPage:
page = paginator.page(paginator.num_pages)
return page
- 我们需要在URL配置中使用视图类创建URL规则。你可能已经使用视图函数idea_list添加过规则,方法相似。在URL规则中包含视图类,需要使用as_view()方法如下:
# myproject/apps/ideas/urls.py
from django.urls import path
from .views import IdeaListView
urlpatterns = [
path("", IdeaListView.as_view(), name="idea_list"),
# other paths...
]
实现原理...
由HTTP GET请求所调用的get()方法中所执行的操作如下:
- 首先,我们创建了form对象,对其传递类似字典的request.GET对象。request.GET对象包含所有使用GET方法传递的查询变量。
- 然后,form被传递给get_queryset_and_facets()方法,它通过包含两个元素的元组返回关联值,这两个元素分别为QuerySet和facets。
- 当前的请求对象及所获取的查询集合被传递给get_page()方法,该方法返回页面对象。
- 最后,我们创建一个上下文字典并渲染请求的响应。
我们也可以提供由HTTP POST请求所调用的 post()方法来进行相应的支持。
扩展知识...
可以看到,get() 和get_page()方法很通用,因此我们可以在core应用中创建带有这两个方法的FilterableListView通用类。然后,在需要使用可过滤列表的应用中,可以创建继承FilterableListView的视图类处理这种场景。这一继承类只需要定义form_class和template_name属性以及get_queryset_and_facets()方法。这种模板化和扩展性代表了视图类的主要好处。
相关内容
- 过滤对象列表一节
- 管理分页列表一节
添加Open Graph和Twitter Card数据
如果希望在社交网站上分享网站的内容,则至少应实现Open Graph和Twitter Card标签。这些元标签定义了网页如何在Facebook或Twitter信息流中显示:展示什么标题和描述,设置什么图片以及关联的URL。本小节中我们将为社交分享准备idea_detail.html模板。
准备工作
我们继续使用前面小节中的ideas应用。
如何实现...
按照如下步骤来完成本小节的学习:
- 确保Idea模板创建了图片字段及各个图片版本规格。参见使用CRUDL函数创建应用一节和上传图片一节了解更多信息。
- 准备好ideas详情视图。参见使用CRUDL函数创建应用一节了解如何添加。
- 将详情视图插入到URL配置中。相关配置参见使用CRUDL函数创建应用一节。
- 在具体环境的设置中,定义WEBSITE_URL和作为完整媒体文件URL的MEDIA_URL,如下例所示:
# myproject/settings/dev.py
from ._base import *
DEBUG = True
WEBSITE_URL = "http://127.0.0.1:8000" # without trailing slash
MEDIA_URL = f"{WEBSITE_URL}/media/"
- 在core应用中,创建返回设置中WEBSITE_URL变量的上下文处理器:
# myproject/apps/core/context_processors.py
from django.conf import settings
def website_url(request):
return {
"WEBSITE_URL": settings.WEBSITE_URL,
}
- 在设置中插入上下文处理器:
# myproject/settings/_base.py
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [os.path.join(BASE_DIR, "myproject", "templates")],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"django.template.context_processors.media",
"django.template.context_processors.static",
"myproject.apps.core.context_processors.website_url",
]
}
}
]
- 创建模板文件idea_detail.html并添加如下内容:
{# ideas/idea_detail.html #}
{% extends "base.html" %}
{% load i18n %}
{% block meta_tags %}
{% if idea.picture_social %}
{% endif %}
{% if idea.picture_social %}
{% endif %}
{% endblock %}
{% block content %}
{% trans "List of ideas" %}
{% blocktrans trimmed with title=idea.translated_title %}
Idea "{{ title }}"
{% endblocktrans %}
{{ idea.translated_content|linebreaks|urlize }}
{% for category in idea.categories.all %}
{{ category.translated_title }}
{% endfor %}
{% trans "Change this idea" %}
{% trans "Delete this idea" %}
{% endblock %}
实现原理...
Open Graph标签是以og:打头特殊名称的元标签,Twitter card标签是以twitter:打头特殊名称的元标签。这些元标签定义了URL、标签、描述和当前页面的图片、站点名、作者和地理位置。在这里要提供完整路径,只提供后面的路径是不够的。
我们使用picture_social图片版本,它是针对社交网站的优化尺寸:1024 × 512 px。
可以通过https://developers.facebook.com/tools/debug/来验证Open Graph的实现情况。
通过https://cards-dev.twitter.com/validator可以验证Twitter Card的实现情况。
相关内容
- 使用CRUDL函数创建应用一节
- 上传图片一节
- 添加schema.org用词一节
添加schema.org用词
搜索引擎优化SEO需要遵循语法的标记。但更进一步提升搜索引擎排名,根据schema.org的用词提供结构化数据是大有裨益的。很多来自谷歌、微软、Pinterest和Yandex等的应用都使用schema.org结构来创造丰富可扩展的体验,如在搜索结果在活动、电脑、作者等的特殊一致性卡片展示。
有一些编码,包括RDFa、Microdata和JSON-LD,可用于创建schema.org用词。本小节中,我们将以JSON-LD格式来为Idea模板准备结构化数据,这一格式是谷歌所推荐的。
准备工作
在项目的虚拟环境中安装django-json-ld包(并将其添加到requirements/_base.txt中):
(env)$ pip install django-json-ld==0.0.4
在设置中将django_json_ld添加到INSTALLED_APPS中:
# myproject/settings/_base.py
INSTALLED_APPS = [
# other apps...
"django_json_ld",
]
如何实现...
按照如下步骤来完成本小节的学习:
- 在Idea模型中使用如下内容添加structured_data属性:
# myproject/apps/ideas/models.py
from django.db import models
from django.utils.translation import gettext_lazy as _
from myproject.apps.core.models import (
CreationModificationDateBase, UrlBase
)
class Idea(CreationModificationDateBase, UrlBase):
# attributes, fields, properties, and methods...
@property
def structured_data(self):
from django.utils.translation import get_language
lang_code = get_language()
data = {
"@type": "CreativeWork",
"name": self.translated_title,
"description": self.translated_content,
"inLanguage": lang_code,
}
if self.author:
data["author"] = {
"@type": "Person",
"name": self.author.get_full_name() or
self.author.username,
}
if self.picture:
data["image"] = self.picture_social.url
return data
- 修改idea_detail.html模板:
{# ideas/idea_detail.html #}
{% extends "base.html" %}
{% load i18n json_ld %}
{% block meta_tags %}
{# Open Graph and Twitter Card meta tags here... #}
{% render_json_ld idea.structured_data %}
{% endblock %}
{% block content %}
{% trans "List of ideas" %}
{% blocktrans trimmed with title=idea.translated_title %}
Idea "{{ title }}"
{% endblocktrans %}
{{ idea.translated_content|linebreaks|urlize }}
{% for category in idea.categories.all %}
{{ category.translated_title }}
{% endfor %}
{% trans "Change this idea" %}
{% trans "Delete this idea" %}
{% endblock %}
实现原理...
{% render_json_ld %}模板标签会像类似下面这样渲染script标签:
structured_data属性根据schema.org用词返回一个嵌套字典,这可由大部分主流搜索引擎所理解。
可通过查阅官方文档来决定对模型应用哪些用词。
相关内容
- 第2章 模型和数据库结构中创建模型mixin来处理元标签一节
- 使用CRUDL函数创建应用一节
- 上传图片一节
- 添加Open Graph和Twitter Card数据一节
生成PDF文档
Django视图不止是能让我们创建HTML页面。还可以创建任意类型的文件。例如,在第4章 模板和JavaScript中的在JavaScript暴露设置一节中,我们视图的输出是JavaScript文件的形式而非HTML。也可以对发票、票务、收据、预订确认等创建PDF文件。本小节中,我们将展示如何为数据库中的每个创建生成传单。这里使用WeasyPrint库来用HTML模板生成PDF文档。
准备工作
WeasyPrint依赖于一些库,需要在电脑上进行安装。macOS中可以使用如下命令通过Homebrew来安装这些库:
$ brew install python3 cairo pango gdk-pixbuf libffi
然后,可以在项目的虚拟环境中安装WeasyPrint本尊。同样请在requirements/_base.txt中进行添加:
(env)$ pip install WeasyPrint==48
对于其它操作系统,请参照安装教程。
另外我们将使用django-qr-code来生成快速链接回到网站的二维码。在虚拟环境中也对其进行安装(并在requirements/_base.txt中添加):
(env)$ pip install django-qr-code==1.0.0
在设置的INSTALLED_APPS中添加qr_code:
# myproject/settings/_base.py
INSTALLED_APPS = [
# Django apps...
"qr_code",
]
如何实现...
按照如下步骤来完成本节的学习:
- 创建用于生成PDF文档的视图:
# myproject/apps/ideas/views.py
from django.shortcuts import get_object_or_404
from .models import Idea
def idea_handout_pdf(request, pk):
from django.template.loader import render_to_string
from django.utils.timezone import now as timezone_now
from django.utils.text import slugify
from django.http import HttpResponse
from weasyprint import HTML
from weasyprint.fonts import FontConfiguration
idea = get_object_or_404(Idea, pk=pk)
context = {"idea": idea}
html = render_to_string(
"ideas/idea_handout_pdf.html", context
)
response = HttpResponse(content_type="application/pdf")
response[
"Content-Disposition"
] = "inline; filename={date}-{name}-handout.pdf".format(
date=timezone_now().strftime("%Y-%m-%d"),
name=slugify(idea.translated_title),
)
font_config = FontConfiguration()
HTML(string=html).write_pdf(
response, font_config=font_config
)
return response
- 将这个视图插入到URL配置中:
# myproject/apps/ideas/urls.py
from django.urls import path
from .views import idea_handout_pdf
urlpatterns = [
# URL configurations...
path("/handout/", idea_handout_pdf, name="idea_handout"),
]
- 为PDF文档创建一个模板:
{# ideas/idea_handout_pdf.html #}
{% extends "base_pdf.html" %}
{% load i18n qr_code %}
{% block content %}
{% trans "Handout" %}
{{ idea.translated_title }}
{{ idea.translated_content|linebreaks|urlize }}
{% for category in idea.categories.all %}
{{ category.translated_title }}
{% endfor %}
{% trans "See more information online:" %}
{% qr_from_text idea.get_url size=20 border=0 as svg_code %}
{{ idea.get_url }}
{% endblock %}
- 另外,创建base_pdf.html模板:
{# base_pdf.html #}
{% load i18n static %}
{% trans "Hello, World!" %}
{% block meta_tags %}{% endblock %}
{% block content %}
{% endblock %}
实现原理...
WeasyPrint生成可供打印、像素精致的文档。本例中可在展示时分发给观众的传递类似下面这样:
译者注:实际测试书中代码所添加的 BootStrap 样式不会生效,而因远程文件会报 ssl 相关错误,在idea_handout_pdf我使用的本地 css (线上下载的 BootStrap 4 css 代码)的方式生成的 PDF,代码如下:
css = CSS(
settings.WEBSITE_URL + settings.STATIC_URL + 'site/css/bootstrap.min.css',
)
HTML(string=html).write_pdf(
response,
font_config=font_config,
stylesheets=[css]
)
测试时发现 font_config 配置并不会产生什么影响,根据官方文档它主要用于兼容@font-face,总体测试发现 WeasyPrint与 BootStrap 的兼容性存在一定问题,如上图中可以看到使用 BootStrap 4时数字的显示存在问题,使用 BootStrap 3时则无此问题,调试时发现 HTML代码显示也是正常的,如果有知道问题的读者欢迎在评论区中告知。
文档的布局由HTML 和CSS进行定义。WeasyPrint有其自身的渲染引擎。阅读官方文档获取更多支持功能。
可以使用保存为矢量图而非位图的SVG图像,这样传单会更为精致。当前还不支持行内SVG,但可以使用带有数据源或外部URL的标签。本例中,我们对二维码及底部的logo使用了SVG图像。
我们来过一下视图中的代码。通过所选的idea来作为html字符串来渲染idea_handout_pdf.html模板。然后,我们创建PDF内容类型的HttpResponse对象,文件名由当前日期转为链接的idea的标题所构成。然后,我们通过HTML创建了一个WeasyPrint的HTML对象,并像写入文件中那样将其写入到响应中。此外,我们使用FontConfiguration对象,它让我们可以在布局的CSS配置中添加并使用网页字体。最后,我们返回响应对象。
相关内容
- 使用CRUDL函数创建应用一节
- 上传图片一节
- 第4章 模板和JavaScript中的在JavaScript暴露设置一节
通过Haystack和Whoosh实现多语言搜索
内容网站的一个主要功能是全文本搜索。Haystack是一个支持Solr、Elasticsearch、Whoosh和Xapian搜索引擎的模块化搜索API。对于项目中希望可进行搜索的模型,需要定义一个可通过模型读取文本信息并将其放到后台中的索引。本节中,我们将学习如何通过Haystack和基于Python的搜索语言来为多语言网站配置搜索。
准备工作
我们使用此前定义的categories和ideas应用。
确保在虚拟环境中安装了django-haystack和Whoosh(并将它们添加到requirements/_base.txt中):
(env)$ pip install django-haystack==2.8.1
(env)$ pip install Whoosh==2.7.4
如何实现...
执行如下步骤来通过Haystack和Whoosh来设置多语种搜索:
- 创建一个search应用,其中包含MultilingualWhooshEngine以及对ideas的搜索索引。搜索引擎放到multilingual_whoosh_backend.py文件:
# myproject/apps/search/multilingual_whoosh_backend.py
from django.conf import settings
from django.utils import translation
from haystack.backends.whoosh_backend import (
WhooshSearchBackend,
WhooshSearchQuery,
WhooshEngine,
)
from haystack import connections
from haystack.constants import DEFAULT_ALIAS
class MultilingualWhooshSearchBackend(WhooshSearchBackend):
def update(self, index, iterable, commit=True, language_specific=False):
if not language_specific and self.connection_alias == "default":
current_language = (translation.get_language() or
settings.LANGUAGE_CODE)[:2]
for lang_code, lang_name in settings.LANGUAGES:
lang_code_underscored = lang_code.replace("-", "_")
using = f"default_{lang_code_underscored}"
translation.activate(lang_code)
backend = connections[using].get_backend()
backend.update(index, iterable, commit, language_specific=True)
translation.activate(current_language)
elif language_specific:
super().update(index, iterable, commit)
class MultilingualWhooshSearchQuery(WhooshSearchQuery):
def __init__(self, using=DEFAULT_ALIAS):
lang_code_underscored = translation.get_language().replace("-", "_")
using = f"default_{lang_code_underscored}"
super().__init__(using=using)
class MultilingualWhooshEngine(WhooshEngine):
backend = MultilingualWhooshSearchBackend
query = MultilingualWhooshSearchQuery
- 创建搜索索引如下:
# myproject/apps/search/search_indexes.py
from haystack import indexes
from myproject.apps.ideas.models import Idea
class IdeaIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True)
def get_model(self):
return Idea
def index_queryset(self, using=None):
"""
Used when the entire index for model is updated.
"""
return self.get_model().objects.all()
def prepare_text(self, idea):
"""
Called for each language / backend
"""
fields = [
idea.translated_title, idea.translated_content
]
fields += [
category.translated_title
for category in idea.categories.all()
]
return "\n".join(fields)
- 使用MultilingualWhooshEngine配置设置文件:
# myproject/settings/_base.py
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__)
)))
#...
INSTALLED_APPS = [
# contributed
#...
# third-party
#...
"haystack",
# local
"myproject.apps.core",
"myproject.apps.categories",
"myproject.apps.ideas",
"myproject.apps.search",
]
LANGUAGE_CODE = "en"
# All official languages of European Union
LANGUAGES = [
("bg", "Bulgarian"),
("hr", "Croatian"),
("cs", "Czech"),
("da", "Danish"),
("nl", "Dutch"),
("en", "English"),
("et", "Estonian"),
("fi", "Finnish"),
("fr", "French"),
("de", "German"),
("el", "Greek"),
("hu", "Hungarian"),
("ga", "Irish"),
("it", "Italian"),
("lv", "Latvian"),
("lt", "Lithuanian"),
("mt", "Maltese"),
("pl", "Polish"),
("pt", "Portuguese"),
("ro", "Romanian"),
("sk", "Slovak"),
("sl", "Slovene"),
("es", "Spanish"),
("sv", "Swedish"),
]
HAYSTACK_CONNECTIONS = {}
for lang_code, lang_name in LANGUAGES:
lang_code_underscored = lang_code.replace("-", "_")
HAYSTACK_CONNECTIONS[f"default_{lang_code_underscored}"] = {
"ENGINE": "myproject.apps.search.multilingual_whoosh_backend.MultilingualWhooshEngine",
"PATH": os.path.join(BASE_DIR, "tmp", f"whoosh_index_{lang_code_underscored}"),
}
lang_code_underscored = LANGUAGE_CODE.replace("-", "_")
HAYSTACK_CONNECTIONS["default"] = HAYSTACK_CONNECTIONS[
f"default_{lang_code_underscored}"
]
- 在URL规则里添加路径:
# myproject/urls.py
from django.contrib import admin
from django.conf.urls.i18n import i18n_patterns
from django.urls import include, path
from django.conf import settings
from django.conf.urls.static import static
from django.shortcuts import redirect
urlpatterns = i18n_patterns(
path("", lambda request: redirect("ideas:idea_list")),
path("admin/", admin.site.urls),
path("accounts/", include("django.contrib.auth.urls")),
path("ideas/", include(("myproject.apps.ideas.urls", "ideas"), namespace="ideas")),
path("search/", include("haystack.urls")),
)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static("/media/", document_root=settings.MEDIA_ROOT)
- 需要一个针对搜索表单和搜索结果的模板,如下:
{# search/search.html #}
{% extends "base.html" %}
{% load i18n %}
{% block sidebar %}
{% endblock %}
{% block main %}
{% if query %}
{% trans "Search Results" %}
{% for result in page.object_list %}
{% with idea=result.object %}
{{ idea.translated_title }}
{% endwith %}
{% empty %}
{% trans "No results found." %}
{% endfor %}
{% include "misc/includes/pagination.html" with object_list=page %}
{% endif %}
{% endblock %}
- 像管理分页列表一节中那样在misc/includes/pagination.html中添加分页模板。
- 调用rebuild_index管理命令来索引数据库中的数据并准备用于全文本搜索:
(env)$ python manage.py rebuild_index --noinput
译者注:Haystack 最新版本与 Django 3.0在测试时发现存在兼容性的问题,主要有
- ImportError: cannot import name 'six' from 'django.utils'
- ImportError: cannot import name 'python_2_unicode_compatible' from 'django.utils.encoding'
这两个错误需要在源码层面进行处理:
修改原导入为 import six 及 from six import python_2_unicode_compatible或将对应文件代码拷贝到 django.utils 中
实现原理...
MultilingualWhooshEngine中指定了两个自定义属性:
- backend指向MultilingualWhooshSearchBackend,确保LANGUAGES设置中给定每种语言对应数据都会被索引,并放到HAYSTACK_CONNECTIONS所定义的相关联Haystack索引位置。
- query引用MultilingualWhooshSearchQuery,后者的职责是确保在搜索关键词时,会使用当前语言所对应的Haystack连接。
每个索引都有一个text字段,这里面存储模型中具体语言的全文本。索引的模型由get_model()所决定,index_queryset()定义应索引哪个查询集合,其中搜索的内容在prepare_text()方法中按新行分隔进行定义。
模板中我们集成了一些Bootstrap 4的元素来对表单进行开箱即用的渲染。这可以通过本章此前的通过django-crispy-forms创建表单布局一节中所介绍的方法进行完善。
最终的搜索页面表单在侧边栏中、搜索结果位于主栏中,类似下面这样:
定期更新搜索索引最简单的方式是通过定时任务(比如每晚)调用rebuild_index management命令。了解这部分知识,请参见第13章 维护中的设置cron job执行定时任务一节。
相关内容
- 通过django-crispy-forms创建表单布局一节
- 管理分页列表一节
- 第13章 维护中的设置cron job执行定时任务一节
通过Elasticsearch DSL实现多语言搜索
Haystack结合Whoosh一种很稳定的搜索机制 ,只需要用一些Python模块,但要实现更好的性能,我们推荐使用Elasticsearch。本节中,我们将展示如何使用它来实现多语种的搜索。
准备工作
首先安装Elasticsearch服务端。在macOS中可以使用Homebrew来进行安装:
$ brew install elasticsearch
在写本书时,Homebrew中最新的Elasticsearch稳定版本为6.8.2。
在虚拟环境中安装django-elasticsearch-dsl(并将其添加到requirements/_base.txt中):
(env)$ pip install django-elasticsearch-dsl==6.4.1
ℹ️注意安装版本相匹配的django-elasticsearch-dsl非常重要。否则在尝试连接Elasticsearch服务或构建索引时会报错。版本兼容性表请见https://github.com/sabricot/django-elasticsearch-dsl。
译者注:Elasticsearch 可通过多种方式,下载源码亦可直接启动,Alan使用 Docker 的方式进行安装, documents.py 中title_bg等配置取决于多语言所选择的方案。另在 ES 7中 StringField 可修改为 KeywordField
docker pull docker.elastic.co/elasticsearch/elasticsearch:7.7.0
docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.7.0
pip install django-elasticsearch-dsl==7.1.1
### 如何实现...
我们来通过如下步骤借助 Elasticsearch DSL设置多语言搜索:
1. 修改配置文件、添加django_elasticsearch_dsl到INSTALLED_APPS中,并配置ELASTICSEARCH_DSL如下:
myproject/settings/_base.py
INSTALLED_APPS = [
# other apps...
"django_elasticsearch_dsl",
]
ELASTICSEARCH_DSL={
'default': {
'hosts': 'localhost:9200'
},
}
2. 在ideas应用中,创建documents.py文件,在其中添加IdeaDocument来进行idea搜索的索引,如下:
myproject/apps/ideas/documents.py
from django.conf import settings
from django.utils.translation import get_language, activate
from django.db import models
from django_elasticsearch_dsl import fields
from django_elasticsearch_dsl.documents import (
Document,
model_field_class_to_field_class,
)
from django_elasticsearch_dsl.registries import registry
from myproject.apps.categories.models import Category
from .models import Idea
def _get_url_path(instance, language):
current_language = get_language()
activate(language)
url_path = instance.get_url_path()
activate(current_language)
return url_path
@registry.register_document
class IdeaDocument(Document):
author = fields.NestedField(
properties={
"first_name": fields.StringField(),
"last_name": fields.StringField(),
"username": fields.StringField(),
"pk": fields.IntegerField(),
},
include_in_root=True,
)
title_bg = fields.StringField()
title_hr = fields.StringField()
# other title_* fields for each language in the LANGUAGES setting...
content_bg = fields.StringField()
content_hr = fields.StringField()
# other content_* fields for each language in the LANGUAGES setting...
picture_thumbnail_url = fields.StringField()
categories = fields.NestedField(
properties=dict(
pk=fields.IntegerField(),
title_bg=fields.StringField(),
title_hr=fields.StringField(),
# other title_* definitions for each language in the LANGUAGES setting...
),
include_in_root=True,
)
url_path_bg = fields.StringField()
url_path_hr = fields.StringField()
# other url_path_* fields for each language in the LANGUAGES setting...
class Index:
name = "ideas"
settings = {"number_of_shards": 1, "number_of_replicas": 0}
class Django:
model = Idea
# The fields of the model you want to be indexed in Elasticsearch
fields = ["uuid", "rating"]
related_models = [Category]
def get_instances_from_related(self, related_instance):
if isinstance(related_instance, Category):
category = related_instance
return category.category_ideas.all()
3. 在IdeaDocument中添加prepare_*方法来准备索引的数据:
def prepare(self, instance):
lang_code_underscored = settings.LANGUAGE_CODE.replace("-", "")
setattr(instance, f"title{lang_code_underscored}", instance.title)
setattr(instance, f"content_{lang_code_underscored}", instance.content)
setattr(
instance,
f"url_path_{lang_code_underscored}",
get_url_path(instance=instance,
language=settings.LANGUAGE_CODE),
)
for lang_code, lang_name in settings.LANGUAGES_EXCEPT_THE_DEFAULT:
lang_code_underscored = lang_code.replace("-", "")
setattr(instance, f"title_{lang_code_underscored}", "")
setattr(instance, f"content_{lang_code_underscored}", "")
translations = instance.translations.filter(language=lang_code).first()
if translations:
setattr(instance, f"title_{lang_code_underscored}", translations.title)
setattr(
instance, f"content_{lang_code_underscored}",
translations.content
)
setattr(
instance,
f"url_path_{lang_code_underscored}",
_get_url_path(instance=instance,
language=lang_code),
)
data = super().prepare(instance=instance)
return data
def prepare_picture_thumbnail_url(self, instance):
if not instance.picture:
return ""
return instance.picture_thumbnail.url
def prepare_author(self, instance):
author = instance.author
if not author:
return []
author_dict = {
"pk": author.pk,
"first_name": author.first_name,
"last_name": author.last_name,
"username": author.username,
}
return [author_dict]
def prepare_categories(self, instance):
categories = []
for category in instance.categories.all():
category_dict = {"pk": category.pk}
lang_code_underscored = settings.LANGUAGE_CODE.replace("-", "_")
category_dict[f"title_{lang_code_underscored}"] = category.title
for lang_code, lang_name in settings.LANGUAGES_EXCEPT_THE_DEFAULT:
lang_code_underscored = lang_code.replace("-", "_")
category_dict[f"title_{lang_code_underscored}"] = ""
translations = category.translations.filter(language= lang_code).first()
if translations:
category_dict[f"title_{lang_code_underscored}"] = translations.title
categories.append(category_dict)
return categories
4. 对IdeaDocument添加一些属性和方法,从索引文档中返回翻译内容:
@property
def translated_title(self):
lang_code_underscored = get_language().replace("-", "")
return getattr(self, f"title{lang_code_underscored}", "")
@property
def translated_content(self):
lang_code_underscored = get_language().replace("-", "_")
return getattr(self, f"content_{lang_code_underscored}", "")
def get_url_path(self):
lang_code_underscored = get_language().replace("-", "_")
return getattr(self, f"url_path_{lang_code_underscored}", "")
def get_categories(self):
lang_code_underscored = get_language().replace("-", "_")
return [
dict(
translated_title=category_dict[f"title_{lang_code_underscored}"],
**category_dict,
)
for category_dict in self.categories
]
5. 在documents.py文件中还要做的一件事是对UUIDField打猴子补丁,因默认Django Elasticsearch DSL不支持这种字段。只需要在导入区块之后添加如下代码即可:
model_field_class_to_field_class[models.UUIDField] = fields.TextField
6. 在ideas应用的forms.py中创建IdeaSearchForm:
myproject/apps/ideas/forms.py
from django import forms
from django.utils.translation import ugettext_lazy as _
from crispy_forms import helper, layout
class IdeaSearchForm(forms.Form):
q = forms.CharField(label=_("Search for"), required=False)
def __init__(self, request, *args, **kwargs):
self.request = request
super().__init__(*args, **kwargs)
self.helper = helper.FormHelper()
self.helper.form_action = self.request.path
self.helper.form_method = "GET"
self.helper.layout = layout.Layout(
layout.Field("q", css_class="input-block-level"),
layout.Submit("search", _("Search")),
)
7. 添加使用Elasticsearch进行搜索的视图:
myproject/apps/ideas/views.py
from django.shortcuts import render
from django.conf import settings
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.utils.functional import LazyObject
from .forms import IdeaSearchForm
PAGE_SIZE = getattr(settings, "PAGE_SIZE", 24)
class SearchResults(LazyObject):
def __init__(self, search_object):
self._wrapped = search_object
def __len__(self):
return self._wrapped.count()
def __getitem__(self, index):
search_results = self._wrapped[index]
if isinstance(index, slice):
search_results = list(search_results)
return search_results
def search_with_elasticsearch(request):
from .documents import IdeaDocument
from elasticsearch_dsl.query import Q
form = IdeaSearchForm(request, data=request.GET)
search = IdeaDocument.search()
if form.is_valid():
value = form.cleaned_data["q"]
lang_code_underscored = request.LANGUAGE_CODE.replace("-", "_")
search = search.query(
Q("match_phrase",
**{f"title_{lang_code_underscored}":
value})
| Q("match_phrase", **{f"content_{
lang_code_underscored}": value})
| Q(
"nested",
path="categories",
query=Q(
"match_phrase",
**{f"categories__title_{
lang_code_underscored}": value},
),
)
)
search_results = SearchResults(search)
paginator = Paginator(search_results, PAGE_SIZE)
page_number = request.GET.get("page")
try:
page = paginator.page(page_number)
except PageNotAnInteger:
# If page is not an integer, show first page.
page = paginator.page(1)
except EmptyPage:
# If page is out of range, show last existing page.
page = paginator.page(paginator.num_pages)
context = {"form": form, "object_list": page}
return render(request, "ideas/idea_search.html", context)
8. 对搜索表单和搜索结果创建idea_search.html模板:
{# ideas/idea_search.html #}
{% extends "base.html" %}
{% load i18n crispy_forms_tags %}
{% block sidebar %}
{% crispy form %}
{% endblock %}
{% block main %}
{% trans "Search Results" %}
{% if object_list %}
{% for idea in object_list %}
{{ idea.translated_title }}
{% endfor %}
{% include "misc/includes/pagination.html" %}
{% else %}
{% trans "No ideas found." %}
{% endif %}
{% endblock %}
9. 像*管理分页列表*一节中那样在 misc/includes/pagination.html中添加分页模板。
10. 调用search_index --rebuild管理命令来索引数据库数据并准备用于搜索的全文本:
(env)$ python manage.py search_index --rebuild
### 实现原理...
Django Elasticsearch DSL文档类似于模型表单。这里可以定义存储模型中的哪个字段供稍后在搜索查询时使用。在我们的IdeaDocument示例中,保存了UUID、评分、 作者、分类、标题、内容和所有语言的URL路径以及图片缩略图URL。Index类定义这个文档的Elasticsearch索引设置。Django类定义从何处获取索引字段。有一个related_models设置在哪个模型修改后更新这个索引。本例中为Category模型。注意使用django-elasticsearch-dsl时,不论何时保存模型都会自动更新。这是通过信号来实现的。
get_instances_from_related()方法表明在修改Category实例时如何获取Idea模型实例。
IdeaDocument的prepare() 和 prepare_*()方法表明从何处获取数据以及对指定字段保存数据。例如,在语言字段等于lt时我们从IdeaTranslations模型的标题字段读取title_lt数据。
IdeaDocument最后面的那些属性和方法用于按当前语言从索引中获取信息。
然后我们来看一个搜索表单视图。表单中有一个名为q的查询字段。在进行提交时,我们在当前语言的标题、内容或分类标题字段中搜索所查询关键词。然后,我们使用懒加载类SearchResults封装搜索结果,这样就可以使用默认的Django分页器。
视图的模板侧边栏中有表单,主栏目中为搜索结果,类似下面这样:
![](https://upload-images.jianshu.io/upload_images/14565748-7c12d5d47876e841.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
### 相关内容
* *使用CRUDL函数创建应用*一节
* *通过Haystack和Whoosh实现多语言搜索*一节
* *通过django-crispy-forms创建表单布局*一节
* *管理分页列表*一节
本文首发地址: [Alan Hou 的人个博客](https://alanhou.org/forms-views/)