Django-Web-开发指南第二版-全-

Django Web 开发指南第二版(全)

原文:zh.annas-archive.org/md5/981fc588a383952aaf97d784d1c66ed2

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

关于本书

你是否想要开发可靠且安全的应用程序,脱颖而出,而不是花费数小时在样板代码上?如果是这样,Django 框架是你应该开始的地方。通常被称为“内置电池”的 Web 开发框架,Django 包含了构建独立应用程序所需的所有核心功能。

《使用 Django 进行 Web 开发》秉承这一理念,并为你提供所需的知识和信心,以便使用 Python 构建现实世界的应用程序。

从 Django 的基本概念开始,你将通过构建一个名为 Bookr 的网站来涵盖其主要功能——一个书评存储库。这个端到端的案例研究被分解成一系列小型的项目,以练习和活动的方式呈现,让你以愉快且可行的方式挑战自己。

随着你不断进步,你将学习各种实用技能,包括如何提供静态文件以添加 CSS、JavaScript 和图像到你的应用程序,如何实现表单以接受用户输入,以及如何管理会话以确保可靠的用户体验。在这本书中,你将涵盖现实世界 Web 应用程序开发周期中的关键日常任务。

在本书结束时,你将拥有技能和信心,以创造性地使用 Django 处理你自己的雄心勃勃的项目。

关于作者

本·肖是新西兰奥克兰的软件工程师。他作为一名开发者工作了超过 14 年,自 2007 年以来一直在使用 Django 构建网站。在这段时间里,他的经验帮助了许多不同类型的公司,从小型初创公司到大型企业。他还对机器学习、数据科学、自动化部署和 DevOps 感兴趣。在不编程的时候,本喜欢户外运动,并享受与他的伴侣和儿子共度时光。

萨乌拉布·巴德瓦尔是一位基础设施工程师,他致力于构建提高开发者生产力的工具和框架。他工作的很大一部分是使用 Python 开发能够扩展到数千个并发用户的服务。他目前在 LinkedIn 工作,负责基础设施性能工具和服务。

安德鲁·伯德是 Vesparum Capital 的数据和分析经理。他领导 Vesparum 的软件和数据科学团队,负责 Django/React 的全栈 Web 开发。他是一位澳大利亚精算师(FIAA,CERA),之前曾在金融服务领域的德勤咨询公司工作。安德鲁目前还在 Draftable Pvt. Ltd.担任全栈开发者,他自愿管理 Effective Altruism Australia 网站捐赠门户的持续开发。安德鲁还合著了我们最畅销的标题之一,“Python 研讨会”。

巴拉特·查德拉·K·S 住在澳大利亚悉尼,拥有超过 10 年的软件行业经验。他对使用 Python 栈进行软件开发非常热情,包括 Flask 和 Django 等框架。他既有与单体架构合作的经验,也有与微服务架构合作的经验,并构建了各种面向公众的应用程序和数据处理后端系统。当他不忙于编写软件应用程序时,他喜欢烹饪食物。

克里斯·古斯特 20 年前开始学习 Python 编程,那时它还是一门鲜为人知的学术语言。从那时起,他在出版、酒店、医疗和学术领域使用了他的 Python 知识。在他的职业生涯中,他与许多 Python 网络开发框架合作,包括 Zope、TurboGears、web2py 和 Flask,尽管他仍然更喜欢 Django。

本书面向对象

《使用 Django 进行 Web 开发》是为那些希望使用 Django 框架获得 Web 开发技能的程序员设计的。为了完全理解本书中解释的概念,你应该具备基本的 Python 编程知识,以及熟悉 JavaScript、HTML 和 CSS。

关于章节

第一章,Django 简介,首先立即设置 Django 项目。你将学习如何启动 Django 项目,响应 Web 请求,并使用 HTML 模板。

第二章,模型和迁移,介绍了 Django 数据模型,这是将数据持久化到 SQL 数据库的方法。

第三章,URL 映射、视图和模板,基于第一章,Django 简介中介绍的技术,更深入地解释了如何将 Web 请求路由到 Python 代码并渲染 HTML 模板。

第四章,Django 管理器简介,展示了如何使用 Django 内置的 Admin GUI 来创建、更新和删除由你的模型存储的数据。

第五章,服务静态文件,解释了如何通过添加样式和图像来增强你的网站,以及 Django 如何使管理这些文件变得更容易。

第六章,表单,展示了如何通过使用 Django 的表单模块在你的网站上收集用户输入。

第七章,高级表单验证和模型表单,在第六章,表单的基础上,增加了更高级的验证逻辑,使你的表单更强大。

第八章,媒体服务和文件上传,展示了如何通过允许用户上传文件并使用 Django 提供这些文件来进一步增强网站。

第九章,会话和认证,介绍了 Django 会话,并展示了如何使用它来存储用户数据并验证用户。

第十章,高级 Django 管理器和定制,从第四章,Django 管理器简介继续。现在你更了解 Django 了,你可以使用高级功能来定制 Django 管理器。

第十一章,高级模板和基于类的视图,让你看到如何通过使用 Django 的一些高级模板特性和类来减少你需要编写的代码量。

第十二章构建 REST API,向您展示了如何将 REST API 添加到 Django 中,以便从不同的应用程序以编程方式访问您的数据。

第十三章生成 CSV、PDF 和其他二进制文件,通过展示如何使用 Django 生成不仅仅是 HTML 的内容,进一步扩展了 Django 的功能。

第十四章测试,是现实世界开发的一个重要部分。本章展示了如何使用 Django 和 Python 测试框架来验证您的代码。

第十五章Django 第三方库,向您介绍了许多社区构建的 Django 库,展示了如何使用现有的第三方代码快速为您的项目添加功能。

第十六章在 Django 中使用前端 JavaScript 库,通过集成 React 和在第十二章构建 REST API中创建的 REST API,为您的网站带来交互性。

第十七章Django 应用程序部署(第一部分 - 服务器设置),通过设置您自己的服务器开始部署应用程序的过程。这也是一个附加章节,可以从本书的 GitHub 仓库中下载。

第十八章部署 Django 应用程序(第二部分 - 配置和代码部署),通过向您展示如何将项目部署到虚拟服务器来结束项目。这也是一个附加章节,可以从本书的 GitHub 仓库中下载。

习惯用法

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL 和用户输入如下所示:“它通过在命令行上运行带有startproject参数的django-admin.py命令来创建和构建。”

您在屏幕上看到的单词,例如在菜单或对话框中,也在文本中如下所示:“在左侧的首选项列表窗格中,打开项目:Bookr项,然后单击项目解释器。”

代码块设置如下:

urlpatterns = [path('admin/', admin.site.urls),\
               path('', reviews.views.index)]

在输入和执行某些代码立即给出输出的情况下,这如下所示:

>>> qd.getlist("k")
['a', 'b', 'c']

在前面的例子中,输入的代码是 qd.getlist("k"),输出是 ['a', 'b', 'c']

新术语和重要词汇如下所示:“Django 模型定义了您应用程序的数据,并通过对象关系映射器ORM)提供对 SQL 数据库访问的抽象层。”

涵盖多行的代码行使用反斜杠(\)进行分割。当代码执行时,Python 将忽略反斜杠,并将下一行的代码视为当前行的直接延续。

例如:

urlpatterns = [path('admin/', admin.site.urls), \ 
               path('', reviews.views.index)]

长代码片段将被截断,并在截断代码的顶部放置相应的 GitHub 代码文件名称。整个代码的永久链接放置在代码片段下方。它应该如下所示:

settings.py
INSTALLED_APPS = ['django.contrib.admin',\
                  'django.contrib.auth',\
                  'django.contrib.contenttypes',\
                  'django.contrib.sessions',\
                  'django.contrib.messages',\
                  'django.contrib.staticfiles',\
                  'reviews']
The full code can be found at http://packt.live/2Kh58RE.

在开始之前

每一段伟大的旅程都始于一个谦卑的步伐。在我们能够使用 Django 做出令人惊叹的事情之前,我们需要准备好一个高效的环境。在本节中,我们将了解如何做到这一点。

安装 Python

在使用 Django 3 版本或更高版本之前,您需要在您的计算机上安装 Python 3。Mac 和 Linux 操作系统通常已经安装了某些版本的 Python,但最好确保您正在运行最新版本。在 Mac 上,对于 Homebrew 用户,您只需输入以下命令:

$ brew install python

在基于 Debian 的 Linux 发行版中,您可以通过输入以下命令来检查哪个版本可用:

$ apt search python3

根据输出,您可以输入类似以下的内容:

$ sudo apt install python3 python3-pip

对于 Windows,您可以从这里下载 Python 3 安装程序:www.python.org/downloads/windows/。一旦您有了安装程序,点击它以运行,然后按照说明进行。务必选择 将 Python 3.x 添加到 PATH 选项。

安装完成后,从命令提示符中,您可以运行 python 来启动 Python 解释器。

注意,在 macOS 和 Linux 上,根据您的配置,python 命令可能会启动 Python 2 版本或 Python 3 版本。为了确保正确,请确保指定 python3。在 Windows 上,您只需运行 python 即可,因为这总是会启动 Python 3 版本。

类似地,使用 pip 命令。在 macOS 和 Linux 上,指定 pip3;在 Windows 上,只需 pip

安装 PyCharm 社区版

在《使用 Django 进行 Web 开发》中,我们将使用 PyCharm 连续版CE)作为我们的 集成开发环境IDE)来编辑我们的代码以及运行和调试它。它可以从 www.jetbrains.com/pycharm/download/ 下载。一旦您有了安装程序,按照您操作系统的常规方法进行安装说明。

您可以在以下链接中找到 macOS、Linux 和 Windows 的详细安装说明:www.jetbrains.com/help/pycharm/installation-guide.html#standalone。PyCharm 的系统要求可以在这里找到:www.jetbrains.com/help/pycharm/installation-guide.html#requirements。有关安装后访问 PyCharm 的更多信息,您可以点击此链接:www.jetbrains.com/help/pycharm/run-for-the-first-time.html

virtualenv

虽然不是必需的,但我们建议使用 Python 虚拟环境,这将使 使用 Django 进行 Web 开发 的 Python 包与您的系统包分开。

首先,我们将探讨如何在 macOS 和 Linux 上设置虚拟环境。需要安装 virtualenv Python 包,这可以通过 pip3 完成:

$ pip3 install virtualenv

然后,我们可以在当前目录中创建一个虚拟环境:

$ python3 -m virtualenv <virtualenvname>

一旦创建了虚拟环境,我们需要源码它,以便当前终端知道使用该环境的 Python 和包。这样做的方式如下:

$ source <virtualenvname>/bin/activate

在 Windows 上,我们可以使用内置的venv库,它的工作方式类似。我们不需要安装任何东西。要创建当前目录中的虚拟环境,我们可以运行以下命令:

> python -m venv <virtualenvname>

一旦创建,使用新虚拟环境中的Scripts目录内的激活脚本激活它:

> <virtualenvname>\Scripts\activate

在 macOS、Linux 和 Windows 上,你会知道虚拟环境已经激活,因为它的名称(括号内)将出现在提示符之前。例如:

(virtualenvname) $ 

安装 Django

激活你的虚拟环境后,使用pip3pip(取决于你的操作系统)安装 Django:

(virtualenvname)$ pip3 install django

只要你的虚拟环境已经激活,它将使用该环境中的pip版本,并在该环境中安装包。

Django 3.0 和 Django 3.1

从 Django 3.1 开始,Django 的作者改变了在 Django 设置文件中连接路径的方法。我们将在第一章Django 简介中深入解释设置文件,但你现在只需要知道这个文件叫做settings.py

在早期版本中,BASE_DIR设置变量(你的项目在磁盘上的路径)被创建为一个字符串,如下所示:

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

os包被导入到settings.py中,路径使用os.path.join函数连接。例如:

STATIC_ROOT = os.path.join(BASE_DIR, "static")  # Django 3.0 and earlier

在 Django 3.1 中,BASE_DIR现在是一个pathlib.Path对象。它被这样分配:

BASE_DIR = Path(__file__).resolve().parent.parent

可以使用pathlib.Path重载的/(除法)运算符将路径对象和字符串连接起来:

STATIC_ROOT = BASE_DIR / "static"  # Django 3.1+

os.path.join函数也可以用来连接pathlib.Path对象,前提是它已经被导入到settings.py中。

由于今天大多数生产中的 Django 项目使用的是 3.1 之前的 Django 版本,我们选择在整个书中使用os.path.join函数来连接路径。当你创建一个新的 Django 项目时,它将使用 Django 的最新版本,这个版本将高于 3.1。因此,为了确保兼容性,你只需确保在settings.py的开头添加一行来导入os模块,如下所示:

import os

一旦添加,你就可以按照书中的说明进行操作,无需修改。我们也会在你开始使用settings.py时提醒你进行此更改。

除了这个小的添加之外,无需对本书中的代码示例、练习和活动进行任何修改,以支持 Django 3.0 或 3.1。虽然我们永远不能 100%确定,但我们相信这段代码也会与 Django 的未来版本兼容。

SQLite 数据库浏览器

本书在开发项目时使用 SQLite 作为磁盘数据库。Django 提供了一个命令行接口,使用文本命令访问其数据,但 GUI 应用程序也可供使用,使数据浏览更加友好。

我们推荐的工具是DB Browser for SQLite,或简称DB Browser。它是一个跨平台(Windows、macOS 和 Linux)的图形用户界面应用程序。

在 Windows 上安装

  1. sqlitebrowser.org/dl/ 下载适用于正确架构的 Windows(32 位或 64 位)的安装程序。

  2. 运行下载的安装程序并遵循设置向导的说明:![图 0.1:设置向导页面 img/B15509_00_01.jpg

    图 0.1:设置向导页面

  3. 接受最终用户许可协议后,你将被要求选择应用程序的快捷方式。建议你为 DB Browser 启用桌面程序菜单快捷方式,以便安装后更容易找到应用程序:![图 0.2:可以在此选择应用程序快捷方式的页面 img/B15509_00_02.jpg

    图 0.2:可以在此选择应用程序快捷方式的页面

  4. 在安装过程中,只需在每个屏幕上点击下一步即可遵循默认设置。

  5. 如果你没有在步骤 3中添加程序菜单桌面快捷方式,那么你需要在C:\Program Files\DB Browser for SQLite中找到 DB Browser。

在 macOS 上安装

  1. sqlitebrowser.org/dl/ 下载适用于 macOS 的应用程序磁盘镜像。

  2. 下载完成后,打开磁盘镜像。你会看到一个像这样的窗口:![图 0.3:磁盘镜像 img/B15509_00_03.jpg

    图 0.3:磁盘镜像

    DB Browser for SQLite应用程序拖放到应用程序文件夹中以安装它。

  3. 安装完成后,您可以从应用程序文件夹中启动DB Browser for SQLite

在 Linux 上安装

Linux 的安装说明将取决于你使用的发行版。你可以在 sqlitebrowser.org/dl/ 找到说明。

使用 DB Browser

这里有一些屏幕截图展示了 DB Browser 的几个功能。这些截图是在 macOS 上拍摄的,但在所有平台上行为相似。打开后的第一步是选择你的 SQLite 数据库文件:

![图 0.4:数据库打开对话框img/B15509_00_04.jpg

图 0.4:数据库打开对话框

一旦打开数据库文件,我们就可以在数据库结构标签页中探索其结构。图 0.5展示了这一点:

![图 0.5:展开一个表的数据库结构img/B15509_00_05.jpg

图 0.5:展开一个表的数据库结构

在前面的屏幕截图中,reviews_book表已被展开,以便我们可以看到其表结构。我们还可以通过切换到浏览数据标签来浏览表内的数据:

![图 0.6:reviews_book 表中的数据img/B15509_00_06.jpg

图 0.6:reviews_book 表中的数据

我们可能想要做的最后一件事是执行 SQL 命令(你将在第二章模型和迁移中了解这些)。这是在执行 SQL标签页中完成的:

![图 0.7:执行 SQL 命令并显示结果图 0.7:执行 SQL 命令的结果

图 0.7:执行 SQL 命令的结果

图 0.7 展示了执行 SQL 语句 SELECT * FROM reviews_book 的结果。

如果你现在还不确定这一切意味着什么(在这个阶段,你甚至还没有 SQLite 文件来尝试),等你开始学习 Django 模型、数据库和 SQL 查询时,一切就会更加清晰。第二章模型和迁移,是你开始使用 DB Browser 的工作的地方。

Bookr 项目

在整本书中,你将逐步构建一个名为 Bookr 的应用。它旨在让用户浏览和添加书评(以及书籍)。随着你完成每一章的练习和活动,你将为应用添加更多功能。本书的 GitHub 仓库包含练习和活动的单独文件夹。这些文件夹通常包括应用代码发生变化的文件。

最终目录

每一章的代码都将有一个名为 final 的目录。这个目录将包含从该章节开始到结束期间为应用编写的所有代码。例如,第五章服务静态文件final 文件夹将包含 Bookr 应用直到该章节结束的完整代码。这样,如果你丢失了进度,你可以使用 final 文件夹中的代码,比如 第五章 的,来开始 第六章

以下截图显示了从 GitHub 仓库下载代码到磁盘后章节的目录结构将如何出现(有关如何从仓库下载代码的更多详细信息,请参阅 安装代码包 部分):

图 0.8:Bookr 的章节级目录结构

图 0.8:Bookr 的章节级目录结构

图 0.8:Bookr 的章节级目录结构

填充数据

当你到达 第二章模型和迁移 时,建议你使用我们提供的样本书籍列表填充你的数据库,以确保你的最终结果与我们的结果大致相似。确保你不会跳过 第二章模型和迁移 中名为 填充 Bookr 数据库 的部分,其中我们提供了一个小脚本,让你可以快速填充数据库。

安装代码包

从 GitHub 在 packt.live/3nIWPvB 下载代码文件。请参考这些代码文件以获取完整的代码包。这里的文件包含练习、活动、活动解决方案、额外章节以及每个章节的一些中间代码。

在 GitHub 仓库的页面上,你可以点击绿色的 Code 按钮,然后点击 Download ZIP 选项,将完整的代码作为 ZIP 文件下载到你的磁盘上(有关如何从仓库下载代码的更多详细信息,请参阅 安装代码包 部分)。然后,你可以将这些代码文件解压到你选择的文件夹中,例如,C:\Code

图 0.9:下载 ZIP 选项

图 0.7:执行 SQL 命令的结果

图 0.9:下载 ZIP 选项

联系我们

欢迎读者反馈。

customercare@packtpub.com

勘误: 尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,如果您能向我们报告,我们将不胜感激。请访问 www.packtpub.com/support/errata 并填写表格。

copyright@packt.com 并附上相关材料的链接。

如果您有兴趣成为作者: 如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问 authors.packtpub.com

请留下评论

通过在亚马逊上留下详细、公正的评论来告诉我们您的想法。我们感谢所有反馈——它帮助我们继续制作优质产品并帮助有抱负的开发者提升技能。请抽出几分钟时间留下您的想法——这对我们来说意义重大。

第一章:1. Django 简介

概述

本章向您介绍 Django 及其在 Web 开发中的作用。您将从学习如何使用manage.py命令(用于协调 Django 操作)开始。您将使用此命令启动 Django 开发服务器并测试您编写的代码是否按预期工作。您还将学习如何使用PyCharm,这是一种流行的 Python 集成开发环境(IDE),您将在本书的整个过程中使用它。您将使用它编写返回给 Web 浏览器的响应的代码。最后,您将学习如何使用 PyCharm 的调试器来调试代码中的问题。到本章结束时,您将具备使用 Django 创建项目的必要技能。

简介

"为完美主义者设计的具有截止日期的 Web 框架。" 这是一条恰如其分的标语,描述了 Django,一个已经存在超过 10 年的框架。它经过实战检验,被广泛使用,每天都有越来越多的人使用它。所有这些可能让你认为 Django 已经过时,不再相关。相反,它的长期存在证明了它的应用程序编程接口(API)是可靠和一致的,甚至那些在 2007 年学习 Django v1.0 的人现在也能为 Django 3 编写相同的代码。Django 仍在积极开发中,每月都会发布错误修复和安全补丁。

与它所写的语言 Python 一样,Django 易于学习,但功能强大且足够灵活,可以满足您的需求增长。它是一个“内置电池”的框架,这意味着您不需要寻找和安装许多其他库或组件来使您的应用程序运行。其他框架,如FlaskPylons,需要手动安装第三方框架来进行数据库连接或模板渲染。相反,Django 内置了对数据库查询、URL 映射和模板渲染的支持(我们很快会详细介绍这些功能)。但仅仅因为 Django 易于使用,并不意味着它有限制。Django 被许多大型网站使用,包括 Disqus (disqus.com/)、Instagram (www.instagram.com/)、Mozilla (www.mozilla.org/)、Pinterest (www.pinterest.com/)、OpenStack (www.openstack.org/)和 National Geographic (www.nationalgeographic.com/)。

Django 在 Web 中处于什么位置?当谈到 Web 框架时,你可能会想到前端 JavaScript 框架,如 ReactJS、Angular 或 Vue。这些框架用于增强或添加交互性到已经生成的网页。Django 位于这些工具的下一层,负责路由 URL、从数据库获取数据、渲染模板以及处理用户表单输入。然而,这并不意味着你必须选择其中一个;JavaScript 框架可以用来增强 Django 的输出,或者与 Django 生成的 REST API 交互。

在这本书中,我们将使用专业 Django 开发者每天使用的方法来构建一个 Django 项目。该应用程序被称为Bookr,允许浏览和添加书籍及书评。本书分为四个部分。在第一部分,我们将从搭建 Django 应用的基础开始,快速构建一些页面,并使用 Django 开发服务器来提供服务。你将能够通过 Django 管理站点向数据库添加数据。

下一部分将专注于增强 Bookr。你将为网站添加样式和图片来提供静态文件。通过使用 Django 的form库,你将添加交互性,并通过使用文件上传,你将能够上传书籍封面和其他文件。然后,你将实现用户登录,并学习如何在会话中存储当前用户的信息。

在第三部分,你将在现有知识的基础上进一步提升。你将定制 Django 管理站点,然后学习高级模板技术。接下来,你将学习如何构建REST API并生成非 HTML 数据(如 CSV 和 PDF),并通过学习测试 Django 来结束本部分。

许多第三方库可用于增强 Django 功能,使开发更简单,从而节省时间。在最后一部分,你将了解一些有用的库以及如何将它们集成到你的应用程序中。应用这些知识,你将集成一个 JavaScript 库来与上一节中构建的 REST 框架通信。最后,你将学习如何将你的 Django 应用程序部署到虚拟服务器。

到本书结束时,你将拥有足够的设计和从头到尾构建自己的 Django 项目的经验。

搭建 Django 项目和应用程序

在深入探讨 Django 范式和 HTTP 请求背后的理论之前,我们将向你展示如何轻松地将 Django 项目搭建起来。在完成本部分和练习之后,你将已经创建了一个 Django 项目,用浏览器向其发送请求,并看到了响应。

Django 项目是一个包含您项目所有数据的目录:代码、设置、模板和资产。它是通过在命令行上运行带有 startproject 参数的 django-admin.py 命令并输入项目名称来创建和构建的。例如,要创建一个名为 myproject 的 Django 项目,运行的命令如下:

django-admin.py startproject myproject

这将创建 myproject 目录,Django 会填充运行项目所需的必要文件。在 myproject 目录内有两个文件(如图 1.1 所示):

图 1.1:myproject 的项目目录

图 1.1:myproject 的项目目录

manage.py 是一个 Python 脚本,在命令行中执行以与您的项目交互。我们将使用它来启动 django-admin.py,命令通过命令行传入。与 django-admin.py 不同,此脚本未映射到您的系统路径,因此我们必须使用 Python 来执行它。我们需要使用命令行来完成此操作。例如,在项目目录内,运行以下命令:

python3 manage.py runserver

这将 runserver 命令传递给 manage.py 脚本,从而启动 Django 开发服务器。我们将在 Django 项目 部分检查 manage.py 接受的更多命令。以这种方式与 manage.py 交互时,我们称这些管理命令。例如,我们可能会说我们在 "执行 runserver 管理命令。"

startproject 命令还创建了一个与项目同名的目录,在本例中为 myproject (图 1.1)。这是一个包含设置和一些其他配置文件(您的项目运行所需的)的 Python 包。我们将在 Django 项目 部分检查其内容。

在启动 Django 项目后,接下来要做的事情是启动一个 Django 应用。我们应该尝试将我们的 Django 项目分割成不同的应用程序,按功能分组。例如,在 Bookr 中,我们将有一个 reviews 应用。这将包含所有与书评工作相关的代码、HTML、资产和数据库类。如果我们决定将 Bookr 扩展为销售书籍,我们可能会添加一个 store 应用程序,包含书店的文件。应用程序通过 startapp 管理命令创建,传入应用程序名称。例如:

python3 manage.py startapp myapp

这将在项目目录内创建应用程序目录 (myapp)。Django 会自动填充此目录,以便在您开始开发时填充应用程序所需的文件。我们将在 Django 应用 部分检查这些文件并讨论什么使一个应用程序变得良好。

现在我们已经介绍了构建 Django 项目和应用程序的基本命令,让我们通过启动本书的第一个练习中的 Bookr 项目来将它们付诸实践。

练习 1.01:创建项目和应用程序,并启动开发服务器

在本书中,我们将构建一个名为 Bookr 的书评网站。它将允许你添加出版商、贡献者、书籍和评论的字段。出版商将出版一本或多本书,每本书将有一个或多个贡献者(作者、编辑、合著者等等)。只有管理员用户才能修改这些字段。一旦用户在该网站上注册了账户,他们就可以开始为书籍添加评论。

在这个练习中,你将构建 bookr Django 项目,通过运行开发服务器来测试 Django 是否工作,然后创建 reviews Django 应用。

你应该已经设置了一个包含 Django 的虚拟环境。要了解如何做,你可以参考 前言。一旦准备好,让我们开始创建 Bookr 项目:

  1. 打开一个终端并运行以下命令来创建 bookr 项目目录和默认子目录:

    django-admin startproject bookr
    

    此命令不会生成任何输出,但将在你运行命令的目录中创建一个名为 bookr 的文件夹。你可以查看这个目录,看看我们之前在 myproject 示例中描述的项目:bookr 包目录和 manage.py 文件。

  2. 我们现在可以通过运行 Django 开发服务器来测试项目和 Django 是否设置正确。启动服务器是通过 manage.py 脚本完成的。

    在你的终端(或命令提示符)中,切换到 bookr 项目目录(使用 cd 命令),然后运行 manage.py runserver 命令。

    python3 manage.py runserver
    Watching for file changes with StatReloader
    Performing system checks...
    System check identified no issues (0 silenced).
    You have 17 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
    Run 'python manage.py migrate' to apply them.
    September 14, 2019 - 09:40:45
    Django version 3.0a1, using settings 'bookr.settings'
    Starting development server at http://127.0.0.1:8000/
    Quit the server with CONTROL-C.
    

    你可能会收到一些关于未应用迁移的警告,但就目前来说这没关系。

  3. 打开一个网页浏览器并访问 http://127.0.0.1:8000/,这将显示 Django 欢迎界面(图 1.2)。如果你看到这个界面,你就知道你的 Django 项目已成功创建,目前一切运行正常:图 1.2:Django 欢迎界面

    图 1.2:Django 欢迎界面

  4. 返回你的终端并使用 Ctrl + C 键组合停止运行的开发服务器。

  5. 我们现在将为 bookr 项目创建 reviews 应用。在你的终端中,确保你处于 bookr 项目目录中,然后执行以下命令来创建 reviews 应用:

    python3 manage.py startapp reviews
    

    注意

    创建 reviews 应用后,你的 bookr 项目目录中的文件将如下所示:packt.live/3nZGy5D

    如果命令成功执行,则没有输出,但已创建一个 reviews 应用目录。你可以查看这个目录,看看创建的文件:migrations 目录、admin.pymodels.py 等等。我们将在 Django 应用 部分详细检查这些文件。

在这个练习中,我们创建了 bookr 项目,通过启动 Django 开发服务器来测试项目是否工作,然后为该项目创建了 reviews 应用。现在我们已经对 Django 项目有了实际操作的经验,我们将回到 Django 设计和 HTTP 请求与响应背后的理论。

模型视图模板

应用程序设计中的一种常见设计模式是 模型视图控制器MVC),其中应用程序的模型(其数据)在一个或多个视图中显示,控制器协调模型和视图之间的交互。Django 遵循一个类似但略有不同的范式,称为 模型视图模板MVT)。

与 MVC 类似,MVT 也使用模型来存储数据。然而,在 MVT 中,视图将查询模型,然后使用模板渲染它。通常,在 MVC 语言中,所有三个组件都需要用相同的语言开发。在 MVT 中,模板可以是不同的语言。在 Django 的情况下,模型和视图是用 Python 编写的,而模板是用 HTML 编写的。这意味着 Python 开发者可以处理模型和视图,而专门的 HTML 开发者可以处理 HTML。我们首先将更详细地解释模型、视图和模板,然后看看它们在哪些示例场景中被使用。

模型

Django 模型定义了应用程序的数据并提供了一个通过 对象关系映射器ORM)访问 SQL 数据库的抽象层。ORM 允许你使用 Python 代码定义你的数据模式(类、字段及其关系),而不需要了解底层数据库。这意味着你可以在 Python 代码中定义你的数据库层,而 Django 将为你生成 SQL 查询。ORM 将在 第二章模型和迁移 中详细讨论。

注意

(SELECT) 添加或更改数据(分别使用 INSERTUPDATE),以及删除数据(使用 DELETE)。有许多 SQL 数据库服务器可供选择,例如 SQLite、PostgreSQL、MySQL 或 Microsoft SQL Server。SQL 语法在数据库之间有很多相似之处,但可能会有一些方言上的差异。Django 的 ORM 会为你处理这些差异:当我们开始编码时,我们将使用 SQLite 数据库在磁盘上存储数据,但当我们部署到服务器时,我们将切换到 PostgreSQL,而无需对代码进行任何更改。

通常,当查询数据库时,结果会以原始 Python 对象的形式返回(例如,字符串列表、整数、浮点数或字节)。当使用 ORM 时,结果会自动转换为已定义的模型类实例。使用 ORM 意味着你自动受到一种称为 SQL 注入攻击的漏洞的保护。

如果你更熟悉数据库和 SQL,你也可以选择编写自己的查询。

视图

Django 视图是定义应用程序大部分逻辑的地方。当用户访问你的网站时,他们的网络浏览器会发送一个请求以从你的网站检索数据(在下一节中,我们将更详细地介绍 HTTP 请求是什么以及它包含的信息)。视图是你编写的一个函数,它将以 Python 对象的形式接收这个请求(具体来说,是一个 Django HttpRequest对象)。你的视图必须决定如何响应请求以及向用户发送什么信息。你的视图必须返回一个HttpResponse对象,该对象封装了提供给客户端的所有信息:内容、HTTP 状态和其他头信息。

视图还可以可选地从请求的 URL 中接收信息,例如,一个 ID 号。视图的一个常见设计模式是通过 Django ORM 使用传递给视图的 ID 查询数据库。然后视图可以通过提供从数据库检索到的模型的数据来渲染一个模板(稍后我们将详细介绍这一点)。渲染的模板成为HttpResponse的内容,并从视图函数返回。Django 负责将数据回传到浏览器。

模板

模板中的<>符号(以及其他符号)是 HTML 中的特殊字符。如果你尝试在变量中使用它们,那么 Django 会自动编码它们,以便在浏览器中正确渲染。

MVT 实践

我们现在将查看一些示例,以说明 MVT 在实际中的应用。在示例中,我们有一个Book模型,它存储有关不同书籍的信息,以及一个Review模型,它存储有关书籍不同评论的信息。

在第一个例子中,我们希望能够编辑书籍或评论的信息。以编辑书籍详情的第一个场景为例。我们会有一个视图从数据库中获取Book数据并提供Book模型。然后,我们会将包含Book对象(和其他数据)的上下文信息传递给一个模板,该模板将显示一个表单以捕获新信息。第二个场景(编辑评论)类似:从数据库中获取Review模型,然后将Review对象和其他数据传递给模板以显示编辑表单。这些场景可能非常相似,以至于我们可以为两者重用相同的模板。请参阅图 1.3

图 1.3:编辑单个书籍或评论

图 1.3:编辑单个书籍或评论

你可以在这里看到我们使用了两种模型、两种视图和一种模板。每个视图都会获取其关联模型的一个实例,但它们都可以使用相同的模板,这是一个通用的 HTML 页面,用于显示表单。视图可以为每种模型类型提供额外的上下文数据,以略微改变模板的显示。图中还展示了用 Python 和 HTML 编写的代码部分。

在第二个例子中,我们希望能够向用户显示存储在应用程序中的书籍或评论列表。此外,我们希望允许用户搜索书籍并获取所有符合他们标准的列表。我们将使用与上一个例子相同的两个模型(BookReview),但我们将创建新的视图和模板。由于有三个场景,我们这次将使用三个视图:第一个获取所有书籍,第二个获取所有评论,最后一个根据某些搜索标准搜索书籍。再次强调,如果我们编写了一个好的模板,我们可能再次只使用一个 HTML 模板。参见 图 1.4

图 1.4:查看多本书或评论

图 1.4:查看多本书或评论

BookReview 模型与上一个例子保持不变。这三个视图将获取许多(零个或多个)书籍或评论。然后,每个视图都可以使用相同的模板,这是一个通用的 HTML 文件,它遍历它给出的对象列表并渲染它们。再次强调,视图可以在上下文中发送额外的数据以改变模板的行为,但模板的大部分内容将尽可能通用。

在 Django 中,不一定需要使用模型来渲染 HTML 模板。视图可以自己生成上下文数据,并使用它渲染一个模板,而不需要任何模型数据。参见 图 1.5,一个视图直接将数据发送到模板:

图 1.5:无需模型从视图到模板

图 1.5:无需模型从视图到模板

在这个例子中,有一个欢迎视图用于欢迎用户访问网站。它不需要从数据库获取任何信息,因此它可以自己生成上下文数据。上下文数据取决于你想要显示的信息类型;例如,如果你想要通过用户名问候他们,你可以传递用户信息。视图也可以在没有上下文数据的情况下渲染模板。如果你有一个包含静态信息的 HTML 文件并希望提供服务,这可能很有用。

HTTP 简介

现在你已经了解了 Django 中的 MVT(模型-视图-模板),我们可以看看 Django 如何处理 HTTP 请求并生成 HTTP 响应。但首先,我们需要更详细地解释 HTTP 请求和响应是什么,以及它们包含哪些信息。

假设有人想访问你的网页。他们输入其 URL 或从他们已经所在的页面点击链接到你的网站。他们的网络浏览器创建一个 HTTP 请求,并将其发送到托管你网站的服务器。一旦网络服务器从你的浏览器接收到 HTTP 请求,它就可以解释它,然后发送回一个响应。服务器发送的响应可能很简单,例如只是从磁盘读取 HTML 或图像文件并发送它。或者,响应可能更复杂,可能使用服务器端软件(如 Django)在发送之前动态生成内容:

![图 1.6:HTTP 请求和 HTTP 响应图 B15509_01_06

图 1.6:HTTP 请求和 HTTP 响应

请求由四个主要部分组成:方法、路径、头和主体。某些类型的请求没有主体。如果你只是访问一个网页,你的浏览器不会发送主体,而如果你正在提交一个表单(例如,通过登录一个网站或执行搜索),那么你的请求将包含一个包含你提交的数据的主体。现在我们将查看两个示例请求来阐述这一点。

第一个请求将是一个示例页面,URL 为 https://www.example.com/page。当你的浏览器访问该页面时,幕后它会发送以下内容:

GET /page HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Firefox/15.0.1
Cookie: sessid=abc123def456

第一行包含方法(GET)和路径(/page)。它还包含 HTTP 版本,在这种情况下,1.1,尽管你不必担心这一点。根据你如何与远程页面交互,可以使用许多不同的 HTTP 方法。一些常见的方法有 GET(检索远程页面)、POST(向远程页面发送数据)、PUT(创建远程页面)和 DELETE(删除远程页面)。请注意,动作的描述有些简化——远程服务器可以选择如何响应不同的方法,即使是经验丰富的开发者也可能对实现特定动作的正确方法意见不一。还重要的是要注意,即使服务器支持某种特定方法,你可能也需要正确的权限才能执行该操作——你不能只是在一个你不喜欢网站上使用 DELETE,例如。

当编写 web 应用程序时,绝大多数时间你只会处理 GET 请求。当你开始接受表单时,你也将不得不使用 POST 请求。只有当你处理创建 REST API 等高级功能时,你才需要担心 PUTDELETE 和其他方法。

回顾一下示例请求,从第二行开始是请求的头。头包含关于请求的额外元数据。每个头都在自己的行上,头名称和其值由冒号分隔。大多数是可选的(除了 Host——稍后会有更多说明)。头名称不区分大小写。为了说明,我们这里只展示了三个常见的头。让我们按顺序查看示例头:

  • Host:如前所述,这是唯一必需的头(对于 HTTP 1.1 或更高版本)。它是必需的,以便 web 服务器知道哪个网站或应用程序应该响应该请求,以防单个服务器上托管了多个网站。

  • User-Agent:你的浏览器通常会向服务器发送一个字符串,以标识其版本和操作系统。你的服务器应用程序可以使用这个信息为不同的设备提供不同的页面(例如,为智能手机提供特定的移动页面)。

  • Cookie:你可能见过当访问网页时显示的消息,告诉你它在浏览器中存储了一个 cookie。这些是网站可以在你的浏览器中存储的小块信息,可以用来识别你或保存你返回网站时的设置。如果你想知道浏览器是如何将这些 cookie 发送回服务器的,它就是通过这个标题。

定义了许多其他标准标题,列出所有这些标题会占用太多空间。它们可以用来向服务器进行身份验证(Authorization),告诉服务器你能够接收什么类型的数据(Accept),或者甚至声明你希望页面使用的语言(Accept-Language,尽管这只有在页面创建者已经将内容提供为请求的特定语言时才会生效)。你甚至可以定义只有你的应用程序知道如何响应的自定义标题。

现在让我们看看一个稍微复杂一点的请求:它向服务器发送一些信息,因此(与之前的示例不同)包含一个正文。在这个示例中,我们通过发送用户名和密码来登录网页。例如,你访问 https://www.example.com/login,它显示一个输入用户名和密码的表单。在你点击 Login 按钮后,这就是发送到服务器的请求:

POST /login HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 32
username=user1&password=password1

如你所见,这看起来与第一个示例类似,但有一些不同。方法现在是 POST,并且引入了两个新的标题(你可以假设浏览器仍然会发送之前示例中的其他标题):

  • Content-Type:这告诉服务器正文中包含的数据类型。在 application/x-www-form-urlencoded 的情况下,正文是一组键值对。HTTP 客户端可以设置此标题来告诉服务器它是否正在发送其他类型的数据,例如 JSON 或 XML。

  • Content-Length:为了服务器知道要读取多少数据,客户端必须告诉它正在发送多少数据。Content-Length 标题包含正文的长度。如果你计算这个示例中正文的长度,你会看到它是 32 个字符。

标题始终由一个空行与正文隔开。通过查看示例,你应该能够了解表单数据是如何在正文中编码的:username 的值为 user1,而 password 的值为 password1

这些请求相当简单,但大多数请求并没有变得更加复杂。它们可能有不同的方法和标题,但应该遵循相同的格式。现在你已经看到了请求,我们将查看从服务器返回的 HTTP 响应。

HTTP 响应看起来与请求类似,由三个主要部分组成:状态、标题和正文。然而,与请求一样,根据响应的类型,它可能没有正文。第一个响应示例是一个简单的成功响应:

HTTP/1.1 200 OK
Server: nginx
Content-Length: 18132
Content-Type: text/html
Set-Cookie: sessid=abc123def46
<!DOCTYPE html><html><head>…

第一行包含 HTTP 版本、数字状态码(200)以及状态码的文本描述(OK——请求成功)。在下一个示例之后,我们将展示更多状态。第 2 行到第 5 行包含标题,类似于请求。您之前已经看到一些标题;我们将在这种情况下解释所有这些标题:

  • Server:这与User-Agent标题类似,但相反:这是服务器告诉客户端它正在运行什么软件。

  • Content-Length:客户端使用此值来确定从服务器读取多少数据以获取主体。

  • Content-Type:服务器使用此标题向客户端指示发送的数据类型。客户端可以选择如何显示数据——例如,图像必须以与 HTML 不同的方式显示。

  • Set-Cookie:我们在第一个请求示例中看到客户端如何向服务器发送 cookie。这是服务器向浏览器设置该 cookie 的相应标题。

在标题之后是一个空行,然后是响应的主体。我们在这里没有展示全部内容,只是展示了接收到的 HTML 的前几个字符,而服务器共发送了 18,132 个字符。

接下来,我们将展示一个示例,说明如果请求的页面未找到时返回的响应:

HTTP/1.1 404 Not Found
Server: nginx
Content-Length: 55
Content-Type: text/html
<!DOCTYPE html><html><body>Page Not Found</body></html>

这与上一个示例类似,但状态现在是404 未找到。如果您曾经在网上浏览并收到404错误,那么这就是您的浏览器收到的响应类型。各种状态码根据它们指示的成功或失败类型分组:

  • 100-199:服务器发送此范围内的代码以指示协议更改或需要更多数据。您不必担心这些问题。

  • 200 OK

  • 301 永久移动302 找到。在发送重定向响应时,服务器还会包括一个包含应重定向到的 URL 的Location标题。

  • 401 未授权(客户端应登录)和403 禁止访问(客户端不允许访问特定资源)。这两个问题可以通过让客户端登录来避免,因此它们被认为是客户端(请求)问题。

  • 500 内部服务器错误。如果您的代码抛出异常,将会生成此错误。另一个常见的是504 网关超时,这可能会发生在您的代码运行时间过长的情况下。其他常见的变体包括502 网关错误503 服务不可用,这通常意味着您的应用程序托管存在问题。

这些只是最常见的 HTTP 状态之一。您可以在developer.mozilla.org/en-US/docs/Web/HTTP/Status找到更完整的列表。然而,与 HTTP 标题一样,状态是任意的,应用程序可以返回自定义状态。这取决于服务器和客户端来决定这些自定义状态和代码的含义。

如果你第一次接触 HTTP 协议,有很多信息需要吸收。幸运的是,Django 做了所有艰苦的工作,并将传入的数据封装到 HttpRequest 对象中。大多数时候,你不需要了解大部分传入的信息,但如果你需要,这些信息都是可用的。同样,在发送响应时,Django 将你的数据封装到 HttpResponse 对象中。通常你只需设置要返回的内容,但你也可以自由地设置 HTTP 状态码和头信息。我们将在本章后面讨论如何访问和设置 HttpRequestHttpResponse 中的信息。

处理请求

这是一个请求和响应流程的基本时间线,这样你可以了解你将在每个阶段编写的代码做什么。在编写代码方面,你将首先编写的是你的视图。你创建的视图将执行一些操作,例如查询数据库以获取数据。然后视图将把数据传递给另一个函数以渲染模板,最后返回包含你想要发送回客户端的数据的 HttpResponse 对象。

接下来,Django 需要知道如何将特定的 URL 映射到你的视图,以便它可以加载请求中作为请求一部分的 URL 的正确视图。你将在 URL 配置 Python 文件中编写这个 URL 映射。

当 Django 接收到一个请求时,它会解析 URL 配置文件,然后找到相应的视图。它调用视图,传入一个代表请求的 HttpRequest 对象。你的视图将返回其 HttpResponse,然后 Django 再次接管,将数据发送到其宿主 Web 服务器,并返回给请求它的客户端:

![图 1.7:请求和响应流程

![图片 B15509_01_07.jpg]

![图 1.7:请求和响应流程]

请求-响应流程如图 图 1.7 所示;标记为 你的代码 的部分是你编写的代码——第一个和最后一步由 Django 处理。Django 为你进行 URL 匹配,调用你的视图代码,然后处理将响应传递回客户端。

Django 项目

我们在前面一个章节中已经介绍了 Django 项目。为了提醒自己运行 startproject(对于名为 myproject 的项目)时会发生什么:该命令创建一个名为 myproject 的目录,其中包含一个名为 manage.py 的文件,以及一个名为 myproject 的目录(这与项目名称匹配,在 练习 1.01创建项目和应用程序,并启动开发服务器 中;这个文件夹被命名为 bookr,与项目名称相同)。目录布局如图 图 1.8 所示。我们现在将更详细地检查 manage.py 文件和 myproject 包的内容:

![图 1.8:myproject 项目目录]

![图片 B15509_01_08.jpg]

![图 1.8:myproject 项目目录]

manage.py

如其名所示,这是一个用于管理您的 Django 项目的脚本。您与项目交互的大多数命令将通过命令行提供给此脚本。命令作为参数传递给此脚本;例如,如果我们说要运行manage.py runserver命令,我们的意思是以这种方式运行manage.py脚本:

python3 manage.py runserver

manage.py提供了许多有用的命令。您将在本书的后续章节中详细了解它们;其中一些更常见的命令在此列出:

  • runserver:启动 Django 开发 HTTP 服务器,在您的本地计算机上提供 Django 应用。

  • startapp:在您的项目中创建一个新的 Django 应用。我们将在稍后更深入地讨论应用是什么。

  • shell:启动一个预加载 Django 设置的 Python 解释器。这对于在不手动加载 Django 设置的情况下与您的应用程序交互非常有用。

  • dbshell:启动一个连接到您的数据库的交互式 shell,使用 Django 设置中的默认参数。您可以通过这种方式运行手动 SQL 查询。

  • makemigrations:从您的模型定义生成数据库更改指令。您将在第二章模型和迁移中了解这意味着什么以及如何使用此命令。

  • migrate:应用由makemigrations命令生成的迁移。您也将在第二章模型和迁移中使用此命令。

  • test:运行您编写的自动化测试。您将在第十四章测试中使用此命令。

所有命令的完整列表可在docs.djangoproject.com/en/3.0/ref/django-admin/找到。

myproject目录

manage.py文件继续,startproject创建的另一个文件项是myproject目录。这是您的项目的实际 Python 包。它包含项目设置,一些用于您的 Web 服务器的配置文件,以及全局 URL 映射。在myproject目录内包含五个文件:

  • __init__.py

  • asgi.py

  • settings.py

  • urls.py

  • wsgi.py![Figure 1.9: The myproject package (inside the myproject project directory)]

    ![Figure 1.9: The myproject package (inside the myproject project directory)]

图 1.9:myproject 包(位于 myproject 项目目录内)

__init__.py

一个空文件,让 Python 知道myproject目录是一个 Python 模块。如果您之前使用过 Python,您会熟悉这些文件。

settings.py

这包含您应用程序的所有 Django 设置。我们将在稍后解释其内容。

urls.py

这包含 Django 将最初用于定位视图或其他子 URL 映射的全局 URL 映射。您很快就会向此文件添加 URL 映射。

asgi.pywsgi.py

这些文件是 ASGI 或 WSGI 网络服务器在将你的 Django 应用部署到生产网络服务器时用来与你的 Django 应用通信的。你通常不需要编辑这些文件,它们在日常开发中也不被使用。它们的使用将在 第十七章Django 应用程序的部署 中进一步讨论。

Django 开发服务器

你已经在 练习 1.01 中启动了 Django 开发服务器,即 创建项目和应用程序,并启动开发服务器。正如我们之前提到的,这是一个仅在开发期间运行在开发者机器上的网络服务器。它不适用于生产环境。

默认情况下,服务器在 localhost (127.0.0.1) 的端口 8000 上监听,但可以通过在 runserver 参数后添加端口号或地址和端口号来更改此设置:

python3 manage.py runserver 8001

服务器将在 localhost (127.0.0.1) 的端口 8001 上监听。

如果你的电脑有多个地址,你也可以让它监听特定的地址,或者对所有地址使用 0.0.0.0

python3 manage.py runserver 0.0.0.0:8000

服务器将在所有电脑的地址上监听端口 8000,如果你想在另一台电脑或你的智能手机上测试应用程序,这可能会很有用。

开发服务器会监视你的 Django 项目目录,每次你保存文件时都会自动重启,这样你做的任何代码更改都会自动重新加载到服务器中。尽管如此,你仍然需要手动刷新浏览器来查看更改。

当你想停止 runserver 命令时,可以在终端中按照停止进程的常规方式操作:通过使用 Ctrl + C 键组合。

Django 应用

现在我们已经介绍了一些关于应用的理论,我们可以更具体地讨论它们的目的。应用目录包含所有必要的模型、视图、模板(以及更多)以提供应用程序功能。Django 项目至少包含一个应用(除非它已经被高度定制,不依赖于大量的 Django 功能)。如果设计得当,一个应用应该能够从项目中移除并移动到另一个项目而无需修改。通常,一个应用将包含单个设计域的模型,这可以是一个有用的方法来确定你的应用是否应该拆分为多个应用。

你的应用可以有任何名称,只要它是有效的 Python 模块名称(即,只使用字母、数字和下划线)并且不与项目目录中的其他文件冲突。例如,正如我们所看到的,项目目录中已经有一个名为 myproject 的目录(包含 settings.py 文件),因此你不能有一个名为 myproject 的应用。正如我们在 练习 1.01创建项目和应用程序,并启动开发服务器 中所看到的,创建应用使用 manage.py startapp appname 命令。例如:

python3 manage.py startapp myapp

startapp 命令在你的项目目录中创建一个以应用程序命名的目录。它还会为应用程序生成文件框架。在 app 目录中包含几个文件和一个文件夹,如图 1.10 所示:

![图 1.10: myapp 应用程序目录的内容

![img/B15509_01_10.jpg]

图 1.10: myapp 应用程序目录的内容

  • __init__.py: 一个空文件,表示这个目录是一个 Python 模块。

  • admin.py: Django 内置了一个具有图形用户界面 (GUI) 的管理站点,用于查看和编辑数据。在这个文件中,你将定义你的应用程序模型如何在 Django 管理站点中暴露。我们将在第四章 Django 管理介绍 中更详细地讲解。

  • apps.py: 这包含了一些关于你的应用程序元数据的配置。你不需要编辑这个文件。

  • models.py: 这是你定义应用程序模型的地方。你将在第二章 模型和迁移 中更详细地了解。

  • migrations: Django 使用迁移文件来自动记录模型变化时底层数据库的变化。这些文件在运行 manage.py makemigrations 命令时由 Django 生成,并存储在这个目录中。它们只有在运行 manage.py migrate 命令后才会应用到数据库中。它们将在第二章 模型和迁移 中详细讲解。

  • tests.py: 为了测试你的代码是否运行正确,Django 支持编写测试(单元、功能或集成)并将它们放在这个文件中。本书中我们将编写一些测试,并在第十四章 测试 中详细讲解。

  • views.py: 你的 Django 视图(响应 HTTP 请求的代码)将放在这里。你将很快创建一个基本视图,视图将在第三章 URL 映射、视图和模板 中详细讲解。

我们将在稍后更详细地检查这些文件的内容,但现在,我们将通过第二个练习来让 Django 在我们的环境中运行起来。

PyCharm 设置

练习 1.01创建项目和应用程序,并启动开发服务器 中,我们确认了 Bookr 项目已经正确设置(因为开发服务器运行成功),因此我们现在可以使用 PyCharm 来运行和编辑我们的项目。PyCharm 是一个 Python 开发 IDE,它包括代码补全、自动格式化风格和内置调试器等功能。然后我们将使用 PyCharm 来编写我们的 URL 映射、视图和模板。它还将用于启动和停止开发服务器,这将允许我们通过设置断点来调试我们的代码。

练习 1.02: 在 PyCharm 中设置项目

在这个练习中,我们将打开 PyCharm 中的 Bookr 项目,并设置项目解释器,以便 PyCharm 可以运行和调试项目:

  1. 打开 PyCharm。当你第一次打开 PyCharm 时,你会看到 欢迎使用 PyCharm 界面,它会询问你想要做什么:![图 1.11: PyCharm 欢迎界面

    ![img/B15509_01_11.jpg]

    图 1.11: PyCharm 欢迎界面

  2. 点击 打开,然后浏览到您刚刚创建的 bookr 项目,然后打开它。请确保您打开的是 bookr 项目目录,而不是 bookr 包目录内部。

    如果您之前没有使用过 PyCharm,它将询问您想要使用哪些设置和主题,一旦您回答了所有这些问题,您将看到 bookr 项目结构在窗口左侧的 项目 窗格中打开:

    ![图 1.12:PyCharm 项目窗格 img/B15509_01_12.jpg

    图 1.12:PyCharm 项目窗格

    您的 项目 窗格应该看起来像 图 1.12 并显示 bookrreviews 目录,以及 manage.py 文件。如果您没有看到这些,而是看到 asgi.pysettings.pyurls.pywsgi.py,那么您已经打开了 bookr 包目录。选择 文件 -> 打开,然后浏览并打开 bookr 项目目录。

    在 PyCharm 知道如何执行您的项目以启动 Django 开发服务器之前,解释器必须设置为虚拟环境中的 Python 二进制文件。这是通过首先将解释器添加到全局解释器设置来完成的。

  3. 在 PyCharm 中打开 首选项(macOS)或 设置(Windows/Linux)窗口。

    macOS:

    PyCharm 菜单 -> 首选项

    Windows 和 Linux:

    文件 -> 设置

  4. 在左侧的偏好设置列表窗格中,打开 项目: bookr 项,然后点击 项目解释器:![图 1.13:项目解释器设置 img/B15509_01_13.jpg

    图 1.13:项目解释器设置

  5. 有时 PyCharm 可以自动确定虚拟环境,因此在这种情况下,项目解释器 可能已经填充了正确的解释器。如果是这样,并且您在包列表中看到 Django,您可以点击 确定 关闭窗口并完成此练习。

    然而,在大多数情况下,必须手动设置 Python 解释器。点击 项目解释器 下拉菜单旁边的齿轮图标,然后点击 添加…

  6. 现在显示 添加 Python 解释器 窗口。选择 现有环境 单选按钮,然后点击 解释器 下拉菜单旁边的省略号 ()。然后您应该浏览并选择虚拟环境中的 Python 解释器:![图 1.14:添加 Python 解释器窗口 img/B15509_01_14.jpg

    图 1.14:添加 Python 解释器窗口

  7. 在 macOS 上(假设您将虚拟环境命名为 bookr),路径通常是 /Users/<您的用户名>/.virtualenvs/bookr/bin/python3。同样,在 Linux 上,它应该是 /home/<您的用户名>/.virtualenvs/bookr/bin/python3

    如果您不确定,您可以在之前运行 python manage.py 命令的终端中运行 which python3 命令,它将告诉您 Python 解释器的路径:

    which python3
    /Users/ben/.virtualenvs/bookr/bin/python3
    

    在 Windows 上,它将是您使用 virtualenv 命令创建虚拟环境的位置。

    选择解释器后,您的 添加 Python 解释器 窗口应该看起来像 图 1.14

  8. 点击 确定 关闭 添加 Python 解释器 窗口。

  9. 现在你应该能看到主偏好设置窗口,Django(以及你虚拟环境中的其他包)将被列出(见 图 1.15):图 1.15:虚拟环境中的包列表

    图 1.15:虚拟环境中的包列表

  10. 在主 偏好设置 窗口中点击 OK 以关闭它。PyCharm 现在将花费几秒钟来索引你的环境和已安装的库。你可以在其底部的状态栏中看到这个过程。等待此过程完成,进度条将消失。

  11. 要运行 Django 开发服务器,Python 需要配置一个运行配置。你现在将设置它。

    点击 PyCharm 项目窗口右上角的 添加配置… 以打开 运行/调试配置 窗口:

    图 1.16:PyCharm 窗口右上角的“添加配置…”按钮

    图 1.16:PyCharm 窗口右上角的“添加配置…”按钮

  12. 在此窗口的左上角点击 + 按钮,从下拉菜单中选择 Python图 1.17:在运行/调试配置窗口中添加新的 Python 配置

    图 1.17:在运行/调试配置窗口中添加新的 Python 配置

  13. 一个新的配置面板将在窗口的右侧显示,其中包含有关如何运行你的项目的字段。你应该按照以下方式填写字段。

    名称 字段可以是任何内容,但应该是可理解的。输入 Django Dev Server

    脚本路径 是你的 manage.py 文件的路径。如果你点击此字段中的文件夹图标,你可以浏览你的文件系统来选择 bookr 项目目录中的 manage.py 文件。

    参数 是在 manage.py 脚本之后出现的参数,就像从命令行运行它一样。我们将在这里使用相同的参数来启动服务器,所以输入 runserver

    注意

    如前所述,runserver 命令也可以接受一个用于监听端口或地址的参数。如果你想,你可以在相同的 参数 字段中添加此参数在 runserver 之后。

    Python 解释器 设置应该已经自动设置为在 步骤 58 中设置的设置。如果不是,你可以点击右侧的箭头下拉菜单来选择它。

    工作目录 应设置为 bookr 项目目录。这很可能已经设置正确了。

    将内容根添加到 PYTHONPATH将源根添加到 PYTHONPATH 都应该被选中。这将确保 PyCharm 将你的 bookr 项目目录添加到 PYTHONPATH(Python 解释器在加载模块时搜索的路径列表)。如果没有选中这些选项,你的项目中的导入将无法正确工作:

    图 1.18:配置设置

    图 1.18:配置设置

    确保你的 运行/调试配置 窗口看起来类似于 图 1.18,然后点击 OK 保存配置。

  14. 现在,你不再需要在终端中启动 Django 开发服务器,而是可以点击 项目 窗口右上角的播放图标来启动它(见 图 1.19):![图 1.19:带有播放、调试和停止按钮的 Django 开发服务器配置 图片

    图 1.19:带有播放、调试和停止按钮的 Django 开发服务器配置

  15. 点击播放图标以启动 Django 开发服务器。

    注意

    确保停止任何正在运行的 Django 开发服务器实例(例如在终端中),否则你启动的服务器将无法绑定到端口 8000 并无法启动。

  16. PyCharm 窗口的底部将打开一个控制台,显示输出信息,表明开发服务器已启动(图 1.20):![图 1.20:运行中的 Django 开发服务器的控制台 图片

    图 1.20:运行中的 Django 开发服务器的控制台

  17. 打开一个网页浏览器并导航到 http://127.0.0.1:8000。你应该会看到与之前在 练习 1.01创建项目和应用程序,以及启动开发服务器 中相同的 Django 示例屏幕(图 1.2),这将确认一切再次设置正确。

在这个练习中,我们在 PyCharm 中打开了 Bookr 项目,然后为我们的项目设置了 Python 解释器。我们接着在 PyCharm 中添加了一个运行配置,这允许我们从 PyCharm 内部启动和停止 Django 开发服务器。我们还将能够在 PyCharm 的调试器中运行项目以进行调试。

查看详情

现在,你已经设置好了一切,可以开始编写自己的 Django 视图并配置映射到它们的 URL。正如我们在本章前面所看到的,视图只是一个函数,它接受一个 HttpRequest 实例(由 Django 构建)以及(可选的)来自 URL 的某些参数。然后它将执行一些操作,例如从数据库中获取数据。最后,它返回 HttpResponse

以我们的 Bookr 应用程序为例,我们可能有一个视图,它接收对某本书的请求。它查询数据库以获取这本书,然后返回一个包含有关这本书信息的 HTML 页面的响应。另一个视图可以接收列出所有书籍的请求,然后返回包含此列表的另一个 HTML 页面的响应。视图还可以创建或修改数据:另一个视图可以接收创建新书的请求;然后它会将这本书添加到数据库中,并返回显示新书信息的 HTML 响应。

在本章中,我们只会使用函数作为视图,但 Django 也支持基于类的视图,这允许你利用面向对象范式(如继承)。这允许你简化多个具有相同业务逻辑的视图所使用的代码。例如,你可能想显示所有书籍或仅显示某个出版商的书籍。两个视图都需要从数据库中查询书籍列表并将其渲染到书籍列表模板中。一个视图类可以继承另一个类,只需实现数据获取的不同方式,其余的功能(如渲染)保持相同。基于类的视图可能更强大,但也更难学习。它们将在你有了更多 Django 经验后,在第十一章高级模板和基于类的视图中介绍。

传递给视图的HttpRequest实例包含与请求相关的所有数据,具有诸如这些属性:

  • method: 一个包含浏览器用于请求页面的 HTTP 方法的字符串;通常这是GET,但如果用户提交了表单,它将是POST。你可以使用这个属性来改变视图的流程,例如,在GET时显示一个空表单,或者在POST时验证并处理表单提交。

  • GET: 一个包含 URL 查询字符串中使用的参数的QueryDict实例。这是 URL 中?之后的部分,如果有的话。我们很快会进一步介绍QueryDict。请注意,即使请求不是GET,此属性也始终可用。

  • POST: 另一个包含在POST请求中发送到视图的参数的QueryDict,例如来自表单提交。通常,你会与 Django 表单一起使用这个属性,这将在第六章表单中介绍。

  • headers: 一个不区分大小写的键字典,包含请求中的 HTTP 头。例如,你可以根据User-Agent头为不同的浏览器提供不同的内容。我们之前在本章中讨论了一些客户端发送的 HTTP 头。

  • path: 这是请求中使用的路径。通常,你不需要检查这个属性,因为 Django 会自动解析路径并将其作为参数传递给视图函数,但在某些情况下这可能很有用。

我们现在不会使用所有这些属性,其他属性将在以后介绍,但现在你可以看到HttpRequest参数在你的视图中扮演的角色。

URL 映射详情

我们在处理请求部分简要提到了 URL 映射。Django 在接收到特定 URL 的请求时不会自动知道应该执行哪个视图函数。URL 映射的作用是建立 URL 和视图之间的这种联系。例如,在 Bookr 中,你可能想将 URL /books/ 映射到你创建的books_list视图。

URL 到视图的映射定义在 Django 自动创建的文件中,该文件名为urls.py,位于bookr包目录内(尽管可以在settings.py中设置不同的文件;关于这一点稍后会有更多说明)。

此文件包含一个变量urlpatterns,它是一个路径列表,Django 将依次评估,直到找到与请求的 URL 匹配的路径。匹配将解析为一个视图函数,或者解析为另一个也包含urlpatterns变量的urls.py文件,它将以相同的方式解析。你可以按这种方式将 URL 文件链式连接,直到你想要的程度。这样,你可以将 URL 映射拆分为单独的文件(例如每个应用一个或多个),这样它们就不会变得太大。一旦找到视图,Django 就会使用一个HttpRequest实例和从 URL 解析出的任何参数来调用它。

规则是通过调用path函数来设置的,该函数将 URL 路径作为第一个参数。路径可以包含命名参数,这些参数将被作为函数参数传递给视图。其第二个参数是一个视图或另一个也包含urlpatterns的文件。

此外,还有一个re_path函数,它与path类似,但它将正则表达式作为第一个参数,用于更高级的配置。URL 映射还有很多其他内容;然而,它将在第三章URL 映射、视图和模板中介绍。

![图 1.21:默认的 urls.py 文件

![img/B15509_01_21.jpg]

图 1.21:默认的 urls.py 文件

为了说明这些概念,图 1.21展示了 Django 生成的默认urls.py文件。你可以看到urlpatterns变量,它列出了所有已设置的 URL。目前,只有一个规则被设置,它将任何以admin/开头的路径映射到管理 URL 映射(admin.site.urls模块)。这不是一个映射到视图的映射;相反,它是一个将 URL 映射链式连接的例子——admin.site.urls模块将定义剩余的路径(admin/之后),这些路径映射到管理视图。我们将在第四章Django 管理简介中介绍 Django 管理站点。

我们现在将编写一个视图,并设置一个 URL 映射到它,以观察这些概念的实际应用。

练习 1.03:编写视图并将 URL 映射到它

我们的第一个视图将非常简单,它只会返回一些静态文本内容。在这个练习中,我们将看到如何编写视图,以及如何设置 URL 映射以解析到视图:

注意

当你在项目中的文件进行更改并保存时,你可能会在运行它的终端或控制台中看到 Django 开发服务器自动重启。这是正常的;它会自动重启以加载你做出的任何代码更改。请注意,如果你编辑模型或迁移,它不会自动将更改应用到数据库中——关于这一点,我们将在第二章模型和迁移中详细说明。

  1. 在 PyCharm 中,在左侧的项目浏览器中展开 reviews 文件夹,然后双击 views.py 文件以打开它。在 PyCharm 的右侧(编辑器)面板中,你应该能看到 Django 自动生成的占位文本:

    from django.shortcuts import render
    # Create your views here.
    

    在编辑器面板中应该看起来像这样:

    ![图 1.22:views.py 默认内容 图片 B15509_01_22.jpg

    图 1.22:views.py 默认内容

  2. views.py 中删除此占位文本,并插入以下内容:

    from django.http import HttpResponse
    def index(request):
        return HttpResponse("Hello, world!")
    

    首先,需要从 django.http 中导入 HttpResponse 类。这是用来创建返回给网页浏览器的响应的。你也可以用它来控制诸如 HTTP 标头或状态码之类的功能。现在,它将只使用默认的标头和 200 Success 状态码。它的第一个参数是要发送为响应正文的字符串内容。

    然后,视图函数返回一个包含我们定义的内容的 HttpResponse 实例(Hello, world!):

    ![图 1.23:编辑后的 views.py 内容 图片 B15509_01_23.jpg

    图 1.23:编辑后的 views.py 内容

  3. 现在我们将设置一个 URL 映射到 index 视图。这将非常简单,不会包含任何参数。在 Project 面板中展开 bookr 目录,然后打开 urls.py。Django 已经自动生成了此文件。

    目前,我们只需添加一个简单的 URL 来替换 Django 提供的默认索引。

  4. 通过在现有导入之后添加此行将你的视图导入到 urls.py 文件中:

    import reviews.views
    
  5. 通过向 urlpatterns 列表中添加对 path 函数的调用(一个空字符串和一个对 index 函数的引用)来将映射添加到索引视图:

    urlpatterns = [path('admin/', admin.site.urls),\
                   path('', reviews.views.index)]
    index function (that is, it should be reviews.views.index and not reviews.views.index()) as we are passing a reference to a function rather than calling it. When you're finished, your urls.py file should like *Figure 1.24*:
    

    ![图 1.24:编辑后的 urls.py 图片 B15509_01_24.jpg

    图 1.24:编辑后的 urls.py

  6. 切换回你的网页浏览器并刷新。Django 默认欢迎屏幕应该被视图定义中的文本 Hello, world! 替换:![图 1.25:现在网页浏览器应该显示 Hello, world! 消息 图片 B15509_01_25.jpg

图 1.25:现在网页浏览器应该显示 Hello, world! 消息

我们刚刚看到了如何编写视图函数并将 URL 映射到它。然后我们通过在网页浏览器中加载它来测试了视图。

GET、POST 和 QueryDict 对象

数据可以通过 HTTP 请求作为 URL 上的参数或 POST 请求正文中的内容传入。你可能已经注意到在浏览网页时 URL 中的参数——? 后面的文本——例如,http://www.example.com/?parameter1=value1&parameter2=value2。我们也在本章前面看到了一个 POST 请求中表单数据的例子,用于用户登录(请求正文是 username=user1&password=password1)。

Django 自动将这些参数字符串解析为QueryDict对象。然后,这些数据在传递给视图函数的HttpRequest对象上可用——具体来说,在HttpRequest.GETHttpRequest.POST属性中,分别对应 URL 参数和表单参数。QueryDict对象是主要像字典一样行为的对象,除了它们可以为键包含多个值。

为了展示访问项的不同方法,我们将使用一个简单的名为qdQueryDict作为示例,它只有一个键(k)。k项在列表中有三个值:字符串abc。以下代码片段显示了 Python 解释器的输出。

首先,QueryDict qd是从一个参数字符串构建的:

>>> qd = QueryDict("k=a&k=b&k=c")

当使用方括号符号或get方法访问项时,返回该键的最后一个值:

>>> qd["k"]
'c'
>>> qd.get("k")
'c'

要访问一个键的所有值,应使用getlist方法:

>>> qd.getlist("k")
['a', 'b', 'c']

getlist始终返回一个列表——如果键不存在,它将是空的:

>>> qd.getlist("bad key")
[]

虽然getlist对于不存在的键不会引发异常,但使用方括号符号访问不存在的键将引发KeyError,就像普通字典一样。使用get方法来避免这个错误。

GETPOSTQueryDict对象是不可变的(它们不能被更改),所以如果你需要更改其值,应使用copy方法来获取一个可变副本:

>>> qd["k"] = "d"
AttributeError: This QueryDict instance is immutable
>>> qd2 = qd.copy()
>>> qd2
<QueryDict: {'k': ['a', 'b', 'c']}>
>>> qd2["k"] = "d"
>>> qd2["k"]
"d"

为了说明QueryDict是如何从 URL 中填充的,想象一个示例 URL:http://127.0.0.1:8000?val1=a&val2=b&val2=c&val3

在幕后,Django 将 URL 中的查询(?之后的所有内容)传递给QueryDict对象以实例化,并将其附加到传递给视图函数的request实例。类似于以下内容:

request.GET = QueryDict("val1=a&val2=b&val2=c&val3")

记住,这在你将request实例传递到视图函数内部之前就完成了;你不需要这样做。

在我们的示例 URL 的情况下,我们可以在视图函数内部如下访问参数:

request.GET["val1"]

使用标准字典访问,它将返回值a

request.GET["val2"]

再次,使用标准字典访问,val2键设置了两个值,因此它会返回最后一个值,c

request.GET.getlist("val2")

这将返回val2的所有值列表:["b", "c"]:

request.GET["val3"]

这个键在查询字符串中存在但没有设置值,因此返回一个空字符串:

request.GET["val4"]

这个键没有设置,所以会引发KeyError。请使用request.GET.get("val4")代替,它将返回None

request.GET.getlist("val4")

由于这个键没有设置,将返回一个空列表([])。

现在,我们将通过GET参数来观察QueryDict的实际应用。你将在第六章表单中进一步了解POST参数。

练习 1.04:探索 GET 值和 QueryDict

现在,我们将对之前练习中的index视图进行一些修改,以便从 URL 的GET属性中读取值,然后我们将尝试传递不同的参数以查看结果:

  1. 在 PyCharm 中打开views.py文件。添加一个名为name的新变量,该变量从GET参数中读取用户的名称。在index函数定义之后添加此行:

    name = request.GET.get("name") or "world"
    
  2. 将返回值修改为使用名称作为返回内容的一部分:

    return HttpResponse("Hello, {}!".format(name))
    

    在 PyCharm 中,更改后的代码将看起来像这样:

    ![图 1.26:更新后的 views.py 文件

    ![图片 B15509_01_26.jpg]

    图 1.26:更新后的 views.py 文件

  3. 在你的浏览器中访问http://127.0.0.1:8000。你应该注意到页面仍然显示Hello, world!这是因为我们没有提供name参数。你可以在 URL 中添加你的名字,例如,http://127.0.0.1:8000?name=Ben:![图 1.27:在 URL 中设置名称

    ![图片 B15509_01_27.jpg]

    图 1.27:在 URL 中设置名称

  4. 尝试添加两个名称,例如,http://127.0.0.1:8000?name=Ben&name=John。正如我们提到的,参数的最后一个值是通过get函数检索的,所以你应该看到Hello, John!:![图 1.28:在 URL 中设置多个名称

    ![图片 B15509_01_28.jpg]

    图 1.28:在 URL 中设置多个名称

  5. 尝试不设置名称,如下所示:http://127.0.0.1:8000?name=。页面应该会回到显示Hello, world!:![图 1.29:URL 中没有设置名称

    ![图片 B15509_01_29.jpg]

图 1.29:URL 中没有设置名称

注意

你可能会想知道为什么我们使用or而不是将'world'作为默认值传递给get函数来将name设置为默认值。考虑一下在步骤 5中我们为name参数传递了一个空白值时发生了什么。如果我们为get函数传递了'world'作为默认值,那么get函数仍然会返回一个空字符串。这是因为有一个name值,只是它是空的。在开发你的视图时请记住这一点,因为没有设置值和设置了空白值是有区别的。根据你的使用情况,你可能会选择传递get的默认值。

在这个练习中,我们使用传入请求的GET属性从我们的视图中检索值。我们看到了如何设置默认值以及如果为同一参数设置了多个值,则检索哪个值。

探索 Django 设置

我们还没有查看 Django 如何存储其设置。现在我们已经看到了 Django 的不同部分,现在是检查settings.py文件的好时机。这个文件包含许多可以用来自定义 Django 的设置。当你开始 Bookr 项目时,为你创建了一个默认的settings.py文件。

现在,我们将讨论文件中的一些重要设置,以及一些在你更熟悉 Django 时可能有用的其他设置。你应该在 PyCharm 中打开你的settings.py文件并跟随,这样你就可以看到你的项目中的值在哪里以及是什么。

此文件中的每个设置都是一个全局变量。我们将讨论设置的顺序与它们在此文件中出现的顺序相同,尽管我们可能会跳过一些——例如,在DEBUGINSTALLED_APPS之间有ALLOWED_HOSTS设置,我们不会在本部分书中介绍(您将在第十七章Django 应用程序的部署(第一部分 - 服务器设置)中看到它):

SECRET_KEY = '…'

这是一个自动生成的值,不应与任何人共享。它用于散列、令牌和其他加密函数。如果您在 cookie 中已有会话并且更改了此值,会话将不再有效。

DEBUG = True

将此值设置为True,Django 将自动将异常显示在浏览器中,以便您调试遇到的任何问题。当您将应用程序部署到生产环境时,应将其设置为False

INSTALLED_APPS = […]

当您编写自己的 Django 应用程序(如reviews应用程序)或安装第三方应用程序(将在第十五章Django 第三方库中介绍)时,它们应添加到此列表中。正如我们所看到的,将它们添加到这里并不是严格必要的(我们的index视图在没有reviews应用程序在此列表中的情况下也能工作)。然而,为了 Django 能够自动找到应用程序的模板、静态文件、迁移和其他配置,它必须列在此处:

ROOT_URLCONF = 'bookr.urls'

这是 Django 首先加载以查找 URL 的 Python 模块。请注意,这是我们之前添加索引视图 URL 映射的文件:

TEMPLATES = […]

目前,您不需要理解这个设置中的所有内容,因为您不会更改它;需要指出的重要行是这一行:

'APP_DIRS': True,

这告诉 Django 在加载模板以进行渲染时应在每个INSTALLED_APP内部的templates目录中查找。我们目前还没有为reviews创建templates目录,但我们将在下一次练习中添加一个。

Django 还有更多设置可用,这些设置在settings.py文件中未列出,因此在这些情况下它将使用其内置默认值。您还可以使用该文件设置您为应用程序创建的任意设置。第三方应用程序可能也希望在此处添加设置。在后面的章节中,我们将为其他应用程序在此处添加设置。您可以在docs.djangoproject.com/en/3.0/ref/settings/找到所有设置的列表及其默认值。

在您的代码中使用设置

有时引用settings.py中的设置在您的代码中可能很有用,无论是 Django 的内置设置还是您自己定义的设置。您可能会想编写如下代码来完成此操作:

from bookr import settings
if settings.DEBUG:  # check if running in DEBUG mode
    do_some_logging()

注意

前面的代码片段中的#符号表示代码注释。注释被添加到代码中以帮助解释特定的逻辑。

由于以下多个原因,这种方法是不正确的:

  • 有可能运行 Django 并指定一个不同的设置文件来读取,在这种情况下,之前的代码会导致错误,因为它将无法找到那个特定的文件。或者,如果文件存在,导入将成功,但将包含错误的设置。

  • Django 有一些设置可能不会在 settings.py 文件中列出,如果没有列出,它将使用其内部默认值。例如,如果你从你的 settings.py 文件中移除了 DEBUG = True 行,Django 将回退到使用其内部的 DEBUG 值(这是 False)。如果你直接使用 settings.DEBUG 尝试访问它,你会得到一个错误。

  • 第三方库可以更改你的设置定义方式,所以你的 settings.py 文件可能看起来完全不同。所有预期的变量可能根本不存在。所有这些应用程序的行为超出了本书的范围,但这是需要注意的。

优先的方式是使用 django.conf 模块,如下所示:

from django.conf import settings  # import settings from here instead
if settings.DEBUG:
    do_some_logging()

当从 django.conf 导入 settings 时,Django 缓解了我们刚才讨论的三个问题:

  • 设置是从指定的任何 Django 设置文件中读取的。

  • 任何默认设置值都会被插值。

  • Django 负责解析由第三方库定义的任何设置。

在我们新的简短示例代码片段中,即使 DEBUGsettings.py 文件中缺失,它也会回退到 Django 内部默认值(这是 False)。对于 Django 定义的所有其他设置也是如此;然而,如果你在这个文件中定义了自己的自定义设置,Django 将不会为它们提供内部值,所以在你代码中,你应该有一些处理它们可能不存在的方案——你的代码如何行为是你的选择,并且超出了本书的范围。

在应用目录中查找 HTML 模板

有许多选项可以告诉 Django 如何查找模板,这些选项可以在 settings.py 文件的 TEMPLATES 设置中设置,但最简单的一个(目前)是在 reviews 目录内创建一个 templates 目录。由于 settings.py 文件中的 APP_DIRS 设置为 True,Django 将会查找这个目录(以及其他应用中的 templates 目录),正如我们在上一节中看到的。

练习 1.05:创建模板目录和基本模板

在这个练习中,你将为 reviews 应用创建一个 templates 目录。然后,你将添加一个 Django 可以渲染到 HTTP 响应中的 HTML 模板文件:

  1. 我们在上一节(探索 Django 设置)中讨论了 settings.py 和其 INSTALLED_APPS 设置。我们需要将 reviews 应用添加到 INSTALLED_APPS 中,以便 Django 能够找到模板。在 PyCharm 中打开 settings.py。更新 INSTALLED_APPS 设置并在末尾添加 reviews。它应该看起来像这样:

    INSTALLED_APPS = ['django.contrib.admin',\
                      'django.contrib.auth',\
                      'django.contrib.contenttypes',\
                      'django.contrib.sessions',\
                      'django.contrib.messages',\
                      'django.contrib.staticfiles',\
                      'reviews']
    

    在 PyCharm 中,文件现在应该看起来像这样:

    图 1.30:已将 reviews 应用添加到 settings.py

    img/B15509_01_30.jpg

    图 1.30:已将 reviews 应用添加到 settings.py

  2. 保存并关闭 settings.py

  3. 在 PyCharm 项目浏览器中,右键单击 reviews 目录并选择 New -> Directory图 1.31:在 reviews 目录内创建新的目录

    图 1.31:在 reviews 目录内创建新的目录

  4. 输入名称 templates 并点击 OK 以创建它:图 1.32:命名目录 templates

    图 1.32:命名目录 templates

  5. 右键单击新创建的 templates 目录并选择 New -> HTML File图 1.33:在 templates 目录中创建新的 HTML 文件

    图 1.33:在 templates 目录中创建新的 HTML 文件

  6. 在出现的窗口中,输入名称 base.html,保持 HTML 5 file 选中状态,然后按 Enter 创建文件:图 1.34:新的 HTML 文件窗口

    图 1.34:新的 HTML 文件窗口

  7. 在 PyCharm 创建文件后,它也会自动打开它。它将包含以下内容:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    </body>
    </html>
    
  8. <body>…</body> 标签之间添加一条简短的消息以验证模板正在被渲染:

    <body>
        Hello from a template!
    </body>
    

    这是在 PyCharm 中的样子:

    图 1.35:包含一些示例文本的 base.html 模板

图 1.35:包含一些示例文本的 base.html 模板

在这个练习中,我们为 reviews 应用程序创建了一个 templates 目录,并向其中添加了一个 HTML 模板。一旦我们在视图中实现了 render 函数的使用,HTML 模板就会被渲染。

使用 render 函数渲染模板

现在我们有了可以使用的模板,但我们需要更新我们的 index 视图,使其渲染模板而不是返回当前显示的 Hello (name)! 文本(参考 图 1.29 了解其当前的外观)。我们将通过使用 render 函数并提供模板的名称来完成此操作。render 是一个快捷函数,它返回一个 HttpResponse 实例。还有其他方法可以渲染模板以提供更多控制渲染的方式,但就目前而言,这个函数对我们的需求来说已经足够好了。render 至少需要两个参数:第一个始终是传递给视图的请求,第二个是正在渲染的模板的名称/相对路径。我们还将使用第三个参数调用它,即渲染上下文,它包含在模板中将可用的所有变量——更多关于这一点将在 练习 1.07在模板中使用变量 中介绍。

练习 1.06:在视图中渲染模板

在这个练习中,你将更新你的 index 视图函数以渲染你在 练习 1.05创建模板目录和基本模板 中创建的 HTML 模板。你将使用 render 函数,该函数从磁盘加载你的模板,渲染它,并将其发送到浏览器。这将替换你当前从 index 视图函数返回的静态文本:

  1. 在 PyCharm 中,打开 reviews 目录中的 views.py

  2. 我们不再手动创建 HttpResponse 实例,因此请删除 HttpResponse 导入行:

    from django.http import HttpResponse
    
  3. 用从 django.shortcuts 导入的 render 函数替换它:

    from django.shortcuts import render
    
  4. 更新 index 函数,使其返回 render 调用,而不是返回 HttpResponse,传递 request 实例和模板名称:

    def index(request):
        return render(request, "base.html")
    

    在 PyCharm 中,它将看起来如下:

    图 1.36:完成的 views.py 文件

    图 1.36:完成的 views.py 文件

  5. 如果开发服务器尚未运行,请启动它。然后,打开您的网页浏览器并刷新 http://127.0.0.1:8000。你应该会看到 图 1.37 中所示的 Hello from a template! 消息被渲染出来。

    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/webdev-dj-2e/img/B15509_01_37.jpg)

图 1.37:您的第一个渲染的 HTML 模板

在模板中渲染变量

模板不仅仅是静态的 HTML。大多数情况下,它们将包含变量,这些变量在渲染过程中被插值。这些变量通过上下文从视图传递到模板:一个包含所有模板可以使用变量名称的字典(或类似字典的对象)。我们将再次以 Bookr 为例。如果你的模板中没有变量,你需要为每个要显示的书籍创建不同的 HTML 文件。相反,我们在模板内部使用一个如 book_name 的变量,然后视图为模板提供一个 book_name 变量,其值设置为已加载的书籍模型标题。当显示不同的书籍时,HTML 不需要改变;视图只需传递一个不同的书籍给它。你可以看到模型、视图和模板现在是如何全部结合在一起的。

与 PHP 等一些其他语言不同,变量必须显式传递到模板中,视图中的变量不会自动对模板可用。这是出于安全考虑,以及避免意外污染模板的命名空间(我们不希望在模板中出现任何意外的变量)。

在模板内部,变量通过双大括号 {{ }} 表示。虽然这并非严格的标准,但这种风格相当常见,并被用于其他模板工具,如 Vue.js 和 Mustache。Symfony(一个 PHP 框架)在其 Twig 模板语言中也使用双大括号,因此你可能在那里看到过类似的用法。

在模板中渲染变量,只需用大括号包裹它:{{ book_name }}。Django 会自动转义输出中的 HTML,这样你就可以在变量中包含特殊字符(如 <>),而不用担心它会破坏你的输出。如果一个变量没有传递到模板中,Django 将在该位置简单地渲染空内容,而不是抛出异常。

使用过滤器渲染变量有更多不同的方式,但这些将在 第三章,URL 路由器、视图和模板 中介绍。

练习 1.07:在模板中使用变量

我们将在 base.html 文件中放置一个简单的变量来演示 Django 的变量插值是如何工作的:

  1. 在 PyCharm 中打开 base.html

  2. 更新 <body> 元素,使其包含一个用于渲染 name 变量的位置:

    <body>
    Hello, {{ name }}!
    </body>
    
  3. 返回您的网页浏览器并刷新(您应该仍然在 http://127.0.0.1:8000)。您会看到页面现在显示 Hello, !。这是因为我们没有在渲染上下文中设置 name 变量:![图 1.38:由于没有设置上下文,模板中没有渲染任何值 图片 B15509_01_38.jpg

    图 1.38:由于没有设置上下文,模板中没有渲染任何值

  4. 打开 views.py 并在 index 函数内添加一个名为 name 的变量,将其值设置为 "world"

    def index(request):
        name = "world"
        return render(request, "base.html")
    
  5. 再次刷新您的浏览器。您应该注意到没有任何变化:我们想要渲染的任何内容都必须明确传递给 render 函数作为 context。这是在渲染时提供的变量字典。

  6. context 字典作为 render 函数的第三个参数添加。将您的 render 行更改为以下内容:

    return render(request, "base.html", {"name": name})
    

    在 PyCharm 中,它应该如下所示:

    ![图 1.39:在渲染上下文中发送了 name 变量的 views.py 图片 B15509_01_39.jpg

    图 1.39:在渲染上下文中发送了 name 变量的 views.py

  7. 再次刷新您的浏览器,您会看到它现在显示 Hello, world!:![图 1.40:使用变量渲染的模板 图片 B15509_01_40.jpg

图 1.40:使用变量渲染的模板

在这个练习中,我们将之前练习中创建的模板与 render 函数结合起来,以渲染一个包含传递到其中的 name 变量的 context 字典的 HTML 页面。

调试和错误处理

在编程时,除非您是那种从不犯错的完美程序员,否则您可能需要在某个时候处理错误或调试您的代码。当您的程序中发生错误时,通常有两种方式可以告知:要么您的代码会引发异常,要么在查看页面时您会得到意外的输出或结果。您可能会更频繁地遇到异常,因为有许多意外的方式可以引发它们。如果您的代码正在生成意外的输出,但没有引发任何异常,您可能想使用 PyCharm 调试器来找出原因。

异常

如果您之前使用过 Python 或其他编程语言,您可能已经遇到过异常。如果没有,这里有一个快速介绍。当发生错误时,会引发(或在其他语言中抛出)异常。程序在代码的该点停止执行,异常沿着函数调用链向上传播,直到被捕获。如果没有被捕获,则程序将崩溃,有时会显示一个描述异常及其发生位置的错误消息。Python 本身会引发异常,您的代码可以在任何位置快速停止执行。这里列出了您在编写 Python 代码时可能会遇到的常见异常:

  • IndentationError

    Python 如果你的代码没有正确缩进或者混合了制表符和空格,将会引发这个错误。

  • SyntaxError

    如果你的代码有无效的语法,Python 会引发这个错误:

    >>> a === 1
      File "<stdin>", line 1
        a === 1
            ^
    SyntaxError: invalid syntax
    
  • ImportError

    当导入失败时(例如,尝试从一个不存在的文件导入,或者尝试导入一个文件中未设置的名字)会引发这个错误:

    >>> import missing_file
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ImportError: No module named missing_file
    
  • NameError

    当尝试访问尚未设置变量的值时会引发这个错误:

    >>> a = b + 5
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    NameError: name 'b' is not defined
    
  • KeyError

    当访问字典(或类似字典的对象)中未设置的关键字时会引发这个错误:

    >>> d = {'a': 1}
    >>> d['b']
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    KeyError: 'b'
    
  • IndexError

    当尝试访问列表长度之外的索引时会引发这个错误:

    >>> l = ['a', 'b']
    >>> l[3]
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    IndexError: list index out of range
    
  • TypeError

    当尝试对一个不支持该操作的对象执行操作,或者使用错误类型的两个对象时(例如,尝试将一个字符串添加到一个整数上)会引发这个错误:

    >>> 1 + '1'
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: unsupported operand type(s) for +: 'int' and 'str'
    

Django 还会引发它自己的自定义异常,你将在整本书中了解到它们。

当你在 settings.py 文件中将 DEBUG 设置为 True 并运行 Django 开发服务器时,Django 会自动捕获代码中发生的异常(而不是崩溃)。然后它会生成一个 HTTP 响应,显示堆栈跟踪和其他信息,以帮助你调试问题。在生产环境中,DEBUG 应该设置为 False。此时 Django 将返回一个标准的内部服务器错误页面,不包含任何敏感信息。你也可以选择显示自定义错误页面。

练习 1.08:生成和查看异常

让我们在视图中创建一个简单的异常,这样你就可以熟悉 Django 如何显示它们。在这种情况下,我们将尝试使用一个不存在的变量,这将引发 NameError

  1. 在 PyCharm 中打开 views.py。在 index 视图函数中,更改发送给 render 函数的上下文,使其使用一个不存在的变量。我们将尝试在上下文字典中使用 invalid_name 而不是 name。不要更改上下文字典的键,只需更改其值:

    return render(request, "base.html", {"name": invalid_name})
    
  2. 返回你的浏览器并刷新页面。你应该会看到一个像 图 1.41 的屏幕:![图 1.41: Django 异常屏幕 图片 B15509_01_41.jpg

    图 1.41: Django 异常屏幕

  3. 页面上方的前几行标题告诉你发生了什么错误:

    NameError at /
    name 'invalid_name' is not defined
    
  4. 在标题下方是异常发生的跟踪信息。你可以点击代码的各个行来展开它们,查看周围的代码,或者点击每个帧的 Local vars 来展开它们,查看变量的值:![图 1.42: 引发异常的行 图片 B15509_01_42.jpg

    图 1.42: 引发异常的行

  5. 在我们的例子中,我们可以看到异常是在 views.py 文件的第 6 行引发的,展开 Local vars,我们看到 name 的值是 world,唯一的另一个变量是传入的 request图 1.42)。

  6. 返回 views.py 并通过将 invalid_name 重命名为 name 来修复你的 NameError

  7. 保存文件并刷新浏览器,Hello World 应该再次显示(如 图 1.40 所示)。

在这个练习中,我们通过尝试使用一个尚未设置的变量来使我们的 Django 代码引发异常(NameError)。我们看到了 Django 自动将此异常的详细信息以及堆栈跟踪发送到浏览器,以帮助我们找到错误的原因。然后我们撤销了代码更改,以确保我们的视图能够正常工作。

调试

当您试图查找代码中的问题时,使用调试器可能会有所帮助。这是一个工具,它允许您逐行执行代码,而不是一次性执行所有代码。每次调试器在特定的代码行上暂停时,您都可以看到所有当前变量的值。这对于找出不会引发异常的代码错误非常有用。

例如,在 Bookr 中,我们讨论了有一个视图从数据库中获取书籍列表并在 HTML 模板中渲染它们的场景。如果你在浏览器中查看页面,当你期望看到多本书时,你可能会只看到一本书。你可以在你的视图函数内部暂停执行,查看从数据库中获取了哪些值。如果你的视图只从数据库接收一本书,那么你知道你的数据库查询某处存在问题。如果你的视图成功获取了多本书但只渲染了一本,那么可能是一个模板的问题。调试可以帮助你缩小这类错误的范围。

PyCharm 内置了调试器,这使得您可以轻松地逐行执行代码并查看每行的执行情况。为了告诉调试器在代码的哪个位置停止执行,您需要在代码的一行或多行上设置一个 断点。它们之所以被这样命名,是因为代码的执行将在那个 中断(停止)。

为了激活断点,PyCharm 需要设置为在调试器中运行您的项目。这会有轻微的性能损失,但通常不明显,因此您可能选择始终在调试器中运行代码,这样您就可以快速设置断点而无需停止和重新启动 Django 开发服务器。

在调试器内运行 Django 开发服务器就像点击调试图标而不是播放图标(见 图 1.19)来启动它。

练习 1.09:调试您的代码

在这个练习中,您将学习 PyCharm 调试器的基础知识。您将在调试器中运行 Django 开发服务器,然后在您的视图函数中设置一个断点以暂停执行,这样您就可以检查变量:

  1. 如果 Django 开发服务器正在运行,可以通过点击 PyCharm 窗口右上角的 停止 按钮来停止它:图 1.43:PyCharm 窗口右上角的停止按钮

    图 1.43:PyCharm 窗口右上角的停止按钮

  2. 通过点击停止按钮左侧的调试图标(图 1.43)再次在调试器内启动 Django 开发服务器。

  3. 服务器将花费几秒钟时间启动,然后你应该能够刷新浏览器中的页面以确保它仍在加载——你不应该注意到任何变化;所有代码的执行与之前相同。

  4. 现在我们可以设置一个断点,这将导致执行停止,以便我们可以看到程序的状态。在 PyCharm 中,点击行号右侧的第 5 行,在编辑器面板左侧的空白处。一个红色圆圈将出现以指示断点现在处于活动状态:图 1.44:第 5 行的断点

    图 1.44:第 5 行的断点

  5. 返回到你的浏览器并刷新页面。你的浏览器将不会显示任何内容;相反,它将继续尝试加载页面。根据你的操作系统,PyCharm 应该再次变得活跃;如果不是,将其带到前台。你应该看到第 5 行被高亮显示,在窗口底部,调试器被显示。堆栈帧(调用当前行的函数链)在左侧,函数的当前变量在右侧:图 1.45:调试器暂停,当前行(5)高亮显示

    图 1.45:调试器暂停,当前行(5)被高亮显示

  6. 当前作用域中有一个变量,request。如果你点击其名称左侧的切换三角形,你可以显示或隐藏它设置的属性:图 1.46:request 变量的属性

    图 1.46:request 变量的属性

    例如,如果你滚动通过属性列表,你可以看到方法是 GET 而路径是 /

  7. 图 1.47 中显示的操作栏位于堆栈帧和变量之上。其按钮(从左到右)如下所示:图 1.47:操作栏

    图 1.47:操作栏

    • Step Over

      执行当前行代码并继续到下一行。

    • Step Into

      进入当前行。例如,如果该行包含一个函数,它将在这个函数内部继续使用调试器。

    • Step Into My Code

      进入正在执行的行,但继续执行直到找到你编写的代码。例如,如果你进入第三方库代码,稍后调用你的代码,它将不会显示第三方代码,而是继续执行直到返回到你编写的代码。

    • Force Step Into

      进入通常不会进入的代码,例如 Python 标准库代码。这仅在少数情况下可用,并且通常不使用。

    • Step Out

      从当前代码返回到调用它的函数或方法。与 Step In 动作相反。

    • Run To Cursor

      如果你有一行代码在你当前所在位置之后,你想执行而不必点击 Step Over 之间的所有行,请将光标移到该行。然后,点击 Run To Cursor,执行将继续到该行。

      注意,并非所有按钮在所有时候都很有用。例如,很容易从你的视图中退出,并最终混淆 Django 库代码。

  8. 点击一次 Step Over 按钮来执行第 5 行.

  9. 你可以看到 name 变量已经被添加到调试器视图中的变量列表中,其值为 world图 1.48:新的名称变量现在在作用域内,其值为 world

    图 1.48:新的名称变量现在在作用域内,其值为 world

  10. 我们现在到达了 index 视图函数的末尾,如果我们继续执行此行代码,它将跳转到 Django 库代码,而我们不想看到这些代码。要继续执行并将响应发送回浏览器,请点击窗口左侧的 Resume Program 按钮(图 1.49)。你应该看到浏览器已经重新加载了页面:图 1.49:控制执行的动作——绿色播放图标    是 Resume Program 按钮

    图 1.49:控制执行的动作——绿色播放图标是 Resume Program 按钮

    图 1.49 中还有更多按钮;从上到下,它们是 Rerun(停止程序并重新启动),Resume Program(继续运行直到下一个断点),Pause Program(在当前执行点中断程序),Stop(停止调试器),View Breakpoints(打开一个窗口以查看你设置的断点),和 Mute Breakpoints(将切换所有断点的开或关,但不会删除它们)。

  11. 现在,通过点击它(第 5 行旁边的红色圆圈)在 PyCharm 中关闭断点:图 1.50:点击位于第 5 行的断点可以禁用它

图 1.50:点击位于第 5 行的断点可以禁用它

这只是对如何在 PyCharm 中设置断点的快速介绍。如果你在其他 IDE 中使用过调试功能,那么你应该熟悉这些概念——你可以逐行执行代码,进入和退出函数,或者评估表达式。一旦设置了断点,你可以右键单击它来更改选项。例如,你可以使断点条件化,以便仅在特定情况下停止执行。所有这些内容都超出了本书的范围,但在尝试解决代码中的问题时了解这些是有用的。

活动 1.01:创建站点欢迎屏幕

我们正在构建的 Bookr 网站需要一个欢迎页面,欢迎用户并告知他们所在的网站。它还将包含到网站其他部分的链接,但这些将在后面的章节中添加。现在,你将创建一个带有欢迎信息的页面。

这些步骤将帮助你完成活动:

  1. 在你的index视图中,渲染base.html模板。

  2. 更新base.html模板以包含欢迎信息。它应位于<head>中的<title>标签和正文中新的<h1>标签中。

    完成活动后,你应该能看到类似这样的内容:

    ![图 1.51:Bookr 欢迎页面

    ![图片 B15509_01_51.jpg]

图 1.51:Bookr 欢迎页面

注意

本活动的解决方案可以在packt.live/2Nh1NTJ找到。

活动一.02:图书搜索框架

对于像 Bookr 这样的网站来说,一个有用的功能是能够搜索数据以快速找到网站上的内容。Bookr 将实现图书搜索功能,允许用户通过书籍标题的一部分来查找特定的书籍。虽然我们目前还没有任何书籍可以查找,但我们仍然可以实施一个显示用户搜索文本的页面。用户将搜索字符串作为 URL 参数的一部分输入。我们将在第六章表单中实现搜索和一个易于文本输入的表单。

这些步骤将帮助你完成活动:

  1. 创建一个搜索结果 HTML 模板。它应该包含一个变量占位符,以显示通过渲染上下文传递的搜索词。在<title><h1>标签中显示传递的变量。在正文中使用<em>标签包围搜索文本,使其变为斜体。

  2. views.py中添加一个搜索视图函数。该视图应从 URL 参数(请求的GET属性)中读取搜索字符串。然后,它应该渲染你在上一步创建的模板,传递要替换的搜索值,使用上下文字典。

  3. 将新视图的 URL 映射添加到urls.py文件中。URL 可以是类似/book-search的内容。

完成此活动后,你应该能够通过 URL 的参数传递一个搜索值,并在结果页面上看到它被渲染。它应该看起来像这样:

![图 1.52:搜索使用 Django 进行 Web 开发

![图片 B15509_01_52.jpg]

图 1.52:搜索使用 Django 进行 Web 开发

你还应该能够传递特殊 HTML 字符,如<>,以查看 Django 如何在模板中自动转义它们:

![图 1.53:注意 HTML 字符是如何被转义的,这样我们就可以防止标签注入

![图片 B15509_01_53.jpg]

图 1.53:注意 HTML 字符是如何被转义的,这样我们就可以防止标签注入

注意

本活动的解决方案可以在packt.live/2Nh1NTJ找到。

你已经构建了图书搜索视图,可以展示如何从GET参数中读取变量。你还可以使用此视图来测试 Django 如何在模板中自动转义特殊 HTML 字符。搜索视图实际上还没有搜索或显示结果,因为数据库中没有书籍,但这一点将在第六章表单中添加。

摘要

本章简要介绍了 Django。你首先了解了 HTTP 协议以及 HTTP 请求和响应的结构。然后我们看到了 Django 如何使用 MVT 范式,以及它是如何解析 URL、生成 HTTP 请求并将其发送到视图以获取 HTTP 响应的。我们为 Bookr 项目搭建了框架,并为它创建了reviews应用。接着,我们构建了两个示例视图来展示如何从请求中获取数据并在渲染模板时使用它。你应该已经尝试过,看看 Django 在渲染模板时如何在 HTML 中转义输出。

你使用 PyCharm IDE 完成了所有这些,并学习了如何设置它以调试你的应用程序。调试器将帮助你找出为什么事情没有按预期工作。在下一章中,你将开始学习 Django 的数据库集成及其模型系统,这样你就可以开始为你的应用程序存储和检索真实数据了。

第二章:2. 模型和迁移

概述

本章向您介绍数据库的概念及其在构建网络应用中的重要性。您将首先使用一个名为SQLite DB Browser的开源数据库可视化工具创建数据库。然后,您将使用 SQL 命令执行一些基本的创建、读取、更新、删除CRUD)数据库操作。接着,您将学习 Django 的对象关系映射ORM),使用它,您的应用程序可以使用简单的 Python 代码与关系型数据库无缝交互和工作,从而消除运行复杂 SQL 查询的需要。您将学习模型迁移,它们是 Django ORM 的一部分,用于将数据库模式更改从应用程序传播到数据库,并执行数据库 CRUD 操作。在本章的末尾,您将研究各种类型的数据库关系,并利用这些知识对相关记录进行查询。

简介

数据是大多数网络应用的核心。除非我们谈论的是一个非常简单的应用,例如计算器,在大多数情况下,我们需要存储数据,处理它,并在页面上向用户展示。由于用户界面网络应用中的大多数操作都涉及数据,因此需要在安全、易于访问和随时可用的地方存储数据。这正是数据库发挥作用的地方。想象一下在计算机出现之前运作的图书馆。图书管理员必须维护书籍库存记录、书籍借阅记录、学生归还记录等等。所有这些都会在物理记录中维护。图书管理员在执行日常活动时,会为每个操作修改这些记录,例如,当向某人借书或书籍归还时。

今天,我们有数据库来帮助我们处理这样的管理任务。数据库看起来像是一个包含记录的电子表格或 Excel 表,其中每个表由多行和多列组成。一个应用程序可以有许多这样的表。以下是一个图书馆书籍库存表的示例:

图 2.1:图书馆书籍库存表

](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/webdev-dj-2e/img/B15509_02_01.jpg)

图 2.1:图书馆书籍库存表

在前面的表中,我们可以看到有关于图书馆中书籍各种属性的详细信息的列,而行包含每本书的条目。为了管理图书馆,可以有多个这样的表作为一个系统协同工作。例如,除了库存之外,我们可能还有其他表,如学生信息、书籍借阅记录等等。数据库是用相同的逻辑构建的,其中软件应用程序可以轻松地管理数据。

在上一章中,我们简要介绍了 Django 及其在开发 Web 应用程序中的应用。然后我们学习了模型-视图-模板(MVT)的概念。随后,我们创建了一个 Django 项目并启动了 Django 开发服务器。我们还简要讨论了 Django 的视图、URL 和模板。

在本章中,我们将首先学习数据库的类型以及使用 SQL 进行的一些基本数据库操作。之后,我们将继续学习 Django 中的模型和迁移概念,这些概念通过提供一层抽象来简化数据库操作,从而加速开发过程。

数据库

数据库是一个结构化的数据集合,有助于轻松管理信息。一个称为数据库管理系统(DBMS)的软件层用于存储、维护和执行数据操作。数据库分为两种类型:关系型数据库和非关系型数据库。

关系型数据库

关系型数据库或结构化查询语言(SQL)数据库将数据存储在预先确定的行和列结构中,这种结构称为表。一个数据库可以由多个这样的表组成,这些表具有固定的属性、数据类型和其他表之间的关系结构。例如,正如我们在 图 2.1 中所看到的,图书库存表具有由 书号作者标题副本数量 组成的固定列结构,而条目则形成表中的行。也可能有其他表,例如 学生信息借阅记录,这些表可能与库存表相关。此外,每当一本书借给学生时,记录将根据多个表之间的关系(例如,学生信息图书库存 表)进行存储。

定义数据类型、表格结构和不同表之间关系的预定义规则结构类似于脚手架或数据库的蓝图。这个蓝图统称为数据库模式。当应用于数据库时,它将准备数据库以存储应用程序数据。为了管理和维护这些数据库,关系型数据库有一个通用的语言,称为 SQL。一些关系型数据库的例子包括 SQLite、PostgreSQL、MySQL 和 OracleDB。

非关系型数据库

非关系型数据库或 NoSQL(不仅限于 SQL)数据库旨在存储非结构化数据。它们非常适合大量生成的不遵循严格规则的数据,正如关系型数据库的情况一样。一些非关系型数据库的例子包括 Cassandra、MongoDB、CouchDB 和 Redis。

例如,假设你需要使用 Redis 在数据库中存储公司的股票价值。在这里,公司名称将被存储为键,股票价值将被存储为值。在这个用例中使用键值类型的 NoSQL 数据库是合适的,因为它为唯一的键存储了所需的价值,并且访问速度更快。

在本书的范围内,我们将只处理关系型数据库,因为 Django 官方不支持非关系型数据库。然而,如果你希望探索,有许多分支项目,如 Django non-rel,支持 NoSQL 数据库。

使用 SQL 进行数据库操作

SQL 使用一系列命令来执行各种数据库操作,如创建条目、读取值、更新条目和删除条目。这些操作统称为CRUD 操作,代表创建(Create)、读取(Read)、更新(Update)和删除(Delete)。为了详细了解数据库操作,让我们首先通过 SQL 命令获得一些实际操作经验。大多数关系型数据库共享类似的 SQL 语法;然而,某些操作可能会有所不同。

在本章的范围内,我们将使用 SQLite 作为数据库。SQLite 是一个轻量级的关系型数据库,它是 Python 标准库的一部分。这就是为什么 Django 将其作为默认数据库配置。然而,我们还将学习如何在第十七章Django 应用程序的部署(第一部分 - 服务器设置)中执行配置更改以使用其他数据库。本章可以从本书的 GitHub 仓库中下载,从packt.live/2Kx6FmR

关系型数据库中的数据类型

数据库为我们提供了一种限制给定列中可以存储的数据类型的方法。这些被称为数据类型。以下是一些关系型数据库(如 SQLite3)的数据类型示例:

  • INTEGER 用于存储整数。

  • TEXT 可以存储文本。

  • REAL 用于存储浮点数。

例如,你可能希望书的标题使用 TEXT 作为数据类型。因此,数据库将强制执行一条规则,即在该列中只能存储文本数据,而不能存储其他类型的数据。同样,书的定价可以使用 REAL 数据类型,等等。

练习 2.01:创建图书数据库

在这个练习中,你将为书评应用程序创建一个图书数据库。为了更好地可视化 SQLite 数据库中的数据,你将安装一个名为DB Browser的 SQLite 开源工具。此工具有助于可视化数据,并提供一个执行 SQL 命令的 shell。

如果你还没有这样做,请访问 URL sqlitebrowser.org,并在下载部分根据你的操作系统安装应用程序并启动它。DB Browser 安装的详细说明可以在前言中找到。

注意

可以使用命令行 shell 执行数据库操作。

  1. 启动应用程序后,通过点击应用程序左上角的“新建数据库”来创建一个新的数据库。创建一个名为 bookr 的数据库,因为你正在开发一个书评应用程序:![图 2.2:创建名为 bookr 的数据库

    ![img/B15509_02_02.jpg]

    图 2.2:创建名为 bookr 的数据库

  2. 接下来,点击左上角的创建表按钮,输入表名为book

    注意

    点击保存按钮后,你可能发现创建表的窗口会自动打开。在这种情况下,你不需要点击创建表按钮;只需按照前面步骤指定的方式继续创建图书表即可。

  3. 现在,点击添加字段按钮,输入字段名为title,并从下拉菜单中选择类型为TEXT。在这里,TEXT是数据库中title字段的类型:![图 2.3:添加名为 title 的文本字段]

    ![图片 B15509_02_03.jpg]

    图 2.3:添加一个名为 title 的文本字段

  4. 类似地,为名为publisherauthor的表添加两个更多字段,并将两个字段的类型都选择为TEXT。然后,点击确定按钮:![图 2.4:创建名为 publisher 和 author 的文本字段]

    ![图片 B15509_02_04.jpg]

图 2.4:创建名为 publisher 和 author 的文本字段

这在bookr数据库中创建了一个名为book的数据库表,包含字段 title、publisher 和author。如下所示:

![图 2.5:包含字段 title、publisher 和 author 的数据库]

![图片 B15509_02_05.jpg]

![图 2.5:包含字段 title、publisher 和 author 的数据库]

在这个练习中,我们使用了一个名为 DB Browser(SQLite)的开源工具来创建我们的第一个数据库bookr,并在其中创建了我们第一个名为book的表。

SQL CRUD 操作

假设我们的书评应用编辑器或用户想要对图书库存进行一些修改,例如向数据库中添加几本书,更新数据库中的条目等。SQL 提供了各种方式来执行此类 CRUD 操作。在我们深入 Django 模型和迁移的世界之前,让我们首先探索这些基本的 SQL 操作。

对于接下来的 CRUD 操作,你将运行几个 SQL 查询。要运行它们,导航到 DB Browser 中的执行 SQL标签页。你可以在SQL 1窗口中键入或粘贴我们在后续部分列出的 SQL 查询。在执行它们之前,你可以花些时间修改和了解这些查询。准备好后,点击看起来像播放按钮的图标或按 F5 键来执行命令。结果将显示在SQL 1窗口下面的窗口中:

![图 2.6:在 DB Browser 中执行 SQL 查询]

![图片 B15509_02_06.jpg]

![图 2.6:在 DB Browser 中执行 SQL 查询]

SQL 创建操作

insert命令,正如其名所示,允许我们将数据插入到数据库中。让我们回到我们的bookr示例。由于我们已经创建了数据库和book表,我们现在可以通过执行以下命令在数据库中创建或插入一个条目:

insert into book values ('The Sparrow Warrior', 'Super Hero   Publications', 'Patric Javagal');

这将把命令中定义的值插入到名为 book 的表中。在这里,The Sparrow Warrior 是标题,Super Hero Publications 是出版社,Patric Javagal 是这本书的作者。请注意,插入的顺序与我们创建表的方式相对应;也就是说,值分别插入到代表标题、出版社和作者的列中。同样,让我们执行另外两个插入操作以填充 book 表:

insert into book values ('Ninja Warrior', 'East Hill Publications',   'Edward Smith');
insert into book values ('The European History', 'Northside   Publications', 'Eric Robbins');

到目前为止已执行了三个插入操作,将三个行插入到 book 表中。但我们如何验证这一点?我们如何知道我们插入的三个条目是否正确地输入到数据库中?让我们在下一节中学习如何做到这一点。

SQL 读取操作

我们可以使用 select SQL 操作从数据库中读取。例如,以下 SQL select 命令检索在 book 表中创建的所选条目:

select title, publisher, author from book;

你应该看到以下输出:

图 2.7:使用选择命令后的输出

图 2.7:使用选择命令后的输出

这里,select 是从数据库中读取的命令,字段 titlepublisherauthor 是我们打算从书籍表中选择的列。由于这些列都是数据库中有的,所以选择语句返回了数据库中所有存在的值。选择语句也被称为 SQL 查询。另一种获取数据库中所有字段的方法是在选择查询中使用通配符 * 而不是明确指定所有列名:

select * from book;

这将返回与前面图中所示相同的输出。现在,假设我们想获取名为《The Sparrow Warrior》的书的作者姓名;在这种情况下,select 查询将如下所示:

select author from book where title="The Sparrow Warrior";

这里,我们添加了一个特殊的 SQL 关键字 where,以便 select 查询只返回与条件匹配的条目。查询的结果当然是 Patric Javagal。现在,如果我们想更改书的出版社的名称呢?

SQL 更新操作

在 SQL 中,更新数据库中记录的方式是通过使用 update 命令:

update book set publisher = 'Northside Publications' where   title='The Sparrow Warrior';

这里,我们设置出版社的值为 Northside Publications,如果标题的值为 The Sparrow Warrior。然后我们可以运行在 SQL 读取操作部分运行的 select 查询,以查看运行 update 命令后更新的表看起来如何:

图 2.8:更新《The Sparrow Warrior》标题的出版社值

图 2.8:更新《The Sparrow Warrior》标题的出版社值

接下来,如果我们想删除刚刚更新的记录的标题呢?

SQL 删除操作

这里是一个使用 delete 命令从数据库中删除记录的示例:

delete from book where title='The Sparrow Warrior';

delete是 SQL 中用于删除操作的关键字。在这里,只有当标题为The Sparrow Warrior时,才会执行此操作。以下是删除操作后书籍表的外观:

![图 2.9:执行删除操作后的输出]

![图 2.9:执行删除操作后的输出]

![图 2.9:执行删除操作后的输出]

这些是 SQL 的基本操作。我们不会深入探讨所有的 SQL 命令和语法,但你可以自由探索使用 SQL 进行数据库基础操作的相关内容。

注意

为了进一步学习,你可以从探索一些使用join语句的高级 SQL select操作开始,这些操作用于查询多个表中的数据。有关 SQL 的详细课程,你可以参考The SQL Workshop (www.packtpub.com/product/the-sql-workshop/9781838642358)。

Django ORM

网络应用程序不断与数据库进行交互,其中一种方式就是使用 SQL。如果你决定不使用像 Django 这样的网络框架,而是单独使用 Python 来编写网络应用程序,那么可以使用 Python 库如psycopg2来直接使用 SQL 命令与数据库进行交互。但在开发包含多个表和字段的网络应用程序时,SQL 命令可能会变得过于复杂,从而难以维护。因此,像 Django 这样的流行网络框架提供了抽象层,使我们能够轻松地与数据库进行交互。Django 中帮助我们完成这一功能的部分被称为ORM,即对象关系映射

Django ORM 将面向对象的 Python 代码转换为实际的数据库结构,如具有数据类型定义的数据库表,并通过简单的 Python 代码简化所有数据库操作。正因为如此,我们在执行数据库操作时无需处理 SQL 命令。这有助于加快应用程序的开发速度,并简化应用程序源代码的维护。

Django 支持关系型数据库,如 SQLite、PostgreSQL、Oracle 数据库和 MySQL。Django 的数据库抽象层确保了相同的 Python/Django 源代码可以在上述任何关系型数据库上使用,只需对项目设置进行很少的修改。由于 SQLite 是 Python 库的一部分,并且 Django 默认配置为 SQLite,因此在本章的学习过程中,我们将使用 SQLite 来了解 Django 模型和迁移。

数据库配置和创建 Django 应用程序

正如我们在第一章Django 简介中已经看到的,当我们创建 Django 项目并运行 Django 服务器时,默认的数据库配置是 SQLite3。数据库配置将位于项目目录中的settings.py文件中。

注意

确保您仔细阅读bookr应用的settings.py文件。通读整个文件一次将有助于您理解后续的概念。您可以通过此链接找到文件:packt.live/2KEdaUM

因此,对于我们的示例项目,数据库配置将位于以下位置:bookr/settings.py。当创建 Django 项目时,该文件中默认的数据库配置如下:

DATABASES = {\
             'default': {\
                         'ENGINE': 'django.db.backends.sqlite3',\
                         'NAME': os.path.join\
                                 (BASE_DIR, 'db.sqlite3'),}}

注意

前面的代码片段使用反斜杠(\)将逻辑拆分到多行。当代码执行时,Python 将忽略反斜杠,并将下一行的代码视为当前行的直接延续。

DATABASES变量被分配了一个包含项目数据库详细信息的字典。在字典中,有一个嵌套字典,其键为default。这包含了 Django 项目的默认数据库配置。我们使用带有default键的嵌套字典的原因是,Django 项目可能需要与多个数据库交互,而默认数据库是 Django 在所有操作中默认使用的数据库,除非明确指定。ENGINE键表示正在使用哪个数据库引擎;在这种情况下,它是sqlite3

NAME键定义了数据库的名称,可以有任何值。但对于 SQLite3,由于数据库是以文件形式创建的,因此NAME可以包含需要创建文件的目录的完整路径。db文件的完整路径是通过将BASE_DIR中定义的先前路径与db.sqlite3连接(或连接)来处理的。请注意,BASE_DIR是在settings.py文件中已定义的项目目录。

如果您使用其他数据库,例如 PostgreSQL、MySQL 等,则需要在此前的数据库设置中进行更改,如下所示:

DATABASES = {\
             'default': {\
                         'ENGINE': 'django.db\
                                    .backends.postgresql',\
                         'NAME': 'bookr',\
                         'USER': <username>,\
                         'PASSWORD': <password>,\
                         'HOST': <host-IP-address>,\
                         'PORT': '5432',}}

在这里,已对ENGINE进行了更改以使用 PostgreSQL。需要分别提供服务器的 IP 地址和端口号作为HOSTPORT。正如其名称所暗示的,USER是数据库用户名,PASSWORD是数据库密码。除了配置更改外,我们还需要安装数据库驱动程序或绑定,包括数据库主机和凭证。这将在后面的章节中详细说明,但就目前而言,由于我们正在使用 SQLite3,默认配置就足够了。请注意,上面的只是一个示例,以展示您需要为使用不同的数据库(如 PostgreSQL)所做的更改,但由于我们正在使用 SQLite,我们将使用现有的数据库配置,并且不需要对数据库设置进行任何修改。

Django 应用

一个 Django 项目可以有多个应用程序,这些应用程序通常作为独立的实体。这就是为什么,在需要时,一个应用程序也可以插入到不同的 Django 项目中。例如,如果我们正在开发一个电子商务网络应用程序,该网络应用程序可以有多个应用程序,例如用于客户支持的聊天机器人或用于接受用户从应用程序购买商品时的支付网关。如果需要,这些应用程序也可以插入到或在不同项目中重用。

Django 默认启用了以下应用程序。以下是从一个项目的 settings.py 文件中摘录的一段内容:

INSTALLED_APPS = ['django.contrib.admin',\
                  'django.contrib.auth',\
                  'django.contrib.contenttypes',\
                  'django.contrib.sessions',\
                  'django.contrib.messages',\
                  'django.contrib.staticfiles',]

这些是一组用于管理站点、身份验证、内容类型、会话、消息传递以及用于收集和管理静态文件的应用程序。在接下来的章节中,我们将深入研究这些内容。然而,在本章范围内,我们将了解为什么 Django 迁移对于这些已安装的应用程序是必要的。

Django 迁移

如我们之前所学的,Django 的 ORM 有助于使数据库操作更简单。操作的主要部分是将 Python 代码转换为数据库结构,如具有指定数据类型的数据库字段和表。换句话说,将 Python 代码转换为数据库结构称为 Exercise 2.01创建一个图书数据库)中的 第 4 步TEXT

由于我们已经设置了 Django 项目,让我们执行第一次迁移。尽管我们还没有向项目中添加任何代码,但我们仍然可以迁移 INSTALLED_APPS 中列出的应用程序。这是必要的,因为 Django 安装的程序需要将相关数据存储在数据库中以供其操作,迁移将创建所需的数据库表以在数据库中存储数据。为此,应在终端或 shell 中输入以下命令:

python manage.py migrate

注意

对于 macOS,您可以在上述命令中使用 python3 而不是 python

在这里,manage.py 是在创建项目时自动创建的脚本。它用于执行管理或行政任务。通过执行此命令,我们创建所有已安装应用程序所需的数据库结构。

由于我们正在使用 DB Browser for SQLite 浏览数据库,让我们看一下执行 migrate 命令后已更改的数据库。

数据库文件将在项目目录下以 db.sqlite3 的名称创建。打开 DB Browser,点击 Open Database,导航直到找到 db.sqlite3 文件,然后打开它。您应该会看到由 Django 迁移创建的一组新表。在 DB Browser 中看起来如下:

图 2.10:db.sqlite3 文件的内容

图 2.10:db.sqlite3 文件的内容

现在,如果我们通过点击数据库表浏览新创建的数据库结构,我们会看到以下内容:

图 2.11:浏览新创建的数据库结构

图 2.11:浏览新创建的数据库结构

注意到创建的数据库表有不同的字段,每个字段都有其对应的数据类型。在 DB Browser 中点击浏览数据标签页,并从下拉菜单中选择一个表。例如,在点击auth_group_permissions表后,您应该看到类似以下的内容:

![Figure 2.12: Viewing the auth_group_permissions tableimg/B15509_02_12.jpg

图 2.12:查看 auth_group_permissions 表

您会看到这些表还没有数据,因为 Django 迁移只创建数据库结构或蓝图,而实际数据是在应用程序运行期间存储在数据库中的。现在,既然我们已经迁移了内置或默认的 Django 应用程序,让我们尝试创建一个应用程序并执行 Django 迁移。

创建 Django 模型和迁移

Django 模型本质上是一个 Python 类,它包含了在数据库中创建表的蓝图。models.py文件可以包含许多这样的模型,每个模型都会转换成一个数据库表。类的属性根据模型定义形成了数据库表的字段和关系。

对于我们的评论应用程序,我们需要创建以下模型及其相应的数据库表:

  • 书籍:这个模型应该存储关于书籍的信息。

  • 贡献者:这个模型应该存储关于为书籍写作的个人(如作者、合著者或编辑)的信息。

  • 出版社:正如其名所示,这指的是书籍的出版社。

  • 评论:这个模型应该存储应用程序用户所写的所有书籍评论。

在我们的应用程序中,每本书都需要有一个出版社,因此让我们创建Publisher作为我们的第一个模型。在reviews/models.py中输入以下代码:

from django.db import models
class Publisher(models.Model):
    """A company that publishes books."""
    name = models.CharField\
           (max_length=50, \
            help_text="The name of the Publisher.")
    website = models.URLField\
              (help_text="The Publisher's website.")
    email = models.EmailField\
            (help_text="The Publisher's email address.")

注意

您可以通过点击以下链接查看 bookr 应用程序的完整models.py文件:packt.live/3hmFQxn

代码的第一行导入了 Django 的models模块。虽然这一行将在创建 Django 应用程序时自动生成,但请确保如果它不存在,您已经添加了它。在导入之后,其余的代码定义了一个名为Publisher的类,它将是 Django 的models.Model的子类。此外,这个类将具有名称、网站和电子邮件等属性或字段。

字段类型

如我们所见,每个字段都被定义为以下类型:

  • CharField:这种字段类型用于存储较短的字符串字段,例如,Packt Publishing。对于非常长的字符串,我们使用TextField

  • EmailField:这与CharField类似,但会验证字符串是否代表一个有效的电子邮件地址,例如,customersupport@packtpub.com。

  • URLField:这又类似于CharField,但会验证字符串是否代表一个有效的 URL,例如,www.packtpub.com

字段选项

Django 提供了一种为模型字段定义字段选项的方法。这些字段选项用于设置值或约束等。例如,我们可以使用default=<value>为字段设置默认值,以确保每次在数据库中为该字段创建记录时,它都被设置为指定的默认值。以下是我们定义Publisher模型时使用的两个字段选项:

  • help_text:这是一个字段选项,帮助我们为字段添加描述性文本,该文本会自动包含在 Django 表单中。

  • max_length:此选项提供给了CharField,其中它定义了字段以字符数表示的最大长度。

Django 有许多更多的字段类型和字段选项,可以在广泛的官方 Django 文档中进行探索。随着我们开发我们的示例书评应用程序,我们将了解用于项目的那些类型和字段。现在让我们将 Django 模型迁移到数据库中。在 shell 或终端中执行以下命令以完成此操作(从存储manage.py文件的文件夹中运行):

python manage.py makemigrations reviews

命令的输出如下:

Migrations for 'reviews':
  reviews/migrations/0001_initial.py
    - Create model Publisher

makemigrations <appname>命令为给定的应用创建迁移脚本;在这种情况下,为 reviews 应用。注意,在运行 makemigrations 之后,在migrations文件夹下创建了一个新文件:

![图 2.13:在 migrations 文件夹下创建的新文件

![图 2.13:在 migrations 文件夹下创建的新文件

图 2.13:在 migrations 文件夹下创建的新文件

这是 Django 创建的迁移脚本。当我们运行不带应用名称的makemigrations时,将为项目中的所有应用创建迁移脚本。接下来,让我们列出项目的迁移状态。记住,之前我们为 Django 已安装的应用程序应用了迁移,现在我们创建了一个新的应用,reviews。以下命令在 shell 或终端中运行时,将显示整个项目中模型迁移的状态(从存储manage.py文件的文件夹中运行):

python manage.py showmigrations

前一个命令的输出如下:

admin
 [X] 0001_initial
 [X] 0002_logentry_remove_auto_add
 [X] 0003_logentry_add_action_flag_choices
auth
 [X] 0001_initial
 [X] 0002_alter_permission_name_max_length
 [X] 0003_alter_user_email_max_length
 [X] 0004_alter_user_username_opts
 [X] 0005_alter_user_last_login_null
 [X] 0006_require_contenttypes_0002
 [X] 0007_alter_validators_add_error_messages
 [X] 0008_alter_user_username_max_length
 [X] 0009_alter_user_last_name_max_length
 [X] 0010_alter_group_name_max_length
 [X] 0011_update_proxy_permissions
contenttypes
 [X] 0001_initial
 [X] 0002_remove_content_type_name
reviews
 [ ] 0001_initial
sessions
 [X] 0001_initial

这里,[X]标记表示迁移已经应用。注意,除了 reviews 之外,所有其他应用的迁移都已应用。可以使用showmigrations命令来了解迁移状态,但在执行模型迁移时这不是一个强制性的步骤。

接下来,让我们了解 Django 如何将模型转换为实际的数据库表。这可以通过运行sqlmigrate命令来理解:

python manage.py sqlmigrate reviews 0001_initial 

我们应该看到以下输出:

BEGIN;
--
-- Create model Publisher
--
CREATE TABLE "reviews_publisher" ("id" integer \
    NOT NULL PRIMARY KEY AUTOINCREMENT, "name" \
    varchar(50) NOT NULL, "website" varchar(200) \
    NOT NULL, "email" varchar(254) NOT NULL);
COMMIT;

前面的代码片段显示了当 Django 迁移数据库时使用的 SQL 命令等效。在这种情况下,我们正在创建一个名为 reviews_publisher 的表,包含名称、网站和电子邮件字段,并定义了相应的字段类型。此外,所有这些字段都被定义为 NOT NULL,这意味着这些字段的条目不能为空,应该有一些值。在执行模型迁移时,sqlmigrate 命令不是必须的步骤。

主键

假设有一个名为 users 的数据库表,正如其名称所暗示的,它存储有关用户的信息。假设它有超过 1,000 条记录,并且至少有 3 个用户的名字叫 Joe Burns。我们如何从应用程序中唯一地识别这些用户呢?解决方案是找到一种方法来唯一地识别数据库中的每条记录。这是通过使用 id 作为主键(整数类型)来实现的,它在创建新记录时会自动递增。

在上一节中,注意 python manage.py sqlmigrate 命令的输出。在创建 Publisher 表时,SQL CREATE TABLE 命令向表中添加了一个额外的字段,称为 idid 被定义为 PRIMARY KEY AUTOINCREMENT。在关系型数据库中,主键用于在数据库中唯一地标识一个条目。例如,书籍表有 id 作为主键,其数字从 1 开始。这个值在新记录创建时会递增 1。id 的整数值在书籍表中总是唯一的。由于迁移脚本已经通过执行 makemigrations 被创建,现在让我们通过执行以下命令来迁移 reviews 应用程序中新建的模型:

python manage.py migrate reviews

你应该得到以下输出:

Operations to perform:
    Apply all migrations: reviews
Running migrations:
    Applying reviews.0001_initial... OK

这个操作创建了 reviews 应用的数据库表。以下是从 DB Browser 中摘录的片段,表明新表 reviews_publisher 已在数据库中创建:

![图 2.14:执行迁移命令后创建的 reviews_publisher 表]

![img/B15509_02_14.jpg]

图 2.14:执行迁移命令后创建的 reviews_publisher 表

到目前为止,我们已经探讨了如何创建一个模型并将其迁移到数据库中。现在让我们着手创建我们书籍评论应用程序的其余模型。正如我们已经看到的,应用程序将具有以下数据库表:

  • Book:这是包含关于书籍本身信息的数据库表。我们已创建了一个 Book 模型并将其迁移到数据库中。

  • Publisher:这个表包含有关书籍出版者的信息。

  • Contributor:这个表包含有关贡献者(即作者、合著者或编辑)的信息。

  • Review:这个表包含由评论者发布的评论信息。

让我们将 BookContributor 模型,如以下代码片段所示,添加到 reviews/models.py 中:

class Book(models.Model):
    """A published book."""
    title = models.CharField\
            (max_length=70, \
             help_text="The title of the book.")
    publication_date = models.DateField\
                       (verbose_name=\
                        "Date the book was published.")
    isbn = models.CharField\
           (max_length=20, \
            verbose_name="ISBN number of the book.")
class Contributor(models.Model):
"""
A contributor to a Book, e.g. author, editor, \
co-author.
"""
  first_names = models.CharField\
                (max_length=50, \
                 help_text=\
                 "The contributor's first name or names.")
    last_names = models.CharField\
                 (max_length=50, \
                  help_text=\
                  "The contributor's last name or names.")
    email = models.EmailField\
            (help_text="The contact email for the contributor.")

代码是自我解释的。Book模型有标题、出版日期和 isbn 字段。Contributor模型有first_nameslast_names字段以及贡献者的电子邮件 ID。还有一些新添加的模型,除了我们在Publisher模型中看到的外。它们有一个新的字段类型DateField,正如其名所示,用于存储日期。还有一个名为verbose_name的新字段选项也被使用。它为字段提供了一个描述性名称。

关系

关系型数据库的一项强大功能是能够在数据库表间建立数据关系。通过在表间建立正确的引用,关系有助于维护数据完整性,进而帮助维护数据库。另一方面,关系规则确保数据一致性并防止重复。

在关系型数据库中,可能存在以下类型的关联:

  • 多对一

  • 多对多

  • 一对一

让我们详细探讨每种关系。

多对一

在这种关系中,一个表中的多个记录(行/条目)可以引用另一个表中的一个记录(行/条目)。例如,可以有一个出版社出版多本书。这是一个多对一关系的例子。为了建立这种关系,我们需要使用数据库的外键。关系型数据库中的外键在一个表中的字段与另一个表的主键之间建立关系。

例如,假设你有一个存储在名为employee_info的表中的员工数据,该表以员工 ID 作为主键,并包含一个存储部门名称的列;此表还包含一个存储该部门 ID 的列。现在,还有一个名为departments_info的表,其中部门 ID 作为主键。在这种情况下,部门 ID 是employee_info表的外键。

在我们的bookr应用中,Book模型可以有一个外键引用Publisher表的主键。由于我们已创建了BookContributorPublisher的模型,现在让我们在BookPublisher模型之间建立多对一关系。对于Book模型,添加最后一行:

class Book(models.Model):
    """A published book."""
    title = models.CharField\
            (max_length=70, \
             help_text="The title of the book.")
    publication_date = models.DateField\
                       (verbose_name=\
                        "Date the book was published.")
    isbn = models.CharField\
           (max_length=20, \
            verbose_name="ISBN number of the book.")
    publisher = models.ForeignKey\
                (Publisher, on_delete=models.CASCADE)

现在新增的publisher字段正在使用外键在BookPublisher之间建立多对一关系。这种关系确保了多对一关系的性质,即多本书可以有一个出版商:

  • models.ForeignKey:这是用于建立多对一关系的字段选项。

  • Publisher:当我们与 Django 中的不同表建立关系时,我们指的是创建表的模型;在这种情况下,Publisher表是由Publisher模型(或 Python 类 Publisher)创建的。

  • on_delete: 这是一个字段选项,用于确定在删除引用对象时要采取的操作。在这种情况下,on_delete 选项设置为 CASCADE(models.CASCADE),这将删除引用对象。

例如,假设一个出版社出版了一系列书籍。由于某种原因,如果需要从应用程序中删除该出版社,则下一步操作是 CASCADE,这意味着从应用程序中删除所有引用的书籍。还有许多其他的 on_delete 操作,例如以下内容:

  • PROTECT: 这防止删除记录,除非所有引用对象都被删除。

  • SET_NULL: 如果数据库字段之前已配置为存储空值,则此操作将设置一个空值。

  • SET_DEFAULT: 在删除引用对象时设置为默认值。

对于我们的书评应用程序,我们将仅使用 CASCADE 选项。

多对多

在这种关系中,一个表中的多个记录可以与另一个表中的多个记录建立关系。例如,一本书可以有多个合著者,而每位作者(贡献者)可能都写过多本书。因此,这形成了 BookContributor 表之间的多对多关系:

图 2.15:书籍与合著者之间的多对多关系

图 2.15

图 2.15:书籍与合著者之间的多对多关系

models.py 中,对于 Book 模型,添加如下所示的最后一条线:

class Book(models.Model):
    """A published book."""
    title = models.CharField\
            (max_length=70, \
             help_text="The title of the book.")
    publication_date = models.DateField\
                       (verbose_name=\
                        "Date the book was published.")
    isbn = models.CharField\
           (max_length=20, \
            verbose_name="ISBN number of the book.")
    publisher = models.ForeignKey\
                (Publisher, on_delete=models.CASCADE)
    contributors = models.ManyToManyField\
                   ('Contributor', through="BookContributor")

新增的贡献者字段通过使用 ManyToManyField 字段类型与书籍和贡献者建立了多对多关系:

  • models.ManyToManyField: 这是用于建立多对多关系的字段类型。

  • through: 这是多对多关系的一个特殊字段选项。当我们有两个表之间的多对多关系时,如果我们想存储关于关系的额外信息,则可以使用此选项通过中介表建立关系。

例如,我们有两个表,即 BookContributor,我们需要存储关于书籍贡献者类型的信息,如作者、合著者或编辑。然后贡献者类型存储在一个称为 BookContributor 的中介表中。以下是 BookContributor 表/模型的外观。确保您将此模型包含在 reviews/models.py 中:

class BookContributor(models.Model):
    class ContributionRole(models.TextChoices):
        AUTHOR = "AUTHOR", "Author"
        CO_AUTHOR = "CO_AUTHOR", "Co-Author"
        EDITOR = "EDITOR", "Editor"
    book = models.ForeignKey\
           (Book, on_delete=models.CASCADE)
    contributor = models.ForeignKey\
                  (Contributor, \
                   on_delete=models.CASCADE)
    role = models.CharField\
           (verbose_name=\
            "The role this contributor had in the book.", \
            choices=ContributionRole.choices, max_length=20)

备注

完整的 models.py 文件可以在此链接查看:packt.live/3hmFQxn

一个中介表,如 BookContributor,通过使用到 BookContributor 表的外键来建立关系。它还可以有额外的字段,可以存储关于 BookContributor 模型与以下字段之间的关系的信息:

  • book: 这是到 Book 模型的外键。正如我们之前所看到的,on_delete=models.CASCADE 将在应用程序中删除相关书籍时从关系表中删除一个条目。

  • Contributor: 这又是 Contributor 模型/表的另一个外键。在删除时也定义为 CASCADE

  • role: 这是中间模型字段,它存储关于 BookContributor 之间关系的额外信息。

  • class ContributionRole(models.TextChoices): 这可以通过创建 models.TextChoices 的子类来定义一组选择。例如,ContributionRole 是从 TextChoices 创建的子类,它用于 roles 字段,将作者、合著者和编辑定义为一组选择。

  • choices: 这指的是在模型中定义的一组选择,当使用模型创建 Django Forms 时非常有用。

    注意

    当在建立多对多关系时没有提供 through 字段选项,Django 会自动创建一个中间表来管理关系。

一对一关系

在这种关系中,一个表中的一个记录将只引用另一个表中的一个记录。例如,一个人只能有一个驾照,因此一个人和他们的驾照可以形成一对一关系:

![图 2.16:一对一关系的示例图片 B15509_02_16.jpg

图 2.16:一对一关系的示例

OneToOneField 可以用来建立一对一关系,如下所示:

class DriverLicence(models.Model):
    person = models.OneToOneField\
             (Person, on_delete=models.CASCADE)
    licence_number = models.CharField(max_length=50)

既然我们已经探讨了数据库关系,让我们回到我们的 bookr 应用程序,并添加一个额外的模型。

添加评论模型

我们已经将 BookPublisher 模型添加到了 reviews/models.py 文件中。我们将要添加的最后一个模型是 Review 模型。以下代码片段应该能帮助我们完成这项工作:

from django.contrib import auth
class Review(models.Model):
    content = models.TextField\
              (help_text="The Review text.")
    rating = models.IntegerField\
             (help_text="The rating the reviewer has given.")
    date_created = models.DateTimeField\
                   (auto_now_add=True, \
                    help_text=\
                    "The date and time the review was created.")
    date_edited = models.DateTimeField\
                  (null=True, \
                   help_text=\
                   "The date and time the review was last edited.")
    creator = models.ForeignKey\
              (auth.get_user_model(), on_delete=models.CASCADE)
    book = models.ForeignKey\
           (Book, on_delete=models.CASCADE, \
            help_text="The Book that this review is for.")

注意

完整的 models.py 文件可以在以下链接查看:packt.live/3hmFQxn

review 模型/表将用于存储用户提供的书籍评论和评分。它有以下字段:

  • content: 此字段存储书籍评论的文本,因此使用的字段类型是 TextField,因为它可以存储大量文本。

  • rating: 此字段存储书籍的评论评分。由于评分将是一个整数,因此使用的字段类型是 IntegerField

  • date_created: 此字段存储评论被撰写的时间和日期,因此字段类型是 DateTimeField

  • date_edited: 此字段存储每次编辑评论时的日期和时间。字段类型再次是 DateTimeField

  • Creator: 此字段指定评论创建者或撰写书评的人。请注意,这是一个指向 auth.get_user_model() 的外键,它引用了 Django 内置认证模块中的 User 模型。它有一个字段选项 on_delete=models.CASCADE。这解释了当从数据库中删除用户时,该用户所写的所有评论也将被删除。

  • Book:评论有一个名为book的字段,它是Book模型的外键。这是因为对于评论应用,评论必须被撰写,一本书可以有多个评论,所以这是一个多对一的关系。这也通过字段选项on_delete=models.CASCADE定义,因为一旦删除书籍,保留应用中的评论就没有意义了。所以,当删除书籍时,所有引用该书籍的评论也将被删除。

模型方法

在 Django 中,我们可以在模型类内部编写方法。这些被称为__str__()。此方法返回Model实例的字符串表示形式,在使用 Django shell 时特别有用。在以下示例中,当__str__()方法添加到Publisher模型时,Publisher对象的字符串表示形式将是出版商的名称:

class Publisher(models.Model):
    """A company that publishes books."""
    name = models.CharField\
           (max_length=50, \
            help_text="The name of the Publisher.")
    website = models.URLField\
              (help_text="The Publisher's website.")
    email = models.EmailField\
            (help_text="The Publisher's email address.")
    def __str__(self):
        return self.name

还要将_str_()方法添加到ContributorBook中,如下所示:

class Book(models.Model):
    """A published book."""
    title = models.CharField\
            (max_length=70, \
             help_text="The title of the book.")
    publication_date = models.DateField\
                       (verbose_name=\
                        "Date the book was published.")
    isbn = models.CharField\
           (max_length=20, \
            verbose_name="ISBN number of the book.")
    publisher = models.ForeignKey\
                (Publisher, \
                 on_delete=models.CASCADE)
    contributors = models.ManyToManyField\
                   ('Contributor', through="BookContributor")
    def __str__(self):
        return self.title
class Contributor(models.Model):
"""
A contributor to a Book, e.g. author, editor, \
co-author.
"""
    first_names = models.CharField\
                  (max_length=50, \
                   help_text=\
                   "The contributor's first name or names.")
    last_names = models.CharField\
                 (max_length=50, \
                  help_text=\
                  "The contributor's last name or names.")
    email = models.EmailField\
            (help_text=\
             "The contact email for the contributor.")
    def __str__(self):
        return self.first_names

迁移评论应用

由于我们已经准备好了整个模型文件,现在让我们将模型迁移到数据库中,就像我们之前对已安装的应用程序所做的那样。由于评论应用有一组我们创建的模型,在运行迁移之前,创建迁移脚本非常重要。迁移脚本有助于识别模型中的任何更改,并在运行迁移时将这些更改传播到数据库中。执行以下命令以创建迁移脚本:

python manage.py makemigrations reviews

你应该得到类似以下输出:

  reviews/migrations/0002_auto_20191007_0112.py
    - Create model Book
    - Create model Contributor
    - Create model Review
    - Create model BookContributor
    - Add field contributors to book
    - Add field publisher to book

迁移脚本将在应用程序文件夹中名为migrations的文件夹中创建。接下来,使用migrate命令将所有模型迁移到数据库中:

python manage.py migrate reviews

你应该看到以下输出:

Operations to perform:
  Apply all migrations: reviews
Running migrations:
  Applying reviews.0002_auto_20191007_0112... OK

执行此命令后,我们已经成功创建了reviews应用中定义的数据库表格。你可以在迁移后使用 DB Browser for SQLite 来探索你刚刚创建的表格。为此,打开 DB Browser for SQLite,点击Open Database按钮(图 2.17),并导航到你的项目目录:

![图 2.17:点击打开数据库按钮img/B15509_02_17.jpg

图 2.17:点击打开数据库按钮

选择名为db.sqlite3的数据库文件以打开它(图 2.18)。

![图 2.18:在 bookr 目录中定位 db.sqlite3img/B15509_02_18.jpg

图 2.18:在 bookr 目录中定位 db.sqlite3

现在你应该能够浏览创建的新表格集。以下图显示了reviews应用中定义的数据库表格:

![图 2.19:评论应用中定义的数据库表格img/B15509_02_19.jpg

图 2.19:评论应用中定义的数据库表格

Django 的数据库 CRUD 操作

由于我们已经为评论应用创建了必要的数据库表格,让我们现在通过 Django 了解基本数据库操作。

我们已经在名为“SQL CRUD 操作”的部分中简要介绍了使用 SQL 语句进行数据库操作。我们尝试使用Insert语句将条目插入数据库,使用select语句从数据库中读取,使用update语句更新条目,以及使用delete语句从数据库中删除条目。

Django 的 ORM 提供了相同的功能,无需处理 SQL 语句。Django 的数据库操作是简单的 Python 代码,因此我们克服了在 Python 代码中维护 SQL 语句的麻烦。让我们看看这些是如何执行的。

要执行 CRUD 操作,我们将通过执行以下命令进入 Django 的命令行 shell:

python manage.py shell

注意

对于本章,我们将使用代码块开头的>>>符号(突出显示)来指定 Django shell 命令。当将查询粘贴到数据库浏览器时,请确保每次都排除这个符号。

当交互式控制台启动时,看起来如下所示:

Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> 

练习 2.02:在 Bookr 数据库中创建条目

在这个练习中,您将通过保存模型实例来在数据库中创建一个新条目。换句话说,您将在数据库表中创建一个条目,而不需要显式运行 SQL 查询:

  1. 首先,从reviews.models导入Publisher类/模型:

    >>>from reviews.models import Publisher
    
  2. 通过传递Publisher模型所需的所有字段值(名称、网站和电子邮件)来创建Publisher类的对象或实例:

    >>>publisher = Publisher(name='Packt Publishing', website='https://www.packtpub.com', email='info@packtpub.com')
    
  3. 接下来,要将对象写入数据库,重要的是调用save()方法,因为直到这个方法被调用,数据库中才不会创建条目:

    >>>publisher.save()
    

    现在,您可以在数据库浏览器中看到一个新条目被创建:

    图 2.20:数据库中创建的条目

    图 2.20:数据库中创建的条目

  4. 使用对象属性对对象进行任何进一步的修改,并将更改保存到数据库中:

    >>>publisher.email
    'info@packtpub.com'
    >>> publisher.email = 'customersupport@packtpub.com'
    >>> publisher.save()
    

    您可以使用以下方式使用数据库浏览器查看更改:

    图 2.21:更新了电子邮件字段的条目

图 2.21:更新了电子邮件字段的条目

在这个练习中,您通过创建模型对象的实例并在数据库中写入模型对象来创建数据库条目使用了save()方法。

注意,通过遵循前面的方法,直到调用save()方法,对类实例的更改都不会被保存。然而,如果我们使用create()方法,Django 将在一步中将更改保存到数据库中。我们将在接下来的练习中使用此方法。

练习 2.03:使用 create()方法创建条目

在这里,您将使用create()方法一步创建contributor表中的记录:

  1. 首先,像之前一样导入Contributor类:

    >>> from reviews.models import Contributor
    
  2. 通过调用create()方法,您可以一步在数据库中创建一个对象。确保您传递所有必需的参数(first_nameslast_namesemail):

    >>> contributor  =   Contributor.objects.create(first_names="Rowel",     last_names="Atienza", email="RowelAtienza@example.com")
    
  3. 使用 DB Browser 验证贡献者记录是否已创建在数据库中。如果你的 DB Browser 还没有打开,请像上一节中那样打开数据库文件db.sqlite3。点击浏览数据并选择所需的表——在本例中,从下拉菜单中选择reviews_contributor表,如图所示——并验证新创建的数据库记录:图 2.22:在 DB Browser 中验证记录的创建

图 2.22:在 DB Browser 中验证记录的创建

在这个练习中,我们学习了使用create()方法,我们可以一次性在数据库中为模型创建记录。

创建具有外键的对象

与我们创建PublisherContributor表中的记录类似,现在让我们为Book表创建一个记录。如果你还记得,Book模型有一个不能为空的外键Publisher。因此,填充出版商外键的一种方法是在书籍的publisher字段中提供创建的publisher对象,如下面的练习所示。

练习 2.04:创建多对一关系的记录

在这个练习中,你将在Book表中创建一个包含指向Publisher模型外键的记录。正如你所知,BookPublisher之间的关系是多对一关系,因此你必须首先获取Publisher对象,然后在创建书籍记录时使用它:

  1. 首先,导入Publisher类:

    >>>from reviews.models import Book, Publisher
    
  2. 使用以下命令从数据库中检索publisher对象。get()方法用于从数据库中检索对象。我们还没有探索数据库的读取操作。现在,使用以下命令;我们将在下一节中深入探讨数据库的读取/检索:

    >>>publisher = Publisher.objects.get(name='Packt Publishing')
    
  3. 在创建书籍时,我们需要提供一个date对象,因为Book模型中的publication_date是一个日期字段。因此,从datetime导入date,以便在创建book对象时提供日期对象,如下代码所示:

    >>>from datetime import date
    
  4. 使用create()方法在数据库中创建书籍的记录。确保你传递所有字段,即titlepublication_dateisbnpublisher对象:

    >>>book = Book.objects.create(title="Advanced Deep Learning   with Keras", publication_date=date(2018, 10, 31),     isbn="9781788629416", publisher=publisher)
    

    注意,由于publisher是一个外键,并且它不可为空(不能持有null值),因此必须传递一个publisher对象。当必需的外键对象publisher未提供时,数据库将抛出完整性错误。

    图 2.23显示了创建第一条记录的Book表。注意,外键字段(publisher_id)指向Publisher表。书籍记录中的publisher_id条目指向一个具有id(主键)1Publisher记录,如下两个屏幕截图所示:

    图 2.23:指向 reviews_book 主键的外键

图 2.23:指向 reviews_book 主键的外键

![图 2.24:指向 reviews_publisher 主键的外键]

![img/B15509_02_24.jpg]

图 2.24:指向 reviews_publisher 主键的外键

在这个练习中,我们了解到在创建数据库记录时,如果是一个外键,可以将对象分配给字段。我们知道Book模型也与Contributor模型有多个对多个的关系。现在,让我们在创建数据库记录时探索建立多个对多个关系的方法。

练习 2.05:使用多对多关系创建记录

在这个练习中,你将使用关系模型BookContributorBookContributor之间创建多对多关系:

  1. 如果你在重启了 shell 后丢失了publisherbook对象,可以通过以下一组 Python 语句从数据库中检索它们:

    >>>from reviews.models import Book
    >>>from reviews.models import Contributor
    >>>contributor = Contributor.objects.get(first_names='Rowel')
    book = Book.objects.get(title="Advanced Deep Learning with Keras")
    
  2. 建立多对多关系的方法是将关系信息存储在中间模型或关系模型中;在这种情况下,它是BookContributor。由于我们已经从数据库中检索了书籍和贡献者记录,让我们在创建BookContributor关系模型记录时使用这些对象。为此,首先创建BookContributor关系类的实例,然后将对象保存到数据库中。在这个过程中,确保你传递了所需的字段,即book对象、contributor对象和role

    >>>from reviews.models import BookContributor
    >>>book_contributor = BookContributor(book=book,   contributor=contributor, role='AUTHOR')
    >>> book_contributor.save()
    

    注意,我们在创建book_contributor对象时指定了角色为AUTHOR。这是一个在建立多对多关系时存储关系数据的经典示例。角色可以是AUTHORCO_AUTHOREDITOR

    这建立了书籍《高级深度学习与 Keras》和贡献者 Rowel(Rowel 是这本书的作者)之间的关系。

在这个练习中,我们使用BookContributor关系模型在BookContributor之间建立了多对多关系。关于我们刚刚创建的多对多关系的验证,我们将在本章稍后的几个练习中详细看到。

练习 2.06:使用 add()方法建立多对多关系

在这个练习中,你将使用add()方法建立多对多关系。当我们不使用关系来创建对象时,我们可以使用through_default传入一个包含定义所需字段的参数的字典。从上一个练习继续,让我们向名为《高级深度学习与 Keras》的书籍添加一个额外的贡献者。这次,这个贡献者是这本书的编辑:

  1. 如果你重启了 shell,运行以下两个命令来导入和检索所需的书籍实例:

    >>>from reviews.models import Book, Contributor
    >>>book = Book.objects.get(title="Advanced Deep Learning with   Keras")
    
  2. 使用create()方法创建一个贡献者,如下所示:

    >>>contributor = Contributor.objects.create(first_names='Packt',   last_names='Example Editor',     email='PacktEditor@example.com')
    
  3. 使用 add() 方法将新创建的贡献者添加到书籍中。确保您提供的关系参数 role 作为 dict。输入以下代码:

    >>>book.contributors.add(contributor,   through_defaults={'role': 'EDITOR'})
    

因此,我们使用了 add() 方法在书籍和贡献者之间建立多对多关系,同时将关系数据角色存储为 Editor。现在让我们看看其他实现这一点的其他方法。

使用 create() 和 set() 方法处理多对多关系

假设书籍 Advanced Deep Learning with Keras 有两位编辑。让我们使用以下方法为这本书添加另一位编辑。如果贡献者尚未存在于数据库中,则我们可以使用 create() 方法同时创建条目以及与书籍建立关系:

>>>book.contributors.create(first_names='Packtp', last_names=  'Editor Example', email='PacktEditor2@example.com',     through_defaults={'role': 'EDITOR'})

同样,我们也可以使用 set() 方法为书籍添加一系列贡献者。让我们创建一个 Publisher 模型,一组两位合著者贡献者,以及一个 book 对象。首先,如果尚未导入,使用以下代码导入 Publisher 模型:

>>>from reviews.models import Publisher

以下代码将帮助我们做到这一点:

>>> publisher = Publisher.objects.create(name='Pocket Books',   website='https://pocketbookssampleurl.com', email='pocketbook@example.com')
>>> contributor1 = Contributor.objects.create(first_names=  'Stephen', last_names='Stephen', email='StephenKing@example.com')
>>> contributor2 = Contributor.objects.create(first_names=  'Peter', last_names='Straub', email='PeterStraub@example.com')
>>> book = Book.objects.create(title='The Talisman',   publication_date=date(2012, 9, 25), isbn='9781451697216',     publisher=publisher)

由于这是一个多对多关系,我们可以通过一次添加一个对象列表,使用 set() 方法。我们可以使用 through_defaults 来指定贡献者的角色;在这种情况下,他们是合著者:

>>> book.contributors.set([contributor1, contributor2],   through_defaults={'role': 'CO_AUTHOR'})

读取操作

Django 为我们提供了允许我们从数据库中读取/检索的方法。我们可以使用 get() 方法从数据库中检索单个对象。在前面的章节中,我们已经创建了一些记录,所以让我们使用 get() 方法来检索一个对象。

练习 2.07:使用 get() 方法检索对象

在这个练习中,你将使用 get() 方法从数据库中检索一个对象:

  1. 获取具有 name 字段值为 Pocket BooksPublisher 对象:

    >>>from reviews.models import Publisher
    >>> publisher = Publisher.objects.get(name='Pocket Books')
    
  2. 重新输入检索到的 publisher 对象并按 Enter 键:

    >>> publisher
    <Publisher: Pocket Books>
    

    注意,输出显示在壳中。这被称为对象的字符串表示。这是在 模型方法 部分为 Publisher 类添加模型方法 __str__() 的结果。

  3. 在检索对象后,你可以访问所有对象的属性。由于这是一个 Python 对象,可以通过使用 . 后跟属性名称来访问对象的属性。因此,你可以使用以下命令检索出版商的名称:

    >>> publisher.name
    'Pocket Books'
    
  4. 同样,检索出版商的网站:

    >>> publisher.website
    'https://pocketbookssampleurl.com'
    

    可以检索出版商的电子邮件地址:

    >>> publisher.email
    'pocketbook@example.com'
    

在这个练习中,我们学习了如何使用 get() 方法检索单个对象。尽管如此,使用这种方法有几个缺点。让我们找出原因。

使用 get() 方法返回对象

重要的是要注意,get() 方法只能获取一个对象。如果有另一个对象具有与字段相同的值,那么我们可以预期会收到一个 "返回了多个" 错误消息。例如,如果 Publisher 表中有两个条目具有相同的名称字段值,我们可能会收到一个错误。在这种情况下,有其他方法可以检索这些对象,我们将在后续章节中探讨。

get() 查询没有返回对象时,我们也可以得到一个 "匹配的查询不存在" 错误消息。get() 方法可以与对象的任何字段一起使用来检索记录。在以下情况下,我们正在使用 website 字段:

>>> publisher = Publisher.objects.get(website='https://pocketbookssampleurl.com')

在检索对象之后,我们仍然可以获取出版者的名字,如下所示:

>>> publisher.name
'Pocket Books'

另一种检索对象的方法是使用其主键 pk,如下所示:

>>> Publisher.objects.get(pk=2)
<Publisher: Pocket Books>

使用主键 pk 是使用主键字段的一种更通用的方式。但对于 Publisher 表,由于我们知道 id 是主键,我们可以简单地使用字段名 id 来创建我们的 get() 查询:

>>> Publisher.objects.get(id=2)
<Publisher: Pocket Books>

注意

对于 Publisher 以及其他所有表,主键是 id,这是 Django 自动创建的。这发生在创建表时没有提到主键字段的情况下。但也可以有字段被明确声明为主键的情况。

练习 2.08:使用 all() 方法检索一组对象

我们可以使用 all() 方法检索一组对象。在这个练习中,你将使用这个方法检索所有贡献者的名字:

  1. 添加以下代码以从 Contributor 表中检索所有对象:

    >>>from reviews.models import Contributor
    >>> Contributor.objects.all()
    <QuerySet [<Contributor: Rowel>, <Contributor: Packt>, <Contributor: Packtp>, <Contributor: Stephen>, <Contributor:   Peter>]>
    

    执行后,您将得到所有对象的 QuerySet

  2. 我们可以使用列表索引来查找特定的对象,或者使用循环遍历列表以执行任何其他操作:

    >>> contributors = Contributor.objects.all()
    
  3. 由于 Contributor 是一个对象列表,你可以使用索引来访问列表中的任何元素,如下面的命令所示:

    >>> contributors[0]
    <Contributor: Rowel>
    

    在这种情况下,列表中的第一个元素是一个具有 'Rowel'first_names 值和 'Atienza'last_names 值的贡献者,如下面的代码所示:

    >>> contributors[0].first_names
    'Rowel'
    >>> contributors[0].last_names
    'Atienza'
    

在这个练习中,我们学习了如何使用 all() 方法检索所有对象,我们还学习了如何使用检索到的对象集作为列表。

通过过滤检索对象

如果一个字段值对应多个对象,那么我们不能使用 get() 方法,因为 get() 方法只能返回一个对象。对于这种情况,我们有 filter() 方法,它可以检索所有符合指定条件的对象。

练习 2.09:使用 filter() 方法检索对象

在这个练习中,你将使用 filter() 方法获取满足特定条件的特定对象集。具体来说,你将检索所有名字为 Peter 的贡献者的名字:

  1. 首先,创建两个额外的贡献者:

    >>>from reviews.models import Contributor
    >>> Contributor.objects.create(first_names='Peter', last_names='Wharton', email='PeterWharton@example.com')
    >>> Contributor.objects.create(first_names='Peter', last_names='Tyrrell', email='PeterTyrrell@example.com')
    
  2. 要检索那些first_names值为Peter的捐助者,请添加以下代码:

    >>> Contributor.objects.filter(first_names='Peter')
    <QuerySet [<Contributor: Peter>, <Contributor: Peter>,   <Contributor: Peter>]>
    
  3. 即使只有一个对象匹配,filter()方法也会返回该对象。你可以在这里看到:

    >>>Contributor.objects.filter(first_names='Rowel')
    <QuerySet [<Contributor: Rowel>]>
    
  4. 此外,如果查询没有匹配项,filter()方法返回一个空的QuerySet。这在这里可以看到:

    >>>Contributor.objects.filter(first_names='Nobody')
    <QuerySet []>
    

在这个练习中,我们看到了如何使用过滤器根据特定条件检索一组少量对象。

通过字段查找进行过滤

现在,让我们假设我们想要通过提供某些条件来过滤和查询一组对象。在这种情况下,我们可以使用所谓的双下划线查找。例如,Book对象有一个名为publication_date的字段;假设我们想要过滤并获取所有在 01-01-2014 之后出版的书籍。我们可以通过使用双下划线方法轻松地查找这些书籍。为此,我们首先导入Book模型:

>>>from reviews.models import Book
>>>book = Book.objects.filter(publication_date__gt=date(2014, 1, 1))

这里,publication_date__gt表示出版日期,大于(gt)某个指定的日期——在本例中是 01-01-2014。类似地,我们有以下缩写:

  • lt:小于

  • lte:小于或等于

  • gte:大于或等于

过滤后的结果如下所示:

>>> book
<QuerySet [<Book: Advanced Deep Learning with Keras>]>

这是查询集中的书籍的出版日期,这证实了出版日期是在 01-01-2014 之后的:

>>> book[0].publication_date
datetime.date(2018, 10, 31)

使用模式匹配进行过滤操作

对于过滤结果,我们还可以查找参数是否包含我们正在寻找的字符串的一部分:

>>> book = Book.objects.filter(title__contains=
    'Deep learning')

这里,title__contains查找所有标题包含字符串'Deep learning'的对象:

>>> book
<QuerySet [<Book: Advanced Deep Learning with Keras>]>
>>> book[0].title
'Advanced Deep Learning with Keras'

类似地,如果字符串匹配需要不区分大小写,我们可以使用icontains。使用startswith匹配以指定字符串开头的任何字符串。

通过排除检索对象

在上一节中,我们学习了通过匹配特定条件来获取一组对象。现在,假设我们想要做相反的操作;也就是说,我们想要获取所有那些不匹配特定条件的对象。在这种情况下,我们可以使用exclude()方法来排除特定条件并获取所有所需的对象。以下是一个所有捐助者的列表:

>>> Contributor.objects.all()
<QuerySet [<Contributor: Rowel>, <Contributor: Packt>,   <Contributor: Packtp>, <Contributor: Stephen>,     <Contributor: Peter>, <Contributor: Peter>,       <Contributor: Peter>]>

现在,从这个列表中,我们将排除所有那些first_names值为Peter的捐助者:

>>> Contributor.objects.exclude(first_names='Peter')
<QuerySet [<Contributor: Rowel>, <Contributor: Packt>,   <Contributor: Packtp>, <Contributor: Stephen>]>

我们可以看到,查询返回了所有那些名字不是 Peter 的捐助者。

使用 order_by()方法检索对象

我们可以使用order_by()方法按指定字段排序来检索对象列表。例如,在以下代码片段中,我们按出版日期对书籍进行排序:

>>> books = Book.objects.order_by("publication_date")
>>> books
<QuerySet [<Book: The Talisman>, <Book: Advanced Deep Learning   with Keras>]>

让我们检查查询的顺序。由于查询集是一个列表,我们可以使用索引来检查每本书的出版日期:

>>> books[0].publication_date
datetime.date(2012, 9, 25)
>>> books[1].publication_date
datetime.date(2018, 10, 31)

注意,索引为 0 的第一本书的出版日期早于索引为 1 的第二本书的出版日期。因此,这证实了查询到的书籍列表已经按照出版日期正确排序。我们还可以使用带有负号的字段参数前缀来按降序排序结果。这可以从以下代码片段中看出:

>>> books = Book.objects.order_by("-publication_date")
>>> books
<QuerySet [<Book: Advanced Deep Learning with Keras>,   <Book: The Talisman>]>

由于我们在出版日期前加上了负号,请注意,现在查询到的书籍集合已经以相反的顺序返回,其中索引为 0 的第一本书对象比索引为 1 的第二本书对象更晚。

>>> books[0].publication_date
datetime.date(2018, 10, 31)
>>> books[1].publication_date
datetime.date(2012, 9, 25)

我们还可以使用字符串字段或数值进行排序。例如,以下代码可以用来按书籍的主键或 id 排序:

>>>books = Book.objects.order_by('id')
<QuerySet [<Book: Advanced Deep Learning with Keras>,   <Book: The Talisman>]>

查询到的书籍集合已经按照书籍 id 升序排序:

>>> books[0].id
1
>>> books[1].id
2

再次,为了按降序排序,我们可以使用负号作为前缀,如下所示:

>>> Book.objects.order_by('-id')
<QuerySet [<Book: The Talisman>, <Book: Advanced Deep Learning   with Keras>]>

现在,查询到的书籍集合已经按照书籍 id 降序排序:

>>> books[0].id
2
>>> books[1].id
1

按照字符串字段按字母顺序排序,我们可以这样做:

>>>Book.objects.order_by('title')
<QuerySet [<Book: Advanced Deep Learning with Keras>, <Book:   The Talisman>]>

由于我们使用了书籍的标题进行排序,查询集已经按字母顺序排序。我们可以如下看到:

>>> books[0]
<Book: Advanced Deep Learning with Keras>
>>> books[1]
<Book: The Talisman>

与之前看到的排序类型类似,负号前缀可以帮助我们按逆字母顺序排序,正如我们在这里可以看到的:

>>> Book.objects.order_by('-title')
<QuerySet [<Book: The Talisman>, <Book: Advanced Deep Learning   with Keras>]>

这将导致以下输出:

>>> books[0]
<Book: The Talisman>
>>> books[1]
<Book: Advanced Deep Learning with Keras>

Django 提供的另一个有用方法是 values()。它帮助我们获取字典查询集而不是对象。在以下代码片段中,我们使用它来查询 Publisher 对象:

>>> publishers = Publisher.objects.all().values()
>>> publishers
<QuerySet [{'id': 1, 'name': 'Packt Publishing', 'website':   'https://www.packtpub.com', 'email':     'customersupport@packtpub.com'}, {'id': 2, 'name':       'Pocket Books', 'website': 'https://pocketbookssampleurl.com',        'email': 'pocketbook@example.com'}]>
>>> publishers[0]
{'id': 1, 'name': 'Packt Publishing', 'website':  'https://www.packtpub.com', 'email':     'customersupport@packtpub.com'}
>>> publishers[0]
{'id': 1, 'name': 'Packt Publishing', 'website':   'https://www.packtpub.com', 'email':    'customersupport@packtpub.com'}

跨关系查询

正如我们在本章中学到的,reviews 应用有两种关系类型——多对一和多对多。到目前为止,我们已经学习了使用 get()、过滤器、字段查找等方法进行查询的各种方式。现在让我们研究如何执行跨关系查询。有几种方法可以做到这一点——我们可以使用外键、对象实例等等。让我们通过一些示例来探讨这些方法。

使用外键进行查询

当我们在两个模型/表之间有关系时,Django 提供了一种使用关系执行查询的方法。本节中显示的命令将通过执行模型关系查询来检索由 Packt Publishing 出版的所有书籍。与之前看到的情况类似,这是通过使用双下划线查找来完成的。例如,Book 模型有一个指向 Publisher 模型的外键 publisher。使用这个外键,我们可以使用双下划线和 Publisher 模型中的 name 字段来执行查询。这可以从以下代码中看出:

>>> Book.objects.filter(publisher__name='Packt Publishing')
<QuerySet [<Book: Advanced Deep Learning with Keras>]>

使用模型名称进行查询

查询的另一种方式是,我们可以使用关系反向进行查询,使用模型名称的小写形式。例如,假设我们想要使用查询中的模型关系查询出版《Advanced Deep Learning with Keras》书籍的出版商。为此,我们可以执行以下语句来检索Publisher信息对象:

>>> Publisher.objects.get(book__title='Advanced Deep Learning   with Keras')
<Publisher: Packt Publishing>

在这里,book是模型名称的小写形式。正如我们已经知道的,Book模型有一个publisher外键,其值为name,即 Packt Publishing。

使用对象实例跨外键关系进行查询

我们还可以使用对象的 外键检索信息。假设我们想要查询标题为《The Talisman》的出版商名称:

>>> book = Book.objects.get(title='The Talisman')
>>> book.publisher
<Publisher: Pocket Books>

在这里使用对象是一个例子,我们使用反向方向通过set.all()方法获取一个出版商出版的所有书籍:

>>> publisher = Publisher.objects.get(name='Pocket Books')
>>> publisher.book_set.all()
<QuerySet [<Book: The Talisman>]>

我们还可以使用查询链来创建查询:

>>> Book.objects.filter(publisher__name='Pocket Books').filter(title='The Talisman')
<QuerySet [<Book: The Talisman>]>

让我们进行更多练习,以巩固我们对迄今为止所学的各种查询类型的知识。

练习 2.10:使用字段查找跨多对多关系查询

我们知道BookContributor之间存在多对多关系。在这个练习中,不创建对象,您将执行查询以检索所有参与编写标题为《The Talisman》的书籍的贡献者:

  1. 首先,导入Contributor类:

    >>> from reviews.models import Contributor
    
  2. 现在,添加以下代码以查询《The Talisman》的所有贡献者:

    >>>Contributor.objects.filter(book__title='The Talisman')
    

    您应该看到以下内容:

    <QuerySet [<Contributor: Stephen>, <Contributor: Peter>]>
    

从前面的输出中,我们可以看到 Stephen 和 Peter 是参与编写《The Talisman》书籍的贡献者。查询使用book模型(小写形式)并使用命令中的双下划线进行title字段的字段查找。

在这个练习中,我们学习了如何使用字段查找执行跨多对多关系的查询。现在,让我们看看使用另一种方法来完成相同任务。

练习 2.11:使用对象进行多对多查询

在这个练习中,使用Book对象,搜索所有参与编写标题为《The Talisman》的书籍的贡献者。以下步骤将帮助您完成此操作:

  1. 导入Book模型:

    >>> from reviews.models import Book
    
  2. 通过添加以下代码行检索标题为《The Talisman》的书籍对象:

    >>> book = Book.objects.get(title='The Talisman')
    
  3. 然后使用book对象检索参与编写《The Talisman》书籍的所有贡献者。为此,请添加以下代码:

    >>>book.contributors.all()
    <QuerySet [<Contributor: Stephen>, <Contributor: Peter>]>
    

再次,我们可以看到 Stephen 和 Peter 是参与编写书籍《The Talisman》的贡献者。由于书籍与贡献者之间存在多对多关系,我们使用了contributors.all()方法来获取所有参与编写该书籍的贡献者的查询集。现在,让我们尝试使用set方法执行类似的任务。

练习 2.12:使用 set()方法进行多对多查询

在这个练习中,您将使用contributor对象检索名为Rowel的贡献者所写的所有书籍:

  1. 导入 Contributor 模型:

    >>> from reviews.models import Contributor
    
  2. 使用 get() 方法获取一个 contributor 对象,其 first_names'Rowel'

    >>> contributor = Contributor.objects.get(first_names='Rowel')
    
  3. 使用 contributor 对象和 book_set() 方法,获取该作者所写的所有书籍:

    >>> contributor.book_set.all()
    <QuerySet [<Book: Advanced Deep Learning with Keras>]>
    

由于 BookContributor 之间存在多对多关系,我们可以使用 set() 方法查询与模型关联的对象集。在这种情况下,contributor.book_set.all() 返回了该作者所写的所有书籍。

练习 2.13:使用 update() 方法

在这个练习中,你将使用 update() 方法更新现有记录:

  1. 更改具有 last_nameTyrrell 的贡献者的 first_names

    >>> from reviews.models import Contributor
    >>> Contributor.objects.filter(last_names='Tyrrell').  update(first_names='Mike')
    1
    

    返回值显示已更新的记录数。在这种情况下,一条记录已被更新。

  2. 使用 get() 方法获取刚刚修改的 contributor 对象,并验证其名字是否已更改为 Mike

    >>> Contributor.objects.get(last_names='Tyrrell').first_names
    'Mike'
    

    注意

    如果过滤操作返回多个记录,则 update() 方法将更新所有返回记录中的指定字段。

在这个练习中,我们学习了如何使用 update 方法更新数据库中的记录。现在,让我们尝试使用 delete() 方法从数据库中删除记录。

练习 2.14:使用 delete() 方法

可以使用 delete() 方法从数据库中删除现有记录。在这个练习中,你将删除 contributors 表中 last_name 值为 Wharton 的记录:

  1. 使用 get 方法获取对象,并按以下方式使用 delete 方法:

    >>> from reviews.models import Contributor
    >>> Contributor.objects.get(last_names='Wharton').delete()
    (1, {'reviews.BookContributor': 0, 'reviews.Contributor': 1})
    

    注意,你没有将 contributor 对象分配给变量就调用了 delete() 方法。由于 get() 方法返回单个对象,你可以直接访问对象的方法,而无需为它创建变量。

  2. 验证具有 last_name'Wharton'contributor 对象已被删除:

    >>> Contributor.objects.get(last_names='Wharton')
    Traceback (most recent call last):
        File "<console>", line 1, in <module>
        File "/../site-packages/django/db/models/manager.py",  line 82, in manager_method
        return getattr(self.get_queryset(), name)(*args, **kwargs)
        File "/../site-packages/django/db/models/query.py",  line 417, in get
        self.model._meta.object_name
    reviews.models.Contributor.DoesNotExist: Contributor   matching query does not exist.
    

如你所见,运行查询时出现了 对象不存在 错误。这是预期的,因为记录已被删除。在这个练习中,我们学习了如何使用 delete 方法从数据库中删除记录。

活动 2.01:为项目管理应用程序创建模型

想象你正在开发一个名为 Juggler 的项目管理应用程序。Juggler 是一个可以跟踪多个项目,并且每个项目可以关联多个任务的应用程序。以下步骤将帮助你完成此活动:

  1. 使用我们迄今为止学到的技术,创建一个名为 juggler 的 Django 项目。

  2. 创建一个名为 projectp 的 Django 应用程序。

  3. juggler/settings.py 文件中添加 app projects。

  4. projectp/models.py 中创建两个相关模型类 ProjectTask

  5. 创建迁移脚本并将模型定义迁移到数据库中。

  6. 现在打开 Django shell 并导入模型。

  7. 使用示例数据填充数据库,并编写一个查询以显示与给定项目关联的任务列表。

    注意

    此活动的解决方案可在 packt.live/2Nh1NTJ 找到。

填充 Bookr 项目的数据库

虽然我们知道如何为项目创建数据库记录,但在接下来的几章中,我们将不得不创建大量记录以与项目一起工作。因此,我们创建了一个脚本,可以使事情变得简单。此脚本通过读取包含许多记录的 .csv (逗号分隔值)文件来填充数据库。按照以下步骤填充项目的数据库:

  1. 在项目目录内创建以下文件夹结构:

    bookr/reviews/management/commands/
    
  2. 从以下位置复制 loadcsv.py 文件和 WebDevWithDjangoData.csv 到创建的文件夹中。这些文件可以在本书的 GitHub 仓库 packt.live/3pvbCLM 中找到。

    因为 loadcsv.py 放置在 management/commands 文件夹中,现在它就像一个 Django 自定义管理命令一样工作。您可以查看 loadcsv.py 文件,并在此链接中了解更多关于编写 Django 自定义管理命令的信息:docs.djangoproject.com/en/3.0/howto/custom-management-commands/

  3. 现在,让我们重新创建一个全新的数据库。删除项目文件夹中存在的 SQL 数据库文件:

    rm reviews/db.sqlite3
    
  4. 要再次创建一个全新的数据库,请执行 Django 的 migrate 命令:

    python manage.py migrate
    

    现在,您可以在 reviews 文件夹下看到新创建的 db.sqlite3 文件。

  5. 执行自定义管理命令 loadcsv 以填充数据库:

    python manage.py loadcsv --csv reviews/management/commands/WebDevWithDjangoData.csv
    
  6. 使用 DB Browser for SQLite,验证由 bookr 项目创建的所有表都已填充。

摘要

在本章中,我们学习了某些基本数据库概念及其在应用程序开发中的重要性。我们使用了一个免费的数据库可视化工具 DB Browser for SQLite,以了解数据库表和字段是什么,记录如何在数据库中存储,并进一步使用简单的 SQL 查询在数据库上执行了一些基本的 CRUD 操作。

然后,我们学习了 Django 提供的一个非常有价值的抽象层,称为 ORM,它帮助我们通过简单的 Python 代码无缝地与关系数据库交互,而无需编写 SQL 命令。作为 ORM 的一部分,我们学习了 Django 模型、迁移以及它们如何帮助将更改传播到数据库中的 Django 模型。

我们通过学习关系型数据库中的数据库关系及其关键类型,巩固了我们对数据库的知识。我们还使用了 Django shell,在那里我们用 Python 代码执行了之前用 SQL 执行过的相同的 CRUD 查询。后来,我们学习了如何使用模式匹配和字段查找以更精细的方式检索我们的数据。在我们学习这些概念的同时,我们在 Bookr 应用程序上也取得了显著的进展。我们为reviews应用程序创建了模型,并获得了与该应用程序数据库中存储的数据交互所需的所有技能。在下一章中,我们将学习如何创建 Django 视图、URL 路由和模板。

第三章:3. URL 映射、视图和模板

概述

本章向您介绍了 Django 的三个核心概念:视图模板URL 映射。您将从探索 Django 中的两种主要视图类型开始:基于函数的视图基于类的视图。接下来,您将学习 Django 模板语言和模板继承的基础知识。使用这些概念,您将创建一个页面来显示Bookr应用程序中所有书籍的列表。您还将创建另一个页面来显示书籍的详细信息、评论和评分。

简介

在上一章中,我们介绍了数据库,并学习了如何从数据库中存储、检索、更新和删除记录。我们还学习了如何创建 Django 模型并应用数据库迁移。

然而,仅凭这些数据库操作本身并不能将应用程序的数据展示给用户。我们需要一种方法,以有意义的方式将所有存储的信息展示给用户;例如,在我们的 Bookr 应用程序数据库中显示所有现有的书籍,在浏览器中以可展示的格式。这就是 Django 视图、模板和 URL 映射发挥作用的地方。视图是 Django 应用程序的一部分,它接收网络请求并提供网络响应。例如,一个网络请求可能是一个用户通过输入网站地址来尝试查看网站,而一个网络响应可能是网站的主页在用户的浏览器中加载。视图是 Django 应用程序最重要的部分之一,应用程序逻辑在这里被编写。这种应用程序逻辑控制与数据库的交互,例如创建、读取、更新或从数据库中删除记录。它还控制数据如何展示给用户。这是通过 Django HTML 模板来实现的,我们将在后面的章节中详细探讨。

Django 视图可以大致分为两种类型,基于函数的视图基于类的视图。在本章中,我们将学习 Django 中的基于函数的视图。

注意

在本章中,我们将仅学习关于基于函数的视图。更高级的基于类的视图将在第十一章“高级模板和基于类的视图”中详细讨论。

基于函数的视图

如其名所示,基于函数的视图是以 Python 函数的形式实现的。为了理解它们是如何工作的,考虑以下代码片段,它展示了一个名为home_page的简单视图函数:

from django.http import HttpResponse
def home_page(request):
    message = "<html><h1>Welcome to my Website</h1></html>"
    return HttpResponse(message)

在这里定义的视图函数,名为home_page,接受一个request对象作为参数,并返回一个包含Welcome to my Website消息的HttpResponse对象。使用基于函数的视图的优势在于,由于它们是以简单的 Python 函数实现的,因此更容易学习和阅读。基于函数的视图的主要缺点是,代码不能被重用,并且对于通用用例来说,不能像基于类的视图那样简洁。

基于类的视图

如其名所示,基于类的视图是以 Python 类的形式实现的。利用类继承的原则,这些类被实现为 Django 通用视图类的子类。与所有视图逻辑都明确表达在函数中的基于函数的视图不同,Django 的通用视图类提供了各种预构建的属性和方法,可以提供编写干净、可重用视图的快捷方式。这个特性在 Web 开发中非常有用;例如,开发者经常需要渲染一个 HTML 页面,而不需要从数据库中插入任何数据,或者任何针对特定用户的定制化。在这种情况下,可以简单地继承 Django 的 TemplateView,并指定 HTML 文件的路径。以下是一个可以显示与基于函数的视图示例中相同信息的基于类的视图示例:

from django.views.generic import TemplateView
class HomePage(TemplateView):
    template_name = 'home_page.html'

在前面的代码片段中,HomePage 是一个继承自 django.views.generic 模块的 Django TemplateView 的基于类的视图。类属性 template_name 定义了在视图被调用时要渲染的模板。对于模板,我们在 templates 文件夹中添加了一个包含以下内容的 HTML 文件:

<html><h1>Welcome to my Website</h1></html>

这是一个基于类的视图的非常基础的例子,将在第十一章 高级模板和基于类的视图 中进一步探讨。使用基于类的视图的主要优势是,与基于函数的视图相比,实现相同功能所需的代码行数更少。此外,通过继承 Django 的通用视图,我们可以使代码更加简洁,避免代码重复。然而,基于类的视图的一个缺点是,对于 Django 新手来说,代码通常不太易读,这意味着学习它通常是一个更长的过程,与基于函数的视图相比。

URL 配置

Django 视图不能独立在 Web 应用程序中工作。当向应用程序发起 Web 请求时,Django 的 URL 配置负责将请求路由到适当的视图函数以处理请求。Django 中 urls.py 文件中的典型 URL 配置如下所示:

from . import views
urlpatterns = [path('url-path/' views.my_view, name='my-view'),]

在这里,urlpatterns 是定义 URL 路径列表的变量,'url-path/' 定义了要匹配的路径。

views.my_view 是在 URL 匹配时调用的视图函数,name='my-view' 是用于引用视图的视图函数的名称。可能存在这样的情况,在应用程序的其他地方,我们想要获取这个视图的 URL。我们不想硬编码这个值,因为这会导致在代码库中需要指定两次。相反,我们可以通过使用视图的名称来访问 URL,如下所示:

from django.urls import reverse
url = reverse('my-view')

如果需要,我们还可以在 URL 路径中使用正则表达式,通过 re_path() 匹配字符串模式:

urlpatterns = [re_path\
               (r'^url-path/(?P<name>pattern)/$', views.my_view, \
                name='my-view')]

在这里,name 指的是模式名称,它可以是指定的任何 Python 正则表达式模式,并且在调用定义的视图函数之前需要匹配。你也可以将参数从 URL 传递到视图本身,例如:

urlpatterns = [path(r'^url-path/<int:id>/', views.my_view, \
               name='my-view')]

在前面的例子中,<int:id> 告诉 Django 查找字符串中此位置的整数 URL,并将该整数的值分配给 id 参数。这意味着如果用户导航到 /url-path/14/,则将 id=14 关键字参数传递给视图。这在视图需要查找数据库中的特定对象并返回相应数据时非常有用。例如,假设我们有一个 User 模型,我们希望视图显示用户的姓名。

视图可以编写如下:

def my_view(request, id):
    user = User.objects.get(id=id)
    return HttpResponse(f"This user's name is \
    { user.first_name } { user.last_name }")

当用户访问 /url-path/14/ 时,调用前面的视图,并将 id=14 参数传递给函数。

当使用网络浏览器调用类似于 http://0.0.0.0:8000/url-path/ 的 URL 时,这是典型的流程:

  1. 将对运行中的应用程序的 URL 路径发起 HTTP 请求。在收到请求后,它会查找 settings.py 文件中存在的 ROOT_URLCONF 设置:

    ROOT_URLCONF = 'project_name.urls'
    

    这确定了首先使用的 URL 配置文件。在这种情况下,它是项目目录 project_name/urls.py 中存在的 URL 文件。

  2. 接下来,Django 会遍历名为 urlpatterns 的列表,一旦它与 URL http://0.0.0.0:8000/url-path/ 中的路径匹配,它就会调用相应的视图函数。

URL 配置有时也被称为 URL conf 或 URL 映射,这些术语通常可以互换使用。为了更好地理解视图和 URL 映射,让我们从简单的练习开始。

练习 3.01:实现基于函数的简单视图

在这个练习中,我们将编写一个非常基本的基于函数的视图,并使用相关的 URL 配置在浏览器中显示消息 Welcome to Bookr!。我们还将告诉用户我们数据库中有多少本书:

  1. 首先,确保 bookr/settings.py 中的 ROOT_URLCONF 指向项目的 URL 文件,通过添加以下命令:

    ROOT_URLCONF = 'bookr.urls'
    
  2. 打开 bookr/reviews/views.py 文件并添加以下代码片段:

    from django.http import HttpResponse
    from .models import Book
    def welcome_view(request):
        message = f"<html><h1>Welcome to Bookr!</h1> "\
    "<p>{Book.objects.count()} books and counting!</p></html>"
        return HttpResponse(message)
    

    首先,我们从 django.http 模块导入 HttpResponse 类。接下来,我们定义 welcome_view 函数,该函数可以在浏览器中显示消息 Welcome to Bookr!。请求对象是一个函数参数,它携带 HTTP request 对象。下一行定义了 message 变量,它包含显示标题的 HTML,然后是一行统计数据库中可用的书籍数量。

    在最后一行,我们返回一个与消息变量关联的 HttpResponse 对象。当调用 welcome_view 视图函数时,它将在浏览器中显示消息 Welcome to Bookr! 2 Books and counting

  3. 现在,创建 URL 映射以调用新创建的视图函数。打开项目 URL 文件,bookr/urls.py,并按照以下方式添加urlpatterns列表:

    from django.contrib import admin
    from django.urls import include, path
    urlpatterns = [path('admin/', admin.site.urls),\
                   path('', include('reviews.urls'))]
    

    urlpatterns列表中的第一行,即path('admin/', admin.site.urls),如果 URL 路径中存在admin/,则路由到管理 URL(例如,http://0.0.0.0:8000/admin)。

    类似地,考虑第二行,path('', include('reviews.urls'))。在这里,提到的路径是一个空字符串''。如果 URL 在http://hostname:port-number/(例如,http://0.0.0.0:8000/)之后没有特定的路径,它将包含review.urls中存在的urlpatterns

    include函数是一个快捷方式,允许您组合 URL 配置。在您的 Django 项目中,通常每个应用程序都保留一个 URL 配置。在这里,我们为reviews应用程序创建了一个单独的 URL 配置,并将其添加到我们的项目级 URL 配置中。

  4. 由于我们还没有reviews.urls URL 模块,创建一个名为bookr/reviews/urls.py的文件,并添加以下代码行:

    from django.contrib import admin
    from django.urls import path
    from . import views
    urlpatterns = [path('', views.welcome_view, \
                        name='welcome_view'),]
    
  5. 在这里,我们再次使用了空字符串作为 URL 路径。因此,当 URL http://0.0.0.0:8000/被调用时,从bookr/urls.py路由到bookr/reviews/urls.py后,这个模式将调用welcome_view视图函数。

  6. 在对两个文件进行修改后,我们已经准备好了必要的 URL 配置,以便调用welcome_view视图。现在,使用python manage.py runserver启动 Django 服务器,并在您的网页浏览器中输入http://0.0.0.0:8000http://127.0.0.1:8000。您应该能看到消息欢迎使用 Bookr!图 3.1:显示“欢迎使用 Bookr!”和主页上的书籍数量

图 3.1:显示“欢迎使用 Bookr!”和主页上的书籍数量

注意

如果没有 URL 匹配,Django 将调用错误处理,例如显示404 页面未找到消息或类似的内容。

在这个练习中,我们学习了如何编写基本的视图函数和相关的 URL 映射。我们创建了一个网页,向用户显示一条简单的消息,并报告我们数据库中当前有多少本书。

然而,细心的读者会注意到,像前面的例子那样,将 HTML 代码放在我们的 Python 函数中看起来并不美观。随着视图变大,这将会变得更加不可持续。因此,我们现在将注意力转向 HTML 代码应该所在的地方——在模板中。

模板

练习 3.01实现基于简单函数的视图中,我们看到了如何创建视图,进行 URL 映射,并在浏览器中显示消息。但如果你还记得,我们在视图函数本身中硬编码了 HTML 消息欢迎使用 Bookr!,并返回了一个HttpResponse对象,如下所示:

message = f"<html><h1>Welcome to Bookr!</h1> "\
"<p>{Book.objects.count()} books and counting!</p></html>"
return HttpResponse(message)

在 Python 模块中硬编码 HTML 不是一种好的做法,因为随着要在网页中渲染的内容增加,我们需要为其编写的 HTML 代码量也会增加。在 Python 代码中包含大量的 HTML 代码可能会使代码在长期内难以阅读和维护。

因此,Django 模板为我们提供了一种更好的方式来编写和管理 HTML 模板。Django 的模板不仅与静态 HTML 内容一起工作,还与动态 HTML 模板一起工作。

Django 的模板配置是在settings.py文件中存在的TEMPLATES变量中完成的。这是默认配置的外观:

TEMPLATES = \
[{'BACKEND': 'django.template.backends.django.DjangoTemplates',\
  'DIRS': [],
  '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',\
            ],\
        },\
    },\
]

让我们逐一分析前面代码片段中出现的每个关键字:

  • 'BACKEND': 'django.template.backends.django.DjangoTemplates':这指的是要使用的模板引擎。模板引擎是 Django 用来与 HTML 模板工作的 API。Django 是用 Jinja2 和DjangoTemplates引擎构建的。默认配置是DjangoTemplates引擎和 Django 模板语言。然而,如果需要,这可以更改为使用不同的一个,例如 Jinja2 或任何其他第三方模板引擎。但是,对于我们的 Bookr 应用程序,我们将保持这个配置不变。

  • 'DIRS': []:这指的是 Django 在给定顺序中搜索模板的目录列表。

  • 'APP_DIRS': True:这告诉 Django 模板引擎是否应该在settings.py文件中定义的INSTALLED_APPS下安装的应用中查找模板。这个选项的默认值是True

  • 'OPTIONS':这是一个包含模板引擎特定设置的字典。在这个字典中,有一个默认的上下文处理器列表,它帮助 Python 代码与模板交互以创建和渲染动态 HTML 模板。

现在的默认设置对于我们来说大多数情况下都是合适的。然而,在下一个练习中,我们将为我们的模板创建一个新的目录,并且我们需要指定这个文件夹的位置。例如,如果我们有一个名为my_templates的目录,我们需要通过将其添加到TEMPLATES设置中来指定其位置,如下所示:

TEMPLATES = \
[{'BACKEND': 'django.template.backends.django.DjangoTemplates',\
  'DIRS': [os.path.join(BASE_DIR, 'my_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',\
            ],\
        },\
    },

BASE_DIR是项目文件夹的目录路径。这定义在settings.py文件中。os.path.join()方法将项目目录与templates目录连接起来,返回模板目录的完整路径。

练习 3.02:使用模板显示问候消息

在这个练习中,我们将创建我们的第一个 Django 模板,就像我们在上一个练习中所做的那样,我们将使用模板显示Welcome to Bookr!消息:

  1. bookr项目目录下创建一个名为templates的目录,并在其中创建一个名为base.html的文件。目录结构应类似于图 3.2图 3.2: bookr 的目录结构

    图 3.2: bookr 的目录结构

    注意

    当使用默认配置时,即DIRS为空列表时,Django 只会在应用程序文件夹的template目录中搜索模板(在书评应用程序的情况下是reviews/templates文件夹)。由于我们将新的模板目录包含在主项目目录中,除非将目录包含在DIRS列表中,否则 Django 的模板引擎将无法找到该目录。

  2. 将文件夹添加到TEMPLATES设置中:

    TEMPLATES = \
    [{'BACKEND': 'django.template.backends.django.DjangoTemplates',\
      'DIRS': [os.path.join(BASE_DIR, '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',\
                ],\
            },\
        },\
                ]
    
  3. 将以下代码行添加到base.html文件中:

    <!doctype html>
    <html lang="en">
    <head>
        <meta charset=»utf-8»>
        <title>Home Page</title>
    </head>
        <body>
            <h1>Welcome to Bookr!</h1>
        </body>
    </html>
    

    这只是一个简单的 HTML,用于在页眉中显示消息欢迎使用 Bookr!

  4. 修改bookr/reviews/views.py中的代码,使其如下所示:

    from django.shortcuts import render
    def welcome_view (request):
        return render(request, 'base.html')
    

    由于我们已经在TEMPLATES配置中配置了'templates'目录,因此base.html可用于模板引擎。代码使用从django.shortcuts模块导入的render方法渲染文件base.html

  5. 保存文件,运行python manage.py runserver,然后打开http://0.0.0.0:8000/http://127.0.0.1:8000/ URL 以检查浏览器中新添加的模板加载情况:![图 3.3:在主页上显示“欢迎使用 Bookr!”]

    图片

图 3.3:在主页上显示“欢迎使用 Bookr!”

在这个练习中,我们创建了一个 HTML 模板,并使用 Django 模板和视图返回消息欢迎使用 Bookr!。接下来,我们将学习 Django 模板语言,它可以用来渲染应用程序的数据以及 HTML 模板。

Django 模板语言

Django 模板不仅返回静态 HTML 模板,还可以在生成模板时添加动态应用程序数据。除了数据,我们还可以在模板中包含一些程序性元素。所有这些加在一起构成了Django 模板语言的基础。本节将探讨 Django 模板语言的一些基本部分。

模板变量

模板变量用两个花括号表示,如下所示:

{{ variable }}

当模板中存在此内容时,变量的值将在模板中被替换。模板变量有助于将应用程序的数据添加到模板中:

template_variable = "I am a template variable."
<body>
        {{ template_variable }}
    </body>

模板标签

标签类似于程序性控制流,例如if条件或for循环。标签用两个花括号和百分号表示。以下是一个使用模板标签遍历列表的for循环示例:

{% for element in element_list %}
{% endfor %}

与 Python 编程不同,我们通过添加end标签来添加控制流的结束,例如{% endfor %}。这可以与模板变量一起使用,以显示列表中的元素,如下所示:

<ul>
    {% for element in element_list %}
        <li>{{ element.title }}</li>
    {% endfor %}
</ul>

注释

Django 模板语言中的注释可以按照以下方式编写;在{% comment %}{% endcomment %}之间的任何内容都将被注释掉:

{% comment %}
    <p>This text has been commented out</p>
{% endcomment %}

过滤器

过滤器可以用来修改一个变量,以不同的格式表示它。过滤器的语法是使用管道(|)符号将变量与过滤器名称分开:

{{ variable|filter }}

这里有一些内置过滤器的示例:

  • {{ variable|lower }}:这会将变量字符串转换为小写。

  • {{ variable|title}}:这会将每个单词的首字母转换为大写。

让我们使用到目前为止学到的概念来开发书评应用。

练习 3.03:显示书籍和评论列表

在这个练习中,我们将创建一个可以显示所有书籍、它们的评分和书评应用中现有评论数量的网页。为此,我们将使用 Django 模板语言的一些功能,如变量和模板标签,将书评应用数据传递到模板中,以在网页上显示有意义的数据:

  1. bookr/reviews/utils.py下创建一个名为utils.py的文件,并添加以下代码:

    def average_rating(rating_list):
        if not rating_list:
            return 0
        return round(sum(rating_list) / len(rating_list))
    

    这是一个将用于计算书籍平均评分的帮助方法。

  2. 删除bookr/reviews/views.py中现有的所有代码,并添加以下代码到其中:

    from django.shortcuts import render
    from .models import Book, Review
    from .utils import average_rating
    def book_list(request):
        books = Book.objects.all()
        book_list = []
        for book in books:
            reviews = book.review_set.all()
            if reviews:
                book_rating = average_rating([review.rating for \
                                              review in reviews])
                number_of_reviews = len(reviews)
            else:
                book_rating = None
                number_of_reviews = 0
            book_list.append({'book': book,\
                              'book_rating': book_rating,\
                              'number_of_reviews': number_of_reviews})
        context = {
            'book_list': book_list
        }
        return render(request, 'reviews/books_list.html', context)
    

    这是一个用于显示书评应用中书籍列表的视图。前五行导入 Django 模块、模型类和刚刚添加的帮助方法。

    在这里,books_list是视图方法。在这个方法中,我们首先查询所有书籍的列表。接下来,对于每一本书,我们计算平均评分和发布的评论数量。每本书的所有这些信息都作为字典列表附加到名为book_list的列表中。然后,这个列表被添加到名为 context 的字典中,并传递给渲染函数。

    渲染函数有三个参数,第一个是传递给视图的请求对象,第二个是 HTML 模板books_list.html,它将显示书籍列表,第三个是上下文,我们将其传递给模板。

    由于我们已经将book_list作为上下文的一部分传递,因此模板将使用它来使用模板标签和模板变量渲染书籍列表。

  3. 在路径bookr/reviews/templates/reviews/books_list.html中创建名为book_list.html的文件,并在文件中添加以下 HTML 代码:

    reviews/templates/reviews/books_list.html
    1  <!doctype html>
    2  <html lang="en">
    3  <head>
    4      <meta charset="utf-8">
    5      <title>Bookr</title>
    6  </head>
    7      <body>
    8          <h1>Book Review application</h1>
    9          <hr>
    You can find the complete code at http://packt.live/3hnB4Qr.
    

    这是一个简单的 HTML 模板,包含模板标签和变量,用于迭代book_list以显示书籍列表。

  4. bookr/reviews/urls.py中,添加以下 URL 模式以调用books_list视图:

    from django.urls import path
    from . import views
    urlpatterns = [path('books/', views.book_list, \
                        name='book_list'),]
    

    这为books_list视图函数执行 URL 映射。

  5. 保存所有修改过的文件,等待 Django 服务重启。在浏览器中打开http://0.0.0.0:8000/books/,你应该会看到类似于图 3.4的内容:图 3.4:书评应用中现有的书籍列表

图 3.4:书评应用中现有的书籍列表

在这个练习中,我们创建了一个视图函数,创建了模板,并进行了 URL 映射,可以显示应用中所有现有书籍的列表。虽然我们能够使用单个模板显示书籍列表,但接下来,让我们探讨一下如何在具有公共或相似代码的应用程序中处理多个模板。

模板继承

随着我们构建项目,模板的数量将会增加。在设计和应用时,某些页面可能会看起来相似,并且具有某些功能的公共 HTML 代码。使用模板继承,我们可以将公共 HTML 代码继承到其他 HTML 文件中。这类似于 Python 中的类继承,其中父类包含所有公共代码,而子类包含那些满足子类需求的独特代码。

例如,让我们考虑以下内容为一个名为base.html的父模板:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Hello World</title>
</head>
    <body>
        <h1>Hello World using Django templates!</h1>
        {% block content %}
        {% endblock %}
    </body>
</html>

以下是一个子模板的示例:

{% extends 'base.html' %}
{% block content %}
<h1>How are you doing?</h1>
{% endblock %}

在前面的代码片段中,行{% extends 'base.html' %}扩展了来自base.html的模板,这是父模板。在从父模板扩展后,任何在块内容之间的 HTML 代码都将与父模板一起显示。一旦子模板被渲染,这就是它在浏览器中的样子:

![图 3.5:扩展 base.html 模板后的问候信息

![img/B15509_03_05.jpg]

图 3.5:扩展 base.html 模板后的问候信息

使用 Bootstrap 进行模板样式化

我们已经看到了如何使用视图、模板和 URL 映射来显示所有书籍。虽然我们能够在浏览器中显示所有信息,但如果我们能添加一些样式并使网页看起来更好,那就更好了。为此,我们可以添加一些Bootstrap元素。Bootstrap 是一个开源的层叠样式表CSS)框架,特别适合设计适用于桌面和移动浏览器的响应式页面。

使用 Bootstrap 很简单。首先,你需要将 Bootstrap CSS 添加到你的 HTML 中。你可以通过创建一个名为example.html的新文件来自行实验。在文件中填充以下代码,并在浏览器中打开它:

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,       initial-scale=1, shrink-to-fit=no">
    <!-- Bootstrap CSS -->
    <link rel="stylesheet"       href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/      css/bootstrap.min.css" integrity="sha384-      Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q       9Ifjh" crossorigin="anonymous">
  </head>
  <body>
    Content goes here
  </body>
</html>

上述代码中的 Bootstrap CSS 链接将 bootstrap CSS 库添加到你的页面中。这意味着某些 HTML 元素类型和类将继承其样式来自 Bootstrap。例如,如果你将btn-primary类添加到按钮的类中,按钮将被渲染为蓝色带白色文字。尝试在<body></body>之间添加以下内容:

<h1>Welcome to my Site</h1>
<button type="button" class="btn btn-primary">Checkout my   Blog!</button>

你会看到标题和按钮都被 Bootstrap 的默认样式优雅地样式化:

![图 3.6:应用 Bootstrap 后的显示效果

![img/B15509_03_06.jpg]

图 3.6:应用 Bootstrap 后的显示效果

这是因为在 Bootstrap CSS 代码中,它使用以下代码指定了btn-primary类的颜色:

.btn-primary {
    color: #fff;
    background-color: #007bff;
    border-color: #007bff
}

你可以看到,使用像 Bootstrap 这样的第三方 CSS 库,你可以快速创建样式优美的组件,而不需要编写太多的 CSS。

注意

我们建议你进一步探索 Bootstrap,请参考他们的教程:getbootstrap.com/docs/4.4/getting-started/introduction/

练习 3.04:添加模板继承和 Bootstrap 导航栏

在这个练习中,我们将使用模板继承从基本模板继承模板元素,并在book_list模板中重新使用它们来显示图书列表。我们还将使用基本 HTML 文件中的某些 Bootstrap 元素,在页面顶部添加一个导航栏。base.html中的 Bootstrap 代码来自getbootstrap.com/docs/4.4/getting-started/introduction/getbootstrap.com/docs/4.4/components/navbar/

  1. bookr/templates/base.html的位置打开base.html文件。删除任何现有的代码,并用以下代码替换:

    bookr/templates/base_html
    1  <!doctype html>
    2  {% load static %}
    3  <html lang="en">
    4    <head>
    5      <!-- Required meta tags -->
    6      <meta charset="utf-8">
    7      <meta name="viewport" content="width=device-width,          initial-scale=1, shrink-to-fit=no">
    8  
    9      <!-- Bootstrap CSS -->
    You can view the entire code for this file at http://packt.live/3mTjlBn.
    

    这是一个包含所有 Bootstrap 元素以进行样式化和导航栏的base.html文件。

  2. 接下来,打开位于bookr/reviews/templates/reviews/books_list.html的模板,删除所有现有的代码,并用以下代码替换:

    reviews/templates/reviews/books_list.html
    1  {% extends 'base.html' %}
    2  
    3  {% block content %}
    4  <ul class="list-group">
    5    {% for item in book_list %}
    6    <li class="list-group-item">
    7        <span class="text-info">Title: </span> <span>{{            item.book.title }}</span>
    8        <br>
    9        <span class="text-info">Publisher: </span><span>{{            item.book.publisher }}</span>
    You can view the complete code for this file at http://packt.live/3aPJv5O.
    

    此模板已配置为继承base.html文件,并且还添加了一些样式元素来显示图书列表。帮助继承base.html文件的模板部分如下:

    {% extends 'base.html' %}
    {% block content %}
    {% endblock %}
    
  3. 在添加了两个新模板后,在您的网络浏览器中打开以下任一 URL http://0.0.0.0:8000/books/http://127.0.0.1:8000/books/,以查看图书列表页面,现在它应该看起来格式整洁:图 3.7:格式整洁的图书列表页面

图 3.7:格式整洁的图书列表页面

在这个练习中,我们使用 Bootstrap 在应用程序中添加了一些样式,并且在显示图书评论应用程序中的图书列表时,我们还使用了模板继承。到目前为止,我们已经广泛地工作了,以显示应用程序中所有存在的图书。在下一个活动中,你将显示单个图书的详细信息及评论。

活动三.01:实现图书详细信息视图

在这个活动中,你将实现一个新的视图、模板和 URL 映射,以显示以下图书的详细信息:标题、出版社、出版日期和总体评分。除了这些详细信息外,页面还应显示所有评论,指定评论者的姓名和评论被撰写及(如适用)修改的日期。以下步骤将帮助你完成这个活动:

  1. 创建一个图书详细信息端点,它扩展了基本模板。

  2. 创建一个书籍详细视图,它接受特定书籍的主键作为参数,并返回一个 HTML 页面,列出书籍的详细信息以及任何相关的评论。

  3. urls.py 中进行所需的 URL 映射。书籍详细视图的 URL 应该是 http://0.0.0.0:8000/books/1/(其中 1 将代表正在访问的书籍的 ID)。你可以使用 get_object_or_404 方法检索具有给定主键的书籍。

    注意

    get_object_or_404 函数是一个用于根据其主键检索实例的有用快捷方式。你也可以使用第二章中描述的 .get() 方法,即 Book.objects.get(pk=pk)。然而,get_object_or_404 有一个额外的优点,即如果对象不存在,它会返回一个 HTTP 404 Not Found 响应。如果我们仅仅使用 get(),并且有人尝试访问一个不存在的对象,我们的 Python 代码将引发异常,并返回一个 HTTP 500 Server Error 响应。这是不可取的,因为它看起来好像我们的服务器未能正确处理请求。

  4. 活动结束时,你应该能够点击书单页面上的“评论”按钮,并获取书籍的详细视图。详细视图应显示以下截图中的所有详细信息:图 3.8:显示书籍详细信息的页面

图 3.8:显示书籍详细信息的页面

注意

该活动的解决方案可在packt.live/2Nh1NTJ找到。

摘要

本章介绍了处理对我们的网站进行 HTTP 请求所需的核心基础设施。请求首先通过 URL 模式映射到适当的视图。URL 中的参数也会传递到视图中,以指定页面上显示的对象。视图负责编译任何必要的信息以显示在网站上,然后将此字典传递给模板,该模板将信息渲染为 HTML 代码,可以作为对用户的响应返回。我们介绍了基于类和基于函数的视图,并学习了 Django 模板语言和模板继承。我们为书评应用程序创建了两个新页面,一个显示所有书籍,另一个是书籍详细视图页面。在下一章中,我们将学习 Django 管理后台和超级用户,注册模型,以及使用管理后台执行 CRUD 操作。

第四章:4. Django 管理简介

概述

本章将向您介绍 Django 管理应用的基本功能。您将首先为 Bookr 应用创建超级用户账户,然后继续在管理应用中执行 ForeignKeys。在本章结束时,您将看到如何通过子类化 AdminSiteModelAdmin 类来根据一组独特的偏好定制管理应用,使其界面更加直观和用户友好。

简介

在开发一个应用时,通常需要填充数据,然后修改这些数据。我们已经在 第二章模型和迁移 中看到,如何使用 Python 的 manage.py 命令行界面来执行这一操作。在 第三章URL 映射、视图和模板 中,我们学习了如何使用 Django 的视图和模板开发一个面向模型的网页表单界面。但上述两种方法都不适用于管理 reviews/models.py 中的类数据。使用命令行管理数据对于非程序员来说过于技术性,而构建单个网页将是一个费力的过程,因为它将使我们重复相同的视图逻辑和非常相似的模板功能,每个模型中的每个表都需要这样做。幸运的是,在 Django 早期开发阶段,就为解决这个问题想出了一个解决方案。

Django 管理界面实际上是一个 Django 应用。它提供了一个直观的网页界面,以便对模型数据进行管理访问。管理界面是为网站管理员设计的,并不打算供没有特权的用户使用,这些用户与网站进行交互。在我们的书评系统案例中,普通的书评者永远不会遇到管理应用。他们将看到应用页面,就像我们在 第三章URL 映射、视图和模板 中使用视图和模板构建的页面一样,并在这些页面上撰写他们的评论。

此外,虽然开发人员投入了大量精力为普通用户创建一个简单且吸引人的网页界面,但针对管理用户的行政界面,仍然保持着实用主义的感觉,通常显示模型的复杂性。可能你已经注意到了,但你已经在你的 Bookr 项目中有一个管理应用。看看 bookr/settings.py 中安装的应用列表:

INSTALLED_APPS = [
    'django.contrib.admin',
    …
]

现在,看看 bookr/urls.py 中的 URL 模式:

urlpatterns = [
    path('admin/', admin.site.urls),
    …
]

如果我们将此路径输入到我们的浏览器中,我们可以看到开发服务器上管理应用的链接是 http://127.0.0.1:8000/admin/。但在使用它之前,我们需要通过命令行创建一个超级用户。

创建超级用户账户

我们的书评应用 Bookr 刚刚发现了一个新用户。她的名字是 Alice,她想要立即开始添加她的评论。已经使用 Bookr 的 Bob 刚刚告诉我们,他的个人资料似乎不完整,需要更新。David 不再想使用这个应用,并希望删除他的账户。出于安全考虑,我们不希望任何用户为我们执行这些任务。这就是为什么我们需要创建一个具有提升权限的 超级用户。让我们先做这件事。

在 Django 的授权模型中,超级用户是指将 Staff 属性设置为 True 的用户。我们将在本章后面探讨这一点,并在第九章 会话和认证 中了解更多关于这个授权模型的信息。

我们可以通过使用我们在前面章节中探索过的 manage.py 脚本来创建超级用户。同样,当我们输入它时,我们需要在项目目录中。我们将通过在命令行中输入以下命令来使用 createsuperuser 子命令(如果你使用的是 Windows,你需要将 python 替换为 python3):

python3 manage.py createsuperuser

让我们继续创建我们的超级用户。

注意

在本章中,我们将使用属于 example.com 域的电子邮件地址。这遵循了一个既定的惯例,即使用这个保留域进行测试和文档。如果你愿意,可以使用你自己的电子邮件地址。

练习 4.01:创建超级用户账户

在这个练习中,你将创建一个超级用户账户,允许用户登录到管理站点。这个功能将在接下来的练习中也被使用,以实现只有超级用户才能执行的改变。以下步骤将帮助你完成这个练习:

  1. 输入以下命令来创建超级用户:

    python manage.py createsuperuser
    

    执行此命令后,系统将提示你创建一个超级用户。此命令将提示你输入超级用户名、可选的电子邮件地址和密码。

  2. 按照以下方式添加超级用户的用户名和电子邮件。在这里,我们在提示符下输入 bookradmin(高亮显示)并按 Enter 键。同样,在下一个提示符,要求你输入电子邮件地址时,你可以添加 bookradmin@example.com(高亮显示)。按 Enter 键继续:

    Username (leave blank to use 'django'): bookradmin to the superuser. Note that you won't see any output immediately.
    
  3. 在 shell 中的下一个提示是要求你的密码。添加一个强大的密码,然后按 Enter 键再次确认:

    Password:
    Password (again): 
    

    你应该在屏幕上看到以下信息:

    Superuser created successfully.
    

    注意,密码的验证是根据以下标准进行的:

    它不能是前 20,000 个最常见的密码之一。

    它应该至少有八个字符。

    它不能只包含数字字符。

    它不能从用户名、名字、姓氏或电子邮件地址中派生出来。

    通过这种方式,你已经创建了一个名为 bookradmin 的超级用户,他可以登录到管理应用。图 4.1 展示了在 shell 中的样子:

    ![图 4.1:创建超级用户

    ![img/B15509_04_01.jpg]

    图 4.1:创建超级用户

  4. 访问 http://127.0.0.1:8000/admin 上的管理应用,并使用你创建的超级用户账户登录:图 4.2 Django 管理登录表单

图 4.2 Django 管理登录表单

在这个练习中,你创建了一个超级用户账户,我们将在这个章节的剩余部分使用它,根据需要分配或删除权限。

注意

本章中使用的所有练习和活动的代码可以在本书的 GitHub 仓库中找到,网址为 packt.live/3pC5CRr

使用 Django 管理应用进行 CRUD 操作

让我们回到我们从鲍勃、爱丽丝和戴维那里收到的请求。作为超级用户,你的任务将涉及创建、更新、检索和删除各种用户账户、评论和标题名称。这些活动统称为 CRUD。CRUD 操作是管理应用行为的核心。结果是,管理应用已经知道来自另一个 Django 应用 Authentication and Authorization 的模型,在 INSTALLED_APPS 中被引用为 'django.contrib.auth'。当我们登录到 http://127.0.0.1:8000/admin/ 时,我们看到了授权应用的模型,如图 4.3 所示:

图 4.3:Django 管理窗口

图 4.3:Django 管理窗口

当管理应用初始化时,它会调用其 autodiscover() 方法来检测是否有其他已安装的应用包含管理模块。如果有,这些管理模型将被导入。在我们的例子中,它发现了 'django.contrib.auth.admin'。现在模块已导入,我们的超级用户账户已准备就绪,让我们先从鲍勃、爱丽丝和戴维的请求开始工作。

创建

在爱丽丝开始撰写她的评论之前,我们需要通过管理应用为她创建一个账户。一旦完成,我们就可以查看我们可以分配给她的管理访问级别。点击 用户 旁边的 + 添加 链接(参见图 4.3),并填写表单,如图 4.4 所示。

注意

我们不希望任何随机用户都能访问 Bookr 用户的账户。因此,选择强大、安全的密码至关重要。

图 4.4:添加用户页面

图 4.4:添加用户页面

表单底部有三个按钮:

  • 保存并添加另一个 创建用户并再次渲染相同的 添加用户 页面,字段为空。

  • 保存并继续编辑 创建用户并加载 更改用户 页面。更改用户 页面允许你添加在 添加用户 页面上未出现的信息,例如 名字姓氏 等(见图 4.5)。请注意,密码 在表单中没有可编辑字段。相反,它显示了存储时使用的哈希技术信息,以及一个链接到单独的 更改密码 表单。

  • 保存 创建用户并允许用户导航到 选择用户以更改 列表页面,如图 4.6 所示。![图 4.5:点击保存并继续编辑后显示的更改用户页面]

    图片 B15509_04_05.jpg

图 4.5:点击保存并继续编辑后显示的更改用户页面

检索

管理任务需要分配给一些用户,为此,管理员(拥有超级用户账户的人)希望查看电子邮件地址以 n@example.com 结尾的用户并将任务分配给这些用户。这就是在 添加用户 页面上的 保存 按钮(参见图 4.4*),我们将被带到 选择用户以更改 列表页面(如图 4.6 所示),执行 创建 表单也可以通过点击 选择用户以更改 列表页面上的 添加用户 按钮来访问。因此,在我们添加了更多用户之后,更改列表将看起来像这样:

![图 4.6:选择用户以更改页面]

图片 B15509_04_06.jpg

![图 4.6:选择用户以更改页面]

表单顶部有一个 搜索 栏,用于搜索用户的用户名、电子邮件地址以及名和姓。右侧是一个 筛选 面板,根据 员工状态超级用户状态活跃状态 的值来缩小选择范围。在 图 4.7 中,我们将看到当我们搜索字符串 n@example.com 并查看结果时会发生什么。这将只返回电子邮件地址以 n 结尾且域名以 example.com 开头的用户名称。我们将只看到三个符合此要求的电子邮件地址的用户 – bookradmin@example.comcarol.brown@example.comdavid.green@example.com

![图 4.7:通过电子邮件地址的一部分搜索用户]

图片 B15509_04_07.jpg

![图 4.7:通过电子邮件地址的一部分搜索用户]

更新

记住 Bob 想要更新他的个人资料。让我们在 选择用户以更改 列表中的 bob 用户名链接:

![图 4.8:从“选择用户以更改”列表中选择 bob]

图片 B15509_04_08.jpg

![图 4.8:从选择用户以更改列表中选择 bob]

这将带我们回到 更改用户 表单,可以在其中输入 电子邮件地址 的值:

![图 4.9:添加个人信息]

图片 B15509_04_09.jpg

![图 4.9:添加个人信息]

图 4.9 所示,我们在这里添加关于 Bob 的个人信息 – 他的名字、姓氏和电子邮件地址,具体而言。

另一种更新操作是“软删除”。Active 布尔属性允许我们停用用户,而不是删除整个记录并丢失所有依赖于该账户的数据。这种使用布尔标志来表示记录为非活动或已删除(并随后从查询中过滤掉这些标记的记录)的做法被称为通过勾选相应的复选框来表示的Staff 状态Superuser 状态

图 4.10:Active、Staff 状态和 Superuser 状态布尔值

图 4.10:Active、Staff 状态和 Superuser 状态布尔值

删除

David 不再想使用 Bookr 应用程序,并要求我们删除他的账户。auth admin 也支持这一点。在“选择要更改的用户”列表页面上选择用户或用户记录,并从“操作”下拉菜单中选择“删除选定的用户”选项。然后点击Go按钮(图 4.11):

图 4.11:从选择要更改的用户列表页面上删除

图 4.11:从选择要更改的用户列表页面上删除

删除对象后,您将看到一个确认屏幕,并被带回到“选择要更改的用户”列表:

图 4.12:用户删除确认

图 4.12:用户删除确认

用户被删除后,您将看到以下消息:

图 4.13:用户删除通知

图 4.13:用户删除通知

在确认之后,你会发现 David 的账户已不再存在。

到目前为止,我们已经学习了如何添加新用户、获取另一个用户的详细信息、更改用户的资料数据以及删除用户。这些技能帮助我们满足了 Alice、Bob 和 David 的请求。随着我们应用用户数量的增长,管理来自数百名用户的请求最终将变得相当困难。解决这个问题的方法之一是将一些管理职责委托给一组选定的用户。我们将在接下来的部分中学习如何做到这一点。

用户和组

Django 的认证模型由用户、组和权限组成。用户可以属于多个组,这是对用户进行分类的一种方式。它还通过允许将权限分配给用户集合以及个人来简化权限的实现。

练习 4.01创建 Superuser 账户 中,我们看到了如何满足 Alice、David 和 Bob 修改其个人资料的需求。这做起来相当容易,我们的应用程序似乎已经准备好处理他们的请求。

当用户数量增加时会发生什么?管理员用户能否一次性管理 100 或 150 个用户?正如您所想象的,这可能是一项相当复杂的任务。为了克服这一点,我们可以给一组特定的用户赋予更高的权限,他们可以帮助减轻管理员的负担。这就是组派上用场的地方。虽然我们将在第九章会话和身份验证中了解更多关于用户、组和权限的内容,但我们可以通过创建一个包含可以访问管理界面但缺乏许多强大功能(如添加、编辑或删除组或添加或删除用户的能力)的帮助台用户组来开始理解组和它们的功能。

练习 4.02:通过管理应用添加和修改用户和组

在这个练习中,我们将授予我们 Bookr 用户之一,Carol,一定级别的管理访问权限。首先,我们将定义组的访问级别,然后我们将 Carol 添加到该组。这将允许 Carol 更新用户资料和检查用户日志。以下步骤将帮助您实施此练习:

  1. 访问管理界面http://127.0.0.1:8000/admin/并使用通过超级用户命令设置的账户以bookradmin身份登录。

  2. 在管理界面中,通过链接到首页身份验证和授权图 4.14:身份验证和授权页面上的组和用户选项

    图 4.14:身份验证和授权页面上的组和用户选项

  3. 在右上角使用ADD GROUP +添加一个新组:图 4.15:添加新组

    图 4.15:添加新组

  4. 将组命名为帮助台用户并赋予以下权限,如图图 4.16所示:

    可以查看日志条目

    可以查看权限

    可以更改用户

    可以查看用户

    图 4.16:选择权限

    图 4.16:选择权限

    可以通过从“可用权限”中选择权限并点击中间的右箭头,使它们出现在“已选权限”下完成此操作。请注意,要一次性添加多个权限,您可以按住Ctrl键(或 Mac 上的Command键)以选择多个:

    图 4.17:将选定的权限添加到已选权限

    图 4.17:将选定的权限添加到已选权限

    一旦您点击保存按钮,您将看到一个确认消息,表明已成功添加了组帮助台用户

    图 4.18:确认已添加帮助台用户组的消息

    图 4.18:确认已添加帮助台用户组的消息

  5. 现在,导航到首页身份验证和授权用户并点击具有名字首字母carol的用户链接:图 4.19:点击用户名 carol

    图 4.19:点击用户名 carol

  6. 滚动到“权限”字段设置,并选择“员工状态”复选框。这是 Carol 能够登录到管理应用所必需的:图 4.20:点击员工状态复选框

    图 4.20:点击员工状态复选框

  7. 通过从“可用组”选择框中选择它(参见图 4.20)并点击右箭头将其移至她的“选择组”列表中(如图 4.21 所示),将 Carol 添加到我们在上一步骤中创建的“帮助台用户”组中。请注意,除非你这样做,否则 Carol 将无法使用她的凭据登录到管理界面:图 4.21:将帮助台用户组移至 Carol 选择的组列表中

    图 4.21:将帮助台用户组移至 Carol 选择的组列表中

  8. 让我们测试一下到目前为止我们所做的是否得到了正确的结果。为此,从管理员站点注销并再次以carol身份登录。注销后,你应该在屏幕上看到以下内容:图 4.22:注销屏幕

图 4.22:注销屏幕

注意

如果你记不起你最初给她设置的密码,你可以在命令行中通过输入python3 manage.py changepassword carol来更改密码。

登录成功后,在管理员仪表板上,你可以看到没有指向“组”的链接:

图 4.23:管理员仪表板

图 4.23:管理员仪表板

由于我们没有将任何组权限,甚至auth | group | Can view group,分配给“帮助台用户”组,当 Carol 登录时,她无法访问“组”管理界面。同样,导航到“首页 › 认证和授权 › 用户”。点击用户链接,你会看到没有编辑或删除用户的选项。这是因为授予了帮助台用户组的权限,而 Carol 是该组成员。该组成员可以查看和编辑用户,但不能添加或删除任何用户。

在这个练习中,我们学习了如何授予我们 Django 应用用户一定量的管理权限。

注册评论模型

假设 Carol 的任务是改进 Bookr 中的评论部分;也就是说,只有最相关和最全面的评论应该显示,而重复或垃圾信息应该被删除。为此,她将需要访问reviews模型。正如我们通过调查组和用户所看到的那样,管理员应用已经包含了来自认证和授权应用的模型的管理页面,但它还没有引用我们的 Reviews 应用中的模型。

为了让管理应用知道我们的模型,我们需要明确地将它们注册到管理应用中。幸运的是,我们不需要修改管理应用的代码来做这件事,因为我们可以将管理应用导入到我们的项目中,并使用它的 API 来注册我们的模型。这已经在认证和授权应用中完成了,所以让我们用我们的“评论”应用试一试。我们的目标是能够使用管理应用来编辑我们的reviews模型中的数据。

查看一下reviews/admin.py文件。这是一个占位符文件,它是通过我们在第一章Django 简介中使用的startapp子命令生成的,目前包含以下行:

from django.contrib import admin
# Register your models here.

现在我们可以尝试扩展这个功能。为了让管理应用知道我们的模型,我们可以修改reviews/admin.py文件并导入模型。然后我们可以使用AdminSite对象,admin.site,来注册模型。AdminSite对象包含 Django 管理应用的实例(稍后我们将学习如何子类化这个AdminSite并覆盖其许多属性)。然后,我们的reviews/admin.py将看起来如下:

from django.contrib import admin
from reviews.models import Publisher, Contributor, \
Book, BookContributor, Review
# Register your models here.
admin.site.register(Publisher)
admin.site.register(Contributor)
admin.site.register(Book)
admin.site.register(BookContributor)
admin.site.register(Review)

admin.site.register方法通过将其添加到admin.site._registry中包含的类注册表中,使模型对管理应用可用。如果我们选择不通过管理界面使模型可访问,我们只需不注册它即可。当你刷新浏览器中的http://127.0.0.1:8000/admin/时,你将在管理应用首页看到以下内容。注意在导入reviews模型后管理页面的外观变化:

图 4.24:管理应用首页

图片

图 4.24:管理应用首页

变更列表

我们现在为我们的模型创建了变更列表。如果我们点击“发布者”链接,我们将被带到http://127.0.0.1:8000/admin/reviews/publisher并看到包含指向发布者链接的变更列表。这些链接由“发布者”对象的id字段指定。

如果你的数据库已经通过第三章中的脚本填充,你将看到一个包含七个发布者的列表,看起来像图 4.25

注意

根据你的数据库状态和已完成的活动,这些示例中的对象 ID、URL 和链接可能与这里列出的不同。

图 4.25:选择要更改的发布者列表

图片

图 4.25:选择要更改的发布者列表

发布者变更页面

http://127.0.0.1:8000/admin/reviews/publisher/1的发布者变更页面包含我们可能预期的内容(见图 4.26)。这里有一个用于编辑发布者详情的表单。这些详情是从reviews.models.Publisher类派生出来的:

图 4.26:发布者变更页面

图片

图 4.26:发布者变更页面

如果我们点击了“添加出版商”按钮,管理应用会返回用于添加出版商的类似表单。管理应用的美妙之处在于,它只通过一行代码——admin.site.register(Publisher)——就为我们提供了所有这些 CRUD 功能,使用reviews.models.Publisher属性的定义作为页面内容的模式:

class Publisher(models.Model):
    """A company that publishes books."""
    name = models.CharField\
           (help_text="The name of the Publisher.",\
            max_length=50)
    website = models.URLField\
              (help_text="The Publisher's website.")
    email = models.EmailField\
            (help_text="The Publisher's email address.")

出版商的“名称”字段被限制为 50 个字符,如模型中指定。在每个字段下方出现的灰色帮助文本是从模型上指定的help_text属性派生出来的。我们可以看到,models.CharFieldmodels.URLFieldmodels.EmailField分别作为 HTML 中的texturlemail类型的输入元素渲染。

表单中的字段在适当的地方带有验证。除非模型字段设置为blank=Truenull=True,否则如果字段留空,表单将抛出错误,例如对于Publisher.name字段。同样,由于Publisher.websitePublisher.email分别定义为models.URLFieldmodels.EmailField的实例,它们将相应地进行验证。在图 4.27中,我们可以看到“名称”作为必填字段的验证,验证“网站”作为 URL,以及验证“电子邮件”作为电子邮件地址:

图 4.27:字段验证

图 4.27:字段验证

检查管理应用如何渲染模型元素,以了解其工作方式是有用的。在您的浏览器中,右键单击“查看页面源代码”并检查此表单已渲染的 HTML。您将看到一个浏览器标签页显示如下内容:

<fieldset class="module aligned ">
    <div class="form-row errors field-name">
        <ul class="errorlist"><li>This field is required.</li></ul>
            <div>
                    <label class="required" for="id_name">Name:</label>
                        <input type="text" name="name" class="vTextField"
                         maxlength="50" required id="id_name">
                    <div class="help">The name of the Publisher.</div>
            </div>
    </div>
    <div class="form-row errors field-website">
        <ul class="errorlist"><li>Enter a valid URL.</li></ul>
            <div>
                    <label class="required" for="id_website">Website:</label>
                        <input type="url" name="website" value="packtcom"
                         class="vURLField" maxlength="200" required
                         id="id_website">
                    <div class="help">The Publisher's website.</div>
            </div>
    </div>
    <div class="form-row errors field-email">
        <ul class="errorlist"><li>Enter a valid email address.</li></ul>
            <div>
                    <label class="required" for="id_email">Email:</label>
                        <input type="email" name="email" value="infoatpackt.com"
                         class="vTextField" maxlength="254" required
                         id="id_email">
                    <div class="help">The Publisher's email address.</div>
            </div>
  </div>
</fieldset>

表单具有publisher_form ID,并包含一个与reviews/models.pyPublisher模型的数据库结构相对应的fieldset,如下所示:

class Publisher(models.Model):
    """A company that publishes books."""
    name = models.CharField\
           (max_length=50,
            help_text="The name of the Publisher.")
    website = models.URLField\
              (help_text="The Publisher's website.")
    email = models.EmailField\
            (help_text="The Publisher's email address.")

注意,对于名称,输入字段被渲染如下:

<input type="text" name="name" value="Packt Publishing"
                   class="vTextField" maxlength="50" required="" id="id_name">

这是一个必填字段,它具有text类型和由模型定义中的max_length参数定义的maxlength为 50:

    name = models.CharField\
           (help_text="The name of the Publisher.",\
            max_length=50)

同样,我们可以看到在模型中定义的网站和电子邮件作为URLFieldEmailField被分别渲染为 HTML 中的urlemail类型的输入元素:

<input type="url" name="website" value="https://www.packtpub.com/"
                     class="vURLField" maxlength="200" required=""
                     id="id_website">            
<input type="email" name="email" value="info@packtpub.com"
                    class="vTextField" maxlength="254" required=""
                    id="id_email">

我们已经了解到,这个 Django 管理应用根据我们提供的模型定义,为 Django 模型生成合理的 HTML 表示形式。

书籍更改页面

类似地,可以通过从“站点管理”页面选择“书籍”并然后在更改列表中选择特定的书籍来访问更改页面:

图 4.28:从站点管理页面选择书籍

图 4.28:从站点管理页面选择书籍

如前一个屏幕截图所示,点击“书籍”后,您会在屏幕上看到以下内容:

图 4.29:书籍更改页面

图 4.29:书籍更改页面

在这种情况下,选择书籍 智能建筑师 将带我们到 URL http://127.0.0.1:8000/admin/reviews/book/3/change/。在上一个示例中,所有模型字段都被呈现为简单的 HTML 文本小部件。models.Book 中使用的 django.db.models.Field 的某些其他子类的呈现值得更仔细地检查:

图 4.30:更改书籍页面

图 4.30:更改书籍页面

在这里,publication_date 使用 models.DateField 定义。它通过日期选择小部件呈现。小部件的视觉表示将在不同的操作系统和浏览器选择中有所不同:

图 4.31:日期选择小部件

图 4.31:日期选择小部件

由于 Publisher 被定义为外键关系,它通过一个 Publisher 下拉菜单呈现,其中包含 Publisher 对象的列表:

图 4.32:出版社下拉菜单

图 4.32:出版社下拉菜单

这带我们来到了管理员应用如何处理删除操作。管理员应用在确定如何实现删除功能时,会从模型的 外键约束中获取线索。在 BookContributor 模型中,Contributor 被定义为外键。reviews/models.py 中的代码如下:

contributor = models.ForeignKey(Contributor, on_delete=models.CASCADE)

通过在外键上设置 on_delete=CASCADE,模型指定了当删除记录时所需的数据库行为;删除将级联到由外键引用的其他对象。

练习 4.03:管理员应用中的外键和删除行为

目前,reviews 模型中的所有 ForeignKey 关系都定义为 on_delete=CASCADE 行为。例如,考虑一个管理员删除出版商的情况。这将删除与出版商关联的所有书籍。我们不希望发生这种情况,这正是我们将在此练习中改变的行为:

  1. 访问 Contributors 变更列表 http://127.0.0.1:8000/admin/reviews/contributor/ 并选择一个要删除的贡献者。确保该贡献者是某本书的作者。

  2. 点击 删除 按钮,但在确认对话框中不要点击 是,我确定。你会看到一个类似于 图 4.33 中的消息:图 4.33:级联删除确认对话框

    图 4.33:级联删除确认对话框

    根据 on_delete=CASCADE 外键参数,我们被警告,删除此 Contributor 对象将对 BookContributor 对象产生级联效应。

  3. reviews/models.py 文件中,将 BookContributorContributor 属性修改为以下内容并保存文件:

    contributor = models.ForeignKey(Contributor, \
                                    on_delete=models.PROTECT)
    
  4. 现在,再次尝试删除 Contributor 对象。你会看到一个类似于 图 4.34 中的消息:图 4.34:外键保护错误

    图 4.34:外键保护错误

    因为on_delete参数是PROTECT,我们尝试删除具有依赖关系的对象将会抛出错误。如果我们在这个模型中使用这种方法,我们需要在删除原始对象之前删除ForeignKey关系中的对象。在这种情况下,这意味着在删除Contributor对象之前删除BookContributor对象。

  5. 现在我们已经了解了管理应用程序如何处理ForeignKey关系,让我们将BookContributor类中的ForeignKey定义恢复为以下内容:

    contributor = models.ForeignKey(Contributor, \
                                    on_delete=models.CASCADE)
    

我们已经检查了管理应用程序的行为如何适应在模型定义中表达出的ForeignKey约束。如果on_delete行为设置为models.PROTECT,管理应用程序将返回一个错误,解释为什么受保护的对象阻止了删除。在构建现实世界的应用程序时,这种功能可能会派上用场,因为经常会有手动错误意外导致删除重要记录的风险。在下一节中,我们将探讨如何自定义我们的管理应用程序界面以获得更流畅的用户体验。

自定义管理界面

在最初开发应用程序时,默认管理界面的便利性对于构建应用程序的快速原型非常出色。确实,对于许多需要最小数据维护的简单应用程序或项目,这个默认管理界面可能完全足够。然而,随着应用程序成熟到发布阶段,通常需要自定义管理界面以促进更直观的使用并稳健地控制数据,同时考虑用户权限。你可能希望保留默认管理界面的某些方面,同时调整某些功能以更好地满足你的需求。例如,你可能希望出版商列表显示出版机构的完整名称,而不是“Publisher(1)”,“Publisher(2)”等等。除了美学吸引力外,这还使得使用和浏览应用程序变得更加容易。

站点范围内的 Django 管理自定义

我们已经看到一页标题为 登录 | Django 站点管理 的页面,其中包含一个 Django 管理 表单。然而,Bookr 应用程序的管理用户可能会对所有的这些 Django 术语感到困惑,如果他们必须处理所有具有相同管理应用程序的多个 Django 应用程序,这将非常令人困惑,并且可能导致错误。作为一个直观且用户友好的应用程序的开发者,你可能会想要自定义这一点。像这样的全局属性被指定为AdminSite对象的属性。以下表格详细说明了如何进行一些简单的自定义,以改善你应用程序管理界面的可用性:

图 4.35:重要的 AdminSite 属性

图 4.35:重要的 AdminSite 属性

图 4.35:重要的 AdminSite 属性

从 Python Shell 检查 AdminSite 对象

让我们更深入地看看AdminSite类。我们之前已经遇到了AdminSite类的一个对象。它是我们在上一节中使用的admin.site对象,即注册评论模型。如果开发服务器没有运行,现在就使用runserver子命令启动它,如下所示(在 Windows 上使用python而不是python3):

python3 manage.py runserver

我们可以通过在 Django shell 中导入 admin 应用来检查admin.site对象,再次使用manage.py脚本:

python3 manage.py shell
>>>from django.contrib import admin

我们可以交互式地检查site_titlesite_headerindex_title的默认值,并看到它们与我们已经在 Django 管理应用渲染的网页上观察到的预期值'Django site admin''Django administration''Site administration'相匹配:

>>> admin.site.site_title
'Django site admin'
>>> admin.site.site_header
'Django administration'
>>> admin.site.index_title
'Site administration'

AdminSite类还指定了用于渲染管理界面并确定其全局行为的表单和视图。

子类化 AdminSite

我们可以对reviews/admin.py文件进行一些修改。我们不再导入django.contrib.admin模块并使用其站点对象,而是导入AdminSite,创建其子类,并实例化我们的自定义admin_site对象。考虑以下代码片段。在这里,BookrAdminSiteAdminSite的一个子类,它包含自定义的site_titlesite_headerindex_title值;admin_siteBookrAdminSite的一个实例;我们可以使用这个实例来代替默认的admin.site对象,以注册我们的模型。reviews/admin.py文件将如下所示:

from django.contrib.admin import AdminSite
from reviews.models import (Publisher, Contributor, Book,\
     BookContributor, Review)
class BookrAdminSite(AdminSite):
    title_header = 'Bookr Admin'
    site_header = 'Bookr administration'
    index_title = 'Bookr site admin'
admin_site = BookrAdminSite(name='bookr')
# Register your models here.
admin_site.register(Publisher)
admin_site.register(Contributor)
admin_site.register(Book)
admin_site.register(BookContributor)
admin_site.register(Review)

由于我们现在创建了自己的admin_site对象,它覆盖了admin.site对象的行为,我们需要在我们的代码中删除对admin.site对象的现有引用。在bookr/urls.py中,我们需要将管理指向新的admin_site对象并更新我们的 URL 模式。否则,我们仍然会使用默认的管理站点,我们的自定义设置将被忽略。更改将如下所示:

from reviews.admin import admin_site
from django.urls import include, path
import reviews.views
urlpatterns = [path('admin/', admin_site.urls),\
               path('', reviews.views.index),\
               path('book-search/', reviews.views.book_search, \
                    name='book_search'),\
               path('', include('reviews.urls'))]

这在登录界面上产生了预期的结果:

![图 4.36:自定义登录界面]

![图片 B15509_04_36.jpg]

图 4.36:自定义登录界面

然而,现在出现了问题;那就是,我们失去了认证对象的界面。之前,管理应用通过自动发现过程在reviews/admin.pydjango.contrib.auth.admin中查找注册的模型,但现在我们通过创建一个新的AdminSite来覆盖了这种行为:

![图 4.37:自定义 AdminSite 缺少认证和授权]

![图片 B15509_04_37.jpg]

图 4.37:自定义 AdminSite 缺少认证和授权

我们可以选择在bookr/urls.py中将两个AdminSite对象都引用到 URL 模式中,但这种方法意味着我们将最终拥有两个独立的用于认证和评论的 admin 应用。因此,URL http://127.0.0.1:8000/admin将带您访问从admin.site对象派生的原始 admin 应用,而http://127.0.0.1:8000/bookradmin将带您到我们的BookrAdminSite admin_site。这不是我们想要做的,因为我们仍然有一个没有添加我们子类化BookrAdminSite时所做的定制的 admin 应用:

from django.contrib import admin
from reviews.admin import admin_site
from django.urls import path
urlpatterns = [path('admin/', admin.site.urls),\
               path('bookradmin/', admin_site.urls),]

这一直是 Django admin 界面中的一个笨拙问题,导致早期版本中出现了许多临时解决方案。自从 Django 2.1 发布以来,有一个简单的方法可以集成自定义的 admin 应用界面,而不会破坏自动发现或其其他默认功能。由于BookrAdminSite是项目特定的,代码实际上并不属于我们的reviews文件夹。我们应该将BookrAdminSite移动到Bookr项目目录顶层的名为admin.py的新文件中:

from django.contrib import admin
class BookrAdminSite(admin.AdminSite):
    title_header = 'Bookr Admin'
    site_header = 'Bookr administration'
    index_title = 'Bookr site admin'

bookr/urls.py中的 URL 设置路径更改为path('admin/', admin.site.urls),我们定义我们的ReviewsAdminConfigreviews/apps.py文件将包含以下附加行:

from django.contrib.admin.apps import AdminConfig
class ReviewsAdminConfig(AdminConfig):
    default_site = 'admin.BookrAdminSite'

django.contrib.admin替换为reviews.apps.ReviewsAdminConfig,因此bookr/settings.py文件中的INSTALLED_APPS将如下所示:

INSTALLED_APPS = ['reviews.apps.ReviewsAdminConfig',\
                  'django.contrib.auth',\
                  'django.contrib.contenttypes',\
                  'django.contrib.sessions',\
                  'django.contrib.messages',\
                  'django.contrib.staticfiles',\
                  'reviews']

使用default_siteReviewsAdminConfig规范,我们不再需要用自定义的AdminSite对象admin_site替换对admin.site的引用。我们可以用最初的admin.site调用替换那些admin_site调用。现在,reviews/admin.py恢复为以下内容:

from django.contrib import admin
from reviews.models import (Publisher, Contributor, Book,\
     BookContributor, Review)
# Register your models here.
admin.site.register(Publisher)
admin.site.register(Contributor)
admin.site.register(Book, BookAdmin)
admin.site.register(BookContributor)
admin.site.register(Review)

我们还可以自定义AdminSite的其他方面,但我们将等到对 Django 的模板和表单有更深入的了解后,在第九章会话和认证中重新讨论这些内容。

活动四点零一:自定义 SiteAdmin

您已经学会了如何在 Django 项目中修改AdminSite对象的属性。此活动将挑战您使用这些技能来自定义一个新项目,并覆盖其站点标题、站点页眉和索引页眉。此外,您将通过创建特定于项目的模板并将其设置在我们的自定义SiteAdmin对象中来替换注销消息。您正在开发一个实现留言板的 Django 项目,称为Comment8orComment8or面向技术受众,因此您需要使措辞简洁并使用缩写:

  1. Comment8or admin 站点将被称作c8admin。这将出现在网站页眉和索引标题中。

  2. 对于标题页眉,它将显示为c8 site admin

  3. 默认的 Django admin 注销消息是Thanks for spending some quality time with the Web site today. 在 Comment8or 中,它将显示为Bye from c8admin.

完成此活动需要遵循以下步骤:

  1. 按照你在 第一章Django 简介 中学到的流程,创建一个新的 Django 项目,名为 comment8or,一个名为 messageboard 的应用,并运行迁移。创建一个名为 c8admin 的超级用户。

  2. 在 Django 源代码中,有一个位于 django/contrib/admin/templates/registration/logged_out.html 的登出页面模板。

  3. 在你的项目目录 comment8or/templates/comment8or 下复制它。根据要求修改模板中的信息。

  4. 在项目内部,创建一个 admin.py 文件,实现一个自定义的 SiteAdmin 对象。根据要求设置属性 index_titletitle_headersite_headerlogout_template 的适当值。

  5. messageboard/apps.py 中添加一个自定义的 AdminConfig 子类。

  6. comment8or/settings.py 中将管理应用替换为自定义的 AdminConfig 子类。

  7. 配置 TEMPLATES 设置,以便项目模板可被发现。

    当项目首次创建时,登录、应用索引和登出页面将如下所示:

    图 4.38:项目的登录页面

图 4.38:项目的登录页面

图 4.39:项目的应用索引页面

图 4.39:项目的应用索引页面

图 4.40:项目的登出页面

图 4.40:项目的登出页面

完成此活动后,登录、应用索引和登出页面将显示以下自定义设置:

图 4.41:自定义后的登录页面

图 4.41:自定义后的登录页面

图 4.42:自定义后的应用索引页面

图 4.42:自定义后的应用索引页面

图 4.43:自定义后的登出页面

图 4.43:自定义后的登出页面

你已经通过继承 AdminSite 成功自定义了管理应用。

注意

此活动的解决方案可以在 packt.live/2Nh1NTJ 找到。

自定义 ModelAdmin 类

现在我们已经学习了如何使用子类化的AdminSite来自定义管理应用的全局外观,我们将探讨如何自定义管理应用界面以适应单个模型。由于管理界面是自动从模型结构生成的,因此它具有过于通用的外观,需要为了美观和可用性进行自定义。点击管理应用中的Books链接,并将其与Users链接进行比较。这两个链接都会带您到变更列表页面。这些页面是 Bookr 管理员在想要添加新书籍或添加或更改用户权限时访问的页面。如上所述,变更列表页面展示了一个模型对象的列表,可以选择其中的一组进行批量删除(或其他批量操作),查看单个对象以便编辑,或添加新对象。注意两个变更列表页面的差异,以便使我们的基本Books页面与Users页面一样功能齐全。

以下是从Authentication and Authorization应用中截取的屏幕截图,其中包含有用的功能,如搜索栏、可排序的重要用户字段列标题和结果过滤器:

图 4.44:用户变更列表包含自定义的 ModelAdmin 功能

图 4.44:用户变更列表包含自定义的 ModelAdmin 功能

列表显示字段

Users变更列表页面上,您将看到以下内容:

  • 展示了一个用户对象列表,通过其USERNAMEEMAIL ADDRESSFIRST NAMELAST NAMESTAFF STATUS属性进行总结。

  • 这些单个属性是可排序的。排序顺序可以通过点击标题来更改。

  • 页面顶部有一个搜索栏。

  • 在右侧列中,有一个选择过滤器,允许选择多个用户字段,包括一些不在列表显示中出现的字段。

然而,Books变更列表页面的行为帮助不大。书籍按标题列出,但不是按字母顺序排列。标题列不可排序,且没有过滤或搜索选项:

图 4.45:书籍变更列表

图 4.45:书籍变更列表

回想一下第二章模型和迁移,我们为PublisherBookContributor类定义了__str__方法。在Book类的情况下,它有一个返回书籍对象标题的__str__()表示:

class Book(models.Model):
    …
    def __str__(self):
        return "{} ({})".format(self.title, self.isbn)

如果我们没有在Book类上定义__str__()方法,它将继承自基类django.db.models.Model

这个基类提供了一种抽象的方式来给出对象的字符串表示。当我们有Book类,其主键为id字段,值为17时,我们将得到一个字符串表示为Book object (17)

![图 4.46:使用 Model str 表示的书籍变更列表

![img/B15509_04_46.jpg]

图 4.46:使用 Model str 表示的 Books 更改列表

在我们的应用程序中,将Book对象表示为几个字段的组合可能是有用的。例如,如果我们想将书籍表示为Title (ISBN),以下代码片段将产生所需的结果:

class Book(models.Model):
    …
    def __str__(self):
        return "{} ({})".format(self.title, self.isbn)

这本身就是一个有用的更改,因为它使得对象在应用中的表示更加直观:

![Figure 4.47: A portion of the Books change list with the custom string representation]

![img/B15509_04_47.jpg]

图 4.47:带有自定义字符串表示的 Books 更改列表的一部分

我们不仅限于在list_display字段中使用对象的__str__表示形式。列表显示中出现的列是由 Django 管理应用中的ModelAdmin类决定的。在 Django shell 中,我们可以导入ModelAdmin类并检查其list_display属性:

python manage.py shell
>>> from django.contrib.admin import ModelAdmin
>>> ModelAdmin.list_display
('__str__',)

这解释了为什么list_display的默认行为是显示对象的__str__表示形式的单列表格,这样我们就可以通过覆盖此值来自定义列表显示。最佳实践是为每个对象子类化ModelAdmin。如果我们想使Book列表显示包含两个单独的列TitleISBN,而不是像图 4.47中那样有一个包含两个值的单列,我们将子类化ModelAdminBookAdmin并指定自定义的list_display。这样做的好处是,我们现在能够按TitleISBN对书籍进行排序。我们可以将此类添加到reviews/admin.py

class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'isbn')

现在我们已经创建了一个BookAdmin类,我们应该在将我们的reviews.models.Book类注册到管理站点时引用它。在同一个文件中,我们还需要修改模型注册以使用BookAdmin而不是admin.ModelAdmin的默认值,因此admin.site.register调用现在变为以下内容:

admin.site.register(Book, BookAdmin)

一旦对reviews/admin.py文件进行了这两项更改,我们将得到一个看起来像这样的Books更改列表页面:

![Figure 4.48: A portion of the Books change list with a two-column list display]

![img/B15509_04_48.jpg]

图 4.48:带有两列列表显示的 Books 更改列表的一部分

这给我们一个关于list_display如何灵活的提示。它可以接受四种类型的值:

  • 它接受模型中的字段名称,例如titleisbn

  • 它接受一个接受模型实例作为参数的函数,例如这个给出一个人姓名初始化版本的函数:

    def initialled_name(obj):
        """ obj.first_names='Jerome David', obj.last_names='Salinger'
            => 'Salinger, JD' """
        initials = ''.join([name[0] for name in \
                            obj.first_names.split(' ')])
        return "{}, {}".format(obj.last_names, initials)
    class ContributorAdmin(admin.ModelAdmin):
        list_display = (initialled_name,)
    
  • 它接受一个从ModelAdmin子类中来的方法,该方法接受模型对象作为单个参数。请注意,这需要指定为一个字符串参数,因为它在类外部,并且未定义:

    class BookAdmin(admin.ModelAdmin):
        list_display = ('title', 'isbn13')
        def isbn13(self, obj):
            """ '9780316769174' => '978-0-31-676917-4' """
            return "{}-{}-{}-{}-{}".format\
                                    (obj.isbn[0:3], obj.isbn[3:4],\
                                     obj.isbn[4:6], obj.isbn[6:12],\
                                     obj.isbn[12:13])
    
  • 它接受模型类的一个方法(或非字段属性),例如__str__,只要它接受模型对象作为参数。例如,我们可以将isbn13转换为Book模型类上的一个方法:

    class Book(models.Model):
        def isbn13(self):
            """ '9780316769174' => '978-0-31-676917-4' """
            return "{}-{}-{}-{}-{}".format\
                                    (self.isbn[0:3], self.isbn[3:4],\
                                     self.isbn[4:6], self.isbn[6:12],\
                                     self.isbn[12:13])
    

    现在,当在 http://127.0.0.1:8000/admin/reviews/book 查看书籍更改列表时,我们可以看到带有连字符的 ISBN13 字段:

图 4.49:带有连字符 ISBN13 的书籍更改列表的一部分

图 4.49:带有连字符 ISBN13 的书籍更改列表的一部分

值得注意的是,如 __str__ 或我们的 isbn13 方法这样的计算字段不适合在摘要页面上排序。此外,我们无法在 display_list 中包含 ManyToManyField 类型的字段。

过滤器

一旦管理界面需要处理大量记录,就方便在更改列表页面上缩小显示的结果。最简单的过滤器是选择单个值。例如,图 4.6 中描述的用户过滤器允许用户通过 staff statussuperuser statusactive 来选择用户。我们已经看到在用户过滤器中,BooleanField 可以用作过滤器。我们还可以在 CharFieldDateFieldDateTimeFieldIntegerFieldForeignKeyManyToManyField 上实现过滤器。在这种情况下,将 publisher 作为 BookForeignKey 添加,它在 Book 类中定义如下:

publisher = models.ForeignKey(Publisher, \
                              on_delete=models.CASCADE)

过滤器是通过 ModelAdmin 子类的 list_filter 属性实现的。在我们的 Bookr 应用中,通过书名或 ISBN 过滤是不切实际的,因为它会产生一个包含大量过滤选项的列表,而这些选项只返回一条记录。占据页面右侧的过滤器将占用比实际更改列表更多的空间。一个实用的选项是按出版商过滤书籍。我们为 Publisher 模型定义了一个自定义的 __str__ 方法,该方法返回出版商的 name 属性,因此我们的过滤选项将以出版商名称列出。

我们可以在 reviews/admin.py 文件中的 BookAdmin 类中指定我们的更改列表过滤器:

    list_filter = ('publisher',)

这是书籍更改页面现在应该看起来的样子:

图 4.50:使用出版商过滤器时书籍页面发生变化

图 4.50:带有出版商过滤器的书籍更改页面

通过这一行代码,我们在书籍更改列表页面上实现了一个有用的出版商过滤器。

练习 4.04:添加日期列表过滤器(list_filter)和日期层次结构(date_hierarchy)

我们已经看到,admin.ModelAdmin 类提供了有用的属性来自定义更改列表页面的过滤器。例如,按日期过滤对于许多应用来说是关键功能,也可以帮助我们使我们的应用更加用户友好。在这个练习中,我们将检查如何通过在过滤器中包含日期字段来实现日期过滤,并查看 date_hierarchy 过滤器:

  1. 编辑 reviews/admin.py 文件并修改 BookAdmin 类中的 list_filter 属性,以包括 'publication_date'

    class BookAdmin(admin.ModelAdmin):
        list_display = ('title', 'isbn')
        list_filter = ('publisher', 'publication_date')
    
  2. 重新加载书籍更改页面并确认过滤器现在包括日期设置:图 4.51:确认书籍更改页面包括日期设置

    图 4.51:确认书籍页面变化包括日期设置

    如果 Bookr 项目接收大量新发布,并且我们想要通过最近 7 天或一个月内出版的书籍来过滤书籍,这个发布日期过滤器将非常方便。有时,我们可能希望按特定年份或特定年份中的特定月份进行过滤。幸运的是,admin.ModelAdmin 类自带一个自定义过滤器属性,专门用于导航时间信息层次结构。它被称为 date_hierarchy

  3. date_hierarchy 属性添加到 BookAdmin 并将其值设置为 publication_date

    class BookAdmin(admin.ModelAdmin):
        date_hierarchy = 'publication_date'
        list_display = ('title', 'isbn')
        list_filter = ('publisher', 'publication_date')
    
  4. 重新加载“书籍”更改页面并确认日期层次结构出现在“操作”下拉菜单上方:图 4.52:确认日期层次结构出现在操作下拉菜单上方

    图 4.52:确认日期层次结构出现在操作下拉菜单上方

  5. 从日期层次结构中选择一年并确认它包含该年包含书名和书籍总列表的月份列表:图 4.53:确认从日期层次结构中选择一年显示该年出版的书籍

    图 4.53:确认从日期层次结构中选择一年显示该年出版的书籍

  6. 确认选择这些月份之一将进一步过滤到月份中的天数:图 4.54:将月份过滤到月份中的天数

图 4.54:将月份过滤到月份中的天数

date_hierarchy 过滤器是一种方便的方式来定制包含大量可按时间排序数据的更改列表,以便加快记录选择,正如我们在这次练习中所看到的。现在,让我们看看在我们的应用中实现搜索栏。

搜索栏

这就带我们来到了我们想要实现的功能的最后一部分——搜索栏。和过滤器一样,基本的搜索栏实现起来相当简单。我们只需要将 search_fields 属性添加到 ModelAdmin 类中。在我们 Book 类中用于搜索的明显字符字段是 titleisbn。目前,“书籍”更改列表显示在更改列表顶部的日期层次结构。搜索栏将出现在这个位置上方:

图 4.55:添加搜索栏之前的书籍更改列表

图 4.55:添加搜索栏之前的书籍更改列表

我们可以从将此属性添加到 BookAdminreviews/admin.py 中并检查结果开始:

    search_fields = ('title', 'isbn')

结果看起来会是这样:

图 4.56:带有搜索栏的书籍更改列表

图 4.56:带有搜索栏的书籍更改列表

现在我们可以对匹配标题字段或 ISBN 的字段执行简单的文本搜索。这个搜索需要精确的字符串匹配,所以 "color" 不会匹配 "colour"。它也缺乏我们从更复杂的搜索设施(如 Books 模型)所期望的深度语义处理。我们可能还想按出版商名称进行搜索。幸运的是,search_fields 足够灵活,可以完成这项任务。要搜索 ForeignKeyFieldManyToManyField,我们只需要指定当前模型上的字段名称和关联模型上的字段名称,两者之间用两个下划线分隔。在这种情况下,Book 有一个外键 publisher,我们想要搜索 Publisher.name 字段,因此可以在 BookAdmin.search_fields 中指定为 'publisher__name'

    search_fields = ('title', 'isbn', 'publisher__name')

如果我们想要将搜索字段限制为精确匹配而不是返回包含搜索字符串的结果,则可以在字段后添加 '__exact' 后缀。因此,将 'isbn' 替换为 'isbn__exact' 将要求匹配完整的 ISBN,而我们不能使用 ISBN 的一部分来匹配。

类似地,我们通过使用 '__startswith' 后缀将搜索字段限制为只返回以搜索字符串开头的搜索结果。将出版商名称搜索字段指定为 'publisher__name__startswith' 意味着我们将得到搜索 "pack" 的结果,但不会得到搜索 "ackt" 的结果。

排除和分组字段

有时在管理界面中限制模型中某些字段的可见性是合适的。这可以通过 exclude 属性实现。

这是带有 Date edited 字段可见的审阅表单屏幕。请注意,Date created 字段没有显示——因为它已经在模型中定义为带有 auto_now_add 参数的隐藏视图:

![图 4.57:审阅表单]

![图片 B15509_04_57.jpg]

图 4.57:审阅表单

如果我们想要从审阅表单中排除 Date edited 字段,我们将在 ReviewAdmin 类中这样做:

exclude = ('date_edited')

然后审阅表单将不会显示 Date edited

![图 4.58:排除 Date edited 字段的审阅表单]

![图片 B15509_04_58.jpg]

图 4.58:排除 Date edited 字段的审阅表单

相反,可能更谨慎的做法是限制管理字段只包括那些已被明确允许的字段。这是通过 fields 属性实现的。这种方法的优点是,如果模型中添加了新的字段,除非它们被添加到 ModelAdmin 子类的 fields 元组中,否则它们不会在管理表单中可用:

fields = ('content', 'rating', 'creator', 'book')

这将给我们之前看到的结果。

另一个选项是使用 ModelAdmin 子类的 fieldsets 属性来指定表单布局为一系列分组字段。fieldsets 中的每个分组由一个标题后跟一个包含一个指向字段名称字符串列表的 'fields' 键的字典组成:

    fieldsets = (('Linkage', {'fields': ('creator', 'book')}),\
                 ('Review content', \
                   {'fields': ('content', 'rating')}))

审阅表单应该看起来如下:

![图 4.59:带有字段集的评审表单]

![图片 B15509_04_59.jpg]

图 4.59:带有字段集的评审表单

如果我们想在字段集中省略标题,我们可以通过将其值设置为 None 来实现:

    fieldsets = ((None, {'fields': ('creator', 'book')}),\
                 ('Review content', \
                   {'fields': ('content', 'rating')}))

现在,评审表单应该如以下截图所示:

![图 4.60:带有未命名第一个字段集的评审表单]

![图片 B15509_04_60.jpg]

图 4.60:带有未命名第一个字段集的评审表单

活动四.02:自定义模型管理员

在我们的数据模型中,Contributor 类用于存储书籍贡献者的数据--他们可以是作者、贡献者或编辑。这个活动侧重于修改 Contributor 类并添加一个 ContributorAdmin 类以提高管理员应用程序的用户友好性。目前,Contributor 变更列表默认基于在 第二章模型和迁移 中创建的 __str__ 方法,基于单个列 FirstNames。我们将探讨一些表示的替代方法。这些步骤将帮助您完成活动:

  1. 编辑 reviews/models.py 以向 Contributor 模型添加额外的功能。

  2. Contributor 添加一个不带参数的 initialled_name 方法(类似于 Book.isbn13 方法)。

  3. initialled_name 方法将返回一个包含 Contributor.last_names 后跟一个逗号和给定名字首字母的字符串。例如,对于一个 Contributor 对象,其 first_namesJerome Davidlast_namesSalingerinitialled_name 将返回 Salinger, JD

  4. Contributor__str__ 方法替换为一个调用 initialled_name() 的方法。

    到目前为止,Contributors 显示列表将看起来像这样:

    ![图 4.61:贡献者显示列表]

    ![图片 B15509_04_61.jpg]

    图 4.61:贡献者显示列表

  5. reviews/admin.py 中添加一个 ContributorAdmin 类。它应该继承自 admin.ModelAdmin

  6. 修改它,以便在 Contributors 变更列表中,记录以两个可排序的列(Last NamesFirst Names)显示。

  7. 添加一个搜索栏,用于搜索“姓氏”和“名字”。修改它,使其只匹配“姓氏”的开头。

  8. Last Names 上添加一个过滤器。

通过完成这个活动,你应该能够看到如下内容:

![图 4.62:预期输出]

![图片 B15509_04_62.jpg]

图 4.62:预期输出

像这样的更改可以提高管理员用户界面的功能。通过将 Contributors 变更列表中的 First NamesLast Names 作为单独的列实现,我们为用户提供了在任一字段上排序的选项。通过考虑在搜索检索和筛选选择中最有用的列,我们可以提高记录的高效检索。

注意

这个活动的解决方案可以在 packt.live/2Nh1NTJ 找到。

摘要

在本章中,我们了解了如何通过 Django 命令行创建超级用户,以及如何使用它们来访问管理应用。在简要浏览了管理应用的基本功能后,我们探讨了如何将我们的模型注册到其中,以生成我们数据的 CRUD 界面。

然后,我们学习了如何通过修改全局特性来细化这个界面。我们通过在管理站点上注册自定义模型管理类来改变管理应用向用户展示模型数据的方式。这使得我们能够对我们的模型界面进行细致的调整。这些修改包括通过添加额外的列、过滤器、日期层次结构和搜索栏来自定义变更列表页面。我们还通过分组和排除字段来修改模型管理页面的布局。

这只是对管理应用功能的一个非常浅显的探索。我们将在第十章“高级 Django 管理及定制”中重新审视AdminSiteModelAdmin的丰富功能。但首先,我们需要学习更多 Django 的中间级特性。在下一章中,我们将学习如何从 Django 应用中组织和提供静态内容,例如 CSS、JavaScript 和图片。

第五章:5. 提供静态文件服务

概述

在本章中,您将首先学习静态响应和动态响应之间的区别。然后您将看到 Django 的 staticfiles 应用如何帮助管理静态文件。在继续对 Bookr 应用进行工作的同时,您将使用图像和 CSS 来增强它。您将了解您可以为项目布局静态文件的不同方式,并检查 Django 如何为生产部署合并它们。Django 包括在模板中引用静态文件的工具,您将看到这些工具如何帮助减少将应用程序部署到生产所需的工作量。在此之后,您将探索 findstatic 命令,它可以用来调试静态文件的问题。稍后,您将了解如何为远程服务编写存储静态文件的代码的概述。最后,您将了解缓存网络资产以及 Django 如何帮助进行缓存失效。

简介

一个仅包含纯 超文本标记语言HTML)的 Web 应用程序相当有限。我们可以使用 层叠样式表CSS)和图像来增强网页的外观,并且我们可以通过 JavaScript 添加交互。我们将所有这些类型的文件称为“静态文件”。它们被开发并作为应用程序的一部分部署。我们可以将它们与动态响应进行比较,动态响应是在请求实时生成时产生的。您所编写的所有视图都通过渲染模板生成动态响应。请注意,我们不会将模板视为静态文件,因为它们不是直接发送给客户端;相反,它们首先被渲染,然后作为动态响应的一部分发送。

在开发期间,静态文件是在开发者的机器上创建的,然后必须将它们移动到生产 Web 服务器。如果您必须在短时间内(比如几个小时)转移到生产,那么收集所有静态资产、将它们移动到正确的目录并将它们上传到服务器可能会很耗时。当使用其他框架或语言开发 Web 应用程序时,您可能需要手动将所有静态文件放入您的 Web 服务器托管的特定目录。更改提供静态文件的 URL 可能意味着在整个代码中更新值。

Django 可以帮助我们管理静态资产,使这个过程更加简单。它提供了在开发期间使用其开发服务器提供这些文件的工具。当您的应用程序进入生产阶段时,它还可以收集所有您的资产并将它们复制到一个文件夹中,由专门的 web 服务器托管。这允许您在开发期间以有意义的方式将静态文件分离,并在部署时自动打包。

此功能由 Django 内置的 staticfiles 应用提供。它为处理和提供静态文件添加了几个有用的功能:

  • static 模板标签用于自动构建资产的静态 URL 并将其包含在您的 HTML 中。

  • 一个名为 static 的视图,用于在开发中提供静态文件。

  • 静态文件查找器用于自定义在文件系统上查找资源的位置。

  • collectstatic 管理命令,用于查找所有静态文件并将它们移动到单个目录以进行部署。

  • findstatic 管理命令,用于显示针对特定请求加载的磁盘上的哪个静态文件。这也有助于调试如果某个文件没有被加载的情况。

在本章的练习和活动中,我们将向 Bookr 应用程序添加静态文件(图像和 CSS)。每个文件在开发过程中都将存储在 Bookr 项目目录中。我们需要为每个文件生成一个 URL,以便模板可以引用它们,浏览器可以下载它们。一旦生成了 URL,Django 需要提供这些文件。当我们部署 Bookr 应用程序到生产环境时,所有静态文件都需要被找到并移动到可以被生产 Web 服务器提供服务的目录。如果存在未按预期加载的静态文件,我们需要某种方法来确定原因。

为了简化,让我们以单个静态文件为例:logo.png。我们将简要介绍上一段中提到的每个功能的角色,并在本章中深入解释:

  • 使用 static 模板标签将文件名转换为模板中可用的 URL 或路径,例如,从 logo.png 转换为 /static/logo.png

  • static 视图接收一个请求,加载路径为 /static/logo.png 的静态文件。它读取文件并将其发送到浏览器。

  • 静态文件查找器(或简称 finder)被 static 视图用来在磁盘上定位静态文件。有不同的查找器,但在这个例子中,查找器只是将 URL 路径 /static/logo.png 转换为磁盘上的路径 bookr/static/logo.png

  • 在部署到生产环境时,使用 collectstatic 管理命令。这将把 logo.png 文件从 bookr 项目目录复制到 Web 服务器目录,例如 /var/www/bookr/static/logo.png

  • 如果一个静态文件无法正常工作(例如,请求它返回一个 404 Not Found 响应,或者正在提供错误的文件),那么我们可以使用 findstatic 管理命令来尝试确定原因。此命令接受文件名作为参数,并将输出搜索过的目录以及它能够定位请求的文件的位置。

这些是日常使用中最常见的功能,但我们还将讨论其他一些功能。

静态文件提供

在介绍中,我们提到 Django 包含一个名为 static 的视图函数,用于提供静态文件。关于静态文件提供的一个重要观点是,Django 并不打算在生产环境中提供这些文件。这不是 Django 的角色,在生产环境中,Django 将拒绝提供静态文件。这是正常且预期的行为。如果 Django 只是读取文件系统并发送文件,那么它在这方面没有比普通 Web 服务器更多的优势,而普通 Web 服务器在此任务上可能表现得更好。此外,如果您使用 Django 提供静态文件,那么在请求期间您将保持 Python 进程忙碌,它将无法处理它更适合处理的动态请求。

由于这些原因,Django 的 static 视图仅设计用于开发期间使用,如果您的 DEBUG 设置为 False,则不会工作。由于在开发期间我们通常只有一个人(开发者)访问网站,因此 Django 可以提供静态文件。我们很快将讨论更多关于 staticfiles 应用如何支持生产部署的内容。整个生产部署过程将在第十七章 Django 应用程序的部署(第一部分 – 服务器设置) 中介绍。您可以从本书的 GitHub 仓库下载此章节,网址为 packt.live/2Kx6FmR

当运行 Django 开发服务器时,如果您的 settings.py 文件满足以下条件,则会自动设置一个指向 static 视图的 URL 映射:

  • DEBUG 设置为 True

  • 在其 INSTALLED_APPS 中包含 'django.contrib.staticfiles'

这两个设置默认都存在。

创建的 URL 映射大致等同于在您的 urlpatterns 中有以下的映射:

path(settings.STATIC_URL, django.conf.urls.static)

任何以 settings.STATIC_URL(默认为 /static/)开头的 URL 都会被映射到 static 视图。

注意

即使没有在 INSTALLED_APPS 中包含 staticfiles,您也可以使用 static 视图,但您必须手动设置等效的 URL 映射。

静态文件查找器简介

Django 需要在磁盘上定位静态文件的三种情况,为此,它使用一个 静态文件查找器。静态文件查找器可以被认为是一个插件。它是一个实现将 URL 路径转换为磁盘路径的方法的类,并遍历项目目录以查找静态文件。

当 Django 需要在磁盘上定位静态文件时,第一次是在 Django static 视图接收到请求以加载特定的静态文件时;然后它需要将 URL 中的路径转换为磁盘上的位置。例如,URL 的路径是 /static/logo.png,它被转换为磁盘上的路径 bookr/static/logo.png。正如我们在上一节中提到的,这仅在开发期间。在生产服务器上,Django 不应接收此请求,因为它将由 Web 服务器直接处理。

第二次是在使用 collectstatic 管理命令时。这会将项目目录中的所有静态文件收集起来,并将它们复制到单个目录中,以便由生产 Web 服务器提供服务。例如,bookr/static/logo.png 将被复制到 Web 服务器根目录,如 /var/www/bookr/static/logo.png。静态文件查找器包含用于在项目目录内定位所有静态文件的代码。

最后一次使用静态文件查找器是在执行 findstatic 管理命令期间。这与第一次使用类似,它接受一个静态文件的名称(例如 logo.png),但它将完整路径(bookr/static/logo.png)输出到终端,而不是加载文件内容。

Django 随带一些内置的查找器,但如果你想要将静态文件存储在自定义目录布局中,你也可以编写自己的查找器。Django 使用的查找器列表由 settings.py 中的 STATICFILES_FINDERS 设置定义。在本章中,我们将分别在第 AppDirectoriesFinderFileSystemFinder 节中介绍默认静态文件查找器 AppDirectoriesFinderFileSystemFinder 的行为。

注意

如果你查看 settings.py,你不会看到默认定义的 STATICFILES_FINDERS 设置。这是因为 Django 将使用其内置的默认设置,该设置定义为列表 ['django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder']。如果你要将 STATICFILES_FINDERS 设置添加到你的 settings.py 文件中,以包含一个自定义查找器,确保如果你在使用它们,要包括这些默认设置。

首先,我们将讨论静态文件查找器及其在第一种情况下的使用——响应请求。然后,我们将介绍一些更多概念,并返回到 collectstatic 的行为以及它是如何使用静态文件查找器的。在章节的后面部分,我们将使用 findstatic 命令来了解如何使用它。

静态文件查找器:在请求期间使用

当 Django 收到对静态文件的请求(记住,Django 只在开发期间提供静态文件服务)时,将查询所有已定义的静态文件查找器,直到在磁盘上找到文件。如果所有查找器都无法定位到文件,则 static 视图将返回 HTTP 404 Not Found 响应。

例如,请求的 URL 可能类似于 /static/main.css/static/reviews/logo.png。每个查找器将依次查询来自 URL 的路径,并将返回一个路径,例如第一个文件为 bookr/static/main.css,第二个文件为 bookr/reviews/static/reviews/logo.png。每个查找器将使用自己的逻辑将 URL 路径转换为文件系统路径——我们将在即将到来的 AppDirectoriesFinderFileSystemFinder 节中讨论这种逻辑。

AppDirectoriesFinder

AppDirectoriesFinder 类用于在每个应用目录中查找名为 static 的目录内的静态文件。应用必须在 settings.py 文件中的 INSTALLED_APPS 设置中列出(我们在 第一章Django 简介 中这样做过)。正如我们在 第一章Django 简介 中提到的,应用自包含是很好的。通过让每个应用都有自己的 static 目录,我们也可以通过在应用目录中存储特定于应用的静态文件来继续自包含设计。

在我们使用 AppDirectoriesFinder 之前,我们将解释如果多个静态文件具有相同的名称可能会出现的问题,以及如何解决这个问题。

静态文件命名空间

静态文件查找器:请求期间使用 部分,我们讨论了提供名为 logo.png 的文件。这将提供 reviews 应用的标志。文件名(logo.png)可能非常常见——你可以想象,如果我们添加一个 store 应用(用于购买书籍),它也将有一个标志。更不用说第三方 Django 应用可能也想使用像 logo.png 这样的通用名称。我们即将描述的问题可能适用于任何具有通用名称的静态文件,例如 styles.cssmain.js

让我们考虑 reviewsstore 的例子。我们可以在这些应用中的每个应用中添加一个 static 目录。然后,每个 static 目录都会有一个 logo.png 文件(尽管它将是不同的标志)。目录结构如图 5.1 所示:

图 5.1:包含静态目录的应用目录布局

](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/webdev-dj-2e/img/B15509_05_01.jpg)

图 5.1:包含静态目录的应用目录布局

我们用来下载静态文件的 URL 路径是相对于静态目录的。因此,如果我们对 /static/logo.png 发起 HTTP 请求,就不知道引用的是哪个 logo.png 文件。Django 将依次检查每个应用中的 static 目录(按照 INSTALLED_APPS 设置中指定的顺序)。它找到的第一个 logo.png 文件,就会提供服务。在这个目录布局中,没有方法可以指定你想要加载的 logo.png 文件。

我们可以通过将 static 目录命名为与应用相同的名称来解决这个问题。reviews 应用在其 static 目录内有一个名为 reviews 的目录,而 store 应用在其 static 目录内有一个名为 store 的目录。相应的 logo.png 文件随后被移动到这些子目录中。新的目录布局如图 5.2 所示:

图 5.2:命名空间目录布局

](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/webdev-dj-2e/img/B15509_05_02.jpg)

图 5.2:命名空间目录布局

要加载特定文件,我们还包括了命名空间目录。对于reviews标志,URL 路径是/static/reviews/logo.png,它在磁盘上映射到bookr/reviews/static/review/logo.png。同样,对于商店标志,其路径是/static/store/logo.png,它在磁盘上映射到bookr/store/static/store/logo.png。你可能已经注意到,logo.png文件的示例路径已经在静态文件查找器:请求期间使用部分中进行了命名空间。

注意

如果你正在考虑编写可能作为独立插件发布的 Django 应用,你可以使用更明确的子目录名称。例如,选择一个包含整个点分项目路径的名称:bookr/reviews/static/bookr.reviews。然而,在大多数情况下,子目录名称仅对项目本身是唯一的就足够了。

现在我们已经介绍了AppDirectoriesFinder和静态文件命名空间,我们可以使用它们来提供我们的第一个静态文件。在本章的第一个练习中,我们将创建一个新的 Django 项目用于基本的商业网站。然后,我们将从这个项目中创建的名为landing的应用中提供标志文件。AppDirectoriesFinder类用于在每个应用目录中查找名为static的目录中的静态文件。应用必须在settings.py文件中的INSTALLED_APPS设置中列出。正如我们在第一章Django 简介中提到的,应用自包含是好的。通过让每个应用都有自己的static目录,我们也可以通过在应用目录中存储特定应用的静态文件来继续自包含设计。

提供静态文件最简单的方法是从应用目录中提供。这是因为我们不需要进行任何设置更改。相反,我们只需要在正确的目录中创建文件,它们将使用默认的 Django 配置提供。

商业网站项目

对于本章的练习,我们将创建一个新的 Django 项目,并使用它来演示静态文件的概念。该项目将是一个基本的商业网站,有一个带有标志的简单着陆页。该项目将有一个名为landing的应用。

你可以参考第一章创建项目和应用程序,并启动开发服务器Django 简介中的练习 1.01,以刷新你对创建 Django 项目的记忆。

练习 5.01:从应用目录提供文件

在这个练习中,你将为landing应用添加一个标志文件。这需要将一个logo.png文件放入landing应用目录内的static目录中。完成此操作后,你可以测试静态文件是否正确提供,并确认提供它的 URL:

  1. 首先创建新的 Django 项目。您可以重复使用已经安装了 Django 的bookr虚拟环境。打开一个新的终端并激活虚拟环境(有关如何创建和激活虚拟环境的说明,请参阅前言)。然后,在终端(或命令提示符)中运行django-admin命令以启动名为business_site的 Django 项目。为此,运行以下命令:

    django-admin startproject business_site
    

    将不会有任何输出。此命令将在名为business_site的新目录中构建 Django 项目。

  2. 通过使用startapp管理命令在此项目中创建一个新的 Django 应用。该应用应命名为landing。为此,请进入business_site目录,然后运行以下命令:

    python3 manage.py startapp landing
    

    注意,将不会再次有任何输出。该命令将在business_site目录内创建名为landing的应用目录。

    注意

    记住,在 Windows 上,命令是python manage.py startapp landing

  3. 启动 PyCharm,然后打开business_site目录。如果您已经打开了一个项目,可以通过选择File -> Open来做到这一点;否则,只需在“欢迎使用 PyCharm”窗口中单击Open。导航到business_site目录,选择它,然后单击Openbusiness_site项目窗口应类似于图 5.3

    注意

    有关如何设置和配置 PyCharm 以与 Django 项目一起工作的详细说明,请参阅练习 1.02PyCharm 中的项目设置,位于第一章Django 简介

    图 5.3:business_site 项目

    图 5.3:business_site 项目

  4. 创建一个新的运行配置来执行项目的manage.py runserver。您可以再次使用bookr虚拟环境。完成操作后,“运行/调试配置”窗口应类似于图 5.4

    注意

    注意,如果您不确定如何在 PyCharm 中配置这些设置,请参阅练习 1.02PyCharm 中的项目设置,来自第一章Django 简介

    图 5.4:运行服务器时的运行/调试配置

    图 5.4:运行服务器时的运行/调试配置

    您可以通过单击Run按钮来测试配置是否设置正确,然后在浏览器中访问http://127.0.0.1:8000/。您应该看到 Django 欢迎屏幕。如果调试服务器无法启动或您看到 Bookr 主页面,那么您可能仍在运行 Bookr 项目。尝试停止 Bookr 的runserver进程(在运行它的终端中按Ctrl + C),然后启动您刚刚设置的新进程。

  5. business_site目录中打开settings.py文件,并将'landing'添加到INSTALLED_APPS设置中。记住我们在第一章Django 简介中的练习 1.05创建模板目录和基本模板步骤 1中学到了如何这样做。

  6. 在 PyCharm 中,在“项目”面板中右键单击landing目录,然后选择New -> Directory

  7. 输入名称 static 并点击 OK:![图 5.5:命名目录 static

    ![img/B15509_05_05.jpg]

    图 5.5:将目录命名为 static

  8. 右键单击您刚刚创建的 static 目录,然后选择 New -> Directory 再次创建目录。

  9. 输入名称 landing 并点击 OK。这是为了实现我们之前讨论的静态文件目录的命名空间:![图 5.6:将新目录命名为 landing,以实现命名空间

    ![img/B15509_05_06.jpg]

    图 5.6:将新目录命名为 landing,以实现命名空间

  10. packt.live/2KM6kfT 下载 logo.png 并将其移动到 landing/static/landing 目录。

  11. 启动 Django 开发服务器(如果尚未运行),然后导航到 http://127.0.0.1:8000/static/landing/logo.png。您应该在浏览器中看到图像正在被提供:![图 5.7:Django 提供的图像

    ![img/B15509_05_07.jpg]

图 5.7:Django 提供的图像

如果您看到的图像与 图 5.7 中的图像相同,您已经正确设置了静态文件服务。现在让我们看看如何自动将此 URL 插入到您的 HTML 代码中。

使用静态模板标签生成静态 URL

练习 5.01 中,从应用目录中提供文件,您设置了 Django 来提供图像文件。您看到图像的 URL 是 http://127.0.0.1:8000/static/landing/logo.png,您可以在 HTML 模板中使用它。例如,要使用 img 标签显示图像,您可以在模板中使用以下代码:

<img src="img/logo.png">

或者,由于 Django 也负责提供媒体文件,并且与动态模板响应有相同的域名,您可以通过只包含路径来简化这个过程,如下所示:

<img src="img/logo.png">

两个地址(URL 和路径)都硬编码到了模板中;也就是说,我们包含了静态文件的完整路径,并假设文件托管的位置。这对于 Django 开发服务器或者如果您将静态文件和 Django 网站托管在同一域名下是可行的。为了在您的网站越来越受欢迎时获得更好的性能,您可能考虑从自己的域名或 内容分发网络CDN)提供静态文件。

注意

CDN 是一种可以为您托管网站的部分或全部内容的服务。它们提供多个 Web 服务器,并且可以无缝地加快您网站的加载速度。例如,他们可能会从地理位置上离用户最近的服务器向用户提供服务。有多个 CDN 提供商,根据它们的设置,它们可能要求您指定一个用于提供静态文件的域名。

以常见的分离方法为例:使用不同的域名来提供静态文件服务。您的主网站托管在 https://www.example.com,但希望从 https://static.example.com 提供静态文件。在开发过程中,我们可以像刚才看到的例子一样,只使用到 logo 文件的路径。但是当我们部署到生产服务器时,我们的 URL 需要更改以包含域名,如下所示:

<img src="img/logo.png">

由于所有链接都是硬编码的,因此每次部署到生产环境时,都需要在我们的模板中的每个 URL 上执行此操作。然而,一旦它们被更改,这些 URL 在 Django 开发服务器上就不再有效。幸运的是,Django 提供了一个解决方案来解决这个问题。

staticfiles应用提供了一个模板标签static,用于在模板中动态生成静态文件的 URL。由于所有 URL 都是动态生成的,我们可以通过更改一个设置(settings.py中的STATIC_URL)来更改所有这些 URL。此外,稍后我们将介绍一种基于使用static模板标签的方法来使浏览器缓存静态文件失效。

static标签非常简单:它接受一个单一参数,即静态资产的工程相对路径。然后,它将输出这个路径,并在其前面加上STATIC_URL设置。它必须首先使用{% load static %}模板标签将其加载到模板中。

Django 提供了一套默认的模板标签和过滤器(或标签集),它自动将这些标签集提供给每个模板。Django(以及第三方库)还提供了不是自动加载的标签集。在这些情况下,我们需要在可以使用它们之前将这些额外的模板标签和过滤器加载到模板中。这是通过使用load模板标签来完成的,它应该在模板的开始附近(尽管如果使用了extends模板标签,它必须在之后)。load模板标签接受一个或多个要加载的包/库,例如:

{% load package_one package_two package_three %}

这将加载由(虚构的)package_onepackage_twopackage_three包提供的模板标签和过滤器集。

load模板标签必须在需要加载包的实际模板中使用。换句话说,如果你的模板扩展了另一个模板,并且基本模板已经加载了某个包,那么你的依赖模板不会自动访问该包。你的模板仍然需要使用load标签来加载该包以访问新的标签集。static模板标签不是默认集的一部分,这就是为什么我们需要加载它的原因。

然后,它可以在模板文件中的任何位置使用。例如,默认情况下,Django 使用/static/作为STATIC_URL。如果我们想为我们的logo.png文件生成静态 URL,我们可以在模板中使用如下标签:

{% static landing/logo.png' %}

模板内的输出将是这样的:

/static/landing/logo.png

通过示例可以使它更清晰,所以让我们看看static标签如何用于生成多个不同资产的 URL。

我们可以使用img标签在页面上包含一个图像作为标志,如下所示:

<img src="img/logo.png' %}">

这在模板中的渲染如下所示:

<img src="img/logo.png">

或者,我们可以使用static标签来生成链接 CSS 文件的 URL,如下所示:

<link href="{% static 'path/to/file.css' %}" 
            rel="stylesheet">

这将被渲染为如下所示:

<link href="/static/path/to/file.css" 
            rel="stylesheet">

它可以在script标签中使用,以包含 JavaScript 文件,如下所示:

<script src="img/file.js' %}">
    </script>

这将被渲染为如下所示:

<script src="img/file.js"></script>

我们甚至可以使用它来生成指向静态文件的下载链接:

<a href="{% static 'path/to/document.pdf' %}">
    Download PDF</a>

注意

注意,这不会生成实际的 PDF 内容;它只会创建一个指向已存在的文件的链接。

它将渲染如下:

<a href="/static/path/to/document.pdf">
    Download PDF</a>

参考这些示例,我们现在可以展示使用static标签而不是硬编码的优势。当我们准备好部署到生产环境时,我们只需在settings.py中更改STATIC_URL的值。模板中的任何值都不需要更改。

例如,我们可以将STATIC_URL改为https://static.example.com/,然后当页面下次渲染时,我们看到的示例将自动更新如下。

以下行显示了这一点,针对图片:

<img src="img/logo.png">

以下是对 CSS 链接的说明:

<link href=
    "https://static.example.com/path/to/files.css" 
    rel="stylesheet">

对于脚本,如下所示:

<script src="
    https://static.example.com/path/to/file.js">
    </script>

最后,以下是对链接的说明:

<a href="
    https://static.example.com/path/to/document.pdf">
    Download PDF</a>

注意,在所有这些示例中,一个字面字符串被作为参数传递(它是引用的)。您也可以使用变量作为参数。例如,假设您正在渲染一个模板,其上下文如下示例代码所示:

def view_function(request):
    context = {"image_file": "logofile.png"}
    return render(request, "example.html", context)

我们正在渲染example.html模板,并使用image_file变量。这个变量的值是logo.png

您将不带引号传递这个变量到static标签中:

<img src="img/{% static image_file %}">

它将渲染成这样(假设我们将STATIC_URL改回/static/):

<img src="img/logo.png">

模板标签也可以与as [variable]后缀一起使用,将结果分配给变量,以便在模板的稍后位置使用。如果静态文件查找需要很长时间,并且您想多次引用相同的静态文件(例如,在多个位置包含图像),这可能会很有用。

第一次引用静态 URL 时,给它一个变量名来分配。在这种情况下,我们创建了logo_path变量:

<img src="img/{% static 'logo.png' as logo_path %}">

这与之前看到的示例渲染相同:

<img src="img/logo.png">

然而,我们可以在模板中稍后再次使用分配的变量(logo_path):

<img src="img/{{ logo_path }}">

这再次渲染相同的内容:

<img src="img/logo.png">

这个变量现在只是模板作用域中的一个普通上下文变量,可以在模板的任何地方使用。但请注意,您可能会覆盖已经定义的变量——尽管这是使用任何分配变量的模板标签时的一个一般警告(例如,{% with %})。

在下一个练习中,我们将把static模板应用到实践中,将 Bookr 评论标志添加到 Bookr 网站上。

练习 5.02:使用静态模板标签

练习 5.01从应用目录中提供文件,您测试了从静态目录提供logo.png文件。在这个练习中,您将继续进行商业网站项目,并创建一个index.html文件作为我们的着陆页模板。然后您将在这个页面上包含标志,使用{% static %}模板标签:

  1. 在 PyCharm 中(确保你处于 business_site 项目),右键单击 business_site 项目目录,创建一个名为 templates 的新文件夹。右键单击此目录,选择 New -> HTML File。选择 HTML 5 file 并将其命名为 index.html图 5.8:新的 index.html

    图 5.8:新的 index.html

  2. index.html 将打开。首先,加载 static 标签库,以便在模板中使用 static 标签。使用 load 模板标签来完成此操作。在文件的第二行(在 <!DOCTYPE html> 之后),添加此行以加载静态库:

    {% load static %}
    
  3. 你还可以通过添加一些额外内容使模板看起来更美观。在 <title> 标签内输入文本 Business Site

    <title>Business Site</title>
    

    然后,在主体内部,添加一个包含文本 Welcome to my Business Site<h1> 元素:

    <h1>Welcome to my Business Site</h1>
    
  4. 在标题文本下方,使用 {% static %} 模板标签设置 <img> 的源。你将使用它来引用来自 练习 5.01从应用目录中提供文件 的标志:

    <img src="img/logo.png' %}">
    
  5. 最后,为了使网站更完整,在 <img> 元素下添加一个 <p> 元素。给它一些关于企业的文本:

    <p>Welcome to the site for my Business. 
        For all your Business needs!</p>
    

    虽然额外的文本和标题并不太重要,但它们给出了如何在使用 {% static %} 模板标签围绕其余内容的方法。保存文件。完成后,它应该看起来像这样:packt.live/37RUVnE

  6. 接下来,设置一个用于渲染模板的 URL。你还将使用内置的 TemplateView 来渲染模板,而无需创建视图。在 business_site 包目录中打开 urls.py 文件。在文件开头,按照以下方式导入 TemplateView

    from django.views.generic import TemplateView
    

    你还可以删除此 Django 管理导入行,因为我们在这个项目中没有使用它:

    from django.contrib import admin
    
  7. /TemplateView 添加一个 URL 映射。TemplateViewas_view 方法接受 template_name 作为参数,它以与传递给 render 函数的路径相同的方式使用。你的 urlpatterns 应该看起来像这样:

    urlpatterns = [path('', TemplateView.as_view\
                            (template_name='index.html')),]
    

    保存 urls.py 文件。完成后,它应该看起来像这样:packt.live/2KLTrlY

  8. 由于我们不是使用 landing 应用模板目录来存储此模板,你需要告诉 Django 使用你在 步骤 1 中创建的 templates 目录。通过在 settings.py 中的 TEMPLATES['DIRS'] 列表中添加该目录来完成此操作。

    business_site 目录中打开 settings.py 文件。向下滚动,直到找到 TEMPLATES 设置。它看起来像这样:

    TEMPLATES = \
    [{'BACKEND': 'django.template.backends.django.DjangoTemplates',\
      'DIRS': [],\
      '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',\
         ],\
      },\
    },]
    

    os.path.join(BASE_DIR, 'templates') 添加到 DIRS 设置中,这样 TEMPLATES 设置看起来就像这样:

    TEMPLATES = \
    [{'BACKEND': 'django.template.backends.django.DjangoTemplates',\
      os module in settings.py. To fix this, at the top of the settings.py file, just add this line:
    
    

    导入 os

    
    Save and close `settings.py`. It should look like this: [`packt.live/3pz4rlo`](http://packt.live/3pz4rlo). 
    
  9. 启动 Django 开发服务器,如果它尚未运行。在浏览器中导航到 http://127.0.0.1:8000/。你应该能看到你的新着陆页面,如图 图 5.9图 5.9:带有标志的网站

图 5.9:带有标志的网站

在这个练习中,我们为 landing 添加了一个基本模板,并将静态库加载到模板中。一旦静态库被加载,我们就能使用 static 模板标签来加载一个图片。然后我们能够在浏览器中看到我们的业务标志被渲染出来。

到目前为止,所有的静态文件加载都使用了 AppDirectoriesFinder,因为它使用它不需要额外的配置。在下一节中,我们将查看 FileSystemFinder,它更灵活,但需要少量配置才能使用。

FileSystemFinder

我们已经学习了 AppDirectoriesFinder,它会在 Django 应用目录内部加载静态文件。然而,设计良好的应用应该是自包含的,因此它们应该只包含它们自己依赖的静态文件。如果我们有其他在网站或不同应用中使用的静态文件,我们应该将它们存储在应用目录之外。

注意

作为一条一般规则,你的 CSS 可能在整个网站上都是一致的,并且可以保存在一个全局目录中。一些图片和 JavaScript 代码可能特定于应用,因此这些将存储在该应用的静态目录中。这只是一般建议:你可以将静态文件存储在项目最有意义的地方。

在我们的业务站点应用中,我们将把 CSS 文件存储在站点静态目录中,因为它不仅会在 landing 应用中使用,随着我们添加更多应用,它还会在整个网站上使用。

Django 通过其 FileSystemFinder 静态文件查找器提供了从任意目录服务静态文件的支持。这些目录可以位于磁盘的任何位置。通常,你会在你的项目目录内部有一个 static 目录,但如果你的公司有一个全局静态目录,它被许多不同的项目(包括非 Django 网络应用)使用,那么你也可以使用它。

FileSystemFinder 使用 settings.py 文件中的 STATICFILES_DIRS 设置来确定搜索静态文件的目录。当项目创建时,这个设置是不存在的,必须由开发者设置。我们将在下一个练习中添加它。构建这个列表有两种选项:

  • 设置目录列表

  • 设置一个元组列表的形式 (prefix, directory)

第二个用例在我们覆盖更多基础知识后更容易理解,所以我们将在解释和演示第一个用例之后返回它。它将在 STATICFILES_DIRS Prefixed Mode 部分的 Exercise 5.04为生产收集静态文件 之后进行说明。现在,我们只解释第一个用例,它只是一个或多个目录的列表。

business_site 中,我们将在项目目录内部添加一个 static 目录(即,在包含 landing 应用和 manage.py 文件的同一目录中)。在构建列表时,我们可以使用 BASE_DIR 设置将其分配给 STATICFILES_DIRS

STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]

我们还在这部分前面提到,你可能想要在这个列表中设置多个目录路径,例如,如果你有一些由多个网络项目共享的公司级静态数据。只需将额外的目录添加到 STATICFILES_DIRS 列表中:

STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static'), \
                    '/Users/username/projects/company-static/']

这些目录将按顺序检查以找到匹配的文件。如果两个目录中都有文件,则首先找到的文件将被提供。例如,如果 static/main.css(在 business_site 项目目录内)和 /Users/username/projects/company-static/bar/main.css 文件都存在,则对 /static/main.css 的请求将提供 business_site 项目的 main.css,因为它在列表中排在第一位。在决定将目录添加到 STATICFILES_DIRS 的顺序时请记住这一点;你可以选择优先考虑项目静态文件或全局文件。

在我们的商业网站(以及稍后的 Bookr)中,我们在这个列表中只会使用一个 static 目录,所以我们不必担心这个问题。

在下一个练习中,我们将添加一个包含 CSS 文件的 static 目录。然后我们将配置 STATICFILES_DIRS 设置以从 static 目录提供。

练习 5.03:从项目静态目录提供

我们已经在 练习 5.01从应用目录中提供文件 中展示了提供特定应用图像文件的示例。现在我们想要提供一个 CSS 文件,该文件将在我们的整个项目中用于设置样式,因此我们将从项目文件夹内的静态目录中提供这个文件。

在这个练习中,你将设置你的项目以从特定目录中提供静态文件,然后再次使用 {% static %} 模板标签将其包含在模板中。这将通过 business_site 示例项目来完成:

  1. 在 PyCharm 中打开 business_site 项目(如果尚未打开)。然后,右键单击 business_site 项目目录(顶级 business_site 目录,而不是 business_site 包目录)并选择 New -> Directory

  2. New Directory 对话框中,输入 static 然后点击 OK

  3. 右键单击你刚刚创建的 static 目录,选择 New -> File

  4. Name New File 对话框中,输入 main.css 并点击 OK

  5. 空的 main.css 文件应该会自动打开。输入一些简单的 CSS 规则,以居中文本并设置字体和背景颜色。将以下文本输入到 main.css 文件中:

    body {
        font-family: Arial, sans-serif;
        text-align: center;
        background-color: #f0f0f0;
    }
    

    你现在可以保存并类 main.css。你可以查看完整的文件以供参考:packt.live/38H8a9N

  6. 打开 business_site/settings.py。在这里,将目录列表设置到 STATICFILES_DIRS 设置中。在这种情况下,列表将只有一个条目。在 settings.py 的底部定义一个新的 STATICFILES_DIRS 变量,使用以下代码:

    STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
    

    settings.py 文件中,BASE_DIR 是一个包含项目目录路径的变量。你可以通过将 staticBASE_DIR 连接来构建你在 步骤 2 中创建的 static 目录的完整路径。然后你将这个路径放入一个列表中。完整的 settings.py 文件应该看起来像这样:packt.live/3hnQQKW

  7. 如果 Django 开发服务器没有运行,请启动它。你可以通过检查是否可以加载 main.css 文件来验证设置是否正确。请注意,这没有命名空间,所以 URL 是 http://127.0.0.1:8000/static/main.css。在浏览器中打开这个 URL 并检查内容是否与你刚刚输入和保存的内容匹配:图 5.10:Django 提供的 CSS

    图 5.10:Django 提供的 CSS

    如果文件没有加载,请检查你的 STATICFILES_DIRS 设置。如果你在修改 settings.py 时 Django 开发服务器正在运行,你可能需要重新启动 Django 开发服务器。

  8. 现在,你需要在索引模板中包含 main.css。在 templates 文件夹中打开 index.html。在关闭 </head> 标签之前,添加以下 <link> 标签来加载 CSS:

    <link rel="stylesheet" href="{% static 'main.css' %}">
    

    这通过使用 {% static %} 模板标签将 main.css 文件链接进来。如前所述,由于 main.css 没有命名空间,你只需包含它的名称即可。保存文件。它应该看起来像这样:packt.live/392aedP

  9. 在你的浏览器中加载 http://127.0.0.1:8000/,你应该会看到背景颜色、字体和对齐方式都发生了变化:图 5.11:使用自定义字体可见的 CSS 应用

图 5.11:使用自定义字体可见的 CSS 应用

你的商业着陆页应该看起来像 图 5.11。由于你在 base.html 模板中包含了 CSS,它将在所有扩展此模板的模板中可用(尽管目前没有,但这是为未来做好规划的好方法)。

在这个练习中,我们将一些 CSS 规则放入它们自己的文件中,并使用 Django 的 FileSystemFinder 来提供它们。这是通过在 business_site 项目目录内创建一个 static 目录并在 Django 设置(settings.py 文件)中使用 STATICFILES_DIRS 设置来实现的。我们使用 static 模板标签将 main.css 文件链接到 base.html 模板中。我们在浏览器中加载了主页面,并看到字体和颜色变化已经应用。

我们现在已经介绍了静态文件查找器在请求期间的使用(在给定 URL 时加载特定的静态文件)。现在我们将看看它们的另一个用例:在运行 collectstatic 管理命令时,查找和复制用于生产部署的静态文件。

静态文件查找器:在 collectstatic 时使用

一旦我们完成对静态文件的工作,它们需要被移动到可以被我们的生产 Web 服务器提供的特定目录中。然后我们可以通过将我们的 Django 代码和静态文件复制到生产 Web 服务器上来部署我们的网站。在business_site的情况下,我们将希望将logo.pngmain.css(以及其他 Django 自身包含的静态文件)移动到一个单独的目录中,以便可以复制到生产 Web 服务器。这就是collectstatic管理命令的作用。

我们已经讨论了 Django 在请求处理期间如何使用静态文件查找器。现在,我们将介绍另一个用例:为部署收集静态文件。运行collectstatic管理命令后,Django 会使用每个查找器列出磁盘上的静态文件。然后,找到的每个静态文件都会被复制到STATIC_ROOT目录(也在settings.py中定义)。这有点像是处理请求的反向操作。不是通过获取 URL 路径并将其映射到文件系统路径,而是将文件系统路径复制到一个前端 Web 服务器可以预测的位置。这允许前端 Web 服务器独立于 Django 处理静态文件的请求。

注意

前端 Web 服务器的设计是为了将请求路由到应用程序(如 Django)或从磁盘读取静态文件。它可以更快地处理请求,但不能像 Django 那样生成动态内容。前端 Web 服务器包括 Apache HTTPD、Nginx 和 lighttpd 等软件。

为了具体说明collectstatic的工作方式,我们将使用来自练习 5.01从应用目录中提供文件,和练习 5.03从项目状态目录中提供的两个文件分别:landing/static/landing/logo.pngstatic/main.css

假设STATIC_ROOT被设置为一个由普通 Web 服务器提供的目录——这可能是类似/var/www/business_site/static的路径。这些文件的存储目的地将分别是/var/www/business_site/static/reviews/logo.png/var/www/business_site/static/main.css

现在当接收到一个静态文件的请求时,由于路径映射是一致的,Web 服务器将能够轻松地提供服务:

  • /static/main.css是从/var/www/business_site/static/main.css文件中提供的。

  • /static/reviews/logo.png是从/var/www/business_site/static/reviews/logo.png文件中提供的。

这意味着 Web 服务器根目录是/var/www/business_site/,静态路径只是以通常方式从磁盘加载,就像 Web 服务器加载文件一样。

我们已经展示了 Django 如何在开发期间定位静态文件并自行提供服务。在生产环境中,我们需要前端 Web 服务器能够不涉及 Django 就提供静态文件,这既是为了安全也是为了速度。

在没有运行 collectstatic 的情况下,网络服务器无法将 URL 映射回路径。例如,它不知道 main.css 必须从项目静态目录加载,而 logo.png 则要从 landing 应用目录加载——它没有关于 Django 目录布局的概念。

你可能会想通过将你的网络服务器根目录设置为这个目录来直接从 Django 项目目录提供文件——请不要这样做。将整个 Django 项目目录共享存在安全风险,因为它会使下载我们的 settings.py 或其他敏感文件成为可能。运行 collectstatic 将文件复制到一个目录,这个目录可以被移动到 Django 项目目录外,到网络服务器根目录,以提高安全性。

到目前为止,我们讨论了使用 Django 直接将静态文件复制到网络服务器根目录的方法。你也可以让 Django 将它们复制到一个中间目录,然后在部署过程中将其移动到 CDN 或其他服务器。我们不会详细介绍具体的部署过程;你如何选择将静态文件复制到网络服务器将取决于你或你公司的现有设置(例如,持续交付管道)。

注意

collectstatic 命令没有考虑到使用 static 模板标签的情况。它将收集 static 目录内所有的静态文件,即使这些文件在你的项目中没有被包含在模板中。

在下一个练习中,我们将看到 collectstatic 命令的实际应用。我们将使用它来将到目前为止所有的 business_site 静态文件复制到一个临时目录中。

练习 5.04:为生产收集静态文件

尽管我们本章不会涉及将应用部署到网络服务器,但我们仍然可以使用 collectstatic 管理命令并查看其结果。在这个练习中,我们将创建一个临时存放位置来存放将要复制的静态文件。这个目录将被命名为 static_production_test,并位于 business_site 项目目录内。作为部署过程的一部分,你可以将这个目录复制到你的生产网络服务器上。然而,由于我们直到第 17 章才会设置网络服务器,即 Django 应用部署(第一部分 – 服务器设置),我们只会检查其内容,以了解文件是如何复制和组织的:

  1. 在 PyCharm 中,创建一个临时目录来存放收集到的文件。右键点击 business_site 项目目录(这是顶级文件夹,不是 business_site 模块),然后选择 新建 -> 目录

  2. 新目录 对话框中,输入名称 static_production_test 并点击 确定

  3. 打开 settings.py 文件,并在文件底部定义一个新的设置 STATIC_ROOT。将其设置为刚刚创建的目录路径:

    STATIC_ROOT = os.path.join(BASE_DIR, 'static_production_test')
    

    这会将static_dirBASE_DIR(业务站点项目路径)连接起来以生成完整路径。保存settings.py文件。它应该看起来像这样:packt.live/2Jq59Cc

  4. 在终端中运行collectstatic manage命令:

    python3 manage.py collectstatic
    

    您应该看到以下类似的输出:

    132 static files copied to \
      '/Users/ben/business_site/static_production_test'.
    

    如果您预期只复制两个文件,这可能会显得很多,但请记住,它将复制所有已安装应用程序的所有文件。在这种情况下,由于您安装了 Django 管理应用程序,所以 132 个文件中的大多数都是为了支持它。

  5. 让我们查看static_production_test目录以查看已创建的内容。此目录的扩展视图(来自 PyCharm 项目页面)如图 5.12 所示,仅供参考。您的应该类似。

    ]

图 5.12:collectstatic 命令的目标目录

您应该注意到里面有三个项目:

cssfontsimgjs

来自您的着陆应用的static目录。其中包含logo.png文件。已创建此目录以匹配我们创建的目录的命名空间。

static目录。由于您没有将其放置在命名空间目录内,因此它已被直接放置在STATIC_ROOT中。

如果你想,可以打开这些文件中的任何一个并验证其内容是否与您刚刚正在工作的文件相匹配——它们应该是匹配的,因为它们只是原始文件的副本。

在这个练习中,我们从business_site(包括 Django 包含的admin静态文件)收集了所有静态文件。它们被复制到由STATIC_ROOT设置定义的目录中(在business_site项目目录中的static_production_test)。我们看到main.css直接在这个文件夹中,但其他静态文件被命名空间在其应用程序目录中(adminreviews)。这个文件夹可以被复制到生产 Web 服务器上以部署我们的项目。

STATICFILES_DIRS 前缀模式

如前所述,STATICFILES_DIRS设置也接受形式为(prefix, directory)的元组项。这些操作模式不是互斥的,STATICFILES_DIRS可以包含非前缀(字符串)或前缀(元组)项。本质上,这允许您将特定的 URL 前缀映射到目录。在 Bookr 中,我们没有足够的静态资源来证明设置这一点的必要性,但如果您想以不同的方式组织静态资源,它可能是有用的。例如,您可以将所有图像保存在某个目录中,所有 CSS 保存在另一个目录中。如果您使用第三方 CSS 生成工具,如使用LESS的 Node.js,可能需要这样做。

注意

LESS 是一个使用 Node.js 的 CSS 预处理器。它允许您使用变量和其他类似编程的概念来编写 CSS,这些概念在原生 CSS 中不存在。然后 Node.js 将其编译为 CSS。更深入的解释超出了本书的范围——简而言之,如果您使用它(或类似的工具),那么您可能希望直接从它保存编译输出的目录提供服务。

解释前缀模式的工作原理的最简单方法是通过一个简短的示例。这将扩展 练习 5.03从项目静态目录中提供服务 中创建的 STATICFILES_DIRS 设置。在这个示例中,向此设置添加了两个前缀目录,一个用于提供图像,另一个用于提供 CSS:

STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static'),\
                    ('images', os.path.join\
                               (BASE_DIR, 'static_images')),\
                    ('css', os.path.join(BASE_DIR, 'static_css'))]

除了已经无前缀提供服务的 static 目录外,我们还在 business_site 项目目录内添加了 static_images 目录的服务。这个前缀是 images。我们还在 Bookr 项目目录内添加了 static_css 目录的服务,前缀是 css

然后,我们可以从 staticstatic_cssstatic_images 目录分别提供 main.jsmain.cssmain.jpg 三个文件。目录布局如图 图 5.13 所示:

图 5.13:用于前缀 URL 的目录布局

图 5.13:用于前缀 URL 的目录布局

在通过 URL 访问这些文件时,映射如图 图 5.14 所示:

图 5.14:基于前缀的 URL 到文件的映射

图 5.14:基于前缀的 URL 到文件的映射

Django 将任何以前缀开头的静态 URL 路由到匹配该前缀的目录。

当使用 static 模板标签时,请使用前缀和文件名,而不是目录名。例如:

{% static 'images/main.jpg' %}

当使用 collectstatic 命令收集静态文件时,它们会被移动到 STATIC_ROOT 内部具有前缀名称的目录中。STATIC_ROOT 目录中的源路径和目标路径如图 图 5.15 所示:

图 5.15:项目目录中的路径映射到 STATIC_ROOT 中的路径

图 5.15:项目目录中的路径映射到 STATIC_ROOT 中的路径

Django 在 STATIC_ROOT 内部创建前缀目录。正因为如此,即使在使用网络服务器且不通过 Django 路由 URL 查找的情况下,路径也可以保持一致。

findstatic 命令

staticfiles 应用程序还提供了一个额外的管理命令:findstatic。此命令允许您输入静态文件的相对路径(与在 static 模板标签内使用的相同)并且 Django 会告诉您该文件的位置。它也可以在详细模式下使用,以输出它正在搜索的目录。

注意

您可能不熟悉详尽性或详细模式的概念。具有更高的详尽性(或简单地打开详细模式)将导致命令生成更多输出。许多命令行应用程序可以以更多或更少的详尽性执行。这有助于尝试调试您正在使用的程序。要查看详细模式的作用示例,您可以尝试以详细模式运行 Python shell。输入 python -v(而不是仅输入 python)并按 Enter。Python 将以详细模式启动并打印出它导入的每个文件的路径。

此命令主要用于调试/故障排除目的。如果加载了错误的文件,或者找不到某个特定文件,您可以使用此命令尝试找出原因。该命令将显示特定路径正在加载的磁盘上的文件,或者通知您文件找不到以及搜索了哪些目录。

这可以帮助解决多个文件具有相同名称且优先级不是您期望的问题。请参阅 FileSystemFinder 部分关于 STATICFILES_DIRS 设置中优先级的一些说明。您还可能看到 Django 没有在您期望的目录中搜索文件,在这种情况下,可能需要将静态目录添加到 STATICFILES_DIRS 设置中。

在下一个练习中,您将执行 findstatic 管理命令,以便熟悉一些输出对于良好(文件正确找到)和不良(文件缺失)场景的含义。

练习 5.05:使用 findstatic 查找文件

您现在将使用各种选项运行 findstatic 命令,并理解其输出的含义。首先,我们将使用它来查找一个存在的文件,并查看它显示文件的路径。然后,我们将尝试查找一个不存在的文件并检查输出的错误。然后,我们将以多个详尽级别和不同的与命令交互的方式重复此过程。虽然这个练习不会对 Bookr 项目进行更改或推进,但熟悉该命令在您需要在自己的 Django 应用程序中工作时很有用:

  1. 打开终端并导航到 business_site 项目目录。

  2. 使用不带选项的 findstatic 命令。它将输出一些帮助信息,解释如何使用:

    python3 manage.py findstatic
    

    帮助输出显示:

    usage: manage.py findstatic 
          [-h] [--first] [--version] [-v {0,1,2,3}]
          [--settings SETTINGS] [--pythonpath PYTHONPATH]
          [--traceback] [--no-color] [--force-color]
          [--skip-checks]
          staticfile [staticfile ...]
    manage.py findstatic: error: Enter at least one label.
    
  3. 您可以一次查找一个或多个文件;让我们从已知的 main.css 文件开始:

    python3 manage.py findstatic main.css
    

    命令将输出 main.css 被找到的路径:

    Found 'main.css' here:
      /Users/ben/business_site/static/main.css
    

    您的完整路径可能不同(除非您也叫 Ben),但您可以看到当 Django 在请求中定位 main.css 时,它将从项目的 static 目录加载 main.css 文件。

    如果您安装的第三方应用程序没有正确命名空间其静态文件并且与您的某个文件冲突,这可能很有用。

  4. 让我们尝试查找一个不存在的文件,logo.png

    python3 manage.py findstatic logo.png
    

    Django 显示错误信息,表示文件找不到:

    No matching file found for 'logo.png'.
    

    Django 无法定位此文件,因为我们已经对其进行了命名空间化——我们必须包含完整的相对路径,就像我们在static模板标签中使用的那样。

  5. 再次尝试查找logo.png,但这次使用完整路径:

    python3 manage.py findstatic landing/logo.png
    

    Django 现在可以找到该文件:

    Found 'landing/logo.png' here:
      /Users/ben/business_site/landing/static/landing/logo.png
    
  6. 同时查找多个文件可以通过将每个文件作为参数添加来完成:

    python3 manage.py findstatic landing/logo.png missing-file.js main.css
    

    每个文件的位置状态都会显示:

    No matching file found for 'missing-file.js'.
    Found 'landing/logo.png' here:
      /Users/ben/business_site/landing/static/landing/logo.png
    Found 'main.css' here:
      /Users/ben/business_site/static/main.css
    
  7. 命令可以以012的详细程度执行。默认情况下,它在详细程度1下执行。要设置详细程度,请使用--verbosity-v标志。将详细程度降低到0以仅输出它找到的路径而不提供任何额外信息。对于缺失的路径不显示错误:

    python3 manage.py findstatic -v0 landing/logo.png missing-file.js main.css
    

    输出仅显示找到的路径——注意对于缺失的文件missing-file.js没有显示错误:

    /Users/ben/business_site/landing/static/landing/logo.png
    /Users/ben/business_site/static/main.css
    

    如果您将输出管道传输到另一个文件或命令,这种详细程度可能很有用。

  8. 要获取更多关于 Django 正在搜索的目录以查找您请求的文件的信息,请将详细程度增加到2

    python3 manage.py findstatic -v2 landing/logo.png missing-file.js main.css
    

    输出包含更多信息,包括已搜索请求文件的目录。您可以看到,随着admin应用的安装,Django 也在 Django admin 应用目录中搜索静态文件:

图 5.16:以详细程度 2 执行 findstatic,显示确切搜索了哪些目录

图 5.16:以详细程度 2 执行 findstatic,显示确切搜索了哪些目录

findstatic命令不是您在处理 Django 时每天都会使用的东西,但在尝试解决静态文件问题时,了解它是有用的。我们看到了命令输出了存在文件的完整路径,以及文件不存在时的错误信息。我们还一次性提供了多个文件并看到了所有文件的信息。最后,我们使用不同级别的详细程度运行了该命令。-v0标志抑制了关于缺失文件的错误。-v1是默认值,显示了找到的路径和错误。使用-v2标志增加详细程度还会打印出正在搜索特定静态文件的目录。

服务器最新文件(用于缓存失效)

如果您不熟悉缓存,基本想法是某些操作可能需要很长时间才能执行。我们可以通过将操作的结果存储在更快访问的地方来加快系统速度,这样在下一次需要它们时,可以快速检索。耗时较长的操作可以是任何事情——从运行时间较长的函数或渲染时间较长的图像,到在互联网上下载时间较长的大型资产。我们对此最后一种情况最感兴趣。

你可能已经注意到,你第一次访问某个特定网站时,它加载很慢,但下次加载就快得多。这是因为你的浏览器已经缓存了网站加载所需的一些(或全部)静态文件。

以我们的商业网站为例,我们有一个包含logo.png文件的页面。第一次访问商业网站时,我们必须下载动态 HTML,它很小且传输速度快。我们的浏览器解析 HTML,并看到应该包含logo.png。然后它可以下载这个文件,这个文件很大,下载可能需要更长的时间。请注意,这个场景假设商业网站现在托管在远程服务器上,而不是在我们的本地机器上——这对我们来说访问速度非常快。

如果网页服务器设置正确,浏览器会将logo.png存储在计算机上。下次我们访问着陆页(或者确实任何包含logo.png的页面),你的浏览器会识别 URL,可以从磁盘加载文件,而不是再次下载,从而加快浏览体验。

注意

我们说浏览器会缓存“如果网页服务器设置正确”。这是什么意思?前端网页服务器应该配置为在静态文件响应中发送特殊的 HTTP 头。它可以发送一个Cache-Control头,它可以有如no-cache(文件永远不应被缓存;换句话说,每次都应该请求最新版本)或max-age=<seconds>(只有在最后检索超过<seconds>秒之前,文件才应再次下载)。响应还可以包含一个Expires头,其值为日期。一旦达到这个日期,文件就被认为是“过时”的,此时应该请求新版本。

计算机科学中最难的问题之一是缓存失效。例如,如果我们更改logo.png,我们的浏览器如何知道它应该下载新版本呢?唯一确定知道它已更改的方法是再次下载文件,并与我们已保存的版本进行比较。当然,这会违背缓存的目的,因为我们每次文件更改时(或没有更改)仍然会下载。我们可以缓存任意时间或服务器指定的时间,但如果静态文件在时间到达之前已更改,我们就不知道。我们会使用旧版本,直到我们认为它已过期,那时我们会下载新版本。如果我们有一个 1 周的过期时间,而静态文件在第二天更改,我们仍然会使用旧版本 6 天。当然,如果你想要强制重新下载所有静态资源,浏览器可以被设置为不使用缓存来重新加载页面(具体如何操作取决于浏览器,例如Shift + F5Cmd + Shift + R)。

没有必要尝试缓存我们的动态响应(渲染的模板)。由于它们被设计成动态的,我们希望确保用户在每次页面加载时都能获得最新版本,因此它们不应该被缓存。它们的大小也相当小(与图像等资产相比),因此缓存它们时速度优势不大。

Django 提供了一个内置的解决方案。在 collectstatic 阶段,当文件被复制时,Django 可以将文件内容的哈希值附加到文件名上。例如,logo.png 源文件将被复制到 static_production_test/landing/logo.f30ba08c60ba.png。这仅在使用 ManifestFilesStorage 存储引擎时才会这样做。由于文件名仅在内容更改时才会更改,因此浏览器将始终下载新的内容。

使用 ManifestFilesStorage 是使缓存失效的一种方法。可能还有其他更适合您应用程序的选项。

注意

哈希是一个单向函数,它根据输入的长度生成一个固定长度的字符串。有几种不同的哈希函数可用,Django 使用的是 a0cc175b9c0f1b6a831c399e269772661。字符串(一个更长的字符串)的 MD5 哈希是 69fc4316c18cdd594a58ec2d59462b97。它们都是 32 个字符长。

通过更改 settings.py 中的 STATICFILES_STORAGE 值来选择存储引擎。这是一个指向要使用的模块和类的点分隔路径的字符串。实现哈希添加功能的类是 django.contrib.staticfiles.storage.ManifestStaticFilesStorage

使用此存储引擎不需要对您的 HTML 模板进行任何更改,前提是您使用 static 模板标签包含静态资产。Django 生成一个包含原始文件名和哈希文件名之间映射的清单文件(staticfiles.json,JSON 格式)。当使用 static 模板标签时,它将自动插入哈希文件名。如果您没有使用 static 标签而是手动插入静态 URL,那么浏览器将尝试加载非哈希路径,并且当缓存应该失效时,URL 不会自动更新。

例如,我们在这里使用 static 标签包含 logo.png

<img src="img/logo.png' %}">

当页面被渲染时,最新哈希值将从 staticfiles.json 中检索,输出将如下所示:

<img src="img/logo.f30ba08c60ba.png">

如果我们没有使用 static 标签,而是硬编码路径,它将始终按原样显示:

<img src="img/logo.png">

由于这不含哈希值,我们的浏览器将不会看到路径变化,因此永远不会尝试下载新文件。

在运行 collectstatic 时,Django 保留带有旧哈希值的文件的前一个版本,因此旧版本的应用程序在需要时仍然可以引用它。文件的最新版本也以不带哈希的形式复制,这样非 Django 应用程序可以引用它而无需查找哈希。

在下一个练习中,我们将更改项目设置以使用 ManifestFilesStorage 引擎,然后运行 collectstatic 管理命令。这将复制所有静态资产,就像在 练习 5.04为生产收集静态文件 中一样;然而,现在它们的哈希将包含在文件名中。

练习 5.06:探索 ManifestFilesStorage 存储引擎

在这个练习中,您将临时更新 settings.py 以使用 ManifestFilesStorage,然后运行 collectstatic 以查看文件如何生成带有哈希的文件:

  1. 在 PyCharm 中(仍在 business_site 项目中),打开 settings.py。在文件底部添加一个 STATICFILES_STORAGE 设置:

    STATICFILES_STORAGE = \
    'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
    

    完成的文件应如下所示:packt.live/2Jq59Cc

  2. 打开终端并导航到 business_site 项目目录。像之前一样运行 collectstatic 命令:

    python3 manage.py collectstatic
    

    如果您的 static_production_test 目录不为空(这可能是文件在 练习 5.04为生产收集静态文件 期间被移动到那里的情况),则您将提示允许覆盖现有文件:

    ![图 5.17:在 collectstatic 期间允许覆盖的提示]

    ![图片 B15509_05_17.jpg]

    0 static files copied to '/Users/ben/business_site
      /static_production_test', 132 unmodified, 
      28 post-processed.
    

    由于自上次运行 collectstatic 以来您没有更改任何文件,因此没有文件被复制。相反,Django 只是后处理文件(28 个文件),即生成它们的哈希并附加到文件名。

    静态文件已按之前的方式复制到 static_production_test 目录;然而,现在每个文件都有两个副本:一个带有哈希,一个不带。

    static/main.css 已被复制到 static_production_test/main.856c74fb7029.css(如果您的 CSS 文件内容不同,例如包含额外的空格或换行符,则此文件名可能不同):

    ![图 5.18:带有哈希文件名的 expanded static_production_test 目录]

    ![图片 B15509_05_18.jpg]

    图 5.18:带有哈希文件名的 expanded static_production_test 目录

    图 5.18 展示了 expanded static_production_test 目录布局。您可以看到每个静态文件有两个副本以及 staticfiles.json 清单文件。以 logo.png 为例,您可以看到 landing/static/landing/logo.png 已被复制到与 static_production_test/landing/logo.ba8d3d8fe184.png 相同的目录。

  3. 让我们对 main.css 文件进行更改,看看哈希如何变化。在文件末尾添加一些空白行然后保存。这不会改变 CSS 的效果,但文件的变化将影响其哈希。在终端中重新运行 collectstatic 命令:

    python3 manage.py collectstatic
    

    再次提醒,您可能需要输入 yes 以确认覆盖:

    You have requested to collect static files at the \
      destination location as specified in your settings:
        /Users/ben/business_site/static_production_test
    This will overwrite existing files!
    Are you sure you want to do this?
    Type 'yes' to continue, or 'no' to cancel: yes
    1 static file copied to '/Users/ben/business_site\
      /static_production_test', 131 unmodified, 28 post-processed.
    

    由于只有一个文件被更改,因此只复制了一个静态文件(main.css)。

  4. 再次查看 static_production_test 目录。你应该会看到保留了带有旧哈希的旧文件,并添加了一个带有新哈希的新文件:![图 5.19:添加了另一个带有最新哈希的 main.css 文件 图片

    图 5.19:添加了另一个带有最新哈希的 main.css 文件

    在这种情况下,我们有 main.856c74fb7029.css(现有)、main.df1234ac4e63.css(新)和 main.css。你的哈希可能不同。

    main.css 文件(无哈希)始终包含最新内容;也就是说,main.df1234ac4e63.cssmain.css 文件的内容是相同的。在执行 collectstatic 命令期间,Django 将复制带有哈希和不带哈希的文件。

  5. 现在检查 Django 生成的 staticfiles.json 文件。这是允许 Django 从正常路径查找哈希路径的映射。打开 static_production_test/staticfiles.json。所有内容可能都在一行中;如果这样的话,请从 视图 菜单 -> 活动编辑器 -> 软换行 启用文本软换行。滚动到文件末尾,你应该会看到一个 main.css 文件的条目,例如:

    "main.css": "main.df1234ac4e63.css"
    

    这就是 Django 如何在模板中使用 static 模板标签时填充正确的 URL:通过查找映射文件中的哈希路径。

  6. 我们已经完成了对 business_site 的使用,我们只是用它来测试。你可以删除项目或者保留它作为活动期间的参考。

    注意

    不幸的是,我们无法检查哈希 URL 在模板中的插值方式,因为在调试模式下运行时,Django 不会查找文件的哈希版本。正如我们所知,Django 开发服务器只在调试模式下运行,所以如果我们关闭调试模式来尝试查看哈希插值,那么 Django 开发服务器将无法启动。当使用前端 Web 服务器进入生产环境时,你需要自己检查这个插值。

在这个练习中,我们通过在 settings.py 中添加 STATICFILES_STORAGE 设置来配置 Django 使用 ManifestFilesStorage 作为其静态文件存储。然后我们执行了 collectstatic 命令来查看哈希是如何生成并添加到复制文件的文件名中的。我们看到了名为 staticfiles.json 的清单文件,它存储了从原始路径到哈希路径的查找。最后,我们清理了在本练习和 练习 5.04为生产收集静态文件 中添加的设置和目录。这些是 STATIC_ROOT 设置、STATICFILES_STORAGE 设置和 static_product_test 目录。

自定义存储引擎

在上一节中,我们将存储引擎设置为 ManifestFilesStorage。这个类由 Django 提供,但也可以编写自定义存储引擎。例如,你可以编写一个存储引擎,在运行 collectstatic 时将你的静态文件上传到 CDN、Amazon S3 或 Google Cloud 存储桶。

编写自定义存储引擎超出了本书的范围。已经存在支持上传到各种云服务的第三方库;其中一个这样的库是django-storages,可以在django-storages.readthedocs.io/找到。

以下代码是一个简短的框架,指示你应该实现哪些方法来创建自定义文件存储引擎:

from django.conf import settings
from django.contrib.staticfiles import storage
class CustomFilesStorage(storage.StaticFilesStorage):
    def __init__(self):
    """
    The class must be able to be instantiated 
    without any arguments.
    Create custom settings in settings.py and read them instead.
    """
    self.setting = settings.CUSTOM_STORAGE_SETTING

该类必须能够不带任何参数进行实例化。__init__函数必须能够从全局标识符(在这种情况下,从我们的 Django 设置)中加载任何设置:

  def delete(self, name):
    """
    Implement delete of the file from the remote service.
    """

此方法应能够从远程服务中删除由name参数指定的文件:

    def exists(self, name):
    """
    Return True if a file with name exists in the remote service.
    """

此方法应查询远程服务以检查由名称指定的文件是否存在。如果文件存在,则返回True,如果不存在,则返回False

    def listdir(self, path):
    """
    List a directory in the remote service. Return should 
    be a 2-tuple of lists, the first a list of directories, 
    the second a list of files.
    """

此方法应查询远程服务以列出path处的目录。然后它应返回一个包含两个列表的 2 元组。第一个元素应该是path内的目录列表,第二个元素应该是文件列表。例如:

return (['directory1', 'directory2'], \
        ['code.py', 'document.txt', 'image.jpg'])

如果path不包含目录或文件,则应返回一个空列表。如果目录为空,则返回两个空列表:

    def size(self, name):
    """
    Return the size in bytes of the file with name.
    """

此方法应查询远程服务并获取由name指定的文件大小:

  def url(self, name):
  """
  Return the URL where the file of with name can be 
  access on the remote service. For example, this 
  might be URL of the file after it has been uploaded 
  to a specific remote host with a specific domain.
  """

此方法应确定访问由name指定的文件的 URL。这可以通过将name附加到特定的静态托管 URL 来构建:

  def _open(self, name, mode='rb'):
  """
  Return a File-like object pointing to file with 
  name. For example, this could be a URL handle for 
  a remote file.
  """

此方法将提供一个远程文件句柄,由name指定。你的实现将取决于远程服务的类型。你可能需要下载文件,然后使用内存缓冲区(例如io.BytesIO对象)来模拟文件的打开:

    def _save(self, name, content):
    """
    Write the content for a file with name. In this 
    method you might upload the content to a 
    remote service.
    """

此方法应将content保存到远程文件name中。实现此方法的方法将取决于你的远程服务。它可能通过 SFTP 传输文件,或者上传到 CDN。

虽然此示例没有实现任何与远程服务的传输,但你可参考它来了解如何实现自定义存储引擎。

在实现你的自定义存储引擎后,你可以通过在settings.py中的STATICFILES_STORAGE设置中设置其点模块路径来使其生效。

Bookr 应用应有一个针对reviews应用页面的特定 Logo。这涉及到添加一个仅针对reviews应用的基模板,并更新我们当前的reviews模板以继承它。然后你将在基模板上包含 Bookr 的reviews Logo。

这些步骤将帮助你完成此活动:

  1. 添加一个 CSS 规则来定位 Logo。将此规则放入现有的base.html中,在.navbar-brand规则之后:

    .navbar-brand > img {
      height: 60px;
    }
    
  2. 添加一个继承模板可以覆盖的 brand 块模板标签。将其放在具有 navbar-brand 类的 <a> 元素内。block 的默认内容应保持为 Book Review

  3. reviews 应用程序中添加一个静态目录,包含一个命名空间目录。从 packt.live/2WYlGjP 下载 reviewslogo.png 并将其放入此目录中。

  4. 创建 Bookr 项目的 templates 目录(在 Bookr 项目目录内)。然后,将 reviews 应用程序的当前 base.html 移动到这个目录中,使其成为整个项目的基模板。

  5. 将新 templates 目录的路径添加到 settings.py 中的 TEMPLATES['DIRS'] 设置(与你在 练习 5.02使用静态模板标签 中所做的一样)。

  6. reviews 应用程序创建另一个 base.html 模板。将其放在 reviews 应用程序的 templates 目录中。新的模板应该扩展现有的 base.html

  7. 新的 base.html 应该覆盖 brand 块的内容。这个块应该只包含一个 <img> 实例,其 src 属性使用 {% static %} 模板标签设置。图像源应该是 步骤 2 中添加的标志。

  8. views.py 中的索引视图应渲染项目 base.html 而不是 reviews 的。

参考以下屏幕截图以查看这些更改后您的页面应该是什么样子。请注意,尽管您正在修改基础模板,但它不会改变主页面的布局:

Figure 5.20: Book list page after adding reviews logos

img/B15509_05_20.jpg

图 5.20:添加评论标志后的图书列表页面

Figure 5.21: Book Details page after adding logo

img/B15509_05_21.jpg

图 5.21:添加标志后的图书详情页面

注意

此活动的解决方案可以在 packt.live/2Nh1NTJ 找到。

活动 5.02:CSS 优化

目前,CSS 被保留在 base.html 模板中。为了最佳实践,它应该移动到自己的文件中,以便可以单独缓存并减小 HTML 下载的大小。作为此过程的一部分,你还将添加一些 CSS 优化,例如字体和颜色,并将 Google Fonts CSS 链接到以支持这些更改。

这些步骤将帮助您完成此活动:

  1. 在 Bookr 项目目录中创建一个名为 static 的目录。然后,在它里面创建一个名为 main.css 的新文件。

  2. 将主 base.html 模板中的 <style> 元素内容复制到新的 main.css 文件中,然后从模板中删除 <style> 元素。将这些额外规则添加到 CSS 文件的末尾:

    body {
      font-family: 'Source Sans Pro', sans-serif;
        background-color: #e6efe8
      color: #393939;
    }
    h1, h2, h3, h4, h5, h6 {
      font-family: 'Libre Baskerville', serif;
    }
    
  3. 使用 <link rel="stylesheet" href="…"> 标签链接到新的 main.css 文件。使用 {% static %} 模板标签生成 href 属性的 URL,并且不要忘记 load static 库。

  4. 在 Google fonts CSS 中添加链接,通过在基础模板中添加以下代码:

    <link rel="stylesheet" 
      href="https://fonts.googleapis.com/css?family
        =Libre+Baskerville|Source+Sans+Pro&display=swap">
    

    注意

    您需要保持一个活跃的互联网连接,以便您的浏览器可以包含这个远程 CSS 文件。

  5. 更新您的 Django 设置以添加STATICFILES_DIRS,设置为在步骤 1中创建的static目录。完成之后,您的 Bookr 应用应该看起来像图 5.22:![图 5.22:带有新字体和背景颜色的书单 img/B15509_05_22.jpg

图 5.22:带有新字体和背景颜色的书单

注意新的字体和背景颜色。这些应该在所有 Bookr 页面上显示。

注意

本活动的解决方案可以在packt.live/2Nh1NTJ找到。

活动五.03:添加全局标志

您已经添加了一个在reviews应用的页面中提供的标志。我们还有一个作为默认值的全局标志,但其他应用将能够覆盖它:

  1. packt.live/2Jx7Ge4下载 Bookr 标志(logo.png)。

  2. 将其保存在项目的主static目录中。

  3. 编辑主base.html文件。我们已经有了一个用于标志的区块(brand),因此可以在这里放置一个<img>实例。使用static模板标签来引用您刚刚下载的标志。

  4. 检查您的页面是否正常工作。在主 URL 上,您应该看到 Bookr 标志,但在书单和详情页面上,您应该看到 Bookr 评论标志。

    完成后,您应该在主页上看到 Bookr 标志:

    ![图 5.23:主页上的 Bookr 标志 img/B15509_05_23.jpg

图 5.23:主页上的 Bookr 标志

当您访问之前有 Bookr 评论标志的页面,例如书单页面时,它应该仍然显示 Bookr 评论标志:

![图 5.24:Bookr 评论页面上仍然显示 Bookr 评论标志img/B15509_05_24.jpg

图 5.24:Bookr 评论标志仍然显示在评论页面上

注意

本活动的解决方案可以在packt.live/2Nh1NTJ找到。

摘要

在本章中,我们展示了如何使用 Django 的staticfiles应用来查找和提供静态文件。我们使用内置的static视图在DEBUG模式下通过 Django 开发服务器提供这些文件。我们展示了存储静态文件的不同位置,使用项目全局的目录或为应用特定的目录;全局资源应存储在前者,而应用特定资源应存储在后者。我们展示了对静态文件目录进行命名空间的重要性,以防止冲突。在提供资产后,我们使用static标签将它们包含在我们的模板中。然后我们演示了如何使用collectstatic命令将所有资产复制到STATIC_ROOT目录,以便生产部署。我们展示了如何使用findstatic命令来调试静态文件的加载。为了自动使缓存失效,我们探讨了使用ManifestFilesStorage将文件内容的哈希添加到静态文件 URL 的方法。最后,我们简要讨论了使用自定义文件存储引擎。

到目前为止,我们只使用已经存在的内文来抓取网页。在下一章中,我们将开始添加表单,这样我们就可以通过向网页发送数据来与它们进行交互,使用 HTTP 协议。

第六章:6. 表单

概述

本章介绍了 Web 表单,这是一种从浏览器向 Web 服务器发送信息的方法。它从对表单的一般介绍开始,并讨论了如何将数据编码以发送到服务器。你将了解在GET HTTP 请求中发送表单数据与在POST HTTP 请求中发送数据的区别,以及如何选择使用哪一个。到本章结束时,你将了解 Django 表单库是如何自动构建和验证表单的,以及它是如何减少你需要编写的手动 HTML 数量的。

简介

到目前为止,我们为 Django 构建的视图都是单向的。我们的浏览器正在从我们编写的视图中检索数据,但没有向它们发送任何数据。在第四章Django Admin 简介中,我们使用 Django admin 创建模型实例并提交表单,但那些是使用 Django 内置的视图,而不是我们创建的。在本章中,我们将使用 Django 表单库开始接受用户提交的数据。数据将通过 URL 参数中的GET请求提供,以及/或请求体中的POST请求。但在我们深入了解细节之前,首先让我们了解 Django 中的表单是什么。

什么是表单?

当与交互式 Web 应用程序一起工作时,我们不仅希望向用户提供数据,还希望从他们那里接收数据,以便自定义我们正在生成的响应或让他们提交数据到网站。在浏览网页时,你肯定已经使用过表单。无论你是登录互联网银行账户、使用浏览器上网、在社交媒体上发帖,还是在在线电子邮件客户端中写电子邮件,在这些所有情况下,你都是在表单中输入数据。表单由定义要提交给服务器的键值对数据的输入组成。例如,当登录到网站时,发送的数据将包含用户名密码键,分别对应你的用户名和密码的值。我们将在输入类型部分更详细地介绍不同类型的输入。表单中的每个输入都有一个名称,这是在服务器端(在 Django 视图中)识别其数据的方式。可以有多个具有相同名称的输入,其数据在包含所有具有此名称的已发布值的列表中可用——例如,具有应用于用户的权限的复选框列表。每个复选框将具有相同的名称但不同的值。表单具有指定浏览器应提交数据到哪个 URL 以及应使用什么方法提交数据的属性(浏览器仅支持GETPOST)。

下一个图中显示的 GitHub 登录表单是一个表单的例子:

图 6.1:GitHub 登录页面是一个表单的例子

图 6.1:GitHub 登录页面是一个表单的例子

图 6.1:GitHub 登录页面是一个表单的例子

它有三个可见的输入:一个文本字段(用户名),一个密码字段,以及一个提交按钮(登录)。它还有一个不可见的字段——其类型是hidden,它包含一个用于安全的特殊令牌,称为登录按钮,表单数据通过POST请求提交。如果你输入了有效的用户名和密码,你将登录;否则,表单将显示以下错误:

图 6.2:提交了错误的用户名或密码的表单

图 6.2:提交了错误的用户名或密码的表单

表单可以有两种状态:提交前提交后。第一种是页面首次加载时的初始状态。所有字段都将有一个默认值(通常是空的)且不会显示任何错误。如果已输入到表单中的所有信息都是有效的,那么通常在提交时,你将被带到显示表单提交结果的页面。这可能是一个搜索结果页面,或者显示你创建的新对象的页面。在这种情况下,你将不会看到表单的提交后状态。

如果你没有在表单中输入有效信息,那么它将再次以提交后的状态呈现。在这个状态下,你会看到你输入的信息以及任何错误,以帮助你解决表单中的问题。错误可能是字段错误非字段错误。字段错误适用于特定字段。例如,遗漏必填字段或输入过大、过小、过长或过短的价值。如果表单要求你输入你的名字而你留空了,这将在该字段旁边显示为字段错误。

非字段错误可能不适用于字段,或者适用于多个字段,并在表单顶部显示。在图 6.2中,我们看到一条消息,表明在登录时用户名或密码可能不正确。出于安全考虑,GitHub 不会透露用户名是否有效,因此这被显示为非字段错误,而不是用户名或密码的字段错误(Django 也遵循这个约定)。非字段错误也适用于相互依赖的字段。例如,在信用卡表单中,如果支付被拒绝,我们可能不知道是信用卡号码还是安全码不正确;因此,我们无法在特定字段上显示该错误。它适用于整个表单。

<form>元素

在表单提交过程中使用的所有输入都必须包含在<form>元素内。你将使用以下三个 HTML 属性来修改表单的行为:

  • method

    这是提交表单时使用的 HTTP 方法,可以是GETPOST。如果省略,则默认为GET(因为这是在浏览器中键入 URL 并按Enter时的默认方法)。

  • action

    这指的是发送表单数据到的 URL(或路径)。如果省略,数据将返回到当前页面。

  • enctype

    这设置了表单的编码类型。只有在你使用表单上传文件时才需要更改此设置。最常用的值是 application/x-www-form-urlencoded(如果省略此值则为默认值)或 multipart/form-data(如果上传文件则设置此值)。请注意,你不需要担心视图中的编码类型;Django 会自动处理不同类型。

下面是一个没有设置任何属性的表单示例:

<form>
    <!-- Input elements go here -->
</form>

它将使用 GET 请求提交数据,到当前表单显示的当前 URL,使用 application/x-www-form-urlencoded 编码类型。

在下一个示例中,我们将在一个表单上设置所有三个属性:

<form method="post" action="/form-submit" enctype="multipart/form-data">
    <!-- Input elements go here -->
</form>

此表单将使用 POST 请求将数据提交到 /form-submit 路径,并将数据编码为 multipart/form-data

GETPOST 请求在数据发送方式上有什么不同?回想一下 第一章Django 简介,我们讨论了浏览器发送的底层 HTTP 请求和响应数据的样子。在接下来的两个示例中,我们将两次提交相同的表单,第一次使用 GET,第二次使用 POST。表单将有两个输入,一个姓和一个名。

使用 GET 提交的表单将数据放在 URL 中,如下所示:

GET /form-submit?first_name=Joe&last_name=Bloggs HTTP/1.1
Host: www.example.com

使用 POST 提交的表单将数据放在请求体中,如下所示:

POST /form-submit HTTP/1.1
Host: www.example.com
Content-Length: 31
Content-Type: application/x-www-form-urlencoded
first_name=Joe&last_name=Bloggs

你会注意到,在两种情况下表单数据都是用相同的方式进行编码;只是 GETPOST 请求放置的位置不同。在接下来的一个部分中,我们将讨论如何在这两种请求类型之间进行选择。

输入类型

我们已经看到了四个输入示例(文本密码提交隐藏)。大多数输入都是通过 <input> 标签创建的,并且它们的类型通过其 type 属性指定。每个输入都有一个 name 属性,它定义了发送到服务器的 HTTP 请求中的键值对的键。

在下一个练习中,让我们看看我们如何使用 HTML 构建表单。这将使你能够熟悉许多不同的表单字段。

注意

本章中使用的所有练习和活动的代码可以在书的 GitHub 仓库中找到,网址为 packt.live/2KGjlaM

练习 6.01:在 HTML 中构建表单

在本章的前几个练习中,我们需要一个 HTML 表单来进行测试。我们将在这个练习中手动编写一个。这还将允许你实验不同字段如何进行验证和提交。这将在一个新的 Django 项目中完成,这样我们就不干扰 Bookr。你可以参考 第一章Django 简介,来刷新你对创建 Django 项目的记忆:

  1. 我们将首先创建新的 Django 项目。你可以重用已经安装了 Django 的 bookr 虚拟环境。打开一个新的终端并激活虚拟环境。然后,使用 django-admin 启动一个名为 form_project 的 Django 项目。为此,请运行以下命令:

    django-admin startproject form_project
    

    这将在名为form_example的目录中构建 Django 项目。

  2. 通过使用startapp管理命令在此项目中创建一个新的 Django 应用。该应用应命名为form_example。为此,请cdform_project目录,然后运行以下命令:

    python3 manage.py startapp form_example
    

    这将在form_project目录内创建form_example应用目录。

  3. 启动 PyCharm,然后打开form_project目录。如果您已经有一个项目打开,可以通过选择文件 -> 打开来完成此操作;否则,只需在欢迎使用 PyCharm窗口中点击打开。导航到form_project目录,选择它,然后点击打开form_project项目窗口应类似于以下所示:图 6.3:form_project 项目已打开

    图 6.3:form_project 项目已打开

  4. 创建一个新的运行配置来执行项目的manage.py runserver。您可以再次使用bookr虚拟环境。完成设置后,运行/调试配置窗口应类似于以下图示:图 6.4:运行/调试配置为 Runserver

    图 6.4:运行/调试配置为 Runserver

    您可以通过点击运行按钮来测试配置是否设置正确,然后在浏览器中访问http://127.0.0.1:8000/。您应该看到 Django 欢迎屏幕。如果调试服务器无法启动或您看到 Bookr 主页面,那么您可能仍然有 Bookr 项目在运行。尝试停止 Bookr 的runserver进程,然后启动您刚刚设置的新进程。

  5. form_project目录中打开settings.py文件,并将'form_example'添加到INSTALLED_APPS设置中。

  6. 设置此新项目的最后一步是为form_example应用创建一个templates目录。在form_example目录上右键单击,然后选择新建 -> 目录。将其命名为templates

  7. 我们需要一个 HTML 模板来显示我们的表单。通过右键单击您刚刚创建的templates目录并选择新建 -> HTML 文件来创建一个。在出现的对话框中,输入名称form-example.html并按Enter键创建它。

  8. form-example.html文件现在应在 PyCharm 的编辑器窗格中打开。首先创建form元素。我们将将其method属性设置为postaction属性将被省略,这意味着表单将提交回加载它的同一 URL。

    <body></body>标签之间插入此代码:

    <form method="post">
    </form>
    
  9. 现在,让我们添加一些输入。为了在各个输入之间添加一些间距,我们将它们包裹在<p>标签内。我们将从一个文本字段和一个密码字段开始。此代码应插入到您刚刚创建的<form>标签之间:

    <p>
        <label for="id_text_input">Text Input</label><br>
        <input id="id_text_input" type="text" name=      "text_input" value="" placeholder="Enter some text">
    </p>
    <p>
        <label for="id_password_input">Password Input</label><br>
        <input id="id_password_input" type="password" name="password_input"       value="" placeholder="Your password">
    </p>
    
  10. 接下来,我们将添加两个复选框和三个单选按钮。在您在上一步中添加的 HTML 代码之后插入此代码;它应该在</form>标签之前:

    <p>
        <input id="id_checkbox_input" type="checkbox"      name="checkbox_on" value="Checkbox Checked" checked>
        <label for="id_checkbox_input">Checkbox</label>
    </p>
    <p>
        <input id="id_radio_one_input" type="radio"      name="radio_input" value="Value One">
        <label for="id_radio_one_input">Value One</label>
        <input id="id_radio_two_input" type="radio"      name="radio_input" value="Value Two" checked>
        <label for="id_radio_two_input">Value Two</label>
        <input id="id_radio_three_input" type="radio"      name="radio_input" value="Value Three">
        <label for="id_radio_three_input">Value Three</label>
    </p>
    
  11. 接下来是一个下拉选择菜单,允许用户选择喜欢的书籍。在上一步骤的代码之后但</form>标签之前添加以下代码:

    <p>
        <label for="id_favorite_book">Favorite Book</label><br>
        <select id="id_favorite_book" name="favorite_book">
            <optgroup label="Non-Fiction">
                <option value="1">Deep Learning with Keras</option>
                <option value="2">Web Development with Django</option>
            </optgroup>
            <optgroup label="Fiction">
                <option value="3">Brave New World</option>
                <option value="4">The Great Gatsby</option>
            </optgroup>
        </select>
    </p>
    

    它将显示四个选项,分为两组。用户只能选择一个选项。

  12. 下一个是多选(通过使用multiple属性实现)。在上一步骤的代码之后但</form>标签之前添加以下代码:

    <p>
        <label for="id_books_you_own">Books You Own</label><br>
        <select id="id_books_you_own" name="books_you_own" multiple>
            <optgroup label="Non-Fiction">
                <option value="1">Deep Learning with Keras</option>
                <option value="2">Web Development with Django</option>
            </optgroup>
            <optgroup label="Fiction">
                <option value="3">Brave New World</option>
                <option value="4">The Great Gatsby</option>
            </optgroup>
        </select>
    </p>
    

    用户可以从四个选项中选择零个或多个。它们分为两组显示。

  13. 接下来是textarea。它就像一个文本字段,但有多行。这段代码应该像在之前的步骤中一样添加,在关闭</form>标签之前:

    <p>
        <label for="id_text_area">Text Area</label><br>
        <textarea name="text_area" id="id_text_area"      placeholder="Enter multiple lines of text"></textarea>
    </p>
    
  14. 接下来,添加一些特定数据类型的字段:在</form>标签之前添加numberemaildate输入。添加以下所有内容:

    <p>
        <label for="id_number_input">Number Input</label><br>
        <input id="id_number_input" type="number"      name="number_input" value="" step="any" placeholder="A number">
    </p>
    <p>
        <label for="id_email_input">Email Input</label><br>
        <input id="id_email_input" type="email"      name="email_input" value="" placeholder="Your email address">
    </p>
    <p>
        <label for="id_date_input">Date Input</label><br>
        <input id="id_date_input" type="date" name=      "date_input" value="2019-11-23">
    </p>
    
  15. 现在添加一些按钮来提交表单。再次,在关闭</form>标签之前插入以下内容:

    <p>
        <input type="submit" name="submit_input" value="Submit Input">
    </p>
    <p>
        <button type="submit" name="button_element" value="Button Element">
            Button With <strong>Styled</strong> Text
        </button>
    </p>
    

    这展示了两种创建提交按钮的方式,要么作为<input>,要么作为<button>

  16. 最后,添加一个隐藏字段。在关闭</form>标签之前插入以下内容:

    <input type="hidden" name="hidden_input" value="Hidden Value">
    

    这个字段既看不见也编辑不了,因此它有一个固定的值。你可以保存并关闭form-example.html

  17. 就像任何模板一样,除非我们有视图来渲染它,否则我们看不到它。打开form_example应用的views.py文件,并添加一个名为form_example的新视图。它应该渲染并返回你刚刚创建的模板,如下所示:

    def form_example(request):
        return render(request, "form-example.html")
    

    你现在可以保存并关闭views.py

  18. 你现在应该熟悉下一步,即添加一个 URL 映射到视图。打开form_project包目录中的urls.py文件。将form-example路径映射到form_example视图的urlpatterns变量。它应该看起来像这样:

    path('form-example/', form_example.views.form_example)
    

    确保你还要添加对form_example.views的导入。保存并关闭urls.py

  19. 启动 Django 开发服务器(如果尚未运行),然后在你的网页浏览器中加载你的新视图;地址是http://127.0.0.1:8000/form-example/。你的页面应该看起来像这样:图 6.5:示例输入页面

    图 6.5:示例输入页面

    你现在可以熟悉网页表单的行为,并查看它们是如何从你指定的 HTML 生成的。一个可以尝试的活动是将无效数据输入到数字、日期或电子邮件输入框中,然后点击提交按钮——内置的 HTML 验证应该阻止表单提交:

    图 6.6:由于无效数字导致的浏览器错误

    图 6.6:由于无效数字导致的浏览器错误

    我们还没有为表单提交设置好一切,所以如果你纠正表单中的所有错误并尝试提交(通过点击任一提交按钮),你将收到一个错误,指出CSRF 验证失败。请求已中止。,正如我们可以在下一张图中看到的那样。我们将在本章后面讨论这意味着什么,以及如何修复它:

    图 6.7:CSRF 验证错误

    图 6.7:CSRF 验证错误

  20. 如果你确实收到了错误,只需在浏览器中返回到输入示例页面。

在这个练习中,你创建了一个展示许多 HTML 输入的示例页面,然后创建了一个视图来渲染它,并创建了一个 URL 来映射它。你在浏览器中加载了这个页面,并尝试更改数据,当表单包含错误时尝试提交它。

带有跨站请求伪造保护的表单安全

在整本书中,我们提到了 Django 包含的一些功能,以防止某些类型的安全漏洞。其中之一就是防止 CSRF 的功能。

CSRF 攻击利用了网站上的表单可以被提交到任何其他网站的事实。"form"的"action"属性只需设置得当。以 Bookr 为例。我们还没有设置这个,但我们将添加一个视图和 URL,允许我们为书籍发表评论。为此,我们将有一个用于发布评论内容和选择评分的表单。它的 HTML 如下所示:

<form method="post" action="http://127.0.0.1:8000/books/4/reviews/">
    <p>
        <label for="id_review_text">Your Review</label><br/>
        <textarea id="id_review_text" name="review_text"          placeholder="Enter your review"></textarea>
    </p>
    <p>
        <label for="id_rating">Rating</label><br/>
        <input id="id_rating" type="number" name="rating"          placeholder="Rating 1-5">
    </p>
    <p>
        <button type="submit">Create Review</button>
    </p
</form>

在网页上,它看起来会是这样:

图 6.8:示例评论创建表单

图 6.8:示例评论创建表单

某人可以拿走这个表单,做一些修改,然后在自己的网站上托管它。例如,他们可以隐藏输入并硬编码一个好评和评分,然后让它看起来像其他类型的表单,如下所示:

<form method="post" action="http://127.0.0.1:8000/books/4/reviews/">
    <input type="hidden" name="review_text" value="This book is great!">
    <input type="hidden" name="rating" value="5">
    <p>
        <button type="submit">Enter My Website</button>
    </p>
</form>

当然,隐藏字段不会显示,所以在恶意网站上表单看起来是这样的。

图 6.9:隐藏输入不可见

图 6.9:隐藏输入不可见

用户会以为他们点击的是一个按钮以进入一个网站,但在点击的过程中,他们会向 Bookr 上的原始视图提交隐藏的值。当然,用户可以检查他们所在页面的源代码来查看正在发送什么数据以及发送到何处,但大多数用户不太可能检查他们遇到的每一个表单。攻击者甚至可以有一个没有提交按钮的表单,只用 JavaScript 来提交它,这意味着用户在甚至没有意识到的情况下就提交了表单。

你可能会认为要求用户登录到 Bookr 可以防止这种攻击,这确实在一定程度上限制了其有效性,因为攻击将仅对已登录用户有效。但由于认证的方式,一旦用户登录,他们的浏览器中就会设置一个 cookie 来识别他们到 Django 应用程序。这个 cookie 在每次请求时都会发送,这样用户就不必在每一页上提供他们的登录凭证。由于网络浏览器的工作方式,它们会在发送到特定服务器的所有请求中包含服务器的认证 cookie。即使我们的表单托管在恶意网站上,最终它还是会发送一个请求到我们的应用程序,所以它会通过我们的服务器 cookie 发送。

我们如何防止 CSRF 攻击?Django 使用一种称为 CSRF 令牌的东西,这是一个对每个网站访客唯一的随机字符串——一般来说,你可以认为一个访客是一个浏览器会话。同一台电脑上的不同浏览器会是不同的访客,而且同一个 Django 用户在两个不同的浏览器上登录也会是不同的访客。当表单被读取时,Django 会将令牌作为隐藏输入放入表单中。CSRF 令牌必须包含在所有发送到 Django 的 POST 请求中,并且它必须与 Django 在服务器端为访客存储的令牌匹配,否则将返回 403 状态 HTTP 响应。这种保护可以禁用——要么是整个站点,要么是单个视图——但除非你真的需要这样做,否则不建议这样做。CSRF 令牌必须添加到每个要发送的表单的 HTML 中,并且使用 {% csrf_token %} 模板标签完成。我们现在将把它添加到我们的示例评论表单中,模板中的代码将看起来像这样:

<form method="post" action="http://127.0.0.1:8000/books/4/reviews/">
    {% csrf_token %}
    <p>
        <label for="id_review_text">Your Review</label><br/>
        <textarea id="id_review_text" name="review_text"          placeholder="Enter your review"></textarea>
    </p>
    <p>
        <label for="id_rating">Rating</label><br/>
        <input id="id_rating" type="number" name="rating"          placeholder="Rating 1-5">
    </p>
    <p>
        <button type="submit">Enter My Website</button>
    </p>
</form>

当模板被渲染时,模板标签会被插值,所以输出的 HTML 最终会像这样(注意,输入仍然在输出中;这里只是为了简洁而移除了它们):

<form method="post" action="http://127.0.0.1:8000/books/4/reviews/">
    <input type="hidden" name="csrfmiddlewaretoken"      value="tETZjLDUXev1tiYqGCSbMQkhWiesHCnutxpt6mutHI6YH64F0nin5k2JW3B68IeJ">
    …
</form>

由于这是一个隐藏字段,页面上的表单看起来与之前没有区别。

CSRF 令牌对网站上的每个访客都是唯一的,并且会定期更改。如果攻击者从我们的网站上复制 HTML,他们会得到一个自己的 CSRF 令牌,这个令牌不会与任何其他用户的令牌匹配,所以当其他人提交表单时,Django 会拒绝该表单。

CSRF 令牌也会定期更改。这限制了攻击者利用特定用户和令牌组合的时间。即使他们能够获取他们试图利用的用户的 CSRF 令牌,他们也只有很短的时间窗口可以使用它。

在视图中访问数据

正如我们在 第一章Django 简介 中所讨论的,Django 在传递给视图函数的 HTTPRequest 实例上提供了两个 QueryDict 对象。这些是 request.GET,它包含通过 URL 传递的参数,以及 request.POST,它包含 HTTP 请求体中的参数。尽管 request.GET 的名字中有 GET,但这个变量即使在非 GET HTTP 请求中也会被填充。这是因为它包含的数据是从 URL 解析出来的。由于所有 HTTP 请求都有一个 URL,所以所有 HTTP 请求都可能包含 GET 数据,即使它们是 POSTPUT 等等。在下一个练习中,我们将向我们的视图添加代码来读取和显示 POST 数据。

练习 6.02:在视图中处理 POST 数据

现在我们将向我们的示例视图添加一些代码,将接收到的 POST 数据打印到控制台。我们还将把生成页面的 HTTP 方法插入到 HTML 输出中。这将使我们能够确定用于生成页面的方法(GETPOST)并查看每种类型的表单如何不同:

  1. 首先,在 PyCharm 中,打开 form_example 应用程序的 views.py 文件。修改 form_example 视图,通过在函数内部添加以下代码,将 POST 请求中的每个值打印到控制台:

        for name in request.POST:
            print("{}: {}".format(name, request.POST.getlist(name)))
    

    此代码遍历请求 POST 数据 QueryDict 中的每个键,并将键和值列表打印到控制台。我们已经知道每个 QueryDict 可以为一个键有多个值,因此我们使用 getlist 函数来获取所有值。

  2. request.method 通过名为 method 的上下文变量传递到模板中。通过更新视图中的 render 调用来完成此操作,使其如下所示:

    return render(request, "form-example.html", \
                  {"method": request.method})
    
  3. 现在,我们将显示模板中的 method 变量。打开 form-example.html 模板,并使用 <h4> 标签显示 method 变量。将其放在 <body> 标签之后,如下所示:

    <body>
        <h4>Method: {{ method }}</h4>
    

    注意,我们可以通过使用 request 方法变量和属性正确地直接在模板中访问方法,而无需将其传递到上下文字典中。我们从 第三章URL 映射、视图和模板 中知道,通过使用 render 快捷函数,请求始终在模板中可用。我们在这里展示了如何访问视图中的方法,因为稍后我们将根据方法更改页面的行为。

  4. 我们还需要将 CSRF 令牌添加到表单 HTML 中。我们通过在 <form> 标签之后放置 {% csrf_token %} 模板标签来完成此操作。表单的开始应如下所示:

    <form method="post">
         {% csrf_token %}
    

    现在,保存文件。

  5. 如果 Django 开发服务器尚未运行,请启动它。在浏览器中加载示例页面(http://127.0.0.1:8000/form-example/),你应该会看到它现在在页面顶部显示了方法(GET):图 6.10:页面顶部的请求方法

    图 6.10:页面顶部的请求方法

  6. 在每个输入框中输入一些文本或数据,然后通过点击 Submit Input 按钮提交表单:图 6.11:点击提交输入按钮提交表单

    图 6.11:点击提交输入按钮提交表单

    你应该会看到页面重新加载,并且显示的方法更改为 POST

    图 6.12:表单提交后方法更新为 POST

    图 6.12:表单提交后方法更新为 POST

  7. 切换回 PyCharm,查看窗口底部的 Run 控制台。如果它不可见,请点击窗口底部的 Run 按钮以显示它:图 6.13:点击窗口底部的运行按钮以显示控制台

    图 6.13:点击窗口底部的运行按钮以显示控制台

    Run 控制台中,应显示已发送到服务器的值列表:

    图 6.14:运行控制台显示的输入值

    图 6.14:运行控制台显示的输入值

    你应该注意以下事项:

    • 所有值都作为文本发送,即使是 numberdate 输入。

    • 对于 select 输入,发送的是选中选项的 value 属性,而不是 option 标签的文本内容。

    • 如果你为 books_you_own 选择多个选项,那么你将在请求中看到多个值。这就是为什么我们使用 getlist 方法,因为为相同的输入名称发送了多个值。

    • 如果复选框被选中,你将在调试输出中看到一个 checkbox_on 输入。如果没有被选中,则该键将根本不存在(即,没有键,而不是键存在一个空字符串或 None 值)。

    • 我们有一个名为 submit_input 的值,其文本为 Submit Input。你通过点击 Submit Input 按钮提交了表单,因此我们收到了它的值。注意,由于该按钮没有被点击,所以 button_element 输入没有设置任何值。

  8. 我们将尝试两种其他提交表单的方式,首先是在你的光标位于类似文本的输入中(如 textpassworddateemail,但不是 text area,因为在其中按 Enter 将添加新行)时按 Enter 键。

    如果你以这种方式提交表单,表单将表现得好像你点击了表单上的第一个提交按钮一样,因此 submit_input 输入值将被包含。你看到的输出应该与之前的图相同。

    提交表单的另一种方式是通过点击 Button Element 提交输入,我们将尝试点击此按钮来提交表单。你应该会看到 submit_button 已不再列表中,而 button_element 现在已经存在:

    ![图 6.15:submit_button 已从输入中移除,并添加了 button_element

    ![img/B15509_06_15.jpg]

图 6.15:submit_button 已从输入中移除,并添加了 button_element

你可以使用这种多提交技术来改变你的视图行为,取决于哪个按钮被点击。你甚至可以有多个具有相同 name 属性的提交按钮,以使逻辑更容易编写。

在这个练习中,你通过使用 {% csrf_token %} 模板标签将 CSRF 令牌添加到你的 form 元素中。这意味着你的表单可以成功提交到 Django,而不会生成 HTTP 权限拒绝响应。然后我们添加了一些代码来输出表单提交时的值。我们尝试用各种值提交表单,以查看它们是如何被解析成 request.POST QueryDict 中的 Python 变量的。现在我们将讨论一些关于 GETPOST 请求之间差异的理论,然后转向 Django 表单库,它使得设计和验证表单变得更加容易。

选择 GET 和 POST

选择何时使用 GETPOST 请求需要考虑许多因素。最重要的是决定请求是否应该是幂等的。如果请求可以被重复执行并且每次都产生相同的结果,则可以说该请求是幂等的。让我们看看一些例子。

如果你将任何网址输入到你的浏览器中(例如我们迄今为止构建的任何 Bookr 页面),它将执行一个GET请求来获取信息。你可以刷新页面,无论你点击刷新多少次,你都会得到相同的数据。你发出的请求不会影响服务器上的内容。你会说这些请求是幂等的。

现在,记得你通过 Django 管理界面(在第四章Django 管理界面简介)添加数据时吗?你在表单中输入了新书的详细信息,然后点击了保存。你的浏览器向服务器发送了一个POST请求来创建新书。如果你重复那个POST请求,服务器将创建另一本书,并且每次你重复请求时都会这样做。由于请求正在更新信息,它不是幂等的。你的浏览器会警告你这一点。如果你曾经尝试刷新在提交表单后发送到你的页面,你可能收到一条消息询问你是否想要重新发送表单数据?(或更详细的,如以下图所示)。这是警告你正在再次发送表单数据,这可能会使你刚刚执行的操作被重复:

图 6.16:Firefox 确认是否应该重新发送信息

图 6.16:Firefox 确认是否应该重新发送信息

这并不是说所有GET请求都是幂等的,而所有POST请求都不是——你的后端应用可以按照你想要的方式设计。尽管这不是最佳实践,开发者可能已经决定在他们的 Web 应用中,在GET请求期间更新数据。当你构建你的应用时,你应该尽量确保GET请求是幂等的,并将数据更改仅留给POST请求。除非你有充分的理由不这样做,否则请坚持这些原则。

另一点需要考虑的是,Django 只对POST请求应用 CSRF 保护。任何GET请求,包括更改数据的请求,都可以在没有 CSRF 令牌的情况下访问。

有时候,判断一个请求是否幂等可能很难;例如,登录表单。在你提交用户名和密码之前,你并未登录,之后服务器认为你已经登录,那么我们是否可以认为非幂等,因为它改变了你与服务器之间的认证状态?另一方面,一旦登录,如果你再次发送凭证,你将保持登录状态。这表明请求是幂等的且可重复的。那么,这个请求应该是GET还是POST

这引出了选择使用哪种方法时需要考虑的第二个问题。如果我们使用 GET 请求发送表单数据,表单参数将可见于 URL 中。例如,如果我们使登录表单使用 GET 请求,登录 URL 可能是 https://www.example.com/login?username=user&password=password1。用户名,更糟糕的是密码,将可见于网络浏览器的地址栏中。它也会存储在浏览器历史记录中,这意味着任何在真实用户之后使用浏览器的用户都可以登录到该网站。URL 通常还会存储在 Web 服务器日志文件中,这意味着凭证也会在那里可见。简而言之,无论请求的幂等性如何,都不要通过 URL 参数传递敏感数据。

有时,知道参数将可见于 URL 中可能正是您所希望的。例如,当使用搜索引擎进行搜索时,通常搜索参数将可见于 URL 中。要查看这一功能如何工作,请尝试访问 www.google.com 并进行搜索。您会注意到包含结果的页面将您的搜索词作为 q 参数。例如,搜索 Django 将带您到 URL www.google.com/search?q=Django。这允许您通过发送此 URL 与他人共享搜索结果。在 活动 6.01,图书搜索 中,您将添加一个类似的搜索表单,该表单会传递一个参数。

另一个考虑因素是,浏览器允许的 URL 最大长度可能比 POST 体的尺寸短得多——有时只有大约 2,000 个字符(或大约 2 KB),而 POST 体的尺寸可以是许多兆字节或千兆字节(假设您的服务器已设置允许这些大小的请求)。

如我们之前提到的,无论正在进行的请求类型是什么(GETPOSTPUT 等),URL 参数都可在 request.GET 中找到。您可能会发现将一些数据通过 URL 参数发送,而将其他数据放在请求体(在 request.POST 中可用)中很有用。例如,您可以在 URL 中指定一个 format 参数,该参数设置某些输出数据将被转换成的格式,但输入数据提供在 POST 体内。

当我们可以在 URL 中放置参数时,为什么还要使用 GET?

Django 允许我们轻松定义包含变量的 URL 映射。例如,我们可以设置一个搜索视图的 URL 映射如下:

path('/search/<str:search>/', reviews.views.search)

这种方法可能一开始看起来很好,但当我们开始想要使用参数自定义结果视图时,它可能会迅速变得复杂。例如,我们可能希望能够从一个结果页面跳转到下一个结果页面,因此我们添加了一个页面参数:

path('/search/<str:search>/<int:page>', reviews.views.search)

然后我们可能还希望按特定类别对搜索结果进行排序,例如作者姓名或发布日期,因此我们为这个目的添加了另一个参数:

path('/search/<str:search>/<int:page>/<str:order >', \
     reviews.views.search)

你可能已经看到了这种方法的缺点——如果我们不提供页面,就无法对结果进行排序。如果我们还想添加results_per_page参数,我们就不能不设置pageorder键来使用它。

与使用查询参数的方法相比:所有这些参数都是可选的,因此你可以这样搜索:

?search=search+term:

或者你可以设置一个像这样的页面:

?search=search+term&page=2

或者你可以只设置结果排序如下:

?search=search+term&order=author

或者你可以将它们全部组合:

?search=search+term&page=2&order=author

使用 URL 查询参数的另一个原因是,在提交表单时,浏览器总是以这种方式发送输入值;无法更改,以便将参数作为 URL 中的路径组件提交。因此,当使用GET提交表单时,必须使用 URL 查询参数作为输入数据。

Django 表单库

我们已经探讨了如何手动在 HTML 中编写表单以及如何使用QueryDict访问请求对象中的数据。我们了解到浏览器为我们提供了一些针对特定字段类型的验证,例如电子邮件或数字,但我们还没有尝试在 Python 视图中验证数据。我们应该在 Python 视图中验证表单,原因有两个:

  • 仅仅依赖基于浏览器的输入数据验证是不安全的。浏览器可能没有实现某些验证功能,这意味着用户可以提交任何类型的数据。例如,旧版浏览器不验证数字字段,因此用户可以输入超出我们预期范围的数字。此外,恶意用户甚至可能尝试发送有害数据而不使用浏览器。浏览器验证应被视为对用户的一种便利,仅此而已。

  • 浏览器不允许我们进行跨字段验证。例如,我们可以使用required属性来指定必须填写的输入字段。然而,通常我们希望根据另一个输入字段的值来设置required属性。例如,如果用户已经勾选了注册我的邮箱复选框,那么电子邮件地址输入字段才应该被设置为required

Django 表单库允许你使用 Python 类快速定义表单。这是通过创建 Django 基础Form类的子类来实现的。然后你可以使用这个类的实例在模板中渲染表单并验证输入数据。我们将我们的类称为表单,类似于我们通过子类化 Django 模型来创建自己的Model类。表单包含一个或多个特定类型的字段(如文本字段、数字字段或电子邮件字段)。你会注意到这听起来像 Django 模型,而且表单确实与模型类似,但使用不同的字段类。你甚至可以自动从模型创建表单——我们将在第七章高级表单验证和模型表单中介绍这一点。

定义表单

创建 Django 表单类似于创建 Django 模型。你定义一个继承自 django.forms.Form 类的类。该类有属性,这些属性是不同 django.forms.Field 子类的实例。当渲染时,类中的属性名称对应于其在 HTML 中的输入 name。为了给你一个关于有哪些字段的快速概念,以下是一些示例:CharFieldIntegerFieldBooleanFieldChoiceFieldDateField。每个字段在渲染为 HTML 时通常对应一个输入,但表单字段类和输入类型之间并不总是存在一对一的映射。表单字段更多地与它们收集的数据类型相关联,而不是它们的显示方式。

为了说明这一点,考虑一个 text 输入和一个 password 输入。它们都接受一些输入的文本数据,但它们之间的主要区别在于,文本在 text 输入中是可见的,而 password 输入中的文本则是隐藏的。在 Django 表单中,这两个字段都是使用 CharField 来表示的。它们显示方式的不同是通过改变字段所使用的 *widget* 来设置的。

注意

如果你不太熟悉单词 widget,它是一个用来描述实际交互的输入及其显示方式的术语。文本输入、密码输入、选择菜单、复选框和按钮都是不同小部件的例子。我们在 HTML 中看到的输入与这些小部件一一对应。在 Django 中,情况并非如此,同一个类型的 Field 类可以根据指定的 widget 以多种方式渲染。

Django 定义了一系列 Widget 类,它们定义了 Field 应如何作为 HTML 渲染。它们继承自 django.forms.widgets.Widget。可以将小部件传递给 Field 构造函数以更改其渲染方式。例如,默认情况下,CharField 实例渲染为 text <input>。如果我们使用 PasswordInput 小部件,它将渲染为 password <input>。我们将使用的其他小部件如下:

  • RadioSelect,它将 ChoiceField 实例渲染为单选按钮而不是 <select> 菜单

  • Textarea,它将 CharField 实例渲染为 <textarea>

  • HiddenInput,它将字段渲染为隐藏的 <input>

我们将查看一个示例表单,并逐个添加字段和功能。首先,让我们先创建一个包含文本输入和密码输入的表单:

from django import forms
class ExampleForm(forms.Form):
    text_input = forms.CharField()
    password_input = forms.CharField(widget=forms.PasswordInput)

widget 参数可以只是一个小部件子类,这在很多情况下都是可以接受的。如果你想进一步自定义输入及其属性的显示,你可以将 widget 参数设置为 widget 类的实例。我们很快将探讨如何进一步自定义小部件的显示。在这种情况下,我们只是使用了 PasswordInput 类,因为我们没有对其进行自定义,只是改变了显示的输入类型。

当表单在模板中渲染时,它看起来是这样的:

图 6.17:在浏览器中渲染的 Django 表单

图 6.17:在浏览器中渲染的 Django 表单

注意,当页面加载时,输入不包含任何内容;文本已被输入以说明不同的输入类型。

如果我们检查页面源代码,我们可以看到 Django 生成的 HTML。对于前两个字段,它看起来像这样(添加了一些间距以提高可读性):

<p>
    <label for="id_text_input">Text input:</label>
    <input type="text" name="text_input" required id="id_text_input">
</p>
<p>
    <label for="id_password_input">Password input:</label>
    <input type="password" name="password_input" required id="id_password_input">
</p>

注意到 Django 已经自动生成一个label实例,其文本来自字段名称。nameid属性已自动设置。Django 还自动将required属性添加到输入中。与模型字段类似,表单字段构造函数也接受一个required参数——默认为True。将其设置为False将从生成的 HTML 中移除required属性。

接下来,我们将看看如何将复选框添加到表单中:

  • 复选框用BooleanField表示,因为它只有两个值,选中或未选中。它以与其他字段相同的方式添加到表单中:

    class ExampleForm(forms.Form):
        …
        checkbox_on = forms.BooleanField()
    

    Django 为这个新字段生成的 HTML 与前面两个字段类似:

    <label for="id_checkbox_on">Checkbox on:</label> 
    <input type="checkbox" name="checkbox_on" required id="id_checkbox_on">
    

接下来是选择输入:

  • 我们需要提供一个要显示在<select>下拉列表中的选择项列表。

  • 字段类构造函数接受一个choices参数。选择项以两个元素的元组的形式提供。每个子元组中的第一个元素是选择项的值,第二个元素是选择项的文本或描述。例如,选择项可以定义如下:

    BOOK_CHOICES = (('1', 'Deep Learning with Keras'),\
                    ('2', 'Web Development with Django'),\
                    ('3', 'Brave New World'),\
                    ('4', 'The Great Gatsby'))
    

    注意,如果你想使用列表而不是元组(或两者的组合),这是可以的。如果你想使你的选择项可变,这可能会很有用:

    BOOK_CHOICES = (['1', 'Deep Learning with Keras'],\
                    ['2', 'Web Development with Django'],\
                    ['3', 'Brave New World'],\
                    ['4', 'The Great Gatsby']]
    
  • 要实现optgroup,我们可以嵌套选择项。为了以与我们的前例相同的方式实现选择项,我们使用如下结构:

    BOOK_CHOICES = (('Non-Fiction', \
                     (('1', 'Deep Learning with Keras'),\
                     ('2', 'Web Development with Django'))),\
                    ('Fiction', \
                     (('3', 'Brave New World'),\
                      ('4', 'The Great Gatsby'))))
    

    通过使用ChoiceField实例将select功能添加到表单中。小部件默认为select输入,因此除了设置choices之外不需要任何配置:

    class ExampleForm(forms.Form):
        …
        favorite_book = forms.ChoiceField(choices=BOOK_CHOICES)
    

这是生成的 HTML:

<label for="id_favorite_book">Favorite book:</label>
<select name="favorite_book" id="id_favorite_book">
    <optgroup label="Non-Fiction">
        <option value="1">Deep Learning with Keras</option>
        <option value="2">Web Development with Django</option>
    </optgroup>
    <optgroup label="Fiction">
        <option value="3">Brave New World</option>
        <option value="4">The Great Gatsby</option>
    </optgroup>
</select>

创建多选需要使用MultipleChoiceField。它接受一个choices参数,其格式与单选的常规ChoiceField相同:

class ExampleForm(forms.Form):
    …
    books_you_own = forms.MultipleChoiceField(choices=BOOK_CHOICES)

它的 HTML 与单选类似,但增加了multiple属性:

<label for="id_books_you_own">Books you own:</label>
<select name="books_you_own" required id="id_books_you_own" multiple>
    <optgroup label="Non-Fiction">
        <option value="1">Deep Learning with Keras</option>
        <option value="2">Web Development with Django</option>
    </optgroup>
    <optgroup label="Fiction">
        <option value="3">Brave New World</option>
        <option value="4">The Great Gatsby</option>
    </optgroup>
</select>

选择项也可以在表单实例化后设置。你可能想在视图中动态生成list/tuple,然后将其分配给字段的choices属性。例如,参见以下内容:

form = ExampleForm()
form.fields["books_you_own"].choices = \
[("1", "Deep Learning with Keras"), …]

接下来是单选输入,它们与选择类似:

  • 与选择类似,单选输入使用ChoiceField,因为它们在多个选项之间提供单一选择。

  • 选项通过choices参数传递给字段构造函数。

  • 选择项以两个元素的元组的形式提供,就像选择一样:

choices = (('1', 'Option One'),\
           ('2', 'Option Two'),\
           ('3', 'Option Three'))

ChoiceField 默认以 select 输入的形式显示,因此必须将小部件设置为 RadioSelect 以使其以单选按钮的形式渲染。将选择设置与此结合,我们可以在表单中添加单选按钮,如下所示:

RADIO_CHOICES = (('Value One', 'Value One'),\
                 ('Value Two', 'Value Two'),\
                 ('Value Three', 'Value Three'))
class ExampleForm(forms.Form):
    …
    radio_input = forms.ChoiceField(choices=RADIO_CHOICES,\
                                    widget=forms.RadioSelect)

生成的 HTML 如下所示:

<label for="id_radio_input_0">Radio input:</label> 
<ul id="id_radio_input">
<li>
    <label for="id_radio_input_0">
        <input type="radio" name="radio_input"          value="Value One" required id="id_radio_input_0">
        Value One
    </label>
</li>
<li>
    <label for="id_radio_input_1">
        <input type="radio" name="radio_input"          value="Value Two" required id="id_radio_input_1">
        Value Two
    </label>
</li>
<li>
    <label for="id_radio_input_2">
        <input type="radio" name="radio_input"          value="Value Three" required id="id_radio_input_2">
        Value Three
    </label>
</li>
</ul>

Django 自动为三个单选按钮中的每一个生成唯一的标签和 ID:

  • 要创建一个 textarea 实例,请使用带有 Textarea 小部件的 CharField

    class ExampleForm(forms.Form):
        …
        text_area = forms.CharField(widget=forms.Textarea)
    

    你可能会注意到 textarea 比我们之前看到的要大得多(参见以下图示):

    ![图 6.18:普通文本框(顶部)与 Django 默认文本框(底部)的比较 图片

图 6.18:普通文本框(顶部)与 Django 默认文本框(底部)的比较

这是因为 Django 自动添加 colsrows 属性。这些属性分别设置文本字段显示的列数和行数:

<label for="id_text_area">Text area:</label>
<textarea name="text_area" cols="40"  rows="10" required id="id_text_area"></textarea>
  • 注意,colsrows 设置不会影响可以输入到字段中的文本量,只会影响一次显示的文本量。另外,textarea 的大小可以使用 CSS 设置(例如,heightwidth 属性)。这将覆盖 colsrows 设置。

    要创建 number 输入,你可能期望 Django 有一个 NumberField 类型,但实际上并没有。

    记住,Django 表单字段是数据驱动的,而不是显示驱动的,因此,Django 根据你想要存储的数值类型提供不同的 Field 类:

  • 对于整数,请使用 IntegerField

  • 对于浮点数,请使用 FloatFieldDecimalField。后两者在将数据转换为 Python 值的方式上有所不同。

  • FloatField 将转换为浮点数,而 DecimalField 是十进制数。

  • 相比于浮点数,十进制值在表示数字时具有更高的精度,但可能无法很好地集成到现有的 Python 代码中。

我们将一次性将所有三个字段添加到表单中:

class ExampleForm(forms.Form):
    …
    integer_input = forms.IntegerField()
    float_input = forms.FloatField()
    decimal_input = forms.DecimalField()

下面是三个文本框的 HTML 代码:

<p>
    <label for="id_integer_input">Integer input:</label>
    <input type="number" name="integer_input"      required id="id_integer_input">
</p>
<p>
    <label for="id_float_input">Float input:</label>
    <input type="number" name="float_input"      step="any" required id="id_float_input">
</p>
<p>
    <label for="id_decimal_input">Decimal       input:</label>
    <input type="number" name="decimal_input"      step="any" required id="id_decimal_input">
</p>

生成的 IntegerField HTML 缺少其他两个字段(FloatFieldDecimalField)所具有的 step 属性,这意味着小部件将只接受整数值。其他两个字段生成的 HTML 非常相似。它们在浏览器中的行为相同;它们仅在 Django 代码中使用其值时有所不同。

如你所猜,可以使用 EmailField 创建一个 email 输入:

class ExampleForm(forms.Form):
    …
    email_input = forms.EmailField()

它的 HTML 与我们手动创建的 email 输入类似:

<label for="id_email_input">Email input:</label>
<input type="email" name="email_input" required id="id_email_input">

在我们手动创建的表单之后,我们将查看下一个字段 DateField

  • 默认情况下,Django 将 DateField 渲染为 text 输入,并且当字段被点击时,浏览器不会显示日历弹出窗口。

我们可以不带参数地将 DateField 添加到表单中,如下所示:

class ExampleForm(forms.Form):
    …
    date_input = forms.DateField()

当渲染时,它看起来就像一个普通的 text 输入:

![图 6.19:表单中默认的 DateField 显示图片

图 6.19:表单中默认的 DateField 显示

默认生成的 HTML 如下所示:

<label for="id_date_input">Date input:</label>
<input type="text" name="date_input" required id="id_date_input">

使用text输入的原因是它允许用户以多种不同的格式输入日期。例如,默认情况下,用户可以以Year-Month-Day(用连字符分隔)或Month/Day/Year(用斜杠分隔)的格式输入日期。可以通过将格式列表传递给DateField构造函数的input_formats参数来指定接受的格式。例如,我们可以接受Day/Month/YearDay/Month/Year-with-century格式的日期,如下所示:

DateField(input_formats = ['%d/m/%y', '%d/%m/%Y'])

我们可以通过将attrs参数传递给小部件构造函数来覆盖字段小部件上的任何属性。这接受一个字典,包含将被渲染到输入 HTML 中的属性键/值。

我们还没有使用这个功能,但在下一章我们将进一步自定义字段渲染时,我们还会再次看到它。现在,我们只需设置一个属性,type,它将覆盖默认的输入类型:

class ExampleForm(forms.Form):
    …
    date_input = forms.DateField\
                 (widget=forms.DateInput(attrs={'type': 'date'}))

当渲染时,它现在看起来就像我们之前拥有的日期字段,点击它将弹出日历日期选择器:

![图 6.20:带有日期输入的 DateField]

图片 B15509_06_20.jpg

图 6.20:带有日期输入的 DateField

现在检查生成的 HTML,我们可以看到它使用的是date类型:

<label for="id_date_input">Date input:</label>
<input type="date" name="date_input" required id="id_date_input">

我们还缺少的最终输入是隐藏输入。

由于 Django 表单的数据中心性质,没有HiddenField。相反,我们选择需要隐藏的字段类型,并将其widget设置为HiddenInput。然后我们可以使用字段构造函数的initial参数设置字段的值:

class ExampleForm(forms.Form):
    …
    hidden_input = forms.CharField\
                   (widget=forms.HiddenInput, \
                    initial='Hidden Value')

下面是生成的 HTML:

<input type="hidden" name="hidden_input"  value="Hidden Value" id="id_hidden_input">

注意,由于这是一个隐藏输入,Django 不会生成label实例或任何周围的p元素。Django 还提供了其他一些以类似方式工作的表单字段。这些包括DateTimeField(用于捕获日期和时间)、GenericIPAddressField(用于 IPv4 或 IPv6 地址)和URLField(用于 URL)。完整的字段列表可在docs.djangoproject.com/en/3.0/ref/forms/fields/找到。

在模板中渲染表单

我们现在已经看到了如何创建表单并添加字段,我们也看到了表单的样式以及生成的 HTML。但是表单实际上是如何在模板中渲染的呢?我们只需实例化Form类,并将其传递给视图中的render函数,使用上下文,就像任何其他变量一样。

例如,以下是传递我们的ExampleForm到模板的方法:

def view_function(request):
    form = ExampleForm()
    return render(request, "template.html", {"form": form})

Django 在渲染模板时不会为你添加<form>元素或提交按钮;你应该在模板中表单放置的位置周围添加这些元素。表单可以像任何其他变量一样进行渲染。

我们之前简要提到过,表单是通过使用as_p方法在模板中渲染的。这个布局方法被选择,因为它与我们手动构建的示例表单最接近。Django 提供了三种可以使用的布局方法:

  • as_table

    表单被渲染为表格行,每个输入都在自己的行中。Django 不会生成周围的table元素,所以你应该自己包裹表单。请参考以下示例:

    <form method="post">
        <table>
            {{ form.as_table }}
        </table>
    </form>
    

    as_table是默认的渲染方法,所以{{ form.as_table }}{{ form }}是等价的。渲染后的表单看起来如下:

    Figure 6.21:以表格形式渲染的表单

    img/B15509_06_21.jpg

Figure 6.21:以表格形式渲染的表单

下面是生成的 HTML 的小样本:

<tr>
    <th>
        <label for="id_text_input">Text input:</label>
    </th>
    <td>
        <input type="text" name="text_input" required id="id_text_input">
    </td>
</tr>
<tr>
    <th>
        <label for="id_password_input">Password input:</label>
    </th>
    <td>
        <input type="password" name="password_input" required id="id_password_input">
    </td>
</tr>
  • as_ul

    这将表单字段渲染为ulol元素内的列表项(li)。与as_table类似,包含元素(<ul><ol>)不是由 Django 创建的,必须由你添加:

    <form method="post">
        <ul>
            {{ form.as_ul }}
        </ul>
    </form>
    

    下面是使用as_ul渲染表单的方式:

    Figure 6.22:使用 as_ul 渲染的表单

    img/B15509_06_22.jpg

Figure 6.22:使用 as_ul 渲染的表单

下面是生成的 HTML 样本:

<li>
    <label for="id_text_input">Text input:</label>
    <input type="text" name="text_input" required id="id_text_input">
</li>
<li>
    <label for="id_password_input">Password input:</label>
    <input type="password" name="password_input" required id="id_password_input">
</li>
  • as_p

    最后,是我们在之前的示例中使用过的as_p方法。每个输入都被包裹在p标签内,这意味着你不需要像之前的方法那样手动包裹表单(在<table><ul>中):

    <form method="post">
        {{ form.as_p }}
    </form>
    

    下面是渲染后的表单的样子:

    Figure 6.23: 使用 as_p 渲染的表单

    img/B15509_06_23.jpg

Figure 6.23:使用 as_p 渲染的表单

你之前已经见过这个了,但再次提醒,这里是一个生成的 HTML 样本:

<p>
    <label for="id_text_input">Text input:</label>
    <input type="text" name="text_input" required id="id_text_input">
</p>
<p>
    <label for="id_password_input">Password input:</label>
    <input type="password" name="password_input" required       id="id_password_input">
</p>

你需要决定使用哪种方法来渲染你的表单,这取决于哪种最适合你的应用程序。在行为和与你的视图一起使用方面,所有的方法都是相同的。在第十五章Django 第三方库中,我们还将介绍一种使用 Bootstrap CSS 类渲染表单的方法。

现在我们已经介绍了 Django 表单,我们可以更新我们的示例表单页面,使用 Django 表单而不是手动编写所有的 HTML。

练习 6.03:构建和渲染 Django 表单

在这个练习中,你将使用我们看到的全部字段构建一个 Django 表单。表单和视图的行为将类似于我们手动构建的表单;然而,你将能够看到使用 Django 编写表单时所需的代码量要少得多。你的表单还将自动获得字段验证,如果我们对表单进行更改,我们不需要对 HTML 进行更改,因为它将根据表单定义动态更新:

  1. 在 PyCharm 中,在form_example应用目录下创建一个名为forms.py的新文件。

  2. 在你的forms.py文件顶部导入 Django 的forms库:

    from django import forms
    
  3. 通过创建一个RADIO_CHOICES变量来定义单选按钮的选择。按照以下方式填充它:

    RADIO_CHOICES = (("Value One", "Value One Display"),\
                     ("Value Two", "Text For Value Two"),\
                     ("Value Three", "Value Three's Display Text"))
    

    当你创建一个名为radio_inputChoiceField实例时,你将很快使用这个方法。

  4. 通过创建一个BOOK_CHOICES变量来定义书籍选择输入的嵌套选择。按照以下方式填充它:

    BOOK_CHOICES = (("Non-Fiction", \
                     (("1", "Deep Learning with Keras"),\
                      ("2", "Web Development with Django"))),\
                     ("Fiction", \
                      (("3", "Brave New World"),\
                       ("4", "The Great Gatsby"))))
    
  5. 创建一个名为ExampleForm的类,它继承自forms.Form类:

    class ExampleForm(forms.Form):
    

    将以下所有字段作为属性添加到类中:

        text_input = forms.CharField()
        password_input = forms.CharField\
                         (widget=forms.PasswordInput)
        checkbox_on = forms.BooleanField()
        radio_input = forms.ChoiceField\
                      (choices=RADIO_CHOICES, \
                       widget=forms.RadioSelect)
        favorite_book = forms.ChoiceField(choices=BOOK_CHOICES)
        books_you_own = forms.MultipleChoiceField\
                        (choices=BOOK_CHOICES)
        text_area = forms.CharField(widget=forms.Textarea)
        integer_input = forms.IntegerField()
        float_input = forms.FloatField()
        decimal_input = forms.DecimalField()
        email_input = forms.EmailField()
        date_input = forms.DateField\
                     (widget=forms.DateInput\
                             (attrs={"type": "date"}))
        hidden_input = forms.CharField\
                       (widget=forms.HiddenInput, initial="Hidden Value")
    

    保存文件。

  6. 打开你的 form_example 应用程序的 views.py 文件。在文件顶部,添加一行以从 forms.py 文件导入 ExampleForm

    from .forms import ExampleForm
    
  7. form_example 视图中,实例化 ExampleForm 类并将其分配给 form 变量:

        form = ExampleForm()
    
  8. 使用 form 键将 form 变量添加到上下文字典中。return 行应该看起来像这样:

        return render(request, "form-example.html",\
                      {"method": request.method, "form": form})
    

    保存文件。确保你没有删除打印出表单已发送数据的代码,因为我们将在本练习的稍后部分再次使用它。

  9. 打开 form-example.html 文件,位于 form_example 应用程序的 templates 目录中。你可以几乎删除 form 元素的全部内容,除了 {% csrf_token %} 模板标签和提交按钮。完成之后,它应该看起来像这样:

    <form method="post">
        {% csrf_token %}
        <p>
            <input type="submit" name="submit_input" value="Submit Input">
        </p>
        <p>
            <button type="submit" name="button_element" value="Button Element">
                Button With <strong>Styled</strong> Text
            </button>
        </p>
    </form>
    
  10. 使用 as_p 方法渲染 form 变量。将此放在 {% csrf_token %} 模板标签之后的行上。现在整个 form 元素应该看起来像这样:

    <form method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <p>
            <input type="submit" name="submit_input" value="Submit Input">
        </p>
        <p>
            <button type="submit" name="button_element"           value="Button Element">
                Button With <strong>Styled</strong> Text
            </button>
        </p>
    </form>
    
  11. 如果 Django 开发服务器尚未运行,请启动它,然后在浏览器中访问表单示例页面,地址为 http://127.0.0.1:8000/form-example/。它应该看起来如下所示:![图 6.24:浏览器中渲染的 Django ExampleForm 图片

    ![图 6.24:浏览器中渲染的 Django ExampleForm1. 在表单中输入一些数据 - 由于 Django 将所有字段标记为必填,你需要输入一些文本或为所有字段选择值,包括确保复选框被勾选。提交表单。1. 切换回 PyCharm,查看窗口底部的调试控制台。你应该会看到表单提交的所有值都打印到了控制台,类似于 练习 6.02,在视图中处理 POST 数据:![图 6.25:Django 表单提交的值 图片

![图 6.25:Django 表单提交的值

你可以看到,值仍然是字符串,名称与 ExampleForm 类的属性名称匹配。请注意,你点击的提交按钮也包括在内,以及 CSRF 令牌。你提交的表单可以是 Django 表单字段和任意字段混合;两者都将包含在 request.POST QueryDict 对象中。

在这个练习中,你创建了一个 Django 表单,包含许多不同类型的表单字段。你将其实例化到视图中的一个变量中,然后传递给 form-example.html,在那里它被渲染为 HTML。最后,你提交了表单并查看它提交的值。请注意,我们编写以生成相同表单的代码量大大减少了。我们不必手动编写任何 HTML,现在我们有一个地方既定义了表单的显示方式,也定义了它的验证方式。在下一节中,我们将探讨 Django 表单如何自动验证提交的数据,以及数据如何从字符串转换为 Python 对象。

验证表单和检索 Python 值

到目前为止,我们已经看到了 Django 表单如何通过 Python 代码自动渲染来简化定义表单的过程。现在,我们将探讨使 Django 表单有用的另一部分:它们能够自动验证表单,并从中检索原生 Python 对象和值。

在 Django 中,表单可以是未绑定绑定的。这些术语描述了表单是否已经接收到用于验证的提交POST数据。到目前为止,我们只看到了未绑定的表单——它们是无参数实例化的,如下所示:

form = ExampleForm()

如果表单使用一些数据调用以用于验证,例如POST数据,则该表单是绑定的。绑定的表单可以创建如下所示:

form = ExampleForm(request.POST)

使用绑定形式后,我们可以开始使用内置的验证相关工具:首先,使用is_valid方法来检查表单的有效性,然后是表单上的cleaned_data属性,它包含从字符串转换为 Python 对象的值。cleaned_data属性仅在表单被清理后可用,这意味着“清理”数据并将其从字符串转换为 Python 对象的过程。清理过程在is_valid调用期间运行。如果你在调用is_valid之前尝试访问cleaned_data,将会引发AttributeError

下面是一个如何访问ExampleForm清理数据的简短示例:

form = ExampleForm(request.POST)
if form.is_valid():
    # cleaned_data is only populated if the form is valid
    if form.cleaned_data["integer_input"] > 5:
        do_something()

在这个例子中,form.cleaned_data["integer_input"]是整数值10,因此它可以与数字5进行比较。将此与已发布的值进行比较,该值是字符串"10"。清理过程为我们执行此转换。其他字段,如日期或布尔值,也会相应转换。

清理过程还会设置表单和字段上的任何错误,这些错误将在表单再次渲染时显示。让我们看看这一切是如何发生的。现代浏览器提供了大量的客户端验证,因此它们会阻止表单提交,除非其基本验证规则得到满足。如果你在之前的练习中尝试提交带有空字段的表单,你可能已经看到了这一点:

图 6.26:浏览器阻止表单提交

图 6.26:浏览器阻止表单提交

图 6.26显示了浏览器阻止表单提交。由于浏览器阻止了提交,Django 从未有机会验证表单本身。为了允许表单提交,我们需要添加一些更高级的验证,浏览器无法自行验证。

我们将在下一节讨论可以应用于表单字段的不同类型的验证,但到目前为止,我们只是将max_digits设置为3添加到ExampleFormdecimal_input中。这意味着用户不应在表单中输入超过三个数字。

注意

为什么 Django 需要在浏览器已经进行验证并阻止提交的情况下验证表单?服务器端应用程序永远不应该信任用户的输入:用户可能正在使用较旧的浏览器或另一个 HTTP 客户端来发送请求,因此不会从他们的“浏览器”收到任何错误。此外,正如我们刚才提到的,浏览器不理解某些类型的验证,因此 Django 必须在它的端进行验证。

ExampleForm的更新如下:

class ExampleForm(forms.Form):
    …
    decimal_input = forms.DecimalField(max_digits=3)
    …

现在视图应该更新为在方法为POST时将request.POST传递给Form类,例如,如下所示:

if request.method == "POST":
    form = ExampleForm(request.POST)
else:
    form = ExampleForm()

如果在方法不是POST时将request.POST传递给表单构造函数,那么表单在首次渲染时将始终包含错误,因为request.POST将是空的。现在浏览器将允许我们提交表单,但如果decimal_input包含超过三个数字,将会显示错误。

![图 6.27:当字段无效时显示的错误图片

图 6.27:当字段无效时显示的错误

当模板中有错误时,Django 会自动以不同的方式渲染表单。但我们如何使视图根据表单的有效性以不同的方式行为?正如我们之前提到的,我们应该使用表单的is_valid方法。使用此检查的视图可能有如下代码:

form = ExampleForm(request.POST)
if form.is_valid():
    # perform operations with data from form.cleaned_data
    return redirect("/success-page")  # redirect to a success page

在这个例子中,如果表单有效,我们将重定向到成功页面。否则,假设执行流程继续如前所述,并将无效表单返回给render函数以显示给用户带有错误的信息。

注意

为什么我们在成功时返回重定向?有两个原因:首先,提前返回防止执行视图的其余部分(即失败分支);其次,防止用户在重新加载页面时收到重新发送表单数据的消息。

在下一个练习中,我们将看到表单验证的实际操作,并根据表单的有效性更改视图执行流程。

练习 6.04:在视图中验证表单

在这个练习中,我们将更新示例视图,根据 HTTP 方法的不同实例化表单。我们还将更改表单以打印出清理后的数据而不是原始的POST数据,但仅当表单有效时:

  1. 在 PyCharm 中,打开form_example应用目录内的forms.py文件。将max_digits=3参数添加到ExampleFormdecimal_input中:

    class ExampleForm(forms.Form):
        …
        decimal_input = forms.DecimalField(max_digits=3)
    

    一旦添加了这个参数,我们就可以提交表单,因为浏览器不知道如何验证这个规则,但 Django 知道。

  2. 打开reviews应用的views.py文件。我们需要更新form_example视图,以便如果请求的方法是POST,则使用POST数据实例化ExampleForm;否则,不带参数实例化。用以下代码替换当前表单初始化:

    def form_example(request):
        if request.method == "POST":
            form = ExampleForm(request.POST)
        else:
            form = ExampleForm()
    
  3. 接下来,对于POST请求方法,我们将使用is_valid方法检查表单是否有效。如果表单有效,我们将打印出所有清理后的数据。在ExampleForm实例化后添加一个条件来检查form.is_valid(),然后将调试打印循环移到这个条件内部。您的POST分支应如下所示:

        if request.method == "POST":
            form = ExampleForm(request.POST)
            if form.is_valid():
                for name in request.POST:
                    print("{}: {}".format\
                                   (name, request.POST.getlist(name)))
    
  4. 我们不会遍历原始request.POST QueryDict(其中所有数据都是string实例),而是遍历formcleaned_data。这是一个正常的字典,包含转换为 Python 对象的值。用以下两个替换for行和print行:

                for name, value in form.cleaned_data.items():
                    print("{}: ({}) {}".format\
                                        (name, type(value), value))
    

    我们不再需要使用getlist(),因为cleaned_data已经将多值字段转换为list实例。

  5. 启动 Django 开发服务器,如果它尚未运行。切换到您的浏览器,浏览到http://127.0.0.1:8000/form-example/的示例表单页面。表单应与之前一样。填写所有字段,但请确保在Decimal 输入字段中输入四个或更多数字以使表单无效。提交表单,当页面刷新时,您应该看到Decimal 输入的错误消息:![图 6.28:表单提交后显示的十进制输入错误 图片

    图 6.28:表单提交后显示的十进制输入错误

  6. 通过确保Decimal 输入字段中只有三位数字来修复表单错误,然后再次提交表单。切换回 PyCharm 并检查调试控制台。您应该看到所有清理后的数据都已打印出来:![图 6.29:表单的清理数据打印输出 图片

图 6.29:表单的清理数据打印输出

注意已经发生的转换。CharField实例已转换为strBooleanField转换为boolIntegerFieldFloatFieldDecimalField分别转换为intfloatDecimalDateField变为datetime.date,而选择字段保留其初始选择值的字符串值。注意books_you_own已自动转换为str实例的列表。

此外,请注意,与我们在遍历所有POST数据不同,cleaned_data只包含表单字段。其他数据(如 CSRF 令牌和点击的提交按钮)存在于POST QueryDict中,但因为它不包含表单字段,所以不包括在内。

在这个练习中,你更新了 ExampleForm,使得浏览器允许提交,尽管 Django 会认为它是无效的。这允许 Django 对表单进行验证。然后你更新了 form_example 视图,根据 HTTP 方法实例化 ExampleForm 类;对于 POST 请求,传递请求的 POST 数据。视图还更新了其调试输出代码,以 printcleaned_data 字典。最后,你测试了提交有效和无效的表单数据,以查看不同的执行路径和表单生成的数据类型。我们看到 Django 会自动将 POST 数据从字符串转换为基于字段类的 Python 类型。

接下来,我们将探讨如何向字段添加更多验证选项,这将使我们能够更严格地控制可以输入的值。

内置字段验证

我们尚未讨论可用于字段的常规验证参数。尽管我们已经提到了 required 参数(默认为 True),但还可以使用许多其他参数来更严格地控制输入字段的数值。以下是一些有用的参数:

  • max_length

    设置可以输入到字段中的最大字符数;在 CharField(以及 FileField,我们将在 第八章媒体服务和文件上传 中介绍)中可用。

  • min_length

    设置必须输入到字段中的最小字符数;在 CharField(以及 FileField;关于这一点,我们将在 第八章媒体服务和文件上传 中详细说明)中可用。

  • max_value

    设置可以输入到数值字段的最高值;在 IntegerFieldFloatFieldDecimalField 中可用。

  • min_value

    设置可以输入到数值字段的最小值;在 IntegerFieldFloatFieldDecimalField 中可用。

  • max_digits

    这设置了可以输入的最大数字位数;这包括小数点前后的数字(如果有的话)。例如,数字 12.34 有四个数字,而数字 56.7 有三个。在 DecimalField 中使用。

  • decimal_places

    这设置了小数点后可以输入的最大数字位数。这通常与 max_digits 一起使用,并且小数位数始终计入数字总数,即使小数位数没有输入到小数点之后。例如,假设使用 max_digits 为四和 decimal_places 为三:如果输入的数字是 12.34,它实际上会被解释为值 12.340;也就是说,会添加零,直到小数点后的数字位数等于 decimal_places 设置。由于我们将 decimal_places 设置为三,所以总数字位数最终是五,超过了 max_digits 设置的四。数字 1.2 是有效的,因为即使扩展到 1.200,总数字位数也只有四。

您可以混合和匹配验证规则(前提是字段支持它们)。CharField 可以有 max_lengthmin_length,数值字段可以同时有 min_valuemax_value,等等。

如果您需要更多的验证选项,您可以编写自定义验证器,我们将在下一节中介绍。现在,我们将向我们的 ExampleForm 添加一些验证器,以便看到它们的作用。

练习 6.05:添加额外字段验证

在这个练习中,我们将添加和修改 ExampleForm 字段的验证规则。然后我们将看到这些更改如何影响表单的行为,无论是在浏览器中还是在 Django 验证表单时:

  1. 在 PyCharm 中,打开 form_example 应用程序目录内的 forms.py 文件。

  2. 我们将使 text_input 至多需要三个字符。将 max_length=3 参数添加到 CharField 构造函数中:

    text_input = forms.CharField(max_length=3)
    
  3. 通过要求至少八个字符来提高 password_input 的安全性。将 min_length=8 参数添加到 CharField 构造函数中:

    password_input = forms.CharField(min_length=8, \
                                     widget=forms.PasswordInput)
    
  4. 用户可能没有任何书籍,因此 books_you_own 字段不应是必需的。将 required=False 参数添加到 MultipleChoiceField 构造函数中:

    books_you_own = forms.MultipleChoiceField\
                    (required=False, choices=BOOK_CHOICES)
    
  5. 用户应在 integer_input 中只能输入介于 1 和 10 之间的值。将 min_value=1max_value=10 参数添加到 IntegerField 构造函数中:

    integer_input = forms.IntegerField\
                    (min_value=1, max_value=10)
    
  6. 最后,将 max_digits=5decimal_places=3 添加到 DecimalField 构造函数中:

    decimal_input = forms.DecimalField\
                    (max_digits=5, decimal_places=3)
    

    保存文件。

  7. 如果 Django 开发服务器尚未运行,请启动它。为了获取这些新的验证规则,我们不需要对其他任何文件进行任何更改,因为 Django 会自动更新 HTML 生成和验证逻辑。这是使用 Django 表单获得的一个巨大好处。只需在您的浏览器中访问或刷新 http://127.0.0.1:8000/form-example/,新的验证规则将自动添加。除非您尝试使用错误的值提交表单,否则表单看起来不会有任何不同。以下是一些可以尝试的事情:

    Text 输入 字段中输入超过三个字符;您将无法做到。

    Password 字段中输入少于八个字符,然后点击离开它。浏览器应该显示一个错误,表明这不是有效的。

    不要为 Books you own 字段选择任何值。这不会阻止您提交表单。

    Integer 输入 上使用步进按钮。您只能输入介于 110 之间的值。如果您输入这个范围之外的值,您的浏览器应该会显示一个错误。

    Decimal 输入 是唯一一个在浏览器中不验证 Django 规则的字段。您需要输入一个无效的值(例如 123.456)并提交表单,然后才会显示错误(由 Django 生成)。

    下图显示了浏览器可以自行验证的一些字段:

    图 6.30:浏览器使用新规则进行验证

图 6.30:浏览器使用新规则进行验证

图 6.31 展示了一个只能由 Django 生成错误的错误,因为浏览器不理解 DecimalField 验证规则:

![图 6.31:浏览器认为表单有效,但 Django 不认为有效]

](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/webdev-dj-2e/img/B15509_06_31.jpg)

图 6.31:浏览器认为表单有效,但 Django 不认为有效

在这个练习中,我们在表单字段上实现了一些基本的验证规则。然后我们在浏览器中加载了表单示例页面,而无需对我们的模板或视图进行任何更改。我们尝试用不同的值提交表单,以查看浏览器如何与 Django 验证表单。

在本章的活动里,我们将使用 Django 表单来实现图书搜索视图。

活动 6.01:图书搜索

在这个活动中,你将完成在 第一章Django 简介 中开始的图书搜索视图。你将构建一个 SearchForm 实例,该实例从 request.GET 中提交并接受一个搜索字符串。它将有一个 select 字段来选择搜索 titlecontributor。然后它将在 Book 实例中搜索包含给定文本的 titleContributorfirst_nameslast_names。然后你将在 search-results.html 模板中渲染这本书的列表。搜索词不应该为必填项,但如果存在,它的长度应为三个或更短字符。由于视图将在使用 GET 方法时进行搜索,因此表单将始终进行验证检查。如果我们使字段为必填项,那么每次页面加载时都会显示错误。

将有两种执行搜索的方法。第一种是通过提交位于 base.html 模板中(因此位于每个页面的右上角)的搜索表单。这将仅通过 Book 标题进行搜索。另一种方法是通过提交在 search-results.html 页面上渲染的 SearchForm 实例。这个表单将显示用于在 titlecontributor 之间进行选择的 ChoiceField 实例。

这些步骤将帮助你完成这个活动:

  1. 在你的 forms.py 文件中创建一个 SearchForm 实例。

  2. SearchForm 应该有两个字段。第一个是一个名为 searchCharField 实例。这个字段不应该为必填项,但应该有最小长度为 3

  3. SearchForm 的第二个字段是一个名为 search_inChoiceField 实例。这将允许在 titlecontributor(分别带有 TitleContributor 标签)之间进行选择。它不应该为必填项。

  4. 更新 book_search 视图,使用 request.GET 中的数据实例化一个 SearchForm

  5. 添加代码以使用title__icontains(用于不区分大小写的搜索)搜索Book模型。如果按title搜索,则应执行此操作。只有在表单有效且包含一些搜索文本时才应执行搜索。search_in值应使用get方法从cleaned_data中检索,因为它可能不存在,因为它不是必需的。将其默认值设置为title

  6. 在搜索贡献者时,使用first_names__icontainslast_names__icontains,然后遍历贡献者并检索每个贡献者的书籍。如果按contributor搜索,则应执行此操作。只有在表单有效且包含一些搜索文本时才应执行搜索。有许多方法可以组合一个或最后一个名字的搜索结果。最简单的方法是使用你迄今为止介绍的技术执行两个查询,一个用于匹配第一个名字,然后是最后一个名字,并分别迭代它们。

  7. 更新render调用以包含form变量和上下文中检索到的书籍(以及已传递的search_text)。模板的位置在第三章URL 映射、视图和模板中已更改,因此相应地更新render的第二个参数。

  8. 我们在第一章Django 简介中创建的search-results.html模板现在基本上是多余的,因此您可以清除其内容。更新search-results.html文件以从base.html扩展,而不是作为一个独立的模板文件。

  9. 添加一个title块,如果表单有效并且设置了search_text,则显示Search Results for <search_text>,否则仅显示Book Search。此块将在本活动的后续部分添加到base.html中。

  10. 添加一个content块,该块应显示一个带有文本Search for Books<h2>标题。在<h2>标题下渲染表单。<form>元素可以没有属性,并且默认将其设置为向同一 URL 发出GET请求。添加一个我们之前活动中使用的带有btn btn-primary类的提交按钮。

  11. 在表单下方,如果表单有效并且输入了搜索文本,则显示Search results for <search_text>消息,否则不显示消息。这应在<h3>标题中显示,并且搜索文本应被包裹在<em>标签中。

  12. 遍历搜索结果并渲染每个结果。显示书籍标题和贡献者的第一个和最后一个名字。书籍标题应链接到book_detail页面。如果书籍列表为空,则显示文本No results found。你应该将结果包裹在具有class list-group<ul>中,并且每个结果应是一个具有class list-group-item<li>实例。这将与book_list页面类似;然而,我们不会显示太多信息(只是标题和贡献者)。

  13. base.html更新以包含一个动作属性在搜索<form>标签中。使用url模板标签来生成此属性的 URL。

  14. 将搜索字段的name属性设置为search,并将value属性设置为输入的搜索文本。同时,确保字段的最小长度为3

  15. base.html中,向被其他模板覆盖的title标签添加一个title块(如步骤 9所示)。在<title> HTML 元素内添加一个block模板标签。它应该包含内容Bookr

完成此活动后,你应该能够打开http://127.0.0.1:8000/book-search/上的图书搜索页面,它将看起来像图 6.32

图 6.32:无搜索的图书搜索页面

图 6.32:无搜索的图书搜索页面

当仅使用两个字符进行搜索时,你的浏览器应该阻止你提交任一搜索字段。如果你搜索的内容没有结果,你将看到一个消息,表明没有找到结果。通过标题(这可以通过任一字段完成)搜索将显示匹配的结果。

类似地,当通过贡献者进行搜索(尽管这只能在下表单中完成)时,你应该看到以下类似的内容:

图 6.33:贡献者搜索

图 6.33:贡献者搜索

注意

此活动的解决方案可以在packt.live/2Nh1NTJ找到。

摘要

本章是 Django 表单的介绍。我们介绍了一些 HTML 输入,用于在网页上输入数据。我们讨论了数据如何提交到 Web 应用程序,以及在何时使用GETPOST请求。然后我们探讨了 Django 表单类如何简化生成表单 HTML 的过程,以及如何使用模型自动构建表单。我们还通过构建图书搜索功能增强了 Bookr。

在下一章中,我们将更深入地探讨表单,学习如何自定义表单字段的显示,如何添加更高级的验证到你的表单,以及如何使用ModelForm类自动保存模型实例。

第七章:7. 高级表单验证和模型表单

概述

在继续使用 Bookr 应用程序的旅程中,你将开始本章,通过添加一个新的表单到你的应用程序中,并使用自定义的多字段验证和表单清理。你将学习如何设置表单的初始值并自定义小部件(正在生成的 HTML 输入元素)。然后,你将介绍ModelForm类,它允许从模型自动创建表单。你将在视图中使用它来自动保存新的或更改的Model实例。

到本章结束时,你将知道如何为 Django 表单添加额外的多字段验证,如何自定义和设置字段的表单小部件,如何使用ModelForms从 Django 模型自动创建表单,以及如何从ModelForms自动创建Model实例。

简介

本章建立在我们在第六章表单中获得的知识之上,我们学习了如何从 HTML 表单提交数据到 Django 视图,无论是手动构建的 HTML 表单还是 Django 表单。我们使用了 Django 的form库来构建和自动验证具有基本验证的表单。例如,现在我们可以构建检查日期是否以期望的格式输入的表单,是否在用户输入年龄时输入了数字,以及用户点击提交按钮之前是否选择了下拉菜单。然而,大多数大型网站需要更高级的验证。

例如,某个字段可能只有在另一个字段被设置时才是必需的。假设我们想添加一个复选框,允许用户注册我们的月度通讯录。它下面有一个文本框,让他们输入他们的电子邮件地址。通过一些基本的验证,我们可以检查以下内容:

  • 用户是否勾选了复选框。

  • 用户已经输入了他们的电子邮件地址。

当用户点击提交按钮时,我们将能够验证两个字段是否都被操作。但如果用户不想注册我们的通讯录呢?如果他们点击提交按钮,理想情况下,两个字段都应该为空。这就是验证每个单独字段可能不起作用的地方。

另一个例子可能是有两个字段,每个字段的最高值是,比如说,50。但每个字段添加的总值必须小于 75。我们将从查看如何编写自定义验证规则来解决此类问题开始本章。

随着本章的深入,我们将探讨如何在表单上设置初始值。这在自动填写用户已知的信息时可能很有用。例如,如果用户已登录,我们可以自动将用户的联系信息放入表单中。

我们将通过查看模型形式来结束本章,这将使我们能够自动从 Django Model类创建一个表单。这减少了创建新的Model实例所需编写的代码量。

自定义字段验证和清理

我们已经看到了 Django 表单如何将 HTTP 请求中的值(字符串)转换为 Python 对象。在非自定义 Django 表单中,目标类型取决于字段类。例如,从 IntegerField 衍生的 Python 类型是 int,字符串值将按用户输入的原文提供给我们。但我们可以为我们的 Form 类实现方法,以任何我们选择的方式更改字段的输出值。这使我们能够清理或过滤用户的输入数据,使其更好地符合我们的预期。我们可以将整数四舍五入到最接近的十的倍数,以便适应批量大小以订购特定项目。或者,我们可以将电子邮件地址转换为小写,以便数据在搜索时保持一致性。

我们还可以实现一些自定义验证器。我们将探讨几种不同的验证字段的方法:通过编写自定义验证器,以及为字段编写自定义的 clean 方法。每种方法都有其优缺点:自定义验证器可以应用于不同的字段和表单,因此你不必为每个字段编写验证逻辑;自定义的 clean 方法必须为每个你想要清理的表单实现,但它更强大,允许使用表单中的其他字段进行验证或更改字段返回的清理值。

自定义验证器

验证器是一个简单的函数,它接受一个值,如果该值无效,则引发 django.core.exceptions.ValidationError – 有效性由你编写的代码确定。该值是一个 Python 对象(即,已经从 POST 请求字符串转换而来的 cleaned_data)。

这里有一个简单的示例,用于验证值是否为小写:

from django.core.exceptions import ValidationError
def validate_lowercase(value):
  if value.lower() != value:
    raise ValidationError("{} is not lowercase."\
                          .format(value))

注意,该函数对于成功或失败都不会返回任何内容。如果值无效,它将仅引发 ValidationError

注意

注意,ValidationError 的行为和处理方式与 Django 中其他异常的行为不同。通常,如果你在视图中引发异常,你将得到 Django 的 500 响应(如果你没有在代码中处理该异常)。

当在验证/清理代码中引发 ValidationError 时,Django 的 form 类会为你捕获错误,然后 formis_valid 方法将返回 False。你不需要在可能引发 ValidationError 的代码周围编写 try/except 处理程序。

验证器可以作为表单中字段构造函数的 validators 参数传递,在列表中;例如,将我们的 text_input 字段从我们的 ExampleForm 传递:

class ExampleForm(forms.Form):
  text_input = forms.CharField(validators=[validate_lowercase])

现在,如果我们提交表单并且字段包含大写值,我们将得到一个错误,如图下所示:

![图 7.1:小写文本验证器在作用中

![img/B15509_07_01.jpg]

图 7.1:小写文本验证器在作用中

验证器函数可以用在任何数量的字段上。在我们的例子中,如果我们想强制许多字段使用小写,可以将validate_lowercase传递给所有这些字段。现在让我们看看我们如何以另一种方式实现它,使用自定义的clean方法。

清理方法

Form类上创建了一个名为clean_field-name格式的clean方法。例如,text_inputclean方法将被调用为clean_text_inputbooks_you_ownclean方法将是clean_books_you_own,依此类推。

清理方法不接受任何参数;相反,它们应该使用self上的cleaned_data属性来访问字段数据。这个字典将包含以标准 Django 方式清理后的数据,正如我们在前面的例子中所看到的。clean方法必须返回清理后的值,这将替换cleaned_data字典中的原始值。即使方法没有改变值,也必须返回一个值。你还可以使用clean方法来引发ValidationError,错误将被附加到字段(与 validator 相同)。

让我们重新实现小写验证器作为一个clean方法,如下所示:

class ExampleForm(forms.Form):
   text_input = forms.CharField()
  …
  def clean_text_input(self):
    value = self.cleaned_data['text_input']
    if value.lower() != value:
      raise ValidationError("{} is not lowercase."\
                            .format(value))\
    return value

你可以看到逻辑基本上是相同的,除了我们必须在最后返回验证过的值。如果我们提交表单,我们会得到与之前尝试时相同的结果(图 7.1)。

让我们再看一个清理示例。当值无效时,我们不是抛出异常,而是直接将值转换为小写。我们可以用以下代码实现:

class ExampleForm(forms.Form):
  text_input = forms.CharField()
  …
  def clean_text_input(self):
    value = self.cleaned_data['text_input']
    return value.lower()

现在,考虑我们以大写形式输入文本到输入框中:

![图 7.2:输入的全大写文本图片 B15509_07_02.jpg

图 7.2:输入的全大写文本

如果我们使用视图的调试输出检查清理后的数据,我们会看到它已经被转换为小写:

![图 7.3:清理后的数据已转换为小写图片 B15509_07_03.jpg

图 7.3:清理后的数据已转换为小写

这些只是使用验证器和clean方法验证字段的一些简单示例。当然,如果你愿意,你可以使每种类型的验证更加复杂,并使用clean方法以更复杂的方式转换数据。

到目前为止,你只学习了简单的表单验证方法,其中你独立地处理每个字段。一个字段是否有效(或无效)仅基于它包含的信息,而不是其他任何东西。如果某个字段的验证性依赖于用户在另一个字段中输入的内容怎么办?这种情况的一个例子可能是有email字段来收集某人的电子邮件地址,如果他们想要注册邮件列表。只有当他们在复选框中勾选表示他们想要注册时,该字段才是必需的。这两个字段本身都不是必需的——我们不希望复选框必须被勾选,但如果它被勾选,那么email字段也应该是必需的。

在下一节中,我们将展示如何通过在你的表单中重写 clean 方法来验证字段相互依赖的表单。

多字段验证

我们刚刚查看的 clean_<field-name> 方法可以添加到 Django 表单中,以清理特定字段。Django 还允许我们重写 clean 方法,在其中我们可以访问所有字段的 cleaned_data,并且我们知道所有自定义字段方法都已调用。这允许基于另一个字段的数据进行字段验证。

参考我们之前的示例,其中有一个只有在复选框被勾选时才需要的电子邮件地址的表单,我们将看到如何使用 clean 方法实现这一点。

首先,创建一个 Form 类并添加两个字段——使用 required=False 参数使它们都为可选:

class NewsletterSignupForm(forms.Form):
  signup = forms.BooleanField\
           (label="Sign up to newsletter?", required=False)
  email = forms.EmailField\
          (help_text="Enter your email address to subscribe", \
           required=False)

我们还引入了两个新参数,可以用于任何字段:

  • label

    这允许设置字段的标签文本。正如我们所见,Django 将自动从字段名称生成标签文本。如果你设置 label 参数,你可以覆盖这个默认值。如果你想要一个更具描述性的标签,请使用此参数。

  • help_text

    如果你需要显示有关字段所需输入的更多信息,你可以使用此参数。默认情况下,它显示在字段之后。

当渲染时,表单看起来像这样:

图 7.4:带有自定义标签和帮助文本的电子邮件注册表单

图 7.5:尝试无电子邮件地址注册时显示的错误

图 7.4:带有自定义标签和帮助文本的电子邮件注册表单

如果我们现在提交表单而不输入任何数据,将不会发生任何事情。两个字段都不是必需的,所以表单验证良好。

现在,我们可以将多字段验证添加到 clean 方法中。我们将检查 signup 复选框是否被勾选,然后检查 email 字段是否有值。内置的 Django 方法已经验证了电子邮件地址的有效性,所以我们只需检查是否存在值。然后我们将使用 add_error 方法为 email 字段设置错误。这是一个你之前没有见过的方法,但它非常简单;它接受两个参数——设置错误的字段名称和错误文本。

这是 clean 方法的代码:

class NewsletterSignupForm(forms.Form):
  …
  def clean(self):
    cleaned_data = super().clean()
    if cleaned_data["signup"] and not cleaned_data.get("email"):
    self.add_error\
    ("email", \
     "Your email address is required if signing up for the newsletter.")

您的 clean 方法必须始终调用 super().clean() 方法来检索清理后的数据。当调用 add_error 添加错误到表单时,表单将不再验证(is_valid 方法返回 False)。

现在如果我们提交表单而不勾选复选框,仍然不会生成错误,但如果你勾选复选框而没有电子邮件地址,你将收到我们刚刚编写的代码错误:

图 7.5:尝试无电子邮件地址注册时显示的错误

图 7.4:带有自定义标签和帮助文本的电子邮件注册表单

图 7.5:尝试无电子邮件地址注册时显示的错误

你可能会注意到我们正在使用get方法从cleaned_data字典中检索电子邮件。这样做的原因是,如果表单中的email值无效,那么email键将不会存在于字典中。浏览器应该阻止用户提交包含无效电子邮件的表单,但用户可能正在使用不支持此客户端验证的较旧浏览器,因此为了安全起见,我们使用get方法。由于signup字段是BooleanField,并且不是必需的,它只有在使用自定义验证函数时才会无效。我们在这里没有使用,所以使用方括号表示法访问其值是安全的。

在进行我们的第一个练习之前,还有一个需要考虑的验证场景,那就是添加不特定于任何字段的错误。Django 将这些称为非字段错误。有许多场景,你可能想在多个字段相互依赖时使用这些错误。

以购物网站为例。你的订单表单可能包含两个数值字段,其总和不能超过某个值。如果总和超过了,任一字段的值可以减少以使总和低于最大值,因此错误不是特定于任一字段的。要添加非字段错误,请使用add_error方法,并将None作为第一个参数。

让我们看看如何实现这一点。在这个例子中,我们将有一个表单,用户可以指定要订购的特定数量的项目,对于项目 A 或项目 B。用户总共不能订购超过 100 个项目。字段将具有max_value100min_value0,但需要在clean方法中编写自定义验证来处理总金额的验证:

class OrderForm(forms.Form):
  item_a = forms.IntegerField(min_value=0, max_value=100)
  item_b = forms.IntegerField(min_value=0, max_value=100)\
  def clean(self):
    cleaned_data = super().clean()
    if cleaned_data.get("item_a", 0) + cleaned_data.get\
                                       ("item_b", 0) > 100:
      self.add_error\
      (None, \
       "The total number of items must be 100 or less.")

字段(item_aitem_b)以正常方式添加,并使用标准验证规则。你可以看到我们像以前一样使用了clean方法。此外,我们在该方法内部实现了最大项目逻辑。以下行是注册超过最大项目时记录非字段错误的:

self.add_error(None, \
               "The total number of items must be 100 or less.")

再次强调,我们使用get方法访问item_aitem_b的值,默认值为0。这是以防用户使用较旧的浏览器(2011 年或更早)并能提交包含无效值的表单。

在浏览器中,字段级验证确保每个字段都输入了 0 到 100 之间的值,否则将阻止表单提交:

![图 7.6:如果一个字段超过最大值,则无法提交表单img/B15509_07_06.jpg

图 7.6:如果一个字段超过最大值,则无法提交表单

然而,如果我们输入两个总和超过 100 的值,我们可以看到 Django 如何显示非字段错误:

![图 7.7:Django 非字段错误在表单开始处显示img/B15509_07_07.jpg

图 7.7:Django 非字段错误在表单开始处显示

Django 的非字段错误始终在表单的开始处显示,在其他字段或错误之前。在下一个练习中,我们将构建一个实现验证函数、字段清理方法和表单清理方法的表单。

练习 7.01:自定义清理和验证方法

在这个练习中,您将构建一个新的表单,允许用户为书籍或杂志创建订单。它必须满足以下验证标准:

  • 用户可以订购最多 80 本杂志和/或 50 本书,但物品总数不得超过 100。

  • 用户可以选择接收订单确认,如果他们这样做,则必须输入电子邮件地址。

  • 如果用户没有选择接收订单确认,则不应输入电子邮件地址。

  • 为了确保他们是我们的公司的一员,电子邮件地址必须是我们的公司域名的一部分(在我们的例子中,我们将只使用example.com)。

  • 为了与其他虚构公司的电子邮件地址保持一致,地址应转换为小写。

这听起来像是一大堆规则,但如果我们逐一解决,使用 Django 就很简单了。我们将继续使用我们在第六章“表单”中开始的form_project应用程序。如果您还没有完成第六章“表单”,可以从packt.live/2LRCczP下载代码:

  1. 在 PyCharm 中,打开form_example应用程序的forms.py文件。

    注意

    确保 Django 开发服务器没有运行,否则,在您更改此文件时,它可能会崩溃,导致 PyCharm 跳入调试器。

  2. 由于我们的ExampleForm工作已完成,您可以将其从该文件中删除。

  3. 创建一个新的类OrderForm,它从forms.Form继承:

    class OrderForm(forms.Form):
    
  4. 按照以下方式向类中添加四个字段:

    • magazine_countIntegerField,最小值为0,最大值为80

    • book_countIntegerField,最小值为0,最大值为50

    • send_confirmationBooleanField,不是必需的

    • emailEmailField,也不是必需的

      类应该看起来像这样:

      class OrderForm(forms.Form):
        magazine_count = forms.IntegerField\
                         (min_value=0, max_value=80)
        book_count = forms.IntegerField\
                     (min_value=0, max_value=50)
        send_confirmation = forms.BooleanField\
                            (required=False)
        email = forms.EmailField(required=False)
      
  5. 添加一个验证函数来检查用户的电子邮件地址是否在正确的域名下。首先,需要导入ValidationError;在文件顶部添加此行:

    from django.core.exceptions import ValidationError
    

    然后在import行之后(在OrderForm类实现之前)编写此函数:

    def validate_email_domain(value):
      if value.split("@")[-1].lower()!= "example.com":\
          raise ValidationError\
          ("The email address must be on the domain example.com.")
    

    该函数将电子邮件地址在@符号处分割,然后检查其后的部分是否等于example.com。这个函数本身将验证非电子邮件地址。例如,字符串not-valid@someotherdomain@example.com不会在这个函数中引发ValidationError。在我们的情况下这是可以接受的,因为我们使用的是EmailField,其他标准字段验证器将检查电子邮件地址的有效性。

  6. validate_email_domain函数作为验证器添加到OrderForm中的email字段。更新EmailField构造函数调用,添加一个validators参数,传递包含验证函数的列表:

    class OrderForm(forms.Form):
      …
      email = forms.EmailField\
              (required=False, \
               validators=[validate_email_domain])
    
  7. 在表单中添加一个clean_email方法,以确保电子邮件地址是小写的:

    class OrderForm(forms.Form):
      # truncated for brevity
      def clean_email(self):
      return self.cleaned_data['email'].lower()
    
  8. 现在,添加clean方法以执行所有跨字段验证。首先,我们将仅添加确保只有在请求订单确认时才输入电子邮件地址的逻辑:

    class OrderForm(forms.Form):
      # truncated for brevity
    email field if Send confirmation is checked but no email address is added: ![Figure 7.8: Error if Send confirmation is checked but no email address is added    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/webdev-dj-2e/img/B15509_07_08.jpg)Figure 7.8: Error if Send confirmation is checked but no email address is addedSimilarly, an error will be added to `email` if an email address is entered but `Send confirmation` is not checked:![Figure 7.9: Error because an email has been entered but the user     has not chosen to receive confirmation    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/webdev-dj-2e/img/B15509_07_09.jpg)Figure 7.9: Error because an email has been entered but the user has not chosen to receive confirmation
    
  9. clean方法内部添加最终的检查。项目总数不应超过 100。如果magazine_countbook_count的总和大于 100,我们将添加一个非字段错误:

    class OrderForm(forms.Form):
      …
      def clean(self):
        …
        None as the first argument to the add_error call.NoteRefer to [`packt.live/3nMP3R7`](http://packt.live/3nMP3R7) for the complete code.Save `forms.py`.
    
  10. 打开reviews应用的views.py文件。我们将更改表单import,以便导入OrderForm而不是ExampleForm。考虑以下导入行:

    from .forms import ExampleForm, SearchForm
    

    按照以下方式更改:

    from .forms import OrderForm, SearchForm
    
  11. form_example视图中,将使用ExampleForm的两行更改为使用OrderForm。考虑以下代码行:

    form = ExampleForm(request.POST)
    

    按照以下方式更改:

    form = OrderForm(request.POST)
    

    类似地,考虑以下代码行:

    form = ExampleForm()
    

    按照以下方式更改:

    form = OrderForm()
    

    函数的其余部分可以保持不变。

    我们不需要更改模板。启动 Django 开发服务器,并在浏览器中导航到http://127.0.0.1:8000/form-example/。你应该会看到像图 7.10中渲染的表单:

    图 7.10:浏览器中的 OrderForm

    图 7.10:浏览器中的 OrderForm

  12. 尝试提交一个Magazine count80Book count50的表单。浏览器会允许这样做,但由于它们的总和超过 100,表单中的clean方法会触发错误并在页面上显示:图 7.11:当允许的最大项目数量超出时在表单上显示的非字段错误    允许的最大项目数量已超出

    图 7.11:当允许的最大项目数量超出时在表单上显示的非字段错误

  13. 尝试提交一个带有Send confirmation勾选但Email字段为空的表单。然后填写Email文本框,但取消勾选Send confirmation。任何一种组合都会给出一个错误,指出两者都必须存在。错误将根据缺少的字段而有所不同:图 7.12:如果没有电子邮件地址时的错误信息

    图 7.12:如果没有电子邮件地址时的错误信息

  14. 现在尝试提交带有Send confirmation勾选和example.com域电子邮件地址的表单。你应该会收到一条消息,指出你的电子邮件地址必须具有example.com域。你还应该收到一条消息,指出email必须设置 – 因为电子邮件最终没有进入cleaned_data字典,因为它不是有效的:图 7.13:当电子邮件域不是 example.com 时显示的错误信息

    图 7.13:当电子邮件域不是 example.com 时显示的错误信息

  15. 最后,输入有效的杂志数量书籍数量(例如2020)。勾选发送确认,并将UserName@Example.Com作为电子邮件输入(确保匹配字母大小写,包括混合的大小写字符):![图 7.14:提交有效值后的表单

    ![img/B15509_07_14.jpg]

    图 7.14:提交有效值后的表单

  16. 切换到 PyCharm 并查看调试控制台。你会看到,当我们的调试代码打印时,电子邮件已经被转换为小写:![图 7.15:电子邮件字段已转换为小写,以及其他字段

    ![img/B15509_07_15.jpg]

图 7.15:电子邮件字段已转换为小写,以及其他字段

这是我们的clean_email方法在起作用——即使我们输入了大小写混合的数据,它已经被转换为全小写。

在这个练习中,我们创建了一个新的OrderForm,该表单实现了表单和字段清理方法。我们使用自定义验证器来确保Email字段符合我们的特定验证规则——只允许特定的域名。我们使用自定义字段清理方法(clean_email)将电子邮件地址转换为小写。然后我们实现了clean方法来验证相互依赖的表单。在这个方法中,我们添加了字段和非字段错误。在下一节中,我们将介绍如何向表单添加占位符和初始值。

占位符和初始值

我们第一个手动构建的表单有两个我们当前的 Django 表单还没有的特性——占位符和初始值。添加占位符很简单;它们只是作为表单字段的widget构造函数的属性添加。这与我们在之前的示例中设置DateField类型的方法类似。

这里有一个例子:

class ExampleForm(forms.Form):
  text_field = forms.CharField\
               (widget=forms.TextInput\
               (attrs={"placeholder": "Text Placeholder"}))
  password_field = forms.CharField(\
    widget=forms.PasswordInput\
           (attrs={"placeholder": "Password Placeholder"}))
  email_field = forms.EmailField\
                (widget=forms.EmailInput\
                 (attrs={"placeholder": "Email Placeholder"}))
  text_area = forms.CharField\
              (widget=forms.Textarea\
              (attrs={"placeholder": "Text Area Placeholder"}))

这就是浏览器中渲染的前一个表单的样子:

![图 7.16:带有占位符的 Django 表单

![img/B15509_07_16.jpg]

图 7.16:带有占位符的 Django 表单

当然,如果我们为每个字段手动设置Widget,我们需要知道要使用哪个Widget类。支持占位符的类有TextInputNumberInputEmailInputURLInputPasswordInputTextarea

当我们检查Form类本身时,我们将探讨设置字段初始值的两种方法之一。我们可以通过在Field构造函数上使用initial参数来实现,如下所示:

text_field = forms.CharField(initial="Initial Value", …)

另一种方法是,在视图中实例化表单时传入一个包含数据的字典。键是字段名称。该字典应该有零个或多个项(即空字典是有效的)。任何额外的键都将被忽略。这个字典应该作为initial参数在我们的视图中提供如下:

initial = {"text_field": "Text Value", \
           "email_field": "user@example.com"}
form = ExampleForm(initial=initial)

或者对于POST请求,像往常一样传入request.POST作为第一个参数:

initial = {"text_field": "Text Value", \
           "email_field": "user@example.com"}
form = ExampleForm(request.POST, initial=initial)

request.POST 中的值将覆盖 initial 中的值。这意味着即使我们对一个必填字段有一个初始值,如果提交时留空,则它将不会验证。该字段不会回退到 initial 中的值。

你决定在 Form 类本身或视图中设置初始值,这取决于你的用例。如果你有一个在多个视图中使用但通常具有相同值的表单,那么在表单中设置 initial 值会更好。否则,在视图中使用 setting 可能会更加灵活。

在下一个练习中,我们将向上一个练习中的 OrderForm 类添加占位符和初始值。

练习 7.02:占位符和初始值

在这个练习中,你将通过添加占位符文本来增强 OrderForm 类。你将模拟向表单传递一个初始电子邮件地址。它将是一个硬编码的地址,但一旦用户可以登录,它可能是与他们的账户关联的电子邮件地址——你将在 第九章会话和身份验证 中学习关于会话和身份验证的内容:

  1. 在 PyCharm 中,打开 reviews 应用程序的 forms.py 文件。你将在 OrderForm 上的 magazine_countbook_countemail 字段中添加占位符,这意味着还需要设置 widget

    magazine_count 字段中,向 attrs 字典添加一个带有 placeholderNumberInput widgetplaceholder 应设置为 杂志数量。请编写以下代码:

    magazine_count = forms.IntegerField\
                     (min_value=0, max_value=80,\
                      widget=forms.NumberInput\
                      (attrs={"placeholder": "Number of Magazines"}))
    
  2. 以相同的方式为 book_count 字段添加占位符。占位符文本应为 书籍数量

    book_count = forms.IntegerField\
                 (min_value=0, max_value=50,\
                  widget=forms.NumberInput\
                  (attrs={"placeholder": "Number of Books"}))
    
  3. OrderForm 的最终修改是向电子邮件字段添加占位符。这次小部件是 EmailInput。占位符文本应为 您的公司电子邮件地址

    email = forms.EmailField\
            (required=False, validators=[validate_email_domain],\
             widget=forms.EmailInput\
             (attrs={"placeholder": "Your company email address"}))
    

    注意,clean_emailclean 方法应保持与 练习 7.01,自定义清理和验证方法 中相同。保存文件。

  4. 打开 reviews 应用程序的 views.py 文件。在 form_example 视图函数中,创建一个新的字典变量 initial,其中有一个键 email,如下所示:

    initial = {"email": "user@example.com"}
    
  5. 在你实例化 OrderForm 的两个地方,也使用 initial 关键字参数传入 initial 变量。第一个实例如下:

    form = OrderForm(request.POST, initial=initial)
    

    第二个实例如下:

    form = OrderForm(initial=initial)
    

    views.py 的完整代码可以在 packt.live/3szaPM6 找到。

    保存 views.py 文件。

  6. 如果 Django 开发服务器尚未运行,请启动它。在浏览器中浏览到 http://127.0.0.1:8000/form-example/。你应该会看到你的表单现在有了占位符和设置的初始值:图 7.17:带有初始值和占位符的订单表单

图 7.17:带有初始值和占位符的订单表单

在这个练习中,我们在表单字段中添加了占位符。这是通过在表单类上定义form字段时设置form小部件,并在attrs字典中设置一个*placeholder*值来完成的。我们还使用字典设置表单的初始值,并通过initial关键字参数将其传递给form实例。

在下一节中,我们将讨论如何使用表单中的数据与 Django 模型一起工作,以及ModelForm如何使这变得更加容易。

创建或编辑 Django 模型

你已经看到了如何定义一个表单,在第二章模型和迁移中,你学习了如何创建 Django 模型实例。通过结合使用这些功能,你可以构建一个显示表单并保存模型实例到数据库的视图。这为你提供了一个简单的方法来保存数据,而无需编写大量的模板代码或创建自定义表单。在 Bookr 中,我们将使用这种方法来允许用户添加评论,而无需访问 Django 管理站点。不使用ModelForm,我们可以这样做:

  • 我们可以基于现有的模型创建一个表单,例如Publisher。表单将被称为PublisherForm

  • 我们可以手动在PublisherForm上定义字段,使用与Publisher模型上定义的相同规则,如下所示:

    class PublisherForm(forms.Form):
      name = forms.CharField(max_length=50)
      website = forms.URLField()
      …
    
  • 在视图中,initial值将从数据库查询到的模型中检索,然后通过initial参数传递给表单。如果我们正在创建一个新的实例,initial值将是空的——就像这样:

    if create:
      initial = {}
    else:
      publisher = Publisher.objects.get(pk=pk)
      initial = {"name": publisher.name, \
                 "website": publisher.website, …}
    form = PublisherForm(initial=initial)
    
  • 然后,在视图的POST流程中,我们可以根据cleaned_data创建或更新模型:

    form = PublisherForm(request.POST, initial=initial)
    if create:
      publisher = Publisher()
    else:
      publisher = Publisher.objects.get(pk=pk)
    publisher.name = form.cleaned_data['name']
    publisher.website = forms.cleaned_data['website']
    …
    publisher.save()
    

这需要做很多工作,我们必须考虑我们有多少重复的逻辑。例如,我们在name表单字段中定义了名称的长度。如果我们在这里犯了一个错误,我们可能会允许字段中的名称比模型允许的更长。我们还要记住设置initial字典中的所有字段,以及使用表单的cleaned_data设置新或更新模型的值。在这里有很多出错的机会,以及记住如果模型发生变化,为每个这些步骤添加或删除字段设置数据。所有这些代码都必须为每个你工作的 Django 模型重复,这加剧了重复问题。

ModelForm 类

幸运的是,Django 提供了一种更简单的方法来从表单构建 Model 实例,使用 ModelForm 类。ModelForm 是一个从特定模型自动构建的表单。它将继承模型的验证规则(例如,字段是否必需或 CharField 实例的最大长度等)。它提供了一个额外的 __init__ 参数(称为 instance),用于自动从现有模型中填充初始值。它还添加了一个 save 方法,用于自动将表单数据持久化到数据库。要设置 ModelForm,只需指定其模型和应使用的字段:这是在 form 类的 class Meta 属性上完成的。让我们看看如何从 Publisher 构建表单。

在包含表单的文件中(例如,我们一直在工作的 forms.py 文件),唯一的改变是模型必须被导入:

from .models import Publisher

然后,可以定义 Form 类。该类需要一个 class Meta 属性,该属性反过来必须定义一个 model 属性以及 fieldsexcludes 属性:

class PublisherForm(forms.ModelForm):
  class Meta:
    model = Publisher
    fields = ("name", "website", "email")

fields 是一个包含在表单中要包含的字段的列表或元组。当手动设置字段列表时,如果你向模型中添加了额外的字段,你也必须在这里添加它们的名称,以便它们在表单中显示。

你也可以使用特殊值 __all__ 来代替列表或元组,以自动包含所有字段,如下所示:

class PublisherForm(forms.ModelForm):
  class Meta:
    model = Publisher
    fields = "__all__"

如果 model 字段的 editable 属性设置为 False,则它将不会自动包含。

相反,exclude 属性将字段设置为不在表单中显示。添加到模型中的任何字段都会自动添加到表单中。我们可以使用 exclude 和任何空元组来定义前面的表单,因为我们想要显示所有字段。代码如下:

class PublisherForm(forms.ModelForm):
  class Meta:
    model = Publisher
    exclude = ()

这节省了一些工作,因为你不需要在模型和 fields 列表中添加字段,然而,这并不安全,因为你可能会自动向最终用户暴露你不希望他们看到的字段。例如,如果你有一个 User 模型和一个 UserForm,你可能会在 User 模型中添加一个 is_admin 字段,以给管理员用户额外的权限。如果这个字段没有 exclude 属性,它就会显示给用户。然后,用户就可以将自己变成管理员,这可能是你不太希望发生的事情。

无论我们决定使用这三种选择要显示的表单的方法中的哪一种,在我们的情况下,它们在浏览器中都会显示相同的内容。这是因为我们选择显示 所有 字段。当在浏览器中渲染时,它们看起来都像这样:

图 7.18:PublisherForm

图 7.18:PublisherForm

注意,Publisher 模型的 help_text 也会自动渲染。

在视图中使用与我们所看到的其他表单类似。此外,如前所述,还有一个额外的参数可以提供,称为instance。它可以设置为None,这将渲染一个空表单。

假设在你的视图函数中,你有一些方法来确定你是否正在创建或编辑模型实例(我们将在稍后讨论如何做到这一点),这将确定一个名为is_create的变量(如果创建实例则为True,如果编辑现有实例则为False)。然后,你的创建表单的视图函数可以写成这样:

if is_create:
  instance = None
else:
  instance = get_object_or_404(Publisher, pk=pk)
if request.method == "POST":
  form = PublisherForm(request.POST, instance=instance)
  if form.is_valid():
    # we'll cover this branch soon
else:
  form = PublisherForm(instance=instance)

如您所见,在任一分支中,实例都传递给了PublisherForm构造函数,尽管在创建模式中它是None

如果表单有效,然后我们可以保存model实例。这是通过在表单上调用save方法来完成的。这将自动创建实例,或者简单地保存对旧实例的更改:

if form.is_valid():
  form.save()
  return redirect(success_url)

save方法返回已保存的model实例。它接受一个可选参数commit,它确定是否将更改写入数据库。你可以传递False,这允许你在手动保存更改之前对实例进行更多更改。这可能需要设置未包含在表单中的属性。正如我们提到的,也许你会在User实例上将is_admin标志设置为False

if form.is_valid():
  new_user = form.save(False)
  new_user.is_admin = False
  new_user.save()
  return redirect(success_url)

在本章末尾的活动 7.02创建 UI 回顾中,我们将使用这个功能。

如果你的模型使用ManyToMany字段,并且你也调用了form.save(False),你应该也调用form.save_m2m()来保存任何已设置的许多对多关系。如果你使用带有commit设置为True(即默认值)的表单save方法,则不需要调用此方法。

可以通过修改其Meta属性来自定义模型表单。可以设置widgets属性。它可以包含一个以字段名称为键的字典,其中包含小部件类或实例作为值。例如,这是如何设置PublisherForm以具有占位符的:

class PublisherForm(forms.ModelForm):
  class Meta:
    model = Publisher
    fields = "__all__"
    widgets = {"name": forms.TextInput\
               (attrs={"placeholder": "The publisher's name."})}

这些值的行为与在字段定义中设置kwarg小部件相同;它们可以是类或实例。例如,要显示CharField作为密码输入,可以使用PasswordInput类;它不需要实例化:

widgets = {"password": forms.PasswordInput}

模型表单也可以通过添加与添加到普通表单相同方式添加的额外字段来增强。例如,假设我们想在保存Publisher对象后发送通知电子邮件的选项。我们可以在PublisherForm中添加一个email_on_save字段,如下所示:

class PublisherForm(forms.ModelForm):
  email_on_save = forms.BooleanField\
                  (required=False, \
                   help_text="Send notification email on save")
  class Meta:
    model = Publisher
    fields = "__all__"

当渲染时,表单看起来像这样:

![图 7.19:带有额外字段的 PublisherForm]

![img/B15509_07_19.jpg]

图 7.19:带有额外字段的 PublisherForm

额外的字段放置在 Model 字段之后。额外的字段不会自动处理——它们在模型上不存在,所以 Django 不会尝试在 model 实例上保存它们。相反,你应该通过检查表单的 cleaned_data 值来处理它们的值的保存,就像使用标准表单一样(在你的视图函数内部):

if form.is_valid():
  if form.cleaned_data.get("email_on_save"):
    send_email()
      # assume this function is defined elsewhere
  # save the instance regardless of sending the email or not
  form.save()  
  return redirect(success_url)

在下一个练习中,你将编写一个新的视图函数来创建或编辑一个 Publisher

练习 7.03:创建和编辑一个出版商

在这个练习中,我们将回到 Bookr。我们想要添加创建和编辑 Publisher 而不使用 Django 管理员的能力。为此,我们将为 Publisher 模型添加一个 ModelForm。它将用于一个新的视图函数。视图函数将接受一个可选参数 pk,它将是正在编辑的 Publisher 的 ID 或 None 以创建一个新的 Publisher。我们将添加两个新的 URL 映射来简化这个过程。当完成时,我们将能够通过它们的 ID 查看 和更新任何出版商。例如,Publisher 1 的信息将在 URL 路径 /publishers/1 上可查看/可编辑:

  1. 在 PyCharm 中打开 reviews 应用的 forms.py 文件。在 forms 导入之后,也导入 Publisher 模型:

    from .models import Publisher
    
  2. 创建一个继承自 forms.ModelFormPublisherForm 类:

    class PublisherForm(forms.ModelForm):
    
  3. PublisherForm 上定义 Meta 属性。Meta 所需的属性是模型 (Publisher) 和字段 ("__all__"):

    class PublisherForm(forms.ModelForm):
      class Meta:
        model = Publisher
        fields = "__all__"
    

    保存 forms.py

    注意

    完整的文件可以在 packt.live/3qh9bww 找到。

  4. 打开 reviews 应用程序的 views.py 文件。在文件顶部,导入 PublisherForm

    from .forms import PublisherForm, SearchForm
    
  5. 确保如果你还没有导入,你已经从 django.shortcuts 中导入了 get_object_or_404redirect 函数:

    from django.shortcuts import render, get_object_or_404, redirect
    
  6. 确保如果你还没有导入 Publisher 模型,你已经导入了它。你可能已经导入了这个和其他模型:

    from .models import Book, Contributor, Publisher
    
  7. 你需要的最后一个导入是 messages 模块。这将允许我们注册一个消息,让用户知道 Publisher 对象已被编辑或创建:

    from django.contrib import messages
    

    再次提醒,如果你还没有导入,请添加此导入。

  8. 创建一个新的视图函数,命名为 publisher_edit。它接受两个参数,requestpk(要编辑的 Publisher 对象的 ID)。这是可选的,如果它是 None,则将创建一个新的 Publisher 对象:

    def publisher_edit(request, pk=None):
    
  9. 在视图函数内部,如果 pk 不是 None,我们需要尝试加载现有的 Publisher 实例。否则,publisher 的值应该是 None

    def publisher_edit(request, pk=None):
      if pk is not None:
        publisher = get_object_or_404(Publisher, pk=pk)
      else:
        publisher = None
    
  10. 在获取到 Publisher 实例或 None 之后,完成 POST 请求的分支。以与本章前面看到的方式实例化表单,但现在确保它接受 instance 作为关键字参数。然后,如果表单有效,使用 form.save() 方法保存它。该方法将返回更新的 Publisher 实例,该实例存储在 updated_publisher 变量中。然后,根据 Publisher 实例是创建还是更新,注册不同的成功消息。最后,由于此时 updated_publisher 总是具有 ID,重定向回此 publisher_edit 视图:

    def publisher_edit(request, pk=None):
      …
      if request.method == "POST":
        form = PublisherForm(request.POST, instance=publisher)
        if form.is_valid():
        updated_publisher = form.save()
          if publisher is None:
            messages.success\
            (request, "Publisher \"{}\" was created."\
                      .format(updated_publisher))
          else:
            messages.success\
            (request, "Publisher \"{}\" was updated."\
                      .format(updated_publisher))\
          return redirect("publisher_edit", updated_publisher.pk)
    

    如果表单无效,执行将跳过,只返回带有无效表单的 render 函数调用(这将在第 12 步中实现)。重定向使用命名 URL 映射,该映射将在练习的后续部分添加。

  11. 接下来,填写代码中的非 POST 分支。在这种情况下,只需使用 instance 实例化表单:

    def publisher_edit(request, pk=None):
      …
      if request.method == "POST":
        …
      else:
        form = PublisherForm(instance=publisher)
    
  12. 最后,你可以重用之前练习中使用的 form-example.html 文件。使用 render 函数渲染它,传入 HTTP 方法以及 form 作为上下文:

    def publisher_edit(request, pk=None):
      …
      return render(request, "form-example.html", \
                    {"method": request.method, "form": form}) 
    

    保存此文件。你可以参考它,请见 packt.live/3nI62En

  13. reviews 目录中打开 urls.py 文件。添加两个新的 URL 映射;它们都将导向 publisher_edit 视图。一个将捕获我们想要编辑的 Publisher 的 ID,并将其作为 pk 参数传递给视图。另一个将使用单词 new,并且不会传递 pk,这表示我们想要创建一个新的 Publisher

    'publishers/<int:pk>/' 映射添加到 urlpatterns 变量中,映射到 reviews.views.publisher_edit 视图,名称为 'publisher_edit'

    还需添加 'publishers/new/' 映射到 reviews.views.publisher_edit 视图,名称为 'publisher_create'

    urlpatterns = [
      …
      path('publishers/<int:pk>/',views.publisher_edit, \
            name='publisher_edit'),\
      path('publishers/new/',views.publisher_edit, \
            name='publisher_create')]
    

    由于第二个映射没有捕获任何内容,传递给 publisher_detail 视图函数的 pkNone

    保存 urls.py 文件。参考的完成版本请见 packt.live/39CpUnw

  14. reviews 应用程序的 templates 目录中创建一个 form-example.html 文件。由于这是一个独立的模板(它不扩展任何其他模板),我们需要在其中渲染消息。在 <body> 标签打开后添加此代码,遍历所有消息并显示它们:

    {% for message in messages %}
    <p><em>{{ message.level_tag|title }}:</em> {{ message }}</p>
    {% endfor %}
    

    这将遍历我们添加的消息,并显示标签(在我们的例子中是 Success)然后是消息。

  15. 然后,添加正常的表单渲染和提交代码:

    <form method="post">
      {% csrf_token %}
      {{ form.as_p }}
      <p>
        <input type="submit" value="Submit">
      </p>
    </form>
    

    保存并关闭此文件。

    你可以参考此文件的完整版本,请见 packt.live/38I8XZx

  16. 启动 Django 开发服务器,然后导航到 http://127.0.0.1:8000/publishers/new/。你应该看到一个空白的 PublisherForm 正在被显示:图 7.20:空白出版者表单

    img/B15509_07_20.jpg

    图 7.20:空白出版者表单

  17. 表单继承了模型的验证规则,因此你不能提交包含过多字符的Name或包含无效的WebsiteEmail的表单。输入一些有效信息,然后提交表单。提交后,你应该看到成功消息,表单将填充保存到数据库中的信息:![图 7.21:提交后的表单 图片 B15509_07_21.jpg

图 7.21:提交后的表单

注意,URL 也已更新,现在包括创建的出版商的 ID。在这种情况下,它是http://127.0.0.1:8000/publishers/19/,但你的设置中的 ID 将取决于你的数据库中已经有多少个Publisher实例。

注意,如果你刷新页面,你将不会收到确认是否重新发送表单数据的消息。这是因为我们在保存后进行了重定向,所以你可以多次刷新这个页面,而不会创建新的Publisher实例。如果你没有重定向,那么每次刷新页面都会创建一个新的Publisher实例。

如果你数据库中有其他Publisher实例,你可以更改 URL 中的 ID 来编辑其他实例。由于这个实例的 ID 是3,我们可以假设Publisher 1Publisher 2已经存在,可以用它们的 ID 来替换以查看现有数据。以下是现有Publisher 1的视图(在http://127.0.0.1:8000/publishers/1/)——你的信息可能不同:

![图 7.22:现有出版商 1 信息图片 B15509_07_22.jpg

图 7.22:现有出版商 1 信息

尝试更改现有的Publisher实例。注意,在你保存后,消息是不同的——它告诉用户Publisher实例已被更新而不是创建

![图 7.23:更新而不是创建后的出版商图片 B15509_07_23.jpg

图 7.23:更新而不是创建后的出版商

在这个练习中,我们从一个模型(PublisherForm是从Publisher创建的)实现了ModelForm,并看到了 Django 如何自动生成带有正确验证规则的表单字段。然后,我们使用表单的内置save方法将更改保存到Publisher实例(或自动创建它)中的publisher_edit视图。我们将两个 URL 映射到该视图。第一个 URL 用于编辑现有的Publisher,将pk传递给视图。另一个没有将pk传递给视图,表示应该创建Publisher实例。最后,我们使用浏览器来尝试创建一个新的Publisher实例,然后编辑现有的一个。

活动 7.01:样式化和集成出版商表单

练习 7.03创建和编辑出版商中,你添加了PublisherForm来创建和编辑Publisher实例。你是通过一个不扩展任何其他模板的独立模板构建的,因此它缺少全局样式。在这个活动中,你将构建一个通用的表单详情页面,该页面将显示 Django 表单,类似于form-example.html,但扩展自基础模板。该模板将接受一个变量以显示正在编辑的模型类型。你还将更新主要的base.html模板以使用 Bootstrap 样式渲染 Django 消息。

这些步骤将帮助你完成这个活动:

  1. 首先编辑base.html项目。将content块包裹在一个带有一些间距的容器div中,以获得更美观的布局。使用class="container-fluid"<div>元素包围现有的content块。

  2. 在你刚刚创建的<div>之后但在content块之前渲染messages中的每个message(类似于练习 7.03创建和编辑出版商中的步骤 14)。你应该使用 Bootstrap 框架类 - 这个片段将帮助你:

    <div class="alert alert-{% if message.level_tag   == 'error' %}danger{% else %}{    {message.level_tag }}{% endif %}"
        role="alert">
      {{ message }}
    </div>
    

    大部分情况下,Bootstrap 类和 Django 的message标签有相应的名称(例如,successalert-success)。例外的是 Django 的error标签,对应的 Bootstrap 类是alert-danger。有关 Bootstrap 警报的更多信息,请参阅getbootstrap.com/docs/4.0/components/alerts/。这就是为什么你需要在这个片段中使用if模板标签的原因。

  3. reviews应用的命名空间templates目录中创建一个新的模板instance-form.html

  4. instance-form.html应该从reviews应用的base.html扩展。

  5. 传递给此模板的上下文将包含一个名为instance的变量。这将是要编辑的Publisher实例,或者如果我们正在创建新的Publisher实例,则为None。上下文还将包含一个model_type变量,它是一个表示模型类型的字符串(在这种情况下,Publisher)。使用这两个变量来填充title块模板标签:

    如果实例是None,标题应该是新出版商

    否则,标题应该是编辑出版商 <出版商名称>

  6. instance-form.html应包含一个content block模板标签以覆盖base.htmlcontent块。

  7. content块内添加一个<h2>元素,并使用与标题相同的逻辑进行填充。为了更好的样式,将出版商名称包裹在一个<em>元素中。

  8. 在模板中添加一个methodpost<form>元素。由于我们正在将数据发送回相同的 URL,因此不需要指定action

  9. <form>体中包含 CSRF 令牌模板标签。

  10. 使用as_p方法在<form>内渲染 Django 表单(其上下文变量将是form)。

  11. 在表单中添加一个submit <button>。其文本应取决于你是在编辑还是创建。对于编辑使用文本保存,对于创建使用创建。你可以在这里使用 Bootstrap 类来设置按钮样式。它应该有属性class="btn btn-primary"

  12. reviews/views.py中,publisher_edit视图不需要很多更改。将render调用更新为渲染instance-form.html而不是form-example.html

  13. 更新传递给render调用的上下文字典。它应包括Publisher实例(已定义的publisher变量)和model_type字符串。上下文字典已经包括form(一个PublisherForm实例)。你可以移除method键。

  14. 由于我们完成了form-example.html模板,它可以被删除。

    当你完成时,Publisher创建页面(在http://127.0.0.1:8000/publishers/new/)应该看起来像图 7.24

    图 7.24:出版商创建页面

图 7.24:出版商创建页面

当编辑一个Publisher(例如,在 URL http://127.0.0.1:8000/publishers/1/),你的页面应该看起来像图 7.25

图 7.25:编辑出版商页面

图 7.25:编辑出版商页面

保存Publisher实例后,无论是创建还是编辑,你应该在页面顶部看到成功消息(图 7.26):

图 7.26:Bootstrap 警告框显示的成功消息

图 7.26:Bootstrap 警告框显示的成功消息

注意

此活动的解决方案可以在packt.live/2Nh1NTJ找到。

活动七.02:评论创建 UI

活动 7.01样式化和集成出版商表单,相当广泛;然而,通过完成它,你已经创建了一个基础,使得添加其他编辑创建视图变得更容易。在这个活动中,当你构建创建和编辑评论的表单时,你将亲身体验这一点。因为instance-form.html模板是通用的,所以你可以在其他视图中重用它。

在这个活动中,你将创建一个评论ModelForm,然后添加一个review_edit视图来创建或编辑Review实例。你可以从活动 7.01样式化和集成出版商表单中重用instance-form.html,并传入不同的上下文变量以使其与Review模型一起工作。当你处理评论时,你将在书籍的上下文中操作,也就是说,review_edit视图必须接受一个书籍的pk作为参数。你将单独获取Book实例并将其分配给你创建的Review实例。

这些步骤将帮助你完成此活动:

  1. forms.py中添加一个ReviewForm子类ModelForm;其模型应该是Review(确保你导入Review模型)。

    ReviewForm应该排除date_editedbook字段,因为用户不应该在表单中设置这些。数据库允许任何评分,但我们可以用需要最小值为0和最大值为5IntegerField覆盖rating字段。

  2. 创建一个新的视图review_edit。它在request之后接受两个参数:必需的book_pk,可选的review_pk(默认为None)。使用get_object_or_404快捷方式(对每种类型调用一次)获取Book实例和Review实例。在获取评论时,确保评论属于该书籍。如果review_pkNone,那么Review实例也应该是None

  3. 如果request方法为POST,则使用request.POST和评论实例实例化一个ReviewForm。确保你导入ReviewForm

    如果表单有效,保存表单但将commit参数设置为saveFalse。然后,将返回的Review实例上的book属性设置为在步骤 2中获取的书籍。

  4. 如果正在更新Review实例而不是创建它,那么你还应该将date_edited属性设置为当前日期和时间。使用from django.utils.timezone.now()函数。然后,保存Review实例。

  5. 通过注册成功消息并将重定向回book_detail视图来完成有效的表单分支。由于Review模型实际上不包含有意义的文本描述,所以使用书籍标题作为消息。例如,Review for "<book title>" created

  6. 如果request方法不是POST,实例化一个ReviewForm并仅传递Review实例。

  7. 渲染instance-form.html模板。在上下文字典中,包括与在publisher_view中使用的相同项:forminstancemodel_typeReview)。包括两个额外项,related_model_type,它应该是Book,以及related_instance,它将是Book实例。

  8. 编辑instance-form.html以添加一个显示在步骤 6中添加的相关实例信息的地方。在<h2>元素下添加一个<p>元素,只有当related_model_typerelated_instance都设置时才显示。它应该显示文本For <related_model_type> <related_instance>。例如:For Book Advanced Deep Learning with Keras。将related_instance输出放在<em>元素中以获得更好的可读性。

  9. reviews应用的urls.py文件中,向review_edit视图添加 URL 映射。URL /books//books/<pk>/ 已经配置好了。添加 URL /books/<book_pk>/reviews/new/ 来创建评论,以及 /books/<book_pk>/reviews/<review_pk>/ 来编辑评论。确保你给出这些名称,例如review_createreview_edit

  10. book_detail.html模板中,添加用户可以点击以创建或编辑评论的链接。在content块内,紧接在endblock关闭模板标签之前添加一个链接。它应该使用url模板标签在创建模式时链接到review_edit视图。同时,使用属性class="btn btn-primary"使链接显示为 Bootstrap 按钮。链接文本应该是添加评论

  11. 最后,在遍历BookReviewsfor循环中添加一个编辑评论的链接。在所有text-info <span>实例之后添加一个链接到review_edit视图,使用url模板标签。你需要提供book.pkreview.pk作为参数。链接的文本应该是编辑评论。完成之后,评论详情页面应该看起来像图 7.27:![图 7.27:添加了添加评论按钮的书籍详情页面 图片

图 7.27:添加了添加评论按钮的书籍详情页面

你可以看到添加评论按钮。点击它将带你到创建书籍评论页面,它应该看起来像图 7.28

![图 7.28:评论创建页面图片

图 7.28:评论创建页面

在表单中输入一些详细信息并点击创建。你将被重定向到书籍详情页面,你应该看到成功消息和你的评论,就像图 7.29

![图 7.29:添加了评论的书籍详情页面图片

图 7.29:添加了评论的书籍详情页面

你也可以看到编辑评论链接,如果你点击它,你将被带到包含你的评论数据的表单(见图 7.30):

![图 7.30:编辑评论时的评论表单图片

图 7.30:编辑评论时的评论表单

保存现有的评论后,你应该在书籍详情页面上看到修改于日期已更新(图 7.31):

![图 7.31:修改日期现在已填充图片

图 7.31:修改日期现在已填充

注意

这个活动的解决方案可以在packt.live/2Nh1NTJ找到。

摘要

本章深入探讨了表单。我们看到了如何通过自定义验证规则、清理数据和验证字段来增强 Django 表单。我们看到了如何通过自定义清理方法转换从表单中获取的数据。我们看到了可以添加到表单中的一个很好的功能,即能够在字段上设置初始值和占位符值,这样用户就不必填写它们。

我们接下来探讨了如何使用ModelForm类从 Django 模型自动创建表单。我们看到了如何只向用户显示一些字段,以及如何将自定义表单验证规则应用到ModelForm上。我们还了解到 Django 如何在视图中自动将新或更新的模型实例保存到数据库中。在本章的活动部分,我们通过添加创建和编辑出版社以及提交评论的表单,进一步增强了 Bookr 的功能。下一章将继续探讨提交用户输入的主题,并在此基础上,我们将讨论 Django 如何处理文件的上传和下载。

第八章:8. 媒体服务和文件上传

概述

本章首先向您介绍媒体文件,然后教您如何设置 Django 以服务它们。一旦您理解了这一点,您将学习如何使用 HTML 构建一个表单,该表单可以将文件上传到一个视图以存储到磁盘。为了增强这个过程并减少代码量,您将使用 Django 表单来生成和验证表单,并学习如何通过它处理文件上传。然后,您将查看 Django 为处理图像文件提供的特定增强功能,并使用FileFieldImageField分别存储文件和图像,并通过 Django 表单上传它们。在此之后,您将自动从模型构建一个ModelForm实例,并仅用一行代码保存模型和文件。在本章结束时,您将通过向Book模型添加封面图像和书籍摘录来增强 Bookr 应用。

简介

媒体文件是指在部署后可以添加的额外文件,用于丰富您的 Django 应用。通常,它们是您在网站上使用的额外图像,但任何类型的文件(包括视频、音频、PDF、文本、文档,甚至是 HTML)都可以作为媒体提供服务。

您可以将它们视为介于动态数据和静态资产之间。它们不是 Django 在动态生成时产生的动态数据,例如在渲染模板时。它们也不是网站开发者在网站部署时包含的静态文件。相反,它们是可以由用户上传或由您的应用程序生成以供以后检索的额外文件。

媒体文件的常见示例(您将在本章后面的活动 8.01书籍图像和 PDF 上传中看到)包括书籍封面和可以附加到Book对象的预览 PDF。您还可以使用媒体文件允许用户上传博客文章的图像或社交媒体网站的头像。如果您想使用 Django 构建自己的视频分享平台,您将存储上传的视频作为媒体。如果所有这些文件都是静态文件,您的网站将无法很好地运行,因为用户将无法上传自己的书籍封面、视频等,并将陷入您部署的状态。

媒体上传和服务的设置

第五章服务静态文件中,我们探讨了如何使用 Django 来服务静态文件。服务媒体文件相当类似。必须在settings.py中配置两个设置:MEDIA_ROOTMEDIA_URL。这些与用于服务静态文件的STATIC_ROOTSTATIC_URL类似。

  • MEDIA_ROOT

    这是磁盘上存储媒体(如上传的文件)的路径。与静态文件一样,您的 Web 服务器应该配置为直接从这个目录提供服务,以减轻 Django 的负担。

  • MEDIA_URL

    这与STATIC_URL类似,但正如您可能猜到的,这是应该用于服务媒体的 URL。它必须以/结尾。通常,您将使用类似/media/的东西。

    注意

    由于安全原因,MEDIA_ROOT 的路径必须与 STATIC_ROOT 的路径不同,并且 MEDIA_URL 必须与 STATIC_URL 不同。如果它们相同,用户可能会用恶意代码替换你的静态文件(如 JavaScript 或 CSS 文件),并利用你的用户。

MEDIA_URL 设计用于在模板中使用,这样你就不需要硬编码 URL,并且可以轻松更改。例如,你可能希望将其设置为特定主机或在下文中的模板中的 MEDIA_URL

开发中服务媒体文件

就像静态文件一样,在生产环境中服务媒体时,你的 Web 服务器应该配置为直接从 MEDIA_ROOT 目录服务,以防止 Django 被绑定在服务请求上。Django 开发服务器可以在开发中服务媒体文件。然而,与静态文件不同,媒体文件的 URL 映射和视图不是自动设置的。

Django 提供了 static URL 映射,可以添加到现有的 URL 映射中,以服务媒体文件。它像这样添加到你的 urls.py 文件中:

from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
    # your existing URL maps
] 
if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL,\
                          document_root=settings.MEDIA_ROOT)

这会将 settings.py 中定义的 MEDIA_ROOT 设置服务到那里也定义的 MEDIA_URL 设置。我们在添加映射之前检查 settings.DEBUG 的原因是为了确保在生产环境中不添加此映射。

例如,如果你的 MEDIA_ROOT 设置为 /var/www/bookr/media,而你的 MEDIA_URL 设置为 /media/,那么 /var/www/bookr/media/image.jpg 文件将在 http://127.0.0.1:8000/media/image.jpg 处可用。

当 Django 的 DEBUG 设置为 False 时,static URL 映射不起作用,因此不能在生产环境中使用。然而,如前所述,在生产环境中,你的 Web 服务器应该服务这些请求,因此 Django 不需要处理它们。

在第一个练习中,你将在 settings.py 文件中创建并添加一个新的 MEDIA_ROOTMEDIA_URL。然后,你将添加 static 媒体服务 URL 映射,并添加一个测试文件以确保媒体服务配置正确。

练习 8.01:配置媒体存储和服务媒体文件

在这个练习中,你将设置一个新的 Django 项目作为本章中使用的示例项目。然后,你将配置它以能够服务媒体文件。你将通过创建一个 media 目录并添加 MEDIA_ROOTMEDIA_URL 设置来实现这一点。然后,你将为 MEDIA_URL 设置 URL 映射。

为了检查一切配置正确并且正确服务,你将在 media 目录中放置一个测试文件:

  1. 就像之前你设置的 Django 项目一样,你可以重用现有的 bookr 虚拟环境。在终端中激活 bookr 虚拟环境。然后,使用 django-admin.py 启动一个名为 media_project 的新项目:

    django-admin.py startproject media_project
    

    切换(或 cd)到创建的 media_project 目录,然后使用 startapp 管理命令启动一个名为 media_example 的应用:

    python3 manage.py startapp media_example
    
  2. 在 PyCharm 中打开media_project目录。以与其他你打开的 Django 项目相同的方式设置runserver命令的运行配置:图 8.1:Runserver 配置

    图 8.1:Runserver 配置

    图 8.1显示了 PyCharm 中项目的runserver配置。

  3. media_project项目目录内创建一个名为media的新目录。然后,在这个目录中创建一个名为test.txt的新文件。这个目录结构将看起来像图 8.2图 8.2:媒体目录和 test.txt 布局

    图 8.2:媒体目录和 test.txt 布局

  4. test.txt也会自动打开。将文本Hello, world!输入其中,然后你可以保存并关闭文件。

  5. media_project包目录内打开settings.py。在文件末尾添加一个MEDIA_ROOT的设置,使用你刚刚创建的媒体目录的路径。确保在文件顶部导入os模块:

    import os
    

    然后使用os.path.join函数将其与BASE_DIR连接:

    MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
    
  6. 步骤 5中添加的行下面,为MEDIA_URL添加另一个设置。这应该只是'/media/'

    MEDIA_URL = '/media/'
    

    然后,保存settings.py。它应该看起来像这样:

    STATIC_URL = '/static/'
    MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
    settings.py should look like this: http://packt.live/34RdhU1.
    
  7. 打开media_project包的urls.py文件。在urlpatterns定义之后,添加以下代码以在DEBUG模式下运行时添加媒体服务 URL。首先,您需要通过在urlpatterns定义上方添加突出显示的导入行来导入 Django 设置和静态服务视图:

    from django.contrib import admin
    from django.urls import path
    from django.conf import settings
    from django.conf.urls.static import static
    urlpatterns = [path('admin/', admin.site.urls),]
    
  8. 然后,在你的urlpatterns定义之后(参考上一步中的代码块)添加以下代码,以有条件地添加从MEDIA_URLstatic视图的映射,它将从MEDIA_ROOT提供服务:

    if settings.DEBUG:
        urlpatterns += static(settings.MEDIA_URL,\
                              document_root=settings.MEDIA_ROOT)
    

    你现在可以保存这个文件了。它应该看起来像这样:packt.live/3nVUiPn

  9. 如果 Django 开发服务器尚未运行,请启动它,然后访问http://127.0.0.1:8000/media/test.txt。如果你一切都做对了,那么你应该在你的浏览器中看到文本Hello, world!图 8.3:服务媒体文件

图 8.3:服务媒体文件

如果你的浏览器看起来像图 8.3,这意味着媒体文件正在从MEDIA_ROOT目录提供服务。我们创建的test.txt文件只是为了测试,但我们将它在练习 8.02模板设置和使用 MEDIA_URL 在模板中中使用,所以现在不要删除它。

在这个练习中,我们配置了 Django 来服务媒体文件。我们只服务了一个测试文件以确保一切按预期工作,并且确实如此。我们现在将看看我们如何在模板中自动生成媒体 URL。

上下文处理器和在模板中使用 MEDIA_URL

要在模板中使用MEDIA_URL,我们可以在视图中通过渲染上下文字典传递它。例如:

from django.conf import settings
def my_view(request):
    return render(request, "template.html",\
                  {"MEDIA_URL": settings.MEDIA_URL,\
                   "username": "admin"})

这将有效,但问题是MEDIA_URL是一个我们可能在许多地方想要使用的通用变量,因此我们几乎必须在每个视图中传递它。

相反,我们可以使用render调用。

上下文处理器是一个接受一个参数的函数,即当前请求。它返回一个包含上下文信息的字典,该字典将与传递给render调用的字典合并。

我们可以查看media上下文处理器的源代码,它说明了它们是如何工作的:

def media(request):
    """
    Add media-related context variables to the context.
    """
    return {'MEDIA_URL': settings.MEDIA_URL}

启用媒体上下文处理器后,MEDIA_URL将被添加到我们的上下文字典中。我们可以将之前看到的render调用更改为以下内容:

return render(request, "template.html", {"username": "admin"})

同样的数据将被发送到模板,因为上下文处理器会添加MEDIA_URL

media上下文处理器的完整模块路径是django.template.context_processors.media。Django 提供的其他上下文处理器的示例包括:

  • django.template.context_processors.debug

    这返回字典 {"DEBUG": settings.DEBUG}

  • django.template.context_processors.request

    这将返回字典 {"request": request},也就是说,它只是将当前的 HTTP 请求添加到上下文中。

要启用上下文处理器,必须将其模块路径添加到TEMPLATES设置的context_processors选项中。例如,要启用媒体上下文处理器,请添加django.template.context_processors.media。我们将在练习 8.02模板设置和使用 MEDIA_URL 在模板中中详细说明如何做到这一点。

一旦启用media上下文处理器,MEDIA_URL变量就可以像普通变量一样在模板中访问:

{{ MEDIA_URL }}

例如,您可以使用它来获取图像:

<img src="img/image.jpg">

注意,与静态文件不同,没有用于加载媒体文件的模板标签(也就是说,没有{% static %}模板标签的等效物)。

也可以编写自定义上下文处理器。例如,回顾我们一直在构建的 Bookr 应用,我们可能希望在每一页的侧边栏中显示最新的五条评论列表。这样的上下文处理器将执行以下操作:

from reviews.models import Review
def latest_reviews(request):
    return {"latest_reviews": \
             Review.objects.order_by('-date_created')[:5]}.

这将在 Bookr 项目目录下的名为context_processors.py的文件中保存,然后在context_processors设置中通过其模块路径context_processors.latest_reviews进行引用。或者我们也可以将其保存在reviews应用中,并作为reviews.context_processors.latest_reviews进行引用。是否将上下文处理器视为项目级或应用级取决于您。然而,请注意,无论其存储位置如何,一旦激活,它将应用于所有应用的render调用。

上下文处理器可以返回包含多个项的字典,甚至可以是零项。如果它有条件,只有当满足某些标准时才添加项,例如,只有当用户登录时才显示最新的评论。让我们在下一项练习中详细探讨这一点。

练习 8.02:模板设置和使用模板中的 MEDIA_URL

在这个练习中,你将继续使用media_project并配置 Django 以自动将MEDIA_URL设置添加到每个模板中。你通过将django.template.context_processors.media添加到TEMPLATEScontext_processors设置来实现这一点。然后,你将添加一个使用这个新变量的模板和一个示例视图来渲染它。你将在本章的练习中修改视图和模板:

  1. 在 PyCharm 中打开settings.py。首先,你需要将media_example添加到INSTALLED_APPS设置中,因为项目设置时没有完成:

    INSTALLED_APPS = [# other apps truncated for brevity\
        'media_example']
    
  2. 在文件大约一半的位置,你会找到TEMPLATES设置,它是一个字典。在这个字典中是OPTIONS项(另一个字典)。在OPTIONS中是context_processors设置。

    将以下内容添加到列表末尾:

    'django.template.context_processors.media'
    

    完整的列表应该看起来像这样:

    TEMPLATES = \
    [{'BACKEND': 'django.template.backends.django.DjangoTemplates',
      'DIRS': [],
      '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'\
                ],\
            },\
        },\
    ]
    

    完整的文件应该看起来像这样:packt.live/3nVOpSx

  3. 打开media_example应用的views.py并创建一个名为media_example的新视图。目前,它只需渲染一个名为media-example.html的模板(你将在步骤 5中创建它)。视图函数的整个代码如下:

    def media_example(request):
        return render(request, "media-example.html")
    

    保存views.py。它应该看起来像这样:packt.live/3pvEGCB

  4. 你需要一个指向media_example视图的 URL 映射。打开media_project包的urls.py文件。

    首先,使用文件中的其他导入导入media_example.views

    import media_example.views
    

    然后在urlpatterns中添加一个path,将media-example/映射到media_example视图:

    path('media-example/', media_example.views.media_example)
    

    你的完整urlpatterns应该像以下代码块所示:

    from django.conf.urls.static import static
    import media_example.views
    urlpatterns = [path('admin/', admin.site.urls),\
                   path('media-example/', \
                        media_example.views.media_example)]
    if settings.DEBUG:
        urlpatterns += static(settings.MEDIA_URL,\
                              document_root=settings.MEDIA_ROOT)
    

    你可以保存并关闭文件。

  5. media_example应用目录内创建一个templates目录。然后,在media_project项目的templates目录内创建一个新的 HTML 文件。选择HTML 5 file并将文件命名为media-example.html图 8.4:创建 media-example.html

    图 8.4:创建 media-example.html

  6. media-example.html文件应该会自动打开。你只需要在文件内添加一个指向你在练习 8.01配置媒体存储和提供媒体文件中创建的test.txt文件的链接。在<body>元素中添加高亮代码:

    <body>
        / between MEDIA_URL and the filename – this is because we already added a trailing slash when we defined it in settings.py. You can save the file. The complete file will look like this: http://packt.live/3nYTvgF. 
    
  7. 如果 Django 开发服务器尚未运行,请启动它,然后访问http://127.0.0.1:8000/media-example/。你应该会看到一个简单的页面,就像图 8.5所示:图 8.5:基本的媒体链接页面

图 8.5:基本的媒体链接页面

如果你点击链接,你将被带到test.txt显示页面,并看到你在练习 8.01配置媒体存储和提供媒体文件中创建的Hello, world!文本(图 8.3)。这意味着你已经正确配置了 Django 的context_processors设置。

我们已经完成了 test.txt,所以你现在可以删除该文件了。我们将在其他练习中使用 media_example 视图和模板,所以请保留它们。在下一节中,我们将讨论如何使用网络浏览器上传文件,以及 Django 如何在视图中访问它们。

使用 HTML 表单进行文件上传

第六章表单 中,我们学习了关于 HTML 表单的内容。我们讨论了如何使用 <form>method 属性进行 GETPOST 请求。尽管我们迄今为止只使用表单提交了文本数据,但也可以使用表单提交一个或多个文件。

在提交文件时,我们必须确保表单上至少有两个属性:methodenctype。你可能还需要其他属性,例如 action。支持文件上传的表单可能看起来像这样:

<form method="post" enctype="multipart/form-data">

文件上传仅适用于 POST 请求。使用 GET 请求是不可能的,因为无法通过 URL 发送文件的全部数据。必须设置 enctype 属性,以便浏览器知道它应该将表单数据作为多个部分发送,一部分用于表单的文本数据,另一部分用于附加到表单的每个文件。这种编码对用户来说是无缝的;他们不知道浏览器是如何编码表单的,也不需要做任何不同的事情。

要将文件附加到表单,你需要创建一个类型为 file 的输入。你可以手动编写 HTML 代码,如下所示:

<input type="file" name="file-upload-name">

当输入在浏览器中渲染时,它看起来如下(为空时):

![图 8.6:空文件输入

![图片 B15509_08_06.jpg]

![图 8.6:空文件输入

按钮的标题可能因你的浏览器而异。

点击 浏览… 按钮将显示一个 文件打开 对话框:

![图 8.7:macOS 上的文件浏览器

![图片 B15509_08_07.jpg]

![图 8.7:macOS 上的文件浏览器

选择文件后,文件名将显示在字段中:

![图 8.8:已选择 cover.jpg 的文件输入

![图片 B15509_08_08.jpg]

![图 8.8:已选择 cover.jpg 的文件输入

图 8.8 显示了一个已选择名为 cover.jpg 的文件输入。

在视图中处理上传的文件

除了文本数据外,如果表单还包含文件上传,Django 将使用这些文件填充 request.FILES 属性。request.FILES 是一个类似于字典的对象,它以 file 输入的 name 属性为键。

在上一节中的表单示例中,文件输入的名称是 file-upload-name。因此,文件可以通过 request.FILES["file-upload-name"] 在视图中访问。

request.FILES 包含的对象是文件类对象(具体来说,是一个 django.core.files.uploadedfile.UploadedFile 实例),因此要使用它们,你必须读取它们的数据。例如,要在你的视图中获取上传文件的文件内容,你可以编写:

content = request.FILES["file-upload-name"].read()

更常见的操作是将文件内容写入磁盘。当文件上传时,它们被存储在临时位置(如果文件大小小于 2.5 MB,则在内存中,否则在磁盘上的临时文件中)。要将文件数据存储在已知位置,必须读取内容并将其写入磁盘的期望位置。UploadedFile实例有一个chunks方法,它将一次读取文件数据的一个块,以防止一次性读取整个文件而占用太多内存。

因此,而不是简单地使用readwrite函数,使用chunks方法一次只读取文件的小块到内存中:

with open("/path/to/output.jpg", "wb+") as output_file:
    uploaded_file = request.FILES["file-upload-name"]
    for chunk in uploaded_file.chunks():
        output_file.write(chunk)

注意,在即将到来的某些示例中,我们将把这个代码称为save_file_upload函数。假设函数定义如下:

def save_file_upload(upload, save_path):
    with open(save_path, "wb+") as output_file:
        for chunk in upload.chunks():
            output_file.write(chunk)

前面的示例代码可以重构为调用以下函数:

uploaded_file = request.FILES["file-upload-name"]
save_file_upload(uploaded_file, "/path/to/output.jpg")

每个UploadedFile对象(前一个示例代码片段中的uploaded_file变量)还包含有关上传文件的额外元数据,例如文件名、大小和内容类型。你将发现最有用的属性是:

  • size:正如其名所示,这是上传文件的字节数。

  • name:这指的是上传文件的名称,例如,image.jpgfile.txtdocument.pdf等等。这个值是由浏览器发送的。

  • content_type:这是上传文件的内容类型(MIME 类型)。例如,image/jpegtext/plainapplication/pdf等等。像name一样,这个值是由浏览器发送的。

  • charset:这指的是上传文件的字符集或文本编码,对于文本文件。这将是类似utf8ascii的东西。同样,这个值也是由浏览器确定并发送的。

这里是一个快速示例,展示如何访问这些属性(例如在视图中):

upload = request.FILES["file-upload-name"]
size = upload.size
name = upload.name
content_type = upload.content_type
charset = upload.charset

浏览器发送值的 安全性和信任

正如我们刚才描述的,UploadedFilenamecontent_typecharset值是由浏览器确定的。这一点很重要,因为恶意用户可能会发送虚假值来代替真实值,以伪装实际上传的文件。Django 不会自动尝试确定上传文件的类型或字符集,因此它依赖于客户端在发送此信息时准确无误。

如果我们手动处理文件上传的保存而没有适当的检查,那么可能会发生如下场景:

  1. 网站的用户上传了一个恶意可执行文件malware.exe,但发送的内容类型为image/jpeg

  2. 我们的代码检查内容类型,并认为它是安全的,因此将malware.exe保存到MEDIA_ROOT文件。

  3. 网站的另一用户下载了他们认为是一本封面图片的文件,但实际上是malware.exe可执行文件。他们打开了文件,然后他们的电脑被恶意软件感染。

这种场景已经被简化了——恶意文件可能有一个不那么明显的名字(比如 cover.jpg.exe),但总体过程已经被说明了。

你如何选择处理上传的安全性将取决于特定的用例,但对于大多数情况,这些提示将有所帮助:

  • 当你将文件保存到磁盘时,生成一个名称而不是使用上传者提供的名称。你应该将文件扩展名替换为你期望的。例如,如果一个文件被命名为 cover.exe 但内容类型是 image/jpeg,则将文件保存为 cover.jpg。你也可以为额外的安全性生成一个完全随机的文件名。

  • 检查文件扩展名是否与内容类型匹配。这种方法并不是万无一失的,因为存在如此多的 MIME 类型,如果你处理不常见的文件,你可能不会得到匹配。内置的 mimetypes Python 模块可以在这里帮助你。它的 guess_type 函数接受一个文件名,并返回一个包含 mimetype(内容类型)和 encoding 的元组。以下是一个展示其使用的简短片段,在 Python 控制台中:

    >>> import mimetypes
    >>> mimetypes.guess_type('file.jpg')
    ('image/jpeg', None)
    >>> mimetypes.guess_type('text.html')
    ('text/html', None)
    >>> mimetypes.guess_type('unknownfile.abc')
    (None, None)
    >>> mimetypes.guess_type('archive.tar.gz')
    ('application/x-tar', 'gzip')
    

    如果类型或编码无法猜测,元组的任一元素都可能为 None。一旦通过 import mimetypes 将其导入到你的文件中,你可以在你的视图函数中使用它:

    upload = request.FILES["file-upload-name"]
    mimetype, encoding = mimetypes.guess_type(upload.name)
    if mimetype != upload.content_type:
        raise TypeError("Mimetype doesn't match file extension.")
    

    这种方法适用于常见的文件类型,如图像,但如前所述,许多不常见的类型可能会返回 mimetypeNone

  • 如果你预计会有图像上传,请使用 Pillow 库尝试以图像的形式打开上传的文件。如果它不是一个有效的图像,那么 Pillow 将无法打开它。这就是 Django 在使用其 ImageField 上传图像时所做的。我们将在 练习 8.05使用 Django 表单的图像上传 中展示如何使用这种技术打开和操作图像。

  • 你还可以考虑使用 python-magic Python 包,该包检查文件的实际内容以尝试确定其类型。它可以通过 pip 安装,其 GitHub 项目是 github.com/ahupp/python-magic。一旦安装,并通过 import magic 导入到你的文件中,你可以在你的视图函数中使用它:

    upload = request.FILES["field_name"]
    mimetype = magic.from_buffer(upload.read(2048), mime=True)
    

然后,你可以验证 mimetype 是否在允许的类型列表中。

这不是一个保护恶意文件上传的所有方法的完整列表。最佳方法将取决于你正在构建的应用程序类型。你可能正在构建一个用于托管任意文件的网站,在这种情况下,你根本不需要进行任何内容检查。

让我们看看我们如何构建一个 HTML 表单和视图,允许上传文件。然后我们将它们存储在 media 目录中,并在我们的浏览器中检索下载的文件。

练习 8.03:文件上传和下载

在这个练习中,您将向 media-example.html 模板添加一个带有文件字段的表单。这将允许您使用浏览器将文件上传到 media_example 视图。您还将更新 media_example 视图,以便将文件保存到 MEDIA_ROOT 目录,以便可供下载。然后,您将通过再次下载文件来测试这一切是否正常工作:

  1. 在 PyCharm 中,打开位于 templates 文件夹内的 media-example.html 模板。在 <body> 元素内部,移除在 练习 8.02步骤 6 中添加的 <a> 链接,用 <form> 元素(如图所示)替换它。确保打开标签具有 method="post"enctype="multipart/form-data"

    </head>
    <body>
        <form method="post" enctype="multipart/form-data">
    </form>
    </body>
    
  2. <form> 主体内部插入 {% csrf_token %} 模板标签。

  3. {% csrf_token %} 之后,添加一个 type="file"name="file_upload"<input> 元素:

    <input type="file" name="file_upload">
    
  4. 最后,在关闭 </form> 标签之前,添加一个 type="submit" 并具有文本内容 提交<button> 元素:

    <button type="submit">Submit</button>
    

    您的 HTML 主体现在应该看起来像这样:

    <body>
        <form method="post" enctype="multipart/form-data">
            {% csrf_token %}
            <input type="file" name="file_upload">
            <button type="submit">Submit</button>
        </form>
    </body>
    

    现在,保存并关闭文件。它应该看起来像这样:packt.live/37XJPh3

  5. 打开 media_example 应用程序的 views.py 文件。在 media_example 视图中,添加代码以将上传的文件保存到 MEDIA_ROOT 目录。为此,您需要从设置中访问 MEDIA_ROOT,因此请在文件顶部导入 Django 设置:

    from django.conf import settings
    

    您还需要使用 os 模块来构建保存路径,因此也要导入它(同样在文件顶部):

    import os
    
  6. 只有当请求方法是 POST 时,上传的文件才应该被保存。在 media_example 视图中,添加一个 if 语句来验证 request.method 是否为 POST

    def media_example(request):
        if request.method == 'POST':
            …
    
  7. 在之前步骤中添加的 if 语句内部,通过将上传的文件名与 MEDIA_ROOT 连接来生成输出路径。然后,以 wb 模式打开此路径,并使用 chunks 方法遍历上传的文件。最后,将每个块写入保存的文件:

    def media_example(request):
        if request.method == 'POST':
    request.FILES dictionary, using the key that matches the name given to the file input (in our case, this is file_upload). You can save and close views.py. It should now look like this: [`packt.live/37TwxSr`](http://packt.live/37TwxSr). 
    
  8. 如果尚未运行,请启动 Django 开发服务器,然后导航到 http://127.0.0.1:8000/media-example/。您应该看到文件上传字段和 提交 按钮,如图所示:![图 8.9:文件上传表单 图片 B15509_08_09.jpg

    ![图 8.9:文件上传表单 点击 浏览…(或在您的浏览器中对应的选项)并选择一个文件进行上传。文件名将出现在文件输入框中。然后,点击 提交。页面将重新加载,表单将再次为空。这是正常的——在后台,文件应该已经被保存。1. 尝试使用 MEDIA_URL 下载您上传的文件。在这个例子中,上传了一个名为 cover.jpg 的文件。它将在 http://127.0.0.1:8000/media/cover.jpg 可以下载。您的 URL 将取决于您上传的文件名。![图 8.10:可见于 MEDIA_URL 的上传文件 图片 B15509_08_10.jpg

![图 8.10:可见于 MEDIA_URL 的上传文件如果你上传了一个图像文件、HTML 文件或其他浏览器可以显示的文件类型,你将能够在浏览器中查看它。否则,你的浏览器将再次将其下载到磁盘上。在两种情况下,这意味着上传是成功的。你也可以通过查看 media_project 项目目录中的 media 目录来确认上传是否成功:图 8.11:媒体目录中的 cover.jpg

图 8.11:媒体目录中的 cover.jpg

图 8.11 展示了 PyCharm 中 media 目录内的 cover.jpg

在这个练习中,你添加了一个 enctype 设置为 multipart/form-data 的 HTML 表单,以便允许文件上传。它包含一个 file 输入来选择要上传的文件。然后你添加了保存功能到 media_example 视图中,以便将上传的文件保存到磁盘上。

在下一节中,我们将探讨如何使用 Django 表单简化表单生成并添加验证。

使用 Django 表单进行文件上传

第六章表单 中,我们看到了 Django 如何使定义表单并自动将其渲染为 HTML 变得容易。在上一个示例中,我们手动定义了表单并编写了 HTML。我们可以用 Django 表单来替换它,并使用 FileField 构造函数实现文件输入。

下面是如何在表单上定义 FileField

from django import forms
class ExampleForm(forms.Form):
    file_upload = forms.FileField()

FileField 构造函数可以接受以下关键字参数:

  • required:对于必填字段,应为 True;如果字段是可选的,则为 False

  • max_length:这指的是上传文件文件名的最大长度。

  • allow_empty_file:具有此参数的字段即使上传的文件为空(大小为 0)也是有效的。

除了这三个关键字参数之外,构造函数还可以接受标准的 Field 参数,例如 widgetFileField 的默认小部件类是 ClearableFileInput。这是一个可以显示复选框的文件输入,可以选中以发送空值并清除模型字段上保存的文件。

在视图中使用带有 FileField 的表单与其他表单类似,但当表单已提交(即 request.METHODPOST)时,则应将 request.FILES 也传递给表单构造函数。这是因为 Django 需要访问 request.FILES 来在验证表单时查找有关上传文件的信息。

因此,在 view 函数中的基本流程如下:

def view(request):
    if request.method == "POST":
        # instantiate the form with POST data and files
        form = ExampleForm(request.POST, request.FILES)
        if form.is_valid():
            # process the form and save files
            return redirect("success-url")
    else:
        # instantiate an empty form as we've seen before
        form = ExampleForm()
    # render a template, the same as for other forms
    return render(request, "template.html", {"form": form})

当处理上传文件和表单时,你可以通过通过 request.FILES 或通过 form.cleaned_data 访问它们来与上传文件进行交互:返回的值将指向同一个对象。在我们的上述示例中,我们可以这样处理上传的文件:

if form.is_valid():
    save_file_upload("/path/to/save.jpg", \
                     request.FILES["file_upload"])
    return redirect("/success-url/")

或者,由于它们包含相同的对象,你可以使用 form.cleaned_data

if form.is_valid():
    save_file_upload("/path/to/save.jpg", \
                     form.cleaned_data["file_upload"])
    return redirect("/success-url/")

保存的数据将保持不变。

注意

第六章表单 中,你尝试了表单和提交无效值。当页面刷新以显示表单错误时,你之前输入的数据会在页面重新加载时被填充。对于文件字段来说,这种情况不会发生;相反,如果表单无效,用户将不得不再次导航并选择文件。

在下一个练习中,我们将通过构建一个示例表单,然后修改我们的视图,仅在表单有效时保存文件,来将我们之前看到的 FileFields 应用到实践中。

练习 8.04:使用 Django 表单上传文件

在之前的练习中,你创建了一个 HTML 表单,并使用它将文件上传到 Django 视图。如果你尝试提交没有选择文件的表单,你会得到一个 Django 异常屏幕。你没有对表单进行任何验证,所以这种方法相当脆弱。

在这个练习中,你将创建一个带有 FileField 的 Django 表单,这将允许你使用表单验证函数使视图更加健壮,同时减少代码量:

  1. 在 PyCharm 中,在 media_example 应用程序内部,创建一个名为 forms.py 的新文件。它将自动打开。在文件开头,导入 Django 的 forms 库:

    from django import forms
    

    然后,创建一个 forms.Form 子类,并将其命名为 UploadForm。向其中添加一个字段,一个名为 file_uploadFileField。你的类应该有如下代码:

    class UploadForm(forms.Form):
        file_upload = forms.FileField()
    

    你可以保存并关闭此文件。完整的文件应如下所示:packt.live/34S5hBV

  2. 打开 form_example 应用的 views.py 文件。在文件的开头,现有 import 语句的下方,你需要导入你的新类,如下所示:

    from .forms import UploadForm
    
  3. 如果你处于视图的 POST 分支,UploadForm 需要使用 request.POSTrequest.FILES 两个参数进行实例化。如果你没有传入 request.FILES,那么 form 实例将无法访问上传的文件。在 if request.method == "POST" 检查下,使用这两个参数实例化 UploadForm

    form = UploadForm(request.POST, request.FILES)
    
  4. 定义 save_path 和存储文件内容的现有行可以保留,但它们应该缩进一个块,并放在表单有效性检查内部,这样它们只有在表单有效时才会执行。添加 if form.is_valid(): 行,然后缩进其他行,使代码看起来像这样:

    if form.is_valid():
        save_path = os.path.join\
                    (settings.MEDIA_ROOT, \
                     request.FILES["file_upload"].name)
        with open(save_path, "wb") as output_file:
            for chunk in request.FILES["file_upload"].chunks():
                output_file.write(chunk)
    
  5. 由于你现在正在使用表单,你可以通过表单访问文件上传。将 request.FILES["file_upload"] 的使用替换为 form.cleaned_data["file_upload"]

    if form.is_valid():
        save_path = os.path.join\
                    (settings.MEDIA_ROOT,\
                     form.cleaned_data["file_upload"].name)
        with open(save_path, "wb") as output_file:
            for chunk in form.cleaned_data["file_upload"].chunks():
                output_file.write(chunk)
    
  6. 最后,添加一个 else 分支来处理非 POST 请求,它只是实例化一个不带任何参数的表单:

    if request.method == 'POST':
        …
    else:
        form = UploadForm()
    
  7. 将上下文字典参数添加到 render 调用中,并将 form 变量设置在 form 键中:

    return render(request, "media-example.html", \
                  {"form": form})
    

    你现在可以保存并关闭此文件。它应该如下所示:packt.live/3psXxyc

  8. 最后,打开 media-example.html 模板,删除你手动定义的文件 <input>。用使用 as_p 方法渲染的 form 替换它(高亮显示):

    <body>
        <form method="post" enctype="multipart/form-data">
            {% csrf_token %}
            {{ form.as_p }}
            <button type="submit">Submit</button>
        </form>
    </body>
    

    你不应该更改文件的任何其他部分。你可以保存并关闭此文件。它应该看起来像这样:packt.live/3qHHSMi

  9. 如果 Django 开发服务器尚未运行,请启动它,然后导航到 http://127.0.0.1:8000/media-example/。你应该会看到 文件上传 字段和 提交 按钮,如下所示:![图 8.12:在浏览器中渲染的文件上传 Django 表单 图片

    图 8.12:在浏览器中渲染的文件上传 Django 表单

  10. 由于我们使用的是 Django 表单,我们自动获得其内置验证。如果你尝试不选择文件就提交表单,你的浏览器应该阻止你并显示错误,如下所示:![图 8.13:浏览器阻止表单提交 图片

    图 8.13:浏览器阻止表单提交

  11. 最后,重复你在 练习 8.03文件上传和下载 中进行的上传测试,通过选择一个文件并提交表单。然后你应该能够使用 MEDIA_URL 检索文件。在这种情况下,正在再次上传一个名为 cover.jpg 的文件(见下图):![图 8.14:上传名为 cover.jpg 的文件 图片

图 8.14:上传名为 cover.jpg 的文件

你可以随后在 http://127.0.0.1:8000/media/cover.jpg 上检索文件,你可以在浏览器中看到如下所示:

![图 8.15:使用 Django 表单上传的文件在浏览器中也是可见的图片

图 8.15:使用 Django 表单上传的文件在浏览器中也是可见的

在这个练习中,我们用一个包含 FileField 的 Django 表单替换了手动构建的表单。我们在视图中通过传递 request.POSTrequest.FILES 来实例化表单。然后我们使用标准的 is_valid 方法来检查表单的有效性,并且只有在表单有效的情况下才保存文件上传。我们测试了文件上传,并看到我们能够使用 MEDIA_URL 检索上传的文件。

在下一节中,我们将查看 ImageField,它类似于 FileField,但专门用于图像。

使用 Django 表单进行图像上传

如果你想在 Python 中处理图像,你将最常使用的库叫做 Image,它从 PIL 导入:

from PIL import Image

注意

Python Imaging Library、PIL 和 Pillow 这些术语经常可以互换使用。你可以假设如果有人提到 PIL,他们指的是最新的 Pillow 库。

Pillow 提供了各种检索图像数据或操作图像的方法。你可以找出图像的宽度和高度,或者缩放、裁剪并对它们应用变换。由于本章中可用的操作太多,我们只介绍一个简单的例子(缩放图像),你将在下一个练习中使用它。

由于图片是用户可能想要上传的最常见的文件类型之一,Django 也包含了一个ImageField实例。这个实例的行为与FileField实例类似,但也会自动验证数据是否为图片文件。这有助于减轻我们期望是图片但用户上传了恶意文件时的安全问题。

来自ImageFieldUploadedFile具有与FileField相同的所有属性和方法(sizecontent_typenamechunks()等),但增加了一个额外的属性:image。这是一个 PIL Image对象的实例,用于验证上传的文件是否为有效的图片。

在检查表单有效后,底层的 PIL Image对象被关闭。这是为了释放内存并防止 Python 进程打开太多文件,这可能会引起性能问题。对于开发者来说,这意味着你可以访问一些关于图片的元数据(如其widthheightformat),但如果不重新打开图片,你无法访问实际的图片数据。

为了说明,我们将有一个包含ImageField的表单,命名为picture

class ExampleForm(forms.Form):
    picture = ImageField()

在视图函数内部,可以在表单的cleaned_data中访问picture字段:

if form.is_valid():
    picture_field = form.cleaned_data["picture"]

然后,可以检索picture字段的Image对象:

image = picture_field.image

现在我们已经在视图中有了图片的引用,我们可以获取一些元数据:

w = image.width  # an integer, e.g. 600
h = image.height  # also an integer, e.g. 420
# the format of the image as a string, e.g. "PNG"
f = image.format

Django 还会自动更新UploadedFilecontent_type属性,使其适用于picture字段。这将覆盖浏览器上传文件时发送的值。

尝试使用访问实际图片数据的方法(而不是仅访问元数据)将引发异常。这是因为 Django 已经关闭了底层的图片文件。

例如,以下代码片段将引发AttributeError

image.getdata()

相反,我们需要重新打开图片。在导入Image类后,可以使用ImageField引用打开图片数据:

from PIL import Image
image = Image.open(picture_field)

现在图片已经打开,你可以对它进行操作。在下一节中,我们将查看一个简单的示例——调整上传的图片大小。

使用 Pillow 调整图片大小

Pillow 支持许多在保存图片之前你可能想要执行的操作。我们无法在这本书中解释所有这些操作,所以我们只使用一个常见的操作:在保存之前将图片调整到特定大小。这将帮助我们节省存储空间并提高下载速度。例如,用户可能在 Bookr 上传了比我们所需更大的封面图片。当保存文件(将其写回磁盘)时,我们必须指定要使用的格式。我们可以通过多种方法确定上传的图片类型(例如检查上传文件的content_typeImage对象的format),但在我们的示例中,我们总是将图片保存为JPEG文件。

PIL 的Image类有一个thumbnail方法,可以将图像调整到最大尺寸同时保持宽高比。例如,我们可以将最大尺寸设置为 50px x 50px。一个 200px x 100px 的图像将被调整到 50px x 25px:通过将最大尺寸设置为 50px 来保持宽高比。每个维度都按 0.25 的因子缩放:

from PIL import Image
size = 50, 50  # a tuple of width, height to resize to
image = Image.open(image_field)  # open the image as before
image.thumbnail(size)  # perform the resize

到目前为止,调整仅在内存中完成。直到调用save方法,更改才不会保存到磁盘,如下所示:

image.save("path/to/file.jpg")

输出格式会自动根据使用的文件扩展名确定,在本例中为 JPEG。save方法也可以接受一个格式参数来覆盖它。例如:

image.save("path/to/file.png", "JPEG")

尽管扩展名为png,格式指定为JPEG,因此输出将是 JPEG 格式。正如你可能想象的那样,这可能会非常令人困惑,因此你可能决定只坚持指定扩展名。

在下一个练习中,我们将更改我们一直在使用的UploadForm,使用ImageField而不是FileField,然后实现在上传的图像保存到媒体目录之前对其进行调整。

练习 8.05:使用 Django 表单上传图片

在这个练习中,你将更新你在练习 8.04中创建的UploadForm类,使用ImageField而不是FileField(这将涉及简单地更改字段的类)。然后你将看到表单在浏览器中渲染。接下来,你将尝试上传一些非图像文件,看看 Django 如何验证表单以禁止它们。最后,你将更新你的视图,在保存到媒体目录之前使用 PIL 调整图像大小,并在实际操作中测试它:

  1. 打开media_example应用的forms.py文件。在UploadForm类中,将file_upload更改为ImageField的实例而不是FileField。更新后,你的UploadForm应该看起来像这样:

    class UploadForm(forms.Form):
        file_upload = forms.forms.py file should look like this: http://packt.live/2KAootD. 
    
  2. 如果 Django 开发服务器尚未运行,请启动它,然后导航到http://127.0.0.1:8000/media-example/。你应该能看到渲染的表单,并且它的外观与我们使用FileField时相同(见下图):图 8.16:ImageField 与 FileField 外观相同

    图 8.16:ImageField 与 FileField 外观相同

  3. 当你尝试上传非图像文件时,你会注意到差异。点击浏览…按钮并尝试选择一个非图像文件。根据你的浏览器或操作系统,你可能无法选择除图像文件以外的任何内容,就像图 8.17所示:图 8.17:只能选择图像文件

    图 8.17:只能选择图像文件

    你的浏览器可能允许选择图像,但在选择后显示错误。或者你的浏览器可能允许你选择文件并提交表单,Django 将引发ValidationError。无论如何,你可以确信在你的视图中,表单的is_valid视图只有在上传了图像时才会返回True

    注意

    你现在不需要测试上传文件,因为结果将与练习 8.04中的相同,即使用 Django 表单上传文件。

  4. 你首先需要确保 Pillow 库已安装。在终端(确保你的虚拟环境已激活)中运行:

    pip3 install pillow
    

    (在 Windows 中,这是pip install pillow。)你将得到类似于图 8.18的输出:

    图 8.18:pip3 安装 Pillow

    图 8.18:pip3 安装 Pillow

    或者,如果 Pillow 已经安装,你将看到输出消息Requirement already satisfied

  5. 现在,我们可以更新media_example视图,在保存图像之前调整其大小。切换回 PyCharm 并打开media_example应用的views.py文件,然后导入 PIL 的Image类。所以,在文件顶部靠近import os语句下方添加此导入行:

    from PIL import Image
    
  6. 前往media_example视图。在生成save_path的行下面,移除打开输出文件的三个行,遍历上传的文件,并写出其块。用以下代码替换这些代码,该代码使用 PIL 打开上传的文件,调整其大小,然后保存:

    image = Image.open(form.cleaned_data["file_upload"])
    image.thumbnail((50, 50))
    image.save(save_path)
    

    第一行通过打开上传的文件创建一个Image实例,下一行执行缩略图转换(最大尺寸为 50px x 50px),第三行将文件保存到我们在之前练习中生成的相同保存路径。你可以保存文件。它应该看起来像这样:packt.live/34PWvof

  7. Django 开发服务器应该仍然在步骤 2中运行,但如果它没有运行,你应该启动它。然后,导航到http://127.0.0.1:8000/media-example/。你会看到熟悉的UploadForm。选择一个图像并提交表单。如果上传和调整大小成功,表单将刷新并再次为空。

  8. 使用MEDIA_URL查看上传的图像。例如,一个名为cover.jpg的文件将从http://127.0.0.1:8000/media/cover.jpg处可下载。你应该看到图像已被调整大小,最大尺寸仅为 50px:图 8.19:调整大小的标志

图 8.19:调整大小的标志

尽管这个尺寸的缩略图可能不是非常有用,但它至少让我们确信图像调整大小已经正确完成。

在这个练习中,我们将UploadForm上的FileField更改为ImageField。我们注意到浏览器不允许我们上传除图像之外的内容。然后我们在media_example视图中添加了代码,使用 PIL 调整上传的图像大小。

我们鼓励使用单独的 Web 服务器来提供静态和媒体文件,出于性能考虑。然而,在某些情况下,你可能想使用 Django 来提供文件,例如,在允许访问之前提供身份验证。在下一节中,我们将讨论如何使用 Django 来提供媒体文件。

使用 Django 提供上传的(和其他)文件

在本章和第五章 静态文件服务中,我们不建议使用 Django 来提供文件服务。这是因为这会无谓地占用一个 Python 进程来仅提供文件服务——这是 Web 服务器能够处理的事情。不幸的是,Web 服务器通常不提供动态访问控制,即仅允许认证用户下载文件。根据你在生产中使用的 Web 服务器,你可能能够让它对 Django 进行身份验证,然后自己提供文件;然而,特定 Web 服务器的具体配置超出了本书的范围。

你可以采取的一种方法是指定MEDIA_ROOT目录的子目录,并让你的 Web 服务器仅阻止对这个特定文件夹的访问。任何受保护的媒体都应该存储在其中。如果你这样做,只有 Django 能够读取其中的文件。例如,你的 Web 服务器可以提供MEDIA_ROOT目录中的所有内容,除了MEDIA_ROOT/protected目录。

另一种方法是将 Django 视图配置为从磁盘提供特定文件。视图将确定要发送的文件在磁盘上的路径,然后使用FileResponse类发送它。FileResponse类接受一个打开的文件句柄作为参数,并尝试从文件的内容中确定正确的 MIME 类型。Django 将在请求完成后关闭文件句柄。

视图函数将接受请求和一个指向要下载文件的相对路径作为参数。这个相对路径是MEDIA_ROOT/protected文件夹内的路径。

在我们的例子中,我们只需检查用户是否匿名(未登录)。我们将通过检查request.user.is_anonymous属性来完成此操作。如果他们未登录,我们将引发一个django.core.exceptions.PermissionDenied异常,该异常会向浏览器返回一个 HTTP 403 Forbidden响应。这将停止视图的执行,并且不会返回任何文件:

import os.path
from django.conf import settings
from django.http import FileResponse
from django.core.exceptions import PermissionDenied
def download_view(request, relative_path):
    if request.user.is_anonymous:
        raise PermissionDenied
    full_path = os.path.join(settings.MEDIA_ROOT, \
                             "protected", relative_path)
    file_handle = open(full_path, "rb")
    return FileResponse(file_handle)
# Django sends the file then closes the handle

到这个视图的 URL 映射可以是这样的,使用<path>路径转换器。在你的urls.py文件中:

urlpatterns = [
    …
    path("downloads/<path:relative_path>", views.download_view)]

你可以选择多种方式来实现发送文件的视图。重要的是要使用FileResponse类,它被设计为以块的形式将文件流式传输到客户端,而不是将其全部加载到内存中。这将减少服务器的负载,并减少在必须使用 Django 发送文件时的资源使用影响。

在模型实例上存储文件

到目前为止,我们手动管理了文件的上传和保存。您还可以通过将保存路径分配给CharField来将文件与模型实例关联。然而,正如 Django 的许多功能一样,这种能力(以及更多)已经通过models.FileField类提供。FileField实例实际上并不存储文件数据;相反,它们存储文件存储的路径(就像CharField一样),但它们还提供了辅助方法。这些方法帮助您加载文件(因此您不必手动打开它们)并根据实例的 ID(或其他属性)为您生成磁盘路径。

FileField在其构造函数中可以接受两个特定的可选参数(以及基本的Field参数,如requireduniquehelp_text等):

  • max_length:与表单中的ImageFieldmax_length类似,这是允许的文件名最大长度。

  • upload_toupload_to参数的行为取决于传递给它的变量类型。其最简单的用法是与字符串或pathlib.Path对象一起使用。路径简单地附加到MEDIA_ROOT

在这个例子中,upload_to只是定义为一个字符串:

class ExampleModel(models.Model):
    file_field = models.FileField(upload_to="files/")

保存到这个FileField的文件将存储在MEDIA_ROOT/files目录中。

您也可以使用pathlib.Path实例来实现相同的结果:

import pathlib
class ExampleModel(models.Model):
    file_field = models.FileField(upload_to=pathlib.Path("files/"))

使用upload_to的另一种方式是使用包含strftime格式化指令的字符串(例如,%Y用于替换当前年份,%m用于当前月份,%d用于当前月份的日期)。这些指令的完整列表非常广泛,可以在docs.python.org/3/library/time.html#time.strftime中找到。Django 会在保存文件时自动插入这些值。

例如,假设您这样定义了模型和FileField

class ExampleModel(models.Model):
    file_field = models.FileField(upload_to="files/%Y/%m/%d/")

对于特定一天上传的第一个文件,Django 会为该天创建目录结构。例如,对于 2020 年 1 月 1 日上传的第一个文件,Django 会创建MEDIA_ROOT/2020/01/01目录,并将上传的文件存储在那里。同一天上传的下一个文件(以及所有后续文件)也会存储在该目录中。同样,在 2020 年 1 月 2 日,Django 会创建MEDIA_ROOT/2020/01/02目录,并将文件存储在那里。

如果您每天上传成千上万的文件,您甚至可以通过在upload_to参数中包含小时和分钟来进一步拆分文件(upload_to="files/%Y/%m/%d/%H/%M/")。但如果上传量很小,这可能不是必要的。

通过利用upload_to参数的这种方法,您可以让 Django 自动隔离上传,防止太多文件存储在单个目录中(这可能很难管理)。

使用upload_to的最终方法是传递一个函数来生成存储路径。请注意,这与upload_to的其他用法不同,因为它应该生成包括文件名在内的完整路径,而不仅仅是目录。该函数接受两个参数:instancefilenameinstance是与FileField相关联的模型实例,filename是上传文件的名称。

这里有一个示例函数,它取文件名的前两个字符来生成保存的目录。这意味着每个上传的文件都将被分组到父目录中,这有助于组织文件并防止一个目录中文件过多:

def user_grouped_file_path(instance, filename):
    return "{}/{}/{}/{}".format(instance.username, \
                                filename[0].lower(), \
                                filename[1].lower(), filename)

如果这个函数用文件名Test.jpg调用,它将返回<username>/t/e/Test.jpg。如果用example.txt调用,它将返回<username>e/x/example.txt,依此类推。username是从正在保存的实例中检索的。为了说明,这里有一个使用此函数的FileField的模型示例。它还有一个用户名,这是一个CharField

class ExampleModel(models.Model):
    file_field = models.FileField\
                 (upload_to=user_grouped_file_path)
    username = models.CharField(unique=True)

你可以在upload_to函数中使用实例的任何属性,但请注意,如果这个实例正在创建过程中,那么文件保存函数将在它保存到数据库之前被调用。因此,实例上的一些自动生成的属性(如id/pk)可能尚未填充,不应用于生成路径。

upload_to函数返回的任何路径都会附加到MEDIA_ROOT,因此上传的文件将被保存在MEDIA_ROOT/<username>/t/e/Test.jpgMEDIA_ROOT/<username>/e/x/example.txt分别。

注意,user_grouped_file_path只是一个说明性函数,故意保持简短,所以它不能正确处理单字符文件名或用户名包含无效字符的情况。例如,如果用户名中包含/,那么这将在生成的路径中充当目录分隔符。

现在我们已经深入探讨了在模型上设置FileField的过程,但我们是如何实际将上传的文件保存到它的呢?这就像将上传的文件分配给模型的属性一样简单,就像分配任何类型的值一样。这里有一个使用视图和我们在本节前面作为示例使用的简单ExampleModel类的快速示例:

class ExampleModel(models.Model):
    file_field = models.FileField(upload_to="files/")
def view(request):
    if request.method == "POST":
        m = ExampleModel()  # Create a new ExampleModel instance
        m.file_field = request.FILES["uploaded_file"]
        m.save()
    return render(request, "template.html")

在这个例子中,我们创建了一个新的ExampleModel类,并将上传的文件(在表单中名为uploaded_file)分配给其file_field属性。当我们保存模型实例时,Django 会自动将文件及其名称写入upload_to目录路径。如果上传的文件名为image.jpg,保存路径将是MEDIA_ROOT/upload_to/image.jpg

我们同样可以更新现有模型上的文件字段或使用表单(在保存之前进行验证)。这里还有一个演示这一点的简单示例:

class ExampleForm(forms.Form):
    uploaded_file = forms.FileField()
def view(request, model_pk):
    form = ExampleForm(request.POST, request.FILES)
    if form.is_valid():    
        # Get an existing model instance
        m = ExampleModel.object.get(pk=model_pk)
        # store the uploaded file on the instance
        m.file_field = form.cleaned_data["uploaded_file"]
        m.save()
    return render(request, "template.html")

你可以看到,更新现有模型实例上的 FileField 与在新的实例上设置它的过程相同;如果你选择使用 Django 表单,或者直接访问 request.FILES,过程同样简单。

在模型实例上存储图像

虽然 FileField 可以存储任何类型的文件,包括图像,但还有一个 ImageField。正如你所期望的,这只是为了存储图像。模型 forms.FileFieldforms.ImageField 之间的关系类似于 models.FileFieldmodels.ImageField 之间的关系,即 ImageField 扩展了 FileField 并为处理图像添加了额外的方法。

ImageField 构造函数接受与 FileField 相同的参数,并添加了两个额外的可选参数:

  • height_field:这是模型中将被更新为图像高度的字段名称,每次保存模型实例时都会更新。

  • width_field:与 height_field 相对应的宽度字段,该字段存储每次保存模型实例时更新的图像宽度。

这两个参数都是可选的,但如果使用,它们命名的字段必须存在。也就是说,可以不设置 height_fieldwidth_field,但如果它们被设置为不存在字段的名称,则将发生错误。这样做的目的是帮助搜索特定尺寸的文件。

这里有一个使用 ImageField 的示例模型,它更新图像尺寸字段:

class ExampleModel(models.Model):
    image = models.ImageField(upload_to="images/%Y/%m/%d/", \
                              height_field="image_height",\
                              width_field="image_width")
    image_height = models.IntegerField()
    image_width = models.IntegerField()

注意,ImageField 使用了 upload_to 参数,该参数包含在保存时更新的日期格式化指令。upload_to 的行为与 FileField 相同。

当保存 ExampleModel 实例时,其 image_height 字段会更新为图像的高度,而 image_width 会更新为图像的宽度。

我们不会展示在视图中设置 ImageField 值的示例,因为这个过程与普通的 FileField 相同。

处理 FieldFile

当你访问模型实例的 FileFieldImageField 属性时,你不会得到一个原生的 Python file 对象。相反,你将使用一个 FieldFile 对象。FieldFile 类是一个围绕 file 的包装器,它添加了额外的方法。是的,有 FileFieldFieldFile 这样的类名可能会让人困惑。

Django 使用 FieldFile 而不是仅仅一个 file 对象的原因有两个。首先,它为打开、读取、删除和生成文件 URL 添加了额外的方法。其次,它提供了一个抽象,允许使用替代存储引擎。

自定义存储引擎

我们在第五章服务静态文件中讨论了自定义存储引擎,关于存储静态文件。我们不会详细检查媒体文件的自定义存储引擎,因为第五章服务静态文件中概述的代码也适用于媒体文件。需要注意的是,您所使用的存储引擎可以在不更新其他代码的情况下更改。这意味着您可以在开发期间将媒体文件存储在本地驱动器上,然后在应用程序部署到生产环境时将其保存到 CDN。

默认存储引擎类可以通过在settings.py中使用DEFAULT_FILE_STORAGE来设置。存储引擎也可以通过storage参数按字段设置(对于FileFieldImageField)。例如:

storage_engine = CustomStorageEngine()
class ExampleModel(models.Model):
    image_field = ImageField(storage=storage_engine)

这演示了当你上传或检索文件时实际发生的情况。Django 委托给存储引擎来分别写入或读取它。即使在保存到磁盘时也是如此;然而,这是基本的,对用户来说是不可见的。

读取存储的字段文件

现在我们已经了解了自定义存储引擎,让我们看看如何从FieldFile中读取。在前面的章节中,我们看到了如何在模型实例上设置文件。再次读取数据同样简单——我们有几种不同的方法可以帮助我们,具体取决于我们的用例。

在以下几个代码片段中,假设我们处于一个视图中,并且以某种方式检索了我们的模型实例,并将其存储在变量m中。例如:

m = ExampleModel.object.get(pk=model_pk)

我们可以使用read方法从文件中读取所有数据:

data = m.file_field.read()

或者,我们可以使用open方法手动打开文件。如果我们想将我们自己生成的数据写入文件,这可能很有用:

with m.file_field.open("wb") as f:
    chunk = f.write(b"test")  # write bytes to the file

如果我们想分块读取文件,可以使用chunks方法。这与我们之前看到的从上传的文件中读取块的方式相同:

for chunk in m.file_field.chunks():
    # assume this method is defined somewhere
    write_chunk(open_file, chunk)

我们也可以通过使用其path属性手动打开文件:

open(m.file_field.path)

如果我们想为下载流式传输FileField,最好的方法是我们之前看到的FileResponse类。结合FileField上的open方法。请注意,如果我们只是尝试提供媒体文件,我们应该只实现一个视图来执行此操作,如果我们试图限制对文件的访问。否则,我们应该使用MEDIA_URL提供文件,并允许 Web 服务器处理请求。以下是我们的download_view如何使用FileField而不是手动指定的路径来编写的示例:

def download_view(request, model_pk):
    if request.user.is_anonymous:
        raise PermissionDenied
    m = ExampleModel.objects.get(pk=model_pk)
    # Django sends the file then closes the handle
    return FileResponse(m.file_field.open())  

Django 打开正确的路径,在响应后关闭它。Django 还将尝试确定文件的正确 MIME 类型。我们假设这个FileFieldupload_to属性设置为一个受保护的目录,该目录被 Web 服务器阻止直接访问。

在 FileField 中存储现有文件或内容

我们已经看到了如何将上传的文件存储在图像字段中——只需将其分配给字段即可:

m.file_field = request.FILES["file_upload"]

但我们如何将 field 值设置为可能已经在磁盘上存在的现有文件的值?你可能认为你可以使用标准的 Python file 对象,但这不会起作用:

# Don't do this
m.file_field = open("/path/to/file.txt", "rb")  

你也可以尝试使用一些内容来设置文件:

m.file_field = "new file content"  # Don't do this

这也不会起作用。

你需要使用 FileFieldsave 方法,它接受 Django FileContentFile 对象的实例(这些类的完整路径分别是 django.core.files.Filedjango.core.files.base.ContentFile)。然后我们将简要讨论 save 方法及其参数,然后返回到这些类。

FileFieldsave 方法接受三个参数:

  • name:你要保存的文件名。这是文件在存储引擎(在我们的例子中,是磁盘,在 MEDIA_ROOT 内部)保存时的名称。

  • Content:这是一个 FileContentFile 的实例,我们刚刚提到;再次,我们很快就会讨论这些。

  • Save:此参数是可选的,默认为 True。这表示在保存文件后是否将模型实例保存到数据库。如果设置为 False(即模型未保存),则文件仍将被写入存储引擎(到磁盘),但关联不会存储在模型中。之前的文件路径(或如果没有设置则没有文件)将仍然存储在数据库中,直到手动调用模型实例的 save 方法。你应该只在打算对模型实例进行其他更改然后手动保存时设置此参数。

回到 FileContentFile:使用哪一个取决于你想要在 FileField 中存储什么。

File 被用作 Python file 对象的包装器,如果你有一个现有的 file 或类似文件的对象需要保存,你应该使用它。类似文件的对象包括 io.BytesIOio.StringIO 实例。要实例化一个 File 对象,只需将原生 file 对象传递给构造函数,例如:

f = open("/path/to/file.txt", "rb")
file_wrapper = File(f)

当你已经有了一些数据加载,无论是 strbytes 对象时,使用 ContentFile。将数据传递给 ContentFile 构造函数:

string_content = ContentFile("A string value")
bytes_content = ContentField(b"A bytes value")

现在你已经有一个 FileContentFile 实例,使用 save 方法将数据保存到 FileField 是很容易的:

m = ExampleModel.objects.first()
with open("/path/to/file.txt") as f:
    file_wrapper = File(f)
    m.file_field.save("file.txt", f)

由于我们没有向 save 方法传递 save 的值,它将默认为 True,因此模型实例将自动持久化到数据库。

接下来,我们将探讨如何将使用 PIL 处理过的图片存储回图像字段。

将 PIL 图片写入 ImageField

练习 8.05使用 Django 表单上传图片中,你使用了 PIL 来调整图片大小并将其保存到磁盘。当与模型一起工作时,你可能想要执行类似的操作,但让 Django 使用 ImageField 来处理文件存储,这样你就不必手动操作。就像练习中那样,你可以将图片保存到磁盘,然后使用 File 类来包装存储的路径——类似于以下内容:

image = Image.open(request.FILES["image_field"])
image.thumbnail((150, 150))
# save thumbnail to temp location
image.save("/tmp/thumbnail.jpg")
with open("/tmp/thumbnail.jpg", "rb") as f:
    image_wrapper = File(f)
    m.image_field.save("thumbnail.jpg", image_wrapper)
os.unlink("/tmp/thumbnail.jpg")  # clean up temp file

在这个例子中,我们使用Image.save()方法将 PIL 存储到一个临时位置,然后重新打开文件。

这种方法虽然可行,但不是最佳选择,因为它涉及到将文件写入磁盘然后再读出来,这有时可能会很慢。相反,我们可以在内存中完成整个流程。

注意

io.BytesIOio.StringIO是非常有用的对象。它们的行为类似于文件,但仅存在于内存中。BytesIO用于存储原始字节,而StringIO接受 Python 3 的本地 Unicode 字符串。你可以像操作普通文件一样readwriteseek它们。不过,与普通文件不同的是,它们不会写入磁盘,而是在程序终止或超出作用域并被垃圾回收时消失。如果函数想要写入类似文件的东西,但又想立即访问数据,它们非常有用。

首先,我们将图像数据保存到一个io.BytesIO对象中。然后,我们将BytesIO对象包装在一个django.core.files.images.ImageFile实例中(这是一个专门用于图像的File子类,提供了widthheight属性)。一旦我们有了这个ImageFile实例,我们就可以在ImageFieldsave方法中使用它。

注意

ImageFile是一个文件或类似文件的包装器,就像File一样。它提供了两个额外的属性:widthheight。如果你使用它来包装非图像文件,ImageFile不会生成任何错误。例如,你可以打开一个文本文件,并将文件句柄传递给ImageFile构造函数而不会出现任何问题。你可以通过尝试访问widthheight属性来检查你传递的图像文件是否有效:如果这些是None,那么 PIL 无法解码图像数据。你可以自己检查这些值的有效性,并在它们是None时抛出异常。

让我们在实践中看看这个例子,在一个视图中:

from io import BytesIO
from PIL import Image
from django.core.files.images import ImageFile
def index(request, pk):
    # trim out logic for checking if method is POST
    # get a model instance, or create a new one
    m = ExampleModel.objects.get(pk=pk)  

    # store the uploaded image in a variable for shorter code
    uploaded_image = request.FILES["image_field"]
    # load a PIL image instance from the uploaded file
    image = Image.open(uploaded)
    # perform the image resize
    image.thumbnail((150, 150))
    # Create a BytesIO file-like object to store
    image_data = BytesIO()
    # Write the Image data back out to the BytesIO object
    # Retain the existing format from the uploaded image
    image.save(fp=image_data, uploaded_image.format)
    # Wrap the BytesIO containing the image data
    image_file = ImageFile(image_data)
    # Save the wrapped image file data with the original name
    m.image_field.save(uploaded_image.name, image_file)
    # this also saves the model instance
    return redirect("/success-url/")

你可以看到这需要一点更多的代码,但它可以节省将数据写入磁盘。你可以根据自己的需要选择使用任何一种方法(或你想到的另一种方法)。

在模板中引用媒体

一旦我们上传了一个文件,我们希望能够在模板中引用它。对于一个上传的图像,比如书的封面,我们希望能够在页面上显示这个图像。我们在练习 8.02模板设置和使用模板中的 MEDIA_URL中看到了如何使用模板中的MEDIA_URL构建一个 URL。当在模型实例上使用FileFieldImageField时,没有必要这样做,因为 Django 为你提供了这个功能。

FileFieldurl属性将自动根据你的设置中的MEDIA_URL生成媒体文件的完整 URL。

注意

注意,本节中我们对FileField的引用也适用于ImageField,因为它是FileField的子类。

这可以在任何你可以访问实例和字段的地方使用,例如在视图中。例如,在视图中:

instance = ExampleModel.objects.first()
url = instance.file_field.url  # Get the URL

或者在一个模板中(假设 instance 已经传递到模板上下文中):

<img src="img/{{ instance.file_field.url }}">

在下一个练习中,我们将创建一个新的具有 FileFieldImageField 的模型,然后展示 Django 如何自动保存这些字段。我们还将演示如何检索上传文件的 URL。

练习 8.06:在模型中使用 FileFieldImageField

在这个练习中,我们将创建一个具有 FileFieldImageField 的模型。完成此操作后,我们将必须生成迁移并应用它。然后,我们将更改我们一直在使用的 UploadForm,使其具有 FileFieldImageFieldmedia_example 视图将被更新以将上传的文件存储在模型实例中。最后,我们将在示例模板中添加一个 <img> 标签以显示之前上传的图像:

  1. 在 PyCharm 中,打开 media_example 应用程序的 models.py 文件。创建一个名为 ExampleModel 的新模型,包含两个字段:一个名为 image_fieldImageField,一个名为 file_fieldFileFieldImageField 应将其 upload_to 设置为 images/,而 FileField 应将其 upload_to 设置为 files/。完成后的模型应如下所示:

    class ExampleModel(models.Model):
        image_field = models.ImageField(upload_to="images/")
        file_field = models.FileField(upload_to="files/")
    

    你的 models.py 应该现在看起来像这样:packt.live/3p4bfrr

  2. 打开终端并导航到 media_project 项目目录。确保你的 bookr 虚拟环境是激活的。运行 makemigrations 管理命令以生成此新模型的迁移(对于 Windows,你可以在以下代码中使用 python 而不是 python3):

    python3 manage.py makemigrations
    (bookr)$ python3 manage.py makemigrations
    Migrations for 'media_example':
      media_example/migrations/0001_initial.py
        - Create model ExampleModel
    
  3. 通过运行 migrate 管理命令来应用迁移:

    python3 manage.py migrate
    

    输出如下所示:

    (bookr)$ python3 manage.py migrate
    Operations to perform:
      Apply all migrations: admin, auth, contenttypes, reviews, sessions
    Running migrations:
      # output trimmed for brevity
       Applying media_example.0001_initial... OK
    

    注意,由于我们在创建项目后没有应用这些初始 Django 迁移,所以所有这些初始迁移也将被应用。

  4. 切换回 PyCharm 并打开 reviews 应用程序的 forms.py 文件。将现有的 ImageFieldfile_upload 重命名为 image_upload。然后,添加一个新的 FileField,命名为 file_upload。在做出这些更改后,你的 UploadForm 代码应该看起来像这样:

    class UploadForm(forms.Form):
        image_upload = forms.ImageField()
        file_upload = forms.FileField()
    

    你可以保存并关闭文件。它应该看起来像这样:packt.live/37RZcaG

  5. 打开 media_example 应用程序的 views.py 文件。首先,将 ExampleModel 导入到文件中。为此,在文件顶部现有 import 语句之后添加此行:

    from .models import ExampleModel
    

    一些导入将不再需要,因此你可以删除这些行:

    import os
    from PIL import Image
    from django.conf import settings
    
  6. media_example 视图中,为将要渲染的实例设置一个默认值,以防没有创建实例。在函数定义之后,定义一个名为 instance 的变量,并将其设置为 None

    def media_example(request):
        instance = None
    
  7. 你可以完全删除 form.is_valid() 分支的内容,因为你不再需要手动保存文件。相反,当 ExampleModel 实例被保存时,它将自动保存。你将实例化一个 ExampleModel 实例,并从上传的表单中设置 fileimage 字段。

    if form.is_valid(): 行下添加此代码:

    instance = ExampleModel()
    instance.image_field = form.cleaned_data["image_upload"]
    instance.file_field = form.cleaned_data["file_upload"]
    instance.save()
    
  8. 将实例通过上下文字典传递给render。使用键instance

    return render(request, "media-example.html", \
                  {"form": form, "instance": instance})
    

    现在,您的完成后的media_example视图应该看起来像这样:packt.live/3hqyYz7

    您现在可以保存并关闭此文件。

  9. 打开media-example.html模板。添加一个<img>元素来显示最后上传的图像。在</form>标签下方,添加一个if模板标签来检查是否提供了instance。如果是,显示一个具有src属性为instance.image_field.url<img>

    {% if instance %}
        <img src="img/{{ instance.image_field.url }}">
    {% endif %}
    

    您可以保存并关闭此文件。现在它应该看起来像这样:packt.live/2X5d5w9

  10. 如果 Django 开发服务器尚未运行,请启动它,然后导航到http://127.0.0.1:8000/media-example/。您应该看到带有两个字段的表单被渲染:图 8.20:带有两个字段的 UploadForm

    图 8.20:带有两个字段的 UploadForm

  11. 为每个字段选择一个文件——对于ImageField,您必须选择一个图像,但FileField允许任何类型的文件。参见图 8.21,它显示了已选择文件的字段:图 8.21:已选择文件的 ImageField 和 FileField

    图 8.21:已选择文件的 ImageField 和 FileField

    然后,提交表单。如果提交成功,页面将重新加载,并显示您最后上传的图像(图 8.22):

    图 8.22:显示最后上传的图像

    图 8.22:显示最后上传的图像

  12. 您可以通过查看MEDIA_ROOT目录来了解 Django 如何存储文件。图 8.23显示了 PyCharm 中的目录布局:图 8.23:Django 创建的上传文件

图 8.23:Django 创建的上传文件

您可以看到 Django 已经创建了filesimages目录。这些目录就是您在模型的ImageFieldFileFieldupload_to参数中设置的。您也可以通过尝试下载它们来验证这些上传,例如,在http://127.0.0.1:8000/media/files/sample.txthttp://127.0.0.1:8000/media/images/cover.jpg

在这个练习中,我们创建了带有FileFieldImageFieldExampleModel,并看到了如何在其中存储上传的文件。我们看到了如何生成一个上传文件的 URL,以便在模板中使用。我们尝试上传了一些文件,并看到 Django 自动创建了upload_to目录(media/filesmedia/images),然后在这些目录中存储了文件。

在下一节中,我们将探讨如何通过使用ModelForm来生成表单并保存模型,从而进一步简化这个过程,而无需在视图中手动设置文件。

模型表单和文件上传

我们已经看到如何在表单上使用 form.ImageField 可以防止上传非图像。我们还看到 models.ImageField 如何使为模型存储图像变得容易。但我们需要意识到 Django 不会阻止你将非图像文件设置为 ImageField。例如,考虑一个既有 FileField 又有 ImageField 的表单:

class ExampleForm(forms.Form):
    uploaded_file = forms.FileField()
    uploaded_image = forms.ImageField()

在以下视图中,如果表单上的 uploaded_image 字段不是图像,则表单将无法验证,因此确保上传数据的数据有效性。例如:

def view(request):
    form = ExampleForm(request.POST, request.FILES)
    if form.is_valid():
        m = ExampleModel()
        m.file_field = form.cleaned_data["uploaded_file"]
        m.image_field = forms.cleaned_data["uploaded_image"]
        m.save()
    return render(request, "template.html")  

由于我们确信表单是有效的,我们知道 forms.cleaned_data["uploaded_image"] 必须包含一个图像。因此,我们永远不会将非图像分配给模型实例的 image_field

然而,如果我们代码中出了错,写成了这样:

m.image_field = forms.cleaned_data["uploaded_file"]

也就是说,如果我们不小心错误地引用了 FileField,Django 不会验证是否将(潜在的)非图像分配给了 ImageField,因此它不会抛出异常或生成任何类型的错误。我们可以通过使用 ModelForm 来减轻这种问题的可能性。

我们在 第七章高级表单验证和模型表单 中介绍了 ModelForm —— 这些表单的字段会自动从模型中定义。我们了解到 ModelForm 有一个 save 方法,它会自动在数据库中创建或更新模型数据。当与具有 FileFIeldImageField 的模型一起使用时,ModelFormsave 方法也会保存上传的文件。

这里是一个使用 ModelForm 在视图中保存新模型实例的示例。在这里,我们只是确保将 request.FILES 传递给 ModelForm 构造函数:

class ExampleModelForm(forms.Model):
    class Meta:
        model = ExampleModel
        # The same ExampleModel class we've seen previously
        fields = "__all__"
def view(request):
    if request.method == "POST":
        form = ExampleModelForm(request.POST, request.FILES)
        form.save()
        return redirect("/success-page")
    else:
        form = ExampleModelForm()
    return (request, "template.html", {"form": form})

与任何 ModelForm 一样,可以通过将 commit 参数设置为 False 来调用 save 方法。然后模型实例将不会保存到数据库中,FileField/ImageField 文件也不会保存到磁盘。应该在模型实例本身上调用 save 方法——这将提交更改到数据库并保存文件。在接下来的简短示例中,我们在保存模型实例之前给它设置了一个值:

def view(request):
    if request.method == "POST":
        form = ExampleModelForm(request.POST, request.FILES)
        m = form.save(False)
        # Set arbitrary value on the model instance before save
        m.attribute = "value"
        # save the model instance, also write the files to disk
        m.save()
        return redirect("/success-page/")
    else:
        form = ExampleModelForm()
    return (request, "template.html", {"form": form})

在模型实例上调用 save 方法既将模型数据保存到数据库,也将上传的文件保存到磁盘。在下一个练习中,我们将从我们在 练习 8.06 中创建的 ExampleModel 构建一个 ModelForm,然后使用它测试上传文件。

练习 8.07:使用 ModelForm 进行文件和图像上传

在这个练习中,你将更新 UploadForm 使其成为 ModelForm 的子类,并自动从 ExampleModel 中构建它。然后你将更改 media_example 视图以自动从表单保存实例,这样你就可以看到代码量可以减少多少:

  1. 在 PyCharm 中,打开 media_example 应用程序的 forms.py 文件。在本章中,你需要使用 ExampleModel,因此请在 from django import forms 语句之后在文件顶部 import 它。插入以下行:

    from .models import ExampleModel
    
  2. UploadForm改为forms.ModelForm的子类。移除class体,并用class Meta定义替换它;其model应该是ExampleModel。将fields属性设置为__all__。完成此步骤后,您的UploadForm应如下所示:

    class UploadForm(forms.ModelForm):
        class Meta:
            model = ExampleModel
            fields = "__all__"
    

    保存并关闭文件。现在它应该如下所示:packt.live/37X49ig

  3. 打开media_example应用的views.py文件。由于您不再需要直接引用ExampleModel,您可以从文件顶部移除它的import。移除以下行:

    from .models import ExampleModel
    
  4. media_example视图中,移除整个form.is_valid()分支,并用一行代码替换它:

    instance = form.save()
    

    表单的save方法将处理将实例持久化到数据库并保存文件。它将返回一个ExampleModel的实例,这与我们在第七章高级表单验证和模型表单中使用的其他ModelForm实例相同。

    完成此步骤后,您的media_example函数应如下所示:packt.live/37V0ly2。保存并关闭views.py

  5. 如果 Django 开发服务器尚未运行,请启动它,然后导航到http://127.0.0.1:8000/media-example/。您应该看到带有两个字段Image fieldFile field的表单渲染(图 8.24):图 8.24:在浏览器中渲染的 UploadForm 作为 ModelForm

    图 8.24:在浏览器中渲染的 UploadForm 作为 ModelForm

    注意,这些字段的名称现在与模型而不是表单匹配,因为表单只是使用模型的字段。

  6. 浏览并选择一个图像和文件(图 8.25),然后提交表单:图 8.25:选择图像和文件

    图 8.25:选择图像和文件

  7. 页面将重新加载,并且与练习 8.06模型上的 FileField 和 ImageField一样,您将看到之前上传的图像(图 8.26):图 8.26:上传后显示的图像

    图 8.26:上传后显示的图像

  8. 最后,检查media目录的内容。您应该看到目录布局与练习 8.06模型上的 FileField 和 ImageField相匹配,图像位于images目录中,文件位于files目录中:图 8.27:上传文件目录与练习 8.06 匹配

图 8.27:上传文件目录与练习 8.06 匹配

在这个练习中,我们将UploadForm改为ModelForm的子类,这使得我们能够自动生成上传字段。我们可以用对表单的save方法的调用替换存储上传文件到模型中的代码。

我们现在已经涵盖了您需要开始通过文件上传来增强 Bookr 的所有内容。在本章的活动里,我们将添加支持上传封面图片和样本文档(PDF、文本文件等)的功能。在保存之前,书的封面将使用 PIL 进行缩放。

活动 8.01:书籍的图像和 PDF 上传

在这个活动中,你将首先清理(删除)我们在本章练习中使用的示例视图、模板、表单、模型和 URL 映射。然后你需要生成并应用一个迁移来从数据库中删除 ExampleModel

然后,你可以开始添加 Bookr 增强,首先向 Book 模型添加 ImageFieldFileField 以存储书籍的 coversample。然后你将创建一个迁移并将其应用到数据库中添加这些字段。然后你可以构建一个表单,它将仅显示这些新字段。你将添加一个视图,使用此表单保存带有上传文件的模型实例,在首先将图像缩放到缩略图大小之后。你可以重用 第七章高级表单验证和模型表单 中的 instance-form.html 模板,并进行一些小的修改以允许文件上传。

这些步骤将帮助你完成活动:

  1. 更新 Django 设置以添加设置 MEDIA_ROOTMEDIA_URL

  2. 应将 /media/ URL 映射添加到 urls.py。使用 static 视图并利用 Django 设置中的 MEDIA_ROOTMEDIA_URL。记住,只有当 DEBUG 为真时,才应该添加此映射。

  3. Book 模型添加一个 ImageField(命名为 cover)和一个 FileField(命名为 sample)。这些字段应分别上传到 book_covers/book_samples/。它们都应该允许 nullblank 值。

  4. 再次运行 makemigrationsmigrate 以将 Book 模型更改应用到数据库。

  5. 创建一个 BookMediaForm 作为 ModelForm 的子类。它的模型应该是 Book,字段应该只包含你在 步骤 3 中添加的字段。

  6. 添加一个 book_media 视图。这个视图将不允许你创建 Book,而是只允许你向现有的 Book 添加媒体(因此它必须接受 pk 作为必选参数)。

  7. book_media 视图应该验证表单,并保存它,但不提交实例。上传的封面首先应该使用 写入 PIL 图像到 ImageField 部分中演示的 thumbnail 方法进行缩放。最大尺寸应为 300 x 300 像素。然后应该将其存储在实例中,并保存实例。记住,cover 字段不是必需的,所以在尝试操作图像之前你应该检查这一点。在成功的 POST 请求后,注册一条成功消息,说明 Book 已更新,然后重定向到 book_detail 视图。

  8. 渲染 instance-form.html,传递一个包含 formmodel_typeinstance 的上下文字典,就像你在 第六章表单 中做的那样。还要传递另一个项目,is_file_upload 设置为 True。这个变量将在下一步中使用。

  9. instance-form.html 模板中,使用 is_file_upload 变量向表单添加正确的 enctype 属性。这将允许你在需要时切换表单模式以启用文件上传。

  10. 最后,添加一个 URL 映射,将/books/<pk>/media/映射到book_media视图。

当你完成时,你应该能够启动 Django 开发服务器并加载book_media视图,例如,在http://127.0.0.1:8000/books/<pk>/media/,例如,http://127.0.0.1:8000/books/2/media/。你应该在浏览器中看到BookMediaForm被渲染,就像在图 8.28中所示:

图 8.28:浏览器中的 BookMediaForm

图 8.28:浏览器中的 BookMediaForm

选择一本书的封面图像和样本文件。你可以使用packt.live/2KyIapl上的图像和packt.live/37VycHn上的 PDF(或者你可以使用你选择的任何其他图像/PDF)。

图 8.29:选定的图书封面图像和样本

图 8.29:选定的图书封面图像和样本

提交表单后,你将被重定向到Book Details视图并看到成功消息(图 8.30):

图 8.30:图书详情页上的成功消息

图 8.30:图书详情页上的成功消息

如果你回到同一本书的媒体页面,你应该看到字段现在已填写,并有一个选项来清除它们中的数据:

图 8.31:带有现有值的 BookMediaForm

图 8.31:带有现有值的 BookMediaForm

活动 8.02显示封面和样本链接中,你将添加这些上传的文件到Book Details视图,但现在,如果你想检查上传是否成功,你可以查看 Bookr 项目中的media目录:

图 8.32:图书媒体

图 8.32:图书媒体

你应该看到创建的目录和上传的文件,如图 8.32所示。打开一个上传的图像,你应该看到它的最大尺寸是 300 像素。

注意

这个活动的解决方案可以在packt.live/2Nh1NTJ找到。

活动八.02:显示封面和样本链接

在这个活动中,你将更新book_detail.html模板以显示Book的封面(如果已设置)。你还将添加一个下载样本的链接,同样,只有当设置了链接时。你将使用FileFieldImageFieldurl属性来生成媒体文件的 URL。

这些步骤将帮助你完成这个活动:

  1. book_detail.html视图中的Book Details显示中,如果书有一个cover图像,添加一个<img>元素。然后,在它里面显示书的封面。在<img>标签后使用<br>,以便图像单独一行。

  2. 出版日期显示之后,添加一个指向样本文件的链接。只有当上传了sample文件时,它才应该被显示。确保你添加另一个<br>标签,以便正确显示。

  3. 在有一个添加评论链接的章节中,添加另一个链接,链接到书的媒体页面。遵循与添加评论链接相同的样式。

当你完成这些步骤后,你应该能够加载一个书籍详情页。如果书籍没有封面样本,那么页面应该看起来与之前非常相似,除了你应该在底部看到新的媒体页面链接(图 8.33):

图 8.33:在书籍详情页上可见的新媒体按钮

图 8.33:在书籍详情页上可见的新媒体按钮

一旦你为Book上传了封面和/或样本,封面图像和样本链接应该会显示出来(图 8.34):

图 8.34:显示的书籍封面和示例链接

图 8.34:显示的书籍封面和示例链接

注意

这个活动的解决方案可以在packt.live/2Nh1NTJ找到。

摘要

在本章中,我们添加了MEDIA_ROOTMEDIA_URL设置以及一个特殊的 URL 映射来服务媒体文件。然后我们创建了一个表单和一个视图来上传文件并将它们保存到media目录中。我们看到了如何添加媒体上下文处理器以自动在所有模板中访问MEDIA_URL设置。然后,我们通过使用带有FileFieldImageField的 Django 表单来增强和简化我们的表单代码,而不是在 HTML 中手动定义一个。

我们探讨了 Django 为ImageField提供的图像增强功能,以及如何使用 Pillow 与图像交互。我们展示了一个示例视图,该视图能够使用FileResponse类提供需要认证的文件。然后,我们看到了如何使用FileFieldImageField在模型上存储文件,并在模板中使用FileField.url属性引用它们。我们通过自动从model实例构建ModelForm来减少必须编写的代码量。最后,在最后的两个活动中,我们通过向Book模型添加封面图像和示例文件来增强 Bookr。在第九章会话和认证中,我们将学习如何向 Django 应用程序添加认证以保护它免受未经授权的用户。

第九章:9. 会话和认证

概述

本章首先简要介绍中间件,然后深入探讨认证模型会话引擎的概念。您将实现 Django 的认证模型以限制权限仅限于特定用户集。然后,您将了解如何利用 Django 认证提供灵活的应用安全方法。之后,您将学习 Django 如何支持多个会话引擎以保留用户数据。到本章结束时,您将熟练使用会话来保留过去用户交互的信息,并在页面被重新访问时维护用户偏好。

简介

到目前为止,我们使用 Django 开发允许用户与应用模型交互的动态应用,但我们尚未尝试保护这些应用免受未经授权的使用。例如,我们的 Bookr 应用允许未经认证的用户添加评论和上传媒体。这对任何在线 Web 应用都是一个关键的安全问题,因为它使网站容易受到垃圾邮件或其他不适当内容的发布以及现有内容的破坏。我们希望内容的创建和修改严格限于已在网站上注册的认证用户。

认证应用为 Django 提供了表示用户、组和权限的模型。它还提供了中间件、实用函数、装饰器和混入,这些可以帮助将用户认证集成到我们的应用中。此外,认证应用允许对某些用户集进行分组和命名。

第四章Django Admin 简介中,我们使用 Admin 应用创建了一个帮助台用户组,具有“可以查看日志条目”、“可以查看权限”、“可以更改用户”和“可以查看用户”的权限。这些权限可以通过它们对应的名称引用:view_logentryview_permissionschange_userview_user。在本章中,我们将学习如何根据特定的用户权限自定义 Django 行为。

权限是定义用户类别可以做什么的指令。权限可以分配给组或直接分配给单个用户。从管理角度来看,将权限分配给组更清晰。组使得建模角色和组织结构更容易。如果创建了一个新的权限,修改几个组比记住将其分配给用户子集要节省时间。

我们已经熟悉了使用多种方法创建用户和组以及分配权限,例如通过脚本使用模型实例化用户和组,以及通过 Django Admin 应用创建它们的便利性。认证应用还提供了创建和删除用户、组和权限以及分配它们之间关系的编程方式。

随着我们进入本章,我们将学习如何使用身份验证和权限来实现应用安全,以及如何存储特定于用户的数据以定制用户的体验。这将帮助我们保护 bookr 项目免受未经授权的内容更改,并使其对不同类型的用户具有上下文相关性。在我们考虑将其部署到互联网之前,向 bookr 项目添加这种基本安全措施至关重要。

身份验证以及会话管理(我们将在 会话 部分中学习),由一个名为 bookr 项目的组件处理,让我们了解一下这个中介栈及其模块。

中介模块

第三章URL 映射、视图和模板 中,我们讨论了 Django 对请求/响应过程的实现,以及其视图和渲染功能。除了这些之外,另一个在 Django 核心网络处理中起着极其重要作用的特性是 中介。Django 的中介指的是各种软件组件,它们介入这个请求/响应过程以集成重要的功能,如安全、会话管理和身份验证。

因此,当我们用 Django 编写视图时,我们不需要在响应头中显式设置一系列重要的安全功能。这些添加到响应对象中的功能是由 SecurityMiddleware 实例在视图返回其响应后自动完成的。由于中介组件封装视图并在请求上执行一系列预处理以及在响应上执行一系列后处理,因此视图不会充斥着大量重复的代码,我们可以专注于编写应用逻辑而不是担心低级服务器行为。而不是将这些功能构建到 Django 核心中,Django 的中介栈实现允许这些组件既可选又可替换。

中介模块

当我们运行 startproject 子命令时,默认的中介模块列表会被添加到 <project>/settings.py 文件中的 MIDDLEWARE 变量中,如下所示:

MIDDLEWARE = ['django.middleware.security.SecurityMiddleware',\
              'django.contrib.sessions.middleware.SessionMiddleware',\
              'django.middleware.common.CommonMiddleware',\
              'django.middleware.csrf.CsrfViewMiddleware',\
              'django.contrib.auth.middleware.AuthenticationMiddleware',\
              'django.contrib.messages.middleware.MessageMiddleware',\
              'django.middleware.clickjacking.XFrameOptionsMiddleware',\]

这是一个适合大多数 Django 应用的最小化中介栈。以下列表详细说明了每个模块的通用目的:

  • SecurityMiddleware 提供了常见的安全增强功能,例如处理 SSL 重定向和添加响应头以防止常见的攻击。

  • SessionMiddleware 启用会话支持,并无缝地将存储的会话与当前请求关联起来。

  • CommonMiddleware 实现了许多杂项功能,例如拒绝来自 DISALLOWED_USER_AGENTS 列表的请求,实现 URL 重写规则,并设置 Content-Length 头。

  • CsrfViewMiddleware 添加了对 跨站请求伪造CSRF)的保护。

  • AuthenticationMiddlewareuser 属性添加到 request 对象中。

  • MessageMiddleware 添加了 "闪存" 消息支持。

  • XFrameOptionsMiddleware可以防止X-Frame-Options头部点击劫持攻击。

中间件模块按照它们在MIDDLEWARE列表中出现的顺序加载。这样做是有道理的,因为我们希望首先调用处理初始安全问题的中间件,以便在进一步处理之前拒绝危险请求。Django 还附带了一些执行重要功能的其他中间件模块,例如使用gzip文件压缩、重定向配置和网页缓存配置。

本章致力于讨论作为中间件组件实现的状态感知应用程序开发的两个重要方面——SessionMiddlewareAuthenticationMiddleware

SessionMiddlewareprocess_request方法将一个session对象作为request对象的属性添加。AuthenticationMiddlewareprocess_request方法将一个user对象作为request对象的属性添加。

如果项目不需要用户认证或保存单个交互状态的方法,则可以编写不包含这些中间件堆栈层的 Django 项目。然而,大多数默认中间件在应用程序安全方面都发挥着重要作用。如果没有充分的理由更改中间件组件,最好是保持这些初始设置。实际上,Admin 应用需要SessionMiddlewareAuthenticationMiddlewareMessageMiddleware才能运行,如果 Admin 应用安装了这些中间件,Django 服务器将抛出如下错误:

django.core.management.base.SystemCheckError: SystemCheckError: System check identified some issues:
ERRORS:
?: (admin.E408) 'django.contrib.auth.middleware.AuthenticationMiddleware' must be in MIDDLEWARE in order to use the admin application.
?: (admin.E409) 'django.contrib.messages.middleware.MessageMiddleware' must be in MIDDLEWARE in order to use the admin application.
?: (admin.E410) 'django.contrib.sessions.middleware.SessionMiddleware' must be in MIDDLEWARE in order to use the admin application.

现在我们已经了解了中间件模块,让我们看看一种使用认证应用视图和模板在我们的项目中启用认证的方法。

实现认证视图和模板

我们已经在第四章,Django Admin 简介中遇到了登录表单。这是访问 Admin 应用的员工用户的认证入口点。我们还需要为想要撰写书评的普通用户创建登录功能。幸运的是,认证应用提供了实现这一功能的工具。

在我们处理认证应用的表单和视图的过程中,我们遇到了其在实现上的很多灵活性。我们可以自由地实现自己的登录页面,在视图级别定义非常简单或细粒度的安全策略,并对外部权威机构进行认证。

认证应用旨在适应许多不同的认证方法,以便 Django 不会严格强制执行单一机制。对于第一次遇到文档的用户来说,这可能会相当令人困惑。在本章的大部分内容中,我们将遵循 Django 的默认设置,但会注意一些重要的配置选项。

Django 项目的settings对象包含登录行为的属性。LOGIN_URL指定登录页面的 URL。默认值是'/accounts/login/'LOGIN_REDIRECT_URL指定成功登录后重定向的路径。默认路径是'/accounts/profile/'

认证应用提供执行典型认证任务的标准表单和视图。表单位于django.contrib.auth.forms,视图位于django.contrib.auth.views

视图通过django.contrib.auth.urls中存在的这些 URL 模式进行引用:

urlpatterns = [path('login/', views.LoginView.as_view(), \
                    name='login'),
               path('logout/', views.LogoutView.as_view(), \
                    name='logout'),
               path('password_change/', \
                    views.PasswordChangeView.as_view()),\
                    (name='password_change'),\
               path('password_change/done/', \
                    views.PasswordChangeDoneView.as_view()),\
                    (name='password_change_done'),\
               path('password_reset/', \
                    views.PasswordResetView.as_view()),\
                    (name='password_reset'),\
               path('password_reset/done/', \
                    views.PasswordResetDoneView.as_view()),\
                    (name='password_reset_done'),\
               path('reset/<uidb64>/<token>/', \
                    views.PasswordResetConfirmView.as_view()),\
                    (name='password_reset_confirm'),\
               path('reset/done/', \
                    views.PasswordResetCompleteView.as_view()),\
                    (name='password_reset_complete'),]

如果这种视图风格看起来不熟悉,那是因为它们是基于类的视图,而不是我们之前遇到过的基于函数的视图。我们将在第十一章高级模板和基于类的视图中了解更多关于基于类的视图。现在,请注意,认证应用利用类继承来分组视图的功能,以避免大量的重复编码。

如果我们想保持认证应用和 Django 设置预设的默认 URL 和视图,我们可以在项目的urlpatterns中包含认证应用的 URL。

通过采取这种方法,我们节省了大量工作。我们只需要将认证应用的 URL 包含到我们的<project>/urls.py文件中,并分配给它'accounts'命名空间。指定这个命名空间确保我们的反向 URL 与视图的默认模板值相对应:

urlpatterns = [path('accounts/', \
                    include(('django.contrib.auth.urls', 'auth')),\
                    (namespace='accounts')),\
               path('admin/', admin.site.urls),\
               path('', include('reviews.urls'))]

虽然认证应用自带了其自己的表单和视图,但它缺少将这些组件渲染为 HTML 所需的模板。图 9.1列出了我们为实现项目中认证功能所需的模板。幸运的是,管理应用实现了一套模板,我们可以用于我们的目的。

我们可以直接从 Django 源代码的django/contrib/admin/templates/registration目录和django/contrib/admin/templates/admin/login.html复制模板文件到我们的项目templates/registration目录。

注意

当我们说 Django 源代码时,指的是你的 Django 安装所在的目录。如果你在虚拟环境中安装了 Django(如前言中详细说明),你可以找到这些模板文件在以下路径:<你的虚拟环境名称>/lib/python3.X/site-packages/django/contrib/admin/templates/registration/。如果你的虚拟环境已激活且 Django 已安装在其中,你还可以通过在终端运行以下命令来检索site-packages目录的完整路径:python -c "import sys; print(sys.path)"

![图 9.1:认证模板的默认路径图片

图 9.1:认证模板的默认路径

注意

我们只需要复制视图的依赖模板,并应避免复制base.htmlbase_site.html文件。

这一开始给出了一个有希望的结果,但正如我们所看到的,管理员模板并不完全符合我们的精确需求,正如登录页面(图 9.2)所示:

![图 9.2:用户登录屏幕的第一次尝试

![img/B15509_09_02.jpg]

图 9.2:用户登录屏幕的第一次尝试

由于这些认证页面继承自 Admin 应用的admin/base_site.html模板,它们遵循 Admin 应用的风格。我们更希望这些页面遵循我们开发的bookr项目的风格。我们可以通过在每个从 Admin 应用复制到我们项目的 Django 模板上遵循以下三个步骤来实现这一点:

  1. 需要做的第一个更改是将{% extends "admin/base_site.html" %}标签替换为{% extends "base.html" %}

  2. 由于template/base.html仅包含以下块定义——titlebrandcontent——因此我们应该从bookr文件夹中的模板中移除所有其他块替换。在我们的应用中,我们没有使用userlinksbreadcrumbs块的内容,因此这些块可以被完全移除。

    其中一些块,如content_titlereset_link,包含与我们的应用相关的 HTML 内容。我们应该从这些 HTML 内容周围移除块,并将其放入内容块中。

    例如,password_change_done.html模板包含大量的块:

    {% extends "admin/base_site.html" %}
    {% load i18n %}
    {% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}  {% if docsroot %}<a href="{{ docsroot }}">{% trans 'Documentation' %}    </a> / {% endif %}{% trans 'Change password' %} / <a href="{% url       'admin:logout' %}">{% trans 'Log out' %}</a>{% endblock %}
    {% block breadcrumbs %}
    <div class="breadcrumbs">
    <a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
    &rsaquo; {% trans 'Password change' %}
    </div>
    {% endblock %}
    {% block title %}{{ title }}{% endblock %}
    {% block content_title %}<h1>{{ title }}</h1>{% endblock %}
    {% block content %}
    <p>{% trans 'Your password was changed.' %}</p>
    {% endblock %}
    

    bookr项目中,它将被简化为以下模板:

    {% extends "base.html" %}
    {% load i18n %}
    {% block title %}{{ title }}{% endblock %}
    {% block content %}
    <h1>{{ title }}</h1>
    <p>{% trans 'Your password was changed.' %}</p>
    {% endblock %}
    
  3. 同样,需要更改反向 URL 模式以反映当前路径,因此{% url 'login' %}被替换为{% url 'accounts:login' %}

考虑到这些因素,下一个练习将专注于将 Admin 应用的登录模板转换为bookr项目的登录模板。

注意

i18n模块用于创建多语言内容。如果您打算为您的网站开发多语言内容,请在模板中保留i18n导入、trans标签和transblock语句。为了简洁,我们将在本章中不详细讨论这些内容。

练习 9.01:重新利用 Admin 应用登录模板

我们在项目中没有登录页面的情况下开始了这一章。通过添加认证的 URL 模式并从 Admin 应用复制模板到我们的项目,我们可以实现登录页面的功能。但是,这个登录页面并不令人满意,因为它直接从 Admin 应用复制而来,并且与 Bookr 设计不匹配。在这个练习中,我们将遵循将 Admin 应用的登录模板重新用于我们项目的步骤。新的登录模板需要直接从bookr项目的templates/base.html继承其样式和格式:

  1. 在您的项目中创建一个名为templates/registration的目录。

  2. 管理员登录模板位于 Django 源目录的django/contrib/admin/templates/admin/login.html路径。它以一个extends标签、一个load标签、导入i18nstatic模块以及一系列覆盖子模板django/contrib/admin/templates/admin/base.html中定义的块的块扩展开始。以下是一个login.html文件的截断片段:

    {% extends "admin/base_site.html" %}
    {% load i18n static %}
    {% block extrastyle %}{{ block.super }}…
    {% endblock %}
    {% block bodyclass %}{{ block.super }} login{% endblock %}
    {% block usertools %}{% endblock %}
    {% block nav-global %}{% endblock %}
    {% block content_title %}{% endblock %}
    {% block breadcrumbs %}{% endblock %}
    
  3. 将此管理员登录模板django/contrib/admin/templates/admin/login.html复制到templates/registration,然后使用 PyCharm 开始编辑文件。

  4. 由于您正在编辑的登录模板位于templates/registration/login.html,并且扩展了基本模板(templates/base.html),因此请替换templates/registration/login.html顶部extends标签的参数:

    {% extends "base.html" %}
    
  5. 我们不需要这个文件的大部分内容。只需保留包含登录表单的content块。模板的其余部分将包括加载i18nstatic标签库:

    {% load i18n static %}
    {% block content %}
    …
    {% endblock %}
    
  6. 现在您必须将templates/registration/login.html中的路径和反向 URL 模式替换为适合您项目的模式。由于您在模板中没有定义app_path变量,它需要被替换为登录的反向 URL,即'accounts:login'。因此,请考虑以下行:

    <form action="{{ app_path }}" method="post" id="login-form">
    

    这一行将按以下方式更改:

    <form action="{% url 'accounts:login' %}" method="post" id="login-form">
    

    在您的项目路径中没有定义'admin_password_reset',因此它将被替换为'accounts:password_reset'

    考虑以下行:

    {% url 'admin_password_reset' as password_reset_url %}
    

    这一行将按以下方式更改:

    {% url 'accounts:password_reset' as password_reset_url %}
    

    您的登录模板将如下所示:

    templates/registration/login.html
    1  {% extends "base.html" %}
    2  {% load i18n static %}
    3
    4  {% block content %}
    5  {% if form.errors and not form.non_field_errors %}
    6  <p class="errornote">
    7  {% if form.errors.items|length == 1 %}{% trans "Please correct the error     below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
    8  </p>
    9  {% endif %}
    You can find the complete code for this file at http://packt.live/2MILJtF.
    
  7. 要使用标准的 Django 认证视图,我们必须将 URL 映射添加到它们中。打开bookr项目目录中的urls.py文件,然后添加以下 URL 模式:

    urlpatterns = [path('accounts/', \
                        include(('django.contrib.auth.urls', 'auth')),\
                        (namespace='accounts')),\
                   path('admin/', admin.site.urls),\
                   path('', include('reviews.urls'))]
    
  8. 现在当您访问http://127.0.0.1:8000/accounts/login/的登录链接时,您将看到这个页面:![图 9.3:Bookr 登录页面 图 9.3:Bookr 登录页面

图 9.3:Bookr 登录页面

通过完成这个练习,您已经创建了项目所需的非管理员认证模板。

注意

在您继续之前,您需要确保registration目录中的其余模板遵循bookr项目的风格;也就是说,它们继承自管理员应用的admin/base_site.html模板。您已经看到了password_change_done.htmllogin.html模板是如何做到这一点的。现在将您在这个练习(以及之前的章节)中学到的知识应用到registration目录中的其余文件上。或者,您也可以从 GitHub 仓库下载修改后的文件:packt.live/3s4R5iU

Django 中的密码存储

Django 不会以纯文本形式在数据库中存储密码。相反,密码会与哈希算法(如PBKDF2/SHA256BCrypt/SHA256Argon2**)进行散列。由于哈希算法是一种单向转换,这可以防止从数据库中存储的哈希中解密用户的密码。这对于期望系统管理员检索他们忘记的密码的用户来说可能是个惊喜,但在安全设计中这是最佳实践。因此,如果我们查询数据库以获取密码,我们将看到如下内容:

sqlite> select password from auth_user;pbkdf2_sha256$180000$qgDCHSUv1E4w$jnh69TEIO6kypHMQPOknkNWMlE1e2ux8Q1Ow4AHjJDU=

此字符串的组成部分是<algorithm>$<iterations>$<salt>$<hash>。由于随着时间的推移,几个哈希算法已被破坏,并且我们有时需要与强制性的安全要求一起工作,Django 足够灵活,可以适应新的算法,并可以维护使用多种算法加密的数据。

个人资料页面和 request.user 对象

当登录成功时,登录视图将重定向到/accounts/profile。然而,此路径未包含在现有的auth.url中,认证应用程序也没有提供相应的模板。为了避免“页面未找到”错误,需要一个视图和适当的 URL 模式。

每个 Django 请求都有一个request.user对象。如果请求是由未经认证的用户发起的,则request.user将是一个AnonymousUser对象。如果请求是由认证用户发起的,则request.user将是一个User对象。这使得在 Django 视图中检索个性化用户信息并在模板中渲染它变得容易。

在下一个练习中,我们将向我们的bookr项目添加一个个人资料页面。

练习 9.02:添加个人资料页面

在这个练习中,我们将向我们的项目添加一个个人资料页面。为此,我们需要在 URL 模式中包含其路径,并在我们的视图和模板中包含它。个人资料页面将简单地显示request.user对象中的以下属性:

  • username

  • first_namelast_name

  • date_joined

  • email

  • last_login

执行以下步骤以完成此练习:

  1. bookr/views.py添加到项目中。它需要一个简单的个人资料函数来定义我们的视图:

    from django.shortcuts import render
    def profile(request):
        return render(request, 'profile.html')
    
  2. 在主bookr项目的模板文件夹中,创建一个名为profile.html的新文件。在这个模板中,可以使用类似{{ request.user.username }}的表示法轻松引用request.user对象的属性:

    {% extends "base.html" %}
    {% block title %}Bookr{% endblock %}
    {% block content %}
    <h2>Profile</h2>
    <div>
      <p>
          Username: {{ request.user.username }} <br>
          Name: {{ request.user.first_name }} {{ request.user.last_name }}<br>
          Date Joined: {{ request.user.date_joined }} <br>
          Email: {{ request.user.email }}<br>
          Last Login: {{ request.user.last_login }}<br>
      </p>
    </div>
    {% endblock %}
    

    此外,我们还添加了一个包含用户个人资料详情的块。更重要的是,我们确保profile.html扩展了base.html

  3. 最后,需要将此路径添加到bookr/urls.py中的urlpatterns列表顶部。首先,导入新的视图,然后添加一个将 URL accounts/profile/链接到bookr.views.profile的路径:

    from bookr.views import profile
    urlpatterns = path('accounts/', \
                        include(('django.contrib.auth.urls', 'auth')),\
                       (namespace='accounts')),\
                   http://localhost:8000/accounts/profile/, it is rendered as shown in the screenshot in *Figure 9.4*. Remember, if the server needs to be started, use the python manage.py runserver command:![Figure 9:4: Alice visits her user profile        ```图 9.4:爱丽丝访问她的用户资料我们已经看到了如何将用户重定向到他们的个人资料页面,一旦他们成功登录。现在让我们讨论如何仅向特定用户提供内容访问。## 认证装饰器和重定向现在我们已经学会了如何允许普通用户登录到我们的项目,我们可以发现如何将内容限制为认证用户。认证模块附带了一些有用的装饰器,可以根据当前用户的认证或访问权限来保护视图。不幸的是,如果比如说用户 Alice 要从 Bookr 注销,资料页面仍然会渲染并显示空详情。为了避免这种情况,我们更希望任何未经认证的访客被引导到登录界面:![图 9.5:一个未认证的用户访问用户资料](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/webdev-dj-2e/img/B15509_09_05.jpg)
    
    

图 9.5:一个未认证的用户访问用户资料

认证应用附带了一些有用的装饰器,可以用于向 Django 视图添加认证行为。在这种情况下,为了保护我们的资料视图,我们可以使用login_required装饰器:

from django.contrib.auth.decorators import login_required
@login_required
def profile(request):
    …

现在,如果一个未认证的用户访问/accounts/profile URL,他们将被重定向到http://localhost:8000/accounts/login/?next=/accounts/profile/

这个 URL 会将用户带到登录 URL。GET变量中的next参数告诉登录视图在登录成功后重定向到哪里。默认行为是重定向回当前视图,但可以通过指定login_required装饰器的login_url参数来覆盖这个行为。例如,如果我们需要登录后重定向到不同的页面,我们可以在装饰器调用中明确指出,如下所示:

@login_required(login_url='/accounts/profile2')

如果我们重写了登录视图,使其期望重定向 URL 在'next'参数之外的其他 URL 参数中指定,我们可以在装饰器调用中使用redirect_field_name参数来明确这一点:

@login_required(redirect_field_argument='redirect_to')

经常会有这样的情况,一个 URL 应该限制给满足特定条件的用户或组。考虑这样一个情况,我们有一个页面供员工用户查看任何用户资料。我们不希望这个 URL 对所有用户都可用,因此我们希望将这个 URL 限制为具有'view_user'权限的用户或组,并将未经授权的请求转发到登录 URL:

from django.contrib.auth.decorators \
import login_required, permission_required
…
@permission_required('view_group')
def user_profile(request, uid):
    user = get_object_or_404(User, id=uid)
    permissions = user.get_all_permissions()
    return render(request, 'user_profile.html',\
                  {'user': user, 'permissions': permissions}

因此,在我们的user_profile视图上应用了这个装饰器后,访问http://localhost:8000/accounts/users/123/profile/的未经授权的用户将被重定向到http://localhost:8000/accounts/login/?next=/accounts/users/123/profile/

然而,有时我们需要构建更微妙的条件权限,这些权限不适用于这两个导演。为此,Django 提供了一个自定义装饰器,它接受一个任意函数作为参数。user_passes_test装饰器需要一个test_func参数:

user_passes_test(test_func, login_url=None, redirect_field_name='next')

这里有一个例子,我们有一个视图veteran_features,这个视图只对在网站上注册超过一年的用户可用:

from django.contrib.auth.decorators import (login_required),\
                                           (permission_required),\
                                           (user_passes_test)
…
def veteran_user(user):
    now = datetime.datetime.now()
    if user.date_joined is None:
        return False
    return now - user.date_joined > datetime.timedelta(days=365)
@user_passes_test(veteran_user)
def veteran_features(request):
    user = request.user
    permissions = user.get_all_permissions()
    return render(request, 'veteran_profile.html',\
                  {'user': user, 'permissions': permissions}

有时候,我们视图中的逻辑无法用这些装饰器处理,我们需要在视图的控制流中应用重定向。我们可以使用redirect_to_login辅助函数来完成此操作。它接受与装饰器相同的参数,如下面的代码片段所示:

redirect_to_login(next, login_url=None, redirect_field_name='next')

练习 9.03:向视图添加认证装饰器

在了解了认证应用的权限和认证装饰器的灵活性之后,我们现在将着手在“评论”应用中使用它们。我们需要确保只有经过认证的用户可以编辑评论,只有工作人员用户可以编辑出版商。有几种实现方式,因此我们将尝试几种方法。这些步骤中的所有代码都在reviews/views.py文件中:

  1. 解决这个问题的第一直觉可能是认为publisher_edit方法需要一个适当的装饰器来强制用户具有edit_publisher权限。为此,你可以轻松地做如下操作:

    from django.contrib.auth.decorators import permission_required 
    …
    @permission_required('edit_publisher')
    def publisher_edit(request, pk=None):
        …
    
  2. 使用这种方法是可以的,这是向视图添加权限检查的一种方法。您还可以使用一种稍微复杂但更灵活的方法。不是使用权限装饰器来强制publisher_edit方法的权限,而是创建一个需要工作人员用户的测试函数,并使用user_passes_test装饰器将此测试函数应用于publisher_edit。编写测试函数允许在验证用户访问权或权限方面进行更多定制。如果您在步骤 1中对views.py文件进行了更改,请随意取消注释(或删除)装饰器,并编写以下测试函数代替:

    from django.contrib.auth.decorators import user_passes_test
    …
    def is_staff_user(user):
        return user.is_staff
    @user_passes_test(is_staff_user)
        …
    
  3. 通过添加适当的装饰器来确保review_editbook_media函数需要登录:

    …
    from django.contrib.auth.decorators import login_required, \
                                               user_passes_test
    …
    @login_required
    def review_edit(request, book_pk, review_pk=None):
    @login_required
    def book_media(request, pk):
    …
    
  4. review_edit方法中,向视图中添加逻辑,要求用户必须是工作人员用户或评论的所有者。review_edit视图控制评论创建和评论更新的行为。我们正在开发的约束仅适用于正在更新现有评论的情况。因此,添加代码的位置是在成功检索到Review对象之后。如果用户不是工作人员账户或评论的创建者与当前用户不匹配,我们需要抛出一个PermissionDenied错误:

    …
    from django.core.exceptions import PermissionDenied
    from PIL import Image
    from django.contrib import messages
    …
    @login_required
    def review_edit(request, book_pk, review_pk=None):
        book = get_object_or_404(Book, pk=book_pk)
        if review_pk is not None:
            review = get_object_or_404(Review),\
                                     (book_id=book_pk),\
                                     (pk=review_pk)
            user = request.user
            if not user.is_staff and review.creator.id != user.id:
                raise PermissionDenied
        else:
            review = None
    …
    

    现在,当非工作人员用户尝试编辑其他用户的评论时,将会抛出一个Forbidden错误,如图 9.6所示。在下一节中,我们将探讨在模板中应用条件逻辑,以便用户不会被带到他们没有足够权限访问的页面:

    ![图 9.6:非工作人员用户无法访问]

    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/webdev-dj-2e/img/B15509_09_06.jpg)

图 9.6:非工作人员用户无法访问

在这个练习中,我们使用了认证装饰器来保护 Django 应用中的视图。所应用的认证装饰器提供了一个简单的机制来限制缺乏必要权限的用户、非工作人员用户和未认证用户的视图。Django 的认证装饰器提供了一个遵循 Django 角色和权限框架的强大机制,而 user_passes_test 装饰器提供了一个开发自定义认证的选项。

使用认证数据增强模板

练习 9.02添加个人资料页面 中,我们了解到我们可以将 request.user 对象传递到模板中,以在 HTML 中渲染当前用户的属性。我们还可以根据用户的类型或权限采取不同的模板渲染方法。假设我们想要添加一个仅对工作人员用户显示的编辑链接。我们可能使用 if 条件来实现这一点:

{% if user.is_staff %}
  <p><a href="{% url 'review:edit' %}">Edit this Review</a></p>
{% endif %}

如果我们没有花时间根据权限条件渲染链接,用户在导航应用程序时会感到沮丧,因为他们点击的许多链接都会导致 403 禁止访问 页面。接下来的练习将展示我们如何使用模板和认证在我们的项目中呈现上下文相关的链接。

练习 9.04:在基础模板中切换登录和登出链接

bookr 项目的基模板中,位于 templates/base.html,我们在页眉中有一个占位符登出链接。它用以下 HTML 编码:

<li class="nav-item">
  <a class="nav-link" href="#">Logout</a>
</li>

我们不希望在用户登出后显示登出链接。因此,这个练习的目的是在模板中应用条件逻辑,以便根据用户是否认证来切换 登录登出 链接:

  1. 编辑 templates/base.html 文件。复制 登出 列表元素的架构,创建一个 登录 列表元素。然后,将占位符链接替换为 登出登录 页面的正确 URL - 分别为 /accounts/logout/accounts/login - 如下所示:

    <li class="nav-item">
      <a class="nav-link" href="/accounts/logout">Logout</a>
    </li>
    <li class="nav-item">
      <a class="nav-link" href="/accounts/login">Login</a>
    </li>
    
  2. 现在将我们的两个 li 元素放入一个 if … else … endif 条件块中。我们正在应用的逻辑条件是 if user.is_authenticated

    {% if user.is_authenticated %}
      <li class="nav-item">
        <a class="nav-link" href="/accounts/logout">Logout</a>
      </li>
        {% else %}
      <li class="nav-item">
        <a class="nav-link" href="/accounts/login">Login</a>
      </li>
    {% endif %}
    
  3. 现在访问用户个人资料页面,网址为 http://localhost:8000/accounts/profile/。当您登录后,您将看到 登出 链接:图 9.7:已认证用户看到的登出链接

    图 9.7:已认证用户看到的登出链接

  4. 现在点击 登出 链接;您将被带到 /accounts/logout 页面。登录 链接出现在菜单中,确认该链接是依赖于用户认证状态的:图 9.8:未认证用户看到的登录链接

图 9.8:未认证用户看到的登录链接

这个练习是一个简单的例子,说明了 Django 模板如何与认证信息一起使用,以创建有状态和上下文化的用户体验。我们也不希望提供用户无权访问的链接或用户权限级别不允许的操作。以下活动将使用这种模板技术来解决 Bookr 中的一些问题。

活动九.01:使用模板中的条件块进行基于认证的内容

在这个活动中,你将在模板中应用条件块,根据用户认证和用户状态修改内容。不应向用户提供他们无权访问的链接或他们无权执行的操作。以下步骤将帮助你完成这个活动:

  1. book_detail模板中,在reviews/templates/reviews/book_detail.html文件中,隐藏非认证用户的“添加评论”和“媒体”按钮。

  2. 此外,隐藏标题“成为第一个写评论的人”,因为这不是非认证用户的选项。

  3. 在相同的模板中,使“编辑评论”链接仅对工作人员或撰写评论的用户显示。模板块的条件逻辑与我们在上一节中使用的review_edit视图中的条件逻辑非常相似:图 9.9:当爱丽丝登录时,爱丽丝的评论中会出现“编辑评论”链接

    图 9.9:当爱丽丝登录时,爱丽丝的评论中会出现“编辑评论”链接

    图 9.10:当鲍勃登录时,爱丽丝的评论中没有“编辑评论”链接

    图 9.10:当鲍勃登录时,爱丽丝的评论中没有“编辑评论”链接

  4. 修改template/base.html,使其在页眉中搜索表单的右侧显示当前已认证用户的用户名,并链接到用户个人资料页面。

    通过完成这个活动,你将在模板中添加反映当前用户认证状态和身份的动态内容,如下面的截图所示:

    图 9.11:搜索表单之后显示已认证用户的名称

图 9.11:搜索表单之后显示已认证用户的名称

注意

这个活动的解决方案可以在packt.live/2Nh1NTJ找到。

会话

值得研究一些理论来了解为什么会话是 Web 应用程序中管理用户内容的一种常见解决方案。HTTP 协议定义了客户端和服务器之间的交互。它被称为“无状态”协议,因为服务器在请求之间不保留任何有状态信息。这种协议设计在万维网早期很好地用于传递超文本信息,但它不适合需要向特定用户交付定制信息的受保护 Web 应用程序的需求。

我们现在已经习惯了看到网站适应我们的个人浏览习惯。购物网站推荐与我们最近浏览过的类似产品,并告诉我们在我们地区受欢迎的产品。所有这些功能都需要一种有状态的网站开发方法。实现有状态网络体验最常见的方法之一是通过会话。会话指的是用户与网络服务器或应用的当前交互,并要求在交互期间持续存储数据。这可能包括用户访问的链接、他们执行的操作以及他们在交互中做出的偏好。

如果用户在一页上将博客网站的配色方案设置为深色主题,那么人们会期待下一页也会使用相同的主题。我们将这种行为描述为“保持状态”。会话密钥存储在客户端作为浏览器 cookie,并且可以通过在用户登录期间持续存在的服务器端信息进行识别。

在 Django 中,会话被实现为一种中间件形式。当我们最初在第四章Django Admin 简介中创建应用时,会话支持默认激活。

会话引擎

需要将当前和已过期的会话信息存储在某个地方。在万维网早期,这是通过在服务器上保存会话信息到文件中实现的,但随着网络服务器架构变得更加复杂以及性能需求的增加,其他更高效的策略,如数据库或内存存储,已成为标准。默认情况下,在 Django 中,会话信息存储在项目数据库中。

这对于大多数小型项目来说是一个合理的默认设置。然而,Django 的会话中间件实现为我们提供了灵活性,可以根据我们的系统架构和性能需求以多种方式存储我们的项目会话信息。这些不同的实现方式被称为会话引擎。如果我们想更改会话配置,我们需要在项目的settings.py文件中指定SESSION_ENGINE设置:

  • 用于此目的的django.contrib.sessions.backends.cachedjango.contrib.sessions.backends.cached_db会话引擎。

  • 基于文件的会话:如前所述,这是一种相对过时的维护会话信息的方式,但可能适合一些性能不是问题且没有理由在数据库中存储动态信息的网站。

  • 基于 cookie 的会话:而不是在服务器端保持会话信息,你可以通过将会话内容序列化为 JSON 并存储在基于浏览器的 cookie 中,将它们完全保存在网络浏览器客户端。

Django 的所有会话实现都需要在用户的网络浏览器中存储一个会话 ID 到 cookie 中。

不论使用哪种会话引擎,所有这些中间件实现都涉及在 Web 浏览器中存储特定于网站的 cookie。在 Web 开发的早期,将会话 ID 作为 URL 参数传递并不罕见,但出于安全考虑,Django 已经放弃了这种方法。

在许多司法管辖区,包括欧盟,网站在用户浏览器中设置 cookie 时,在法律上必须警告用户。如果您打算在某个地区运营网站,并且该地区有此类立法要求,您有责任确保代码符合这些义务。请确保使用最新的实现,并避免使用未跟上立法变化的废弃项目。

注意

为了适应这些变化和立法要求,有许多有用的应用程序,如Django Simple Cookie ConsentDjango Cookie Law,它们旨在与多个立法框架一起工作。您可以通过以下链接了解更多信息:

pypi.org/project/django-simple-cookie-consent/

github.com/TyMaszWeb/django-cookie-law

存在许多 JavaScript 模块实现了类似的 cookie 同意机制。

Pickle 或 JSON 存储

Python 在其标准库中提供了pickle模块,用于将 Python 对象序列化为字节流表示。pickle 是一种二进制结构,具有在不同架构和不同版本的 Python 之间互操作的优势,因此 Python 对象可以在 Windows PC 上序列化为 pickle,并在 Linux Raspberry Pi 上反序列化为 Python 对象。

这种灵活性伴随着安全漏洞,因此不建议用它来表示不受信任的数据。考虑以下 Python 对象,它包含多种类型的数据。它可以使用pickle进行序列化:

import datetime
data = dict(viewed_books=[17, 18, 3, 2, 1],\
            search_history=['1981', 'Machine Learning', 'Bronte'],\
            background_rgb=(96, 91, 92),\
            foreground_rgb=(17, 17, 17),\
            last_login_login=datetime.datetime(2019, 12, 3, 15, 30, 30),\
            password_change=datetime.datetime(2019, 9, 2, 8, 41, 25),\
            user_class='Veteran',\
            average_rating=4.75,\
            reviewed_books={18, 3, 7})

使用pickle模块的dumps(导出字符串)方法,我们可以将数据对象序列化以生成字节表示:

import pickle
data_pickle = pickle.dumps(data)

pickle格式:

import json
data_json = json.dumps(data)

因为数据包含 Python 的datetimeset对象,这些对象不能与 JSON 序列化,当我们尝试序列化结构时,会抛出类型错误:

TypeError: Object of type datetime is not JSON serializable

对于将数据序列化为 JSON,我们可以将datetime对象转换为string,将set转换为列表:

data['last_login_login'] = data['last_login_login'].strftime("%Y%d%m%H%M%S")
data['password_change'] = data['password_change'].strftime("%Y%d%m%H%M%S")
data['reviewed_books'] = list(data['reviewed_books'])

由于 JSON 数据是可读的,因此很容易检查:

{"viewed_books": [17, 18, 3, 2, 1], "search_history": ["1981", "Machine Learning", "Bronte"], "background_rgb": [96, 91, 92], "foreground_rgb": [17, 17, 17], "last_login_login": "20190312153030", "password_change": "20190209084125", "user_class": "Veteran", "average_rating": 4.75, "reviewed_books": [18, 3, 7]}

注意,我们不得不显式地将datetimeset对象转换为,但 JSON 会自动将元组转换为列表。Django 附带PickleSerializerJSONSerializer。如果需要更改序列化器,可以通过在项目的settings.py文件中设置SESSION_SERIALIZER变量来实现:

SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer'

练习 9.05:检查会话密钥

本练习的目的是查询项目的 SQLite 数据库并对会话表进行查询,以便熟悉会话数据的存储方式。然后,您将创建一个用于检查使用 JSONSerializer 存储的会话数据的 Python 脚本:

  1. 在命令提示符下,使用以下命令打开项目数据库:

    sqlite3 db.sqlite3
    
  2. 使用 .schema 指令来观察 django_session 表的结构如下:

    sqlite> .schema django_session
    CREATE TABLE IF NOT EXISTS "django_session" ("django_session table in the database stores session information in the following fields:`session_key``session_data``expire_date`
    
  3. 使用 SQL 命令 select * from django_session; 查询 django_session 表中的数据:![图 9.12:查询 django_session 表中的数据 图片

    图 9.12:查询 django_session 表中的数据

    注意

    要退出 sqlite3,在 Linux 和 macOS 上按 Ctrl + D,或在 Windows 上按 Ctrl + Z 然后按 Enter

  4. 我们观察到会话数据以 base64 格式编码。我们可以在 Python 命令行中使用 base64 模块解密此数据。一旦从 base64 解码,session_key 数据包含一个 binary_key 和一个由冒号分隔的 JSON 有效载荷:

    b'\x82\x1e"z\xc9\xb4\xd7\xbf8\x83K…5e02:{"_auth_user_id":"1"…}'
    

    这段 Python 代码展示了如何获取有效载荷:

    ![图 9.13:使用 Python shell 解码会话密钥 图片

    图 9.13:使用 Python shell 解码会话密钥

    我们可以看到有效载荷中编码的结构。有效载荷表示会话中存储的最小数据。它包含 _auth_user_id_auth_user_backend_auth_user_hash 键,其值分别来自 User.idModelBackend 类名和从用户的密码信息中派生的哈希值。我们将在下一节学习如何添加额外的数据。

  5. 我们将开发一个简单的 Python 实用程序来解密此会话信息。它需要我们使用的模块以及用于格式化输出的 pprint 模块和用于检查命令行参数的 sys 模块:

    import base64
    import json
    import pprint
    import sys
    
  6. import 语句之后,编写一个函数来解码会话密钥并加载 JSON 有效载荷作为 Python 字典:

    def get_session_dictionary(session_key):
        binary_key, payload = base64.b64decode\
                              (session_key).split(b':', 1)
        session_dictionary = json.loads(payload.decode())
        return session_dictionary
    
  7. 添加一个代码块,以便当运行此实用程序时,它接受命令行中指定的 session_key 参数,并使用 get_session_dictionary 函数将其转换为字典。然后,使用 pprint 模块打印字典结构的缩进版本:

    if __name__ == '__main__':
        if len(sys.argv)>1:
            session_key = sys.argv[1]
            session_dictionary = get_session_dictionary(session_key)
            pp = pprint.PrettyPrinter(indent=4)
            pp.pprint(session_dictionary)
    
  8. 现在您可以使用这个 Python 脚本来检查存储在数据库中的会话数据。您可以通过以下方式在命令行中调用它,将会话数据作为参数传递:

        python session_info.py <session_data>
    

    当您尝试最终活动时,这将有助于调试会话行为:

    ![图 9.14:Python 脚本 图片

图 9.14:Python 脚本

此脚本输出解码后的会话信息。目前,会话只包含三个键:

_auth_user_backend 是用户后端类的字符串表示。由于我们的项目在模型中存储用户凭据,因此使用 ModelBackend

_auth_user_hash 是用户密码的哈希值。

_auth_user_id是从模型的User.id属性中获取的用户 ID。

这个练习帮助你熟悉了 Django 中会话数据是如何存储的。现在,我们将把注意力转向向 Django 会话添加更多信息。

在会话中存储数据

我们已经介绍了 Django 中会话的实现方式。现在,我们将简要探讨一些我们可以利用会话来丰富用户体验的方法。在 Django 中,会话是request对象的一个属性。它被实现为一个类似字典的对象。在我们的视图中,我们可以像典型的字典一样向session对象分配键,如下所示:

request.session['books_reviewed_count'] = 39

但是有一些限制。首先,会话中的键必须是字符串,因此不允许整数和时间戳。其次,以下划线开头的键是为内部系统使用保留的。数据限于可以编码为 JSON 的值,因此一些无法解码为 UTF-8 的字节序列,如之前列出的binary_key,不能作为 JSON 数据存储。另一个警告是避免将request.session重新赋值为不同的值。我们只应分配或删除键。所以,不要这样做:

request.session = {'books_read_count':30, 'books_reviewed_count': 39}

而应该这样做:

request.session['books_read_count'] = 30
request.session['books_reviewed_count'] = 39

考虑到这些限制,我们将研究在我们的评论应用中可以如何使用会话数据。

练习 9.06:将最近浏览的书籍存储在会话中

本练习的目的是使用会话来保存已认证用户最近浏览的10本书的信息。这些信息将在bookr项目的个人资料页上显示。当浏览一本书时,会调用book_detail视图。在本练习中,我们将编辑reviews/views.py文件,并向book_detail方法添加一些额外的逻辑。我们将向会话中添加一个名为viewed_books的键。利用基本的 HTML 和 CSS 知识,可以创建页面以显示个人资料详情和存储在页面不同部分的已浏览书籍,如下所示:

图 9.15:包含已浏览书籍的个人资料页面

图 9.15:包含已浏览书籍的个人资料页面

  1. 编辑reviews/views.pybook_detail方法。我们只对为已认证用户添加会话信息感兴趣,因此添加一个条件语句来检查用户是否已认证,并将max_viewed_books_length(已浏览书籍列表的最大长度)设置为10

    def book_detail(request, pk):
        …
        if request.user.is_authenticated:
            max_viewed_books_length = 10
    
  2. 在相同的条件块中,添加代码以检索当前request.session['viewed_books']的值。如果此键不在会话中,则从一个空列表开始:

            viewed_books = request.session.get('viewed_books', [])
    
  3. 如果当前书籍的主键已经在viewed_books中,以下代码将删除它:

            viewed_book = [book.id, book.title]
            if viewed_book in viewed_books:
                viewed_books.pop(viewed_books.index(viewed_book))
    
  4. 以下代码将当前书籍的主键插入到viewed_books列表的开头:

            viewed_books.insert(0, viewed_book)
    
  5. 添加以下键以仅保留列表的前 10 个元素:

            viewed_books = viewed_books[:max_viewed_books_length]
    
  6. 以下代码将我们的 viewed_books 添加回 session[ 'viewed_books'],以便在后续请求中可用:

            request.session['viewed_books'] = viewed_books
    
  7. 如前所述,在 book_detail 函数的末尾,根据请求和上下文数据渲染 reviews/book_detail.html 模板:

        return render(request, "reviews/book_detail.html", context)
    

    完成后,book_detail 视图将包含以下条件块:

    def book_detail(request, pk):
        …
        if request.user.is_authenticated:
            max_viewed_books_length = 10
            viewed_books = request.session.get('viewed_books', [])
            viewed_book = [book.id, book.title]
            if viewed_book in viewed_books:
                viewed_books.pop(viewed_books.index(viewed_book))
            viewed_books.insert(0, viewed_book)
            viewed_books = viewed_books[:max_viewed_books_length]
            request.session['viewed_books'] = viewed_books
        return render(request, "reviews/book_detail.html", context)
    
  8. 修改 templates/profile.html 的页面布局和 CSS,以适应查看书籍的分区。由于我们可能在未来向此页面添加更多分区,一个方便的布局概念是将水平排列在页面上的 div 实例。我们将内部 div 实例称为 infocell 实例,并用绿色边框和圆角进行样式设计:

    <style>
    .flexrow { display: flex;
               border: 2px black;
    }
    .flexrow > div { flex: 1; }
    .infocell {
      border: 2px solid green;
      border-radius: 5px 25px;
      background-color: white;
      padding: 5px;
      margin: 20px 5px 5px 5px;
    }
    </style>
      <div class="flexrow" >
        <div class="infocell" >
          <p>Profile</p>
          …
        </div>
        <div class="infocell" >
          <p>Viewed Books</p>
          …
        </div>
      </div>
    
  9. 修改 templates/profile.html 中的 Viewed Books div,以便如果有书籍存在,它们的标题将被显示,并链接到单个书籍详情页面。这将如下所示:

    <a href="/books/1">Advanced Deep Learning with Keras</a><br>
    

    如果列表为空,应该显示一条消息。整个 div,包括遍历 request.session.viewed_books,将看起来像这样:

        <div class="infocell" >
          <p>Viewed Books</p>
          <p>
          {% for book_id, book_title in request.session.viewed_books %}
          <a href="/books/{{ book_id }}">{{ book_title }}</a><br>
          {% empty %}
                No recently viewed books found.
          {% endfor %}
          </p>
        </div>
    

    一旦所有这些更改都被纳入,这将是一个完整的个人资料模板:

templates/profile.html
1  {% extends "base.html" %}
2
3  {% block title %}Bookr{% endblock %}
4
5  {% block heading %}Profile{% endblock %}
6
7  {% block content %}
8
9  <style>
You can find the complete code for this file at http://packt.live/3btvSJZ.

通过添加最近查看的书籍列表,这项练习增强了个人资料页面。现在,当您访问 http://127.0.0.1:8000/accounts/profile/ 上的登录链接时,您将看到此页面:

![图 9.16:最近查看的书籍图片 B15509_09_16.jpg

图 9.16:最近查看的书籍

我们可以使用我们在 练习 9.04在基础模板中切换登录和注销链接 中开发的 session_info.py 脚本来检查用户会话,一旦此功能实现。它可以通过在命令行中传递会话数据作为参数来调用:

    python session_info.py <session_data>

我们可以看到,书籍的 ID 和标题列在 viewed_books 键下。记住,编码后的数据是通过查询 SQLite 数据库中的 django_session 表获得的:

![图 9.17:查看的书籍存储在会话数据中图片 B15509_09_17.jpg

图 9.17:查看的书籍存储在会话数据中

在这个练习中,我们使用了 Django 的会话机制来存储关于用户与 Django 项目交互的临时信息。我们学习了如何从用户会话中检索这些信息,并在一个通知用户其最近活动的视图中显示这些信息。

活动 9.02:使用会话存储书籍搜索页面

会话是存储短期信息的有用方式,有助于在网站上维护一个有状态的用户体验。用户经常重新访问诸如搜索表单之类的页面,当用户返回这些页面时,存储他们最近使用的表单设置将非常方便。在 第三章URL 映射、视图和模板 中,我们为 bookr 项目开发了一个书籍搜索功能。书籍搜索页面有两个 搜索范围 选项 – 标题贡献者。目前,每次访问页面时,它默认为 标题

![图 9.18:图书搜索表单的搜索字段图片

图 9.18:图书搜索表单的搜索字段

在这个活动中,你将使用会话存储,以便当访问/book-search图书搜索页面时,它将默认显示最近使用的搜索选项。你还将向个人资料页面添加一个包含最近使用的搜索术语链接列表的第三个infocell。以下是完成此活动的步骤:

  1. 编辑book_search视图并从会话中检索search_history

  2. 当表单接收到有效输入并且用户已登录时,将搜索选项和搜索文本追加到会话的搜索历史列表中。

    如果表单尚未填写(例如,当首次访问页面时),则使用之前使用的搜索在选项渲染表单,即标题贡献者(如图 9.19 所示):

    ![图 9.19:在搜索页面选择贡献者 图片

    图 9.19:在搜索页面选择贡献者

  3. 在个人资料模板中,包括一个额外的infocell分区用于搜索历史

  4. 将搜索历史以一系列链接的形式列出到图书搜索页面。链接将采用以下形式:/book-search?search=Python&search_in=title

这个活动将挑战你将会话数据应用于解决网络表单中的可用性问题。这种方法在许多实际情况下都有适用性,并给你一些关于在创建有状态的 Web 体验中使用会话的思路。完成此活动后,个人资料页面将包含如图 9.20 所示的第三个infocell

![图 9.20:带有搜索历史信息单元格的个人资料页面图片

图 9.20:带有搜索历史信息单元格的个人资料页面

注意

该活动的解决方案可以在packt.live/2Nh1NTJ找到。

摘要

在本章中,我们检查了 Django 的认证和会话中间件实现。我们学习了如何将认证和权限逻辑集成到视图和模板中。我们可以对特定页面设置权限并限制其访问权限仅限于认证用户。我们还检查了如何在用户的会话中存储数据并在后续页面中渲染它。

现在你已经拥有了定制 Django 项目以提供个性化网络体验的技能。你可以限制内容仅对认证用户或特权用户可见,并且可以根据用户之前的交互来个性化用户体验。在下一章中,我们将重新审视 Admin 应用,学习一些高级技术来自定义我们的用户模型,并对模型的管理界面进行细粒度更改。

第十章:10. 高级 Django 管理及自定义

概述

本章向您介绍了对Django 管理站点的高级自定义,以便您可以根据您的 Web 项目定制 Django 管理仪表板的外观,使其与您的 Web 项目融为一体。您将了解如何将新功能和能力添加到您的 Web 项目的 Django 管理界面,使其在实现项目目标方面更加强大和有用。这些自定义是通过添加自定义模板来实现的,这些模板有助于修改现有页面的外观和感觉。这些自定义模板还添加了新的视图,可以帮助扩展管理仪表板的默认功能。完成本章学习后,您将掌握不仅可以让您自定义界面,还可以自定义基于 Django 的项目管理页面的功能。

简介

假设我们想要自定义一个大组织管理站点的首页。我们希望展示组织内不同系统的健康状况,并查看任何活跃的高优先级警报。如果这是一个基于 Django 构建的内部网站,我们就需要对其进行自定义。添加这些类型的功能将需要 IT 团队的开发者自定义默认管理面板并创建他们自己的自定义AdminSite模块,这将渲染与默认管理站点提供的不同的索引页面。幸运的是,Django 使这些类型的自定义变得容易。

在本章中,我们将探讨如何利用 Django 框架及其可扩展性来自定义 Django 的默认管理界面(如图10.1所示)。我们不仅将学习如何使界面更加个性化;我们还将学习如何控制管理站点的不同方面,使 Django 加载自定义管理站点而不是默认框架提供的那一个。当我们需要在管理站点中引入默认情况下不存在的功能时,这种自定义会非常有用。

图 10.1:默认 Django 管理面板界面

图 10.1:默认 Django 管理面板界面

本章基于我们在第四章,“Django 管理简介”中练习的技能。为了回顾,我们学习了如何使用 Django 管理站点来控制我们的 Bookr 应用的管理和授权。我们还学习了如何注册模型以读取和编辑其内容,以及如何使用admin.site属性来自定义 Django 的管理界面。现在,让我们通过查看如何利用 Django 的AdminSite模块来开始自定义管理站点,并添加到我们的 Web 应用管理门户中的强大新功能,来进一步扩展我们的知识。

自定义管理站点

Django 作为 Web 框架,为构建 Web 应用提供了大量的自定义选项。当我们构建项目管理员应用时,我们将使用 Django 提供的相同自由度。

第四章Django 管理员介绍 中,我们探讨了如何使用 admin.site 属性来自定义 Django 管理界面的元素。但如果我们需要更多控制管理员站点的行为呢?例如,假设我们想要为登录页面(或注销页面)使用自定义模板,以便用户访问 Bookr 管理面板时显示。在这种情况下,admin.site 提供的属性可能不够用,我们需要构建可以扩展默认管理员站点行为的自定义功能。幸运的是,通过扩展 Django 管理模型中的 AdminSite 类,我们可以轻松实现这一点。但在我们开始构建管理员站点之前,让我们首先了解 Django 如何发现管理员文件以及我们如何利用这个管理员文件发现机制在 Django 中构建一个新应用,该应用将作为我们的管理员站点应用。

在 Django 中发现管理员文件

当我们在 Django 项目中构建应用时,我们经常使用 admin.py 文件来注册我们的模型或创建自定义 ModelAdmin 类,以便在管理员界面中与模型进行交互。这些 admin.py 文件存储并提供这些信息给我们的项目管理员界面。当我们把 django.contrib.admin 添加到 settings.py 文件中的 INSTALLED_APPS 部分时,Django 会自动发现这些文件:

图 10.2:Bookr 应用结构

图 10.2:Bookr 应用结构

如前图所示,我们在 reviews 应用目录下有一个 admin.py 文件,Django 使用它来自定义 Bookr 的管理员站点。

当管理员应用被添加时,它会尝试在我们正在工作的 Django 项目的每个应用中寻找 admin 模块,如果找到一个模块,它就会从该模块加载内容。

Django 的 AdminSite 类

在我们开始自定义 Django 的管理员站点之前,我们必须了解默认管理员站点是如何由 Django 生成和处理的。

为了为我们提供默认的管理站点,Django 打包了一个名为admin的模块,该模块包含一个名为AdminSite的类。这个类实现了很多有用的功能和一些智能默认设置,Django 社区认为这些对于实现大多数 Django 网站的有用管理面板非常重要。默认的AdminSite类提供了很多内置属性,不仅控制了默认管理站点在浏览器中的外观和感觉,还控制了我们与之交互的方式以及特定交互将导致哪些操作。其中一些默认设置包括站点模板属性,如显示在站点标题中的文本、显示在浏览器标题栏中的文本、与 Django 的auth模块集成以验证管理站点,以及一系列其他属性。

在我们构建自定义 Django 网络项目的管理站点的道路上不断前进时,保留 Django 的AdminSite类中已经构建的许多有用功能是非常理想的。这正是 Python 面向对象编程概念帮助我们的时候。

当我们开始创建自定义管理站点时,我们将尝试利用 Django 默认的AdminSite类提供的现有有用功能集。为此,我们不会从头开始构建一切,而是将致力于创建一个新的子类,该子类继承自 Django 的AdminSite类,以利用 Django 已经为我们提供的现有功能集和有用集成。这种方法的优点是,我们可以专注于向我们的自定义管理站点添加新的和有用的功能集,而不是花费时间从头实现基本功能集。例如,以下代码片段展示了我们如何创建 Django 的AdminSite类的子类:

class MyAdminSite(admin.AdminSite):
    …

为了开始为我们网络应用构建自定义管理站点,让我们首先通过使用我们将要工作的自定义AdminSite类来覆盖 Django 管理面板的一些基本属性。

可以覆盖的一些属性包括site_headersite_title等。

注意

当创建一个自定义管理站点时,我们必须再次注册任何之前使用默认的admin.site变量注册的ModelModelAdmin类。这是因为自定义管理站点不会从 Django 提供的默认管理站点继承实例详情,除非我们重新注册我们的ModelModelAdmin接口,否则我们的自定义管理站点将不会显示它们。

现在,了解了 Django 如何发现加载到管理界面中的内容以及我们如何开始构建自定义管理站点后,让我们继续尝试为 Bookr 创建自定义管理应用程序,该应用程序扩展了 Django 提供的现有admin模块。在接下来的练习中,我们将使用 Django 的AdminSite类为 Bookr 应用程序创建自定义管理站点界面。

练习 10.01:为 Bookr 创建自定义管理站点

在这个练习中,您将创建一个新的应用程序,该应用程序扩展了默认的 Django 管理站点,并允许您自定义界面的组件。因此,您将自定义 Django 管理面板的默认标题。完成此操作后,您将覆盖 Django 的admin.site属性的默认值,将其指向您自定义的管理站点:

  1. 在您开始工作于自定义管理站点之前,首先需要确保您位于项目中可以运行 Django 应用程序管理命令的正确目录。为此,使用终端或 Windows 命令提示符导航到bookr目录,然后运行以下命令创建一个名为bookr_admin的新应用程序,该应用程序将作为 Bookr 的管理站点:

    python3 manage.py startapp bookr_admin
    

    一旦这个命令成功执行,您应该在项目中有一个名为bookr_admin的新目录。

  2. 现在,默认结构配置完成后,下一步是创建一个名为BookrAdmin的新类,该类将扩展 Django 提供的AdminSite类以继承默认管理站点的属性。为此,打开 PyCharm 中bookr_admin目录下的admin.py文件。一旦文件打开,您将看到文件中已经存在以下代码片段:

    from django.contrib import admin
    

    现在,保持这个import语句不变,从下一行开始,创建一个名为BookrAdmin的新类,该类继承自您之前导入的admin模块提供的AdminSite类:

    class BookrAdmin(admin.AdminSite):
    

    在这个新的BookrAdmin类内部,覆盖site_header变量的默认值,该变量负责在 Django 管理面板中渲染站点标题,通过设置site_header属性,如下所示:

        site_header = "Bookr Administration"
    

    通过这样,自定义管理站点类现在已经定义好了。要使用这个类,您首先需要创建这个类的实例。这可以通过以下方式完成:

    admin_site = BookrAdmin(name='bookr_admin')
    
  3. 保存文件,但不要关闭它;我们将在第 6 步中再次访问它。接下来,让我们编辑bookr应用中的urls.py文件。

  4. 现在自定义类已经定义好了,下一步是修改urlpatterns列表,将我们项目中/admin端点映射到您创建的新AdminSite类。为此,打开 PyCharm 中Bookr项目目录下的urls.py文件,并将/admin端点的映射更改为指向我们的自定义站点:

    admin_site object from the admin module of the bookr_admin app. Then, we used the urls property of the object to map to the admin endpoint in our application as follows:
    
    

    path('admin/', admin_site.urls)

    
    In this case, the `urls` property of our `admin_site` object is being automatically populated by the `admin.AdminSite` base class provided by Django's `admin` module. Once complete, your `urls.py` file should look like this: [`packt.live/3qjx46J`](http://packt.live/3qjx46J).
    
  5. 现在,配置完成后,让我们在浏览器中运行我们的管理应用。为此,从包含 manage.py 文件的项目的根目录运行以下命令:

    python manage.py runserver localhost:8000
    

    然后,导航到 http://localhost:8000/admin(或 http://127.0.0.1:8000/admin),这将打开一个类似于以下截图的页面:

    ![图 10.3:自定义 Bookr 管理站点的首页视图 图片

    图 10.3:自定义 Bookr 管理站点的首页视图

    在前面的截图(图 10.3)中,你会看到 Django 显示消息,“你没有权限查看或编辑任何内容”。没有足够权限的问题发生是因为,到目前为止,我们还没有将任何模型注册到我们的自定义 AdminSite 实例中。这个问题也适用于与 Django auth 模块一起提供的 User 和 Groups 模型。因此,让我们通过从 Django 的 auth 模块注册 User 模型,使我们的自定义管理站点更有用。

  6. 要从 Django 的 auth 模块注册 User 模型,打开 PyCharm 中 bookr_admin 目录下的 admin.py 文件,并在文件顶部添加以下行:

    from django.contrib.auth.admin import User
    

    在文件末尾,使用您的 BookrAdmin 实例按以下方式注册此模型:

    admin_site.register(User)
    

    到目前为止,您的 admin.py 文件应该看起来像这样:

    from django.contrib import admin
    from django.contrib.auth.admin import User
    class BookrAdmin(admin.AdminSite):
        site_header = "Bookr Administration"
    admin_site = BookrAdmin(name='bookr_admin')
    admin_site.register(User)
    

    完成此操作后,重新加载网络服务器并访问 http://localhost:8000/admin。现在,你应该能够在管理界面中看到用于编辑的 User 模型,如图所示:

    ![图 10.4:显示我们在 Bookr 管理站点注册的模型的首页视图 在 Bookr 管理站点 图片

图 10.4:显示我们在 Bookr 管理站点注册的模型的首页视图

通过这种方式,我们刚刚创建了我们的管理站点应用,现在我们也可以验证自定义站点有一个不同的标题——“Bookr 管理”。

覆盖默认的 admin.site

在上一节中,在我们创建自己的 AdminSite 应用程序之后,我们看到了我们必须手动注册模型。这是因为我们之前构建的大多数应用程序仍然使用 admin.site 属性来注册它们的模型,如果我们想使用我们的 AdminSite 实例,我们必须更新所有这些应用程序以使用我们的实例,如果项目中有许多应用程序,这可能会变得很繁琐。

幸运的是,我们可以通过覆盖默认的 admin.site 属性来避免这个额外的负担。为此,我们首先必须创建一个新的 AdminConfig 类,它将为我们覆盖默认的 admin.site 属性,使我们的应用程序被标记为默认管理站点,从而覆盖我们项目中的 admin.site 属性。在下一个练习中,我们将探讨如何将我们的自定义管理站点映射为应用程序的默认管理站点。

练习 10.02:覆盖默认 Admin Site

在这个练习中,您将使用 AdminConfig 类来覆盖您项目的默认管理站点,这样您就可以继续使用默认的 admin.site 变量来注册模型、覆盖站点属性等:

  1. 打开 bookr_admin 目录下的 admin.py 文件,并移除对 User 模型的导入以及 BookrAdmin 实例创建,这些是在 练习 10.01步骤 6 中编写的。完成此操作后,文件内容应类似于以下内容:

    from django.contrib import admin
    class BookrAdmin(admin.AdminSite):
        site_header = "Bookr Administration"
    
  2. 您需要为自定义管理站点创建一个 AdminConfig 类,以便 Django 识别 BookrAdmin 类作为 AdminSite 并覆盖 admin.site 属性。为此,打开 bookr_admin 目录中的 apps.py 文件,并用以下内容覆盖文件内容:

    from django.contrib.admin.apps import AdminConfig
    class BookrAdminConfig(AdminConfig):
        default_site = 'bookr_admin.admin.BookrAdmin'
    

    在此,我们首先从 Django 的 admin 模块中导入了 AdminConfig 类。此类用于定义应用作默认管理站点的应用程序,并覆盖 Django 管理站点的默认行为。

    对于我们的用例,我们创建了一个名为 BookrAdminConfig 的类,它作为 Django 的 AdminConfig 类的子类,并覆盖了 default_site 属性,将其指向我们的 BookrAdmin 类,即我们的自定义管理站点:

    default_site = 'bookr_admin.admin.BookrAdmin'
    

    完成此操作后,我们需要在我们的 Bookr 项目中将我们的应用程序设置为管理应用程序。为此,打开 Bookr 项目的 settings.py 文件,并在 INSTALLED_APPS 部分下,将 'reviews.apps.ReviewsAdminConfig' 替换为 'bookr_admin.apps.BookrAdminConfig'settings.py 文件应如下所示:packt.live/3siv1lf

  3. 将应用程序映射为管理应用程序后,最后一步是修改 URL 映射,以便 'admin/' 端点使用 admin.site 属性来找到正确的 URL。为此,打开 bookr 项目下的 urls.py 文件。考虑 urlpatterns 列表中的以下条目:

    path('admin/', adminadmin_site.urls is a module, while admin.site is a Django internal property. Once the preceding steps are complete, let's reload our web server and check whether our admin site loads by visiting `http://localhost:8000/admin`. If the website that loads looks like the one shown here, we have our own custom admin app now being used for the admin interface:![Figure 10.5: Home page view of the custom Bookr Administration site    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/webdev-dj-2e/img/B15509_10_05.jpg)
    

图 10.5:自定义 Bookr 管理站点的首页视图

如您所见,一旦我们用我们的管理应用覆盖了 admin.site,之前使用 admin.site.register 属性注册的模型就会自动显示出来。

通过这种方式,我们现在有一个自定义的基本模板,我们可以利用它来构建我们 Django 管理自定义的其余部分。随着我们通过本章的学习,我们将发现一些有趣的定制,使我们能够将管理仪表板集成到我们的应用程序中。

使用 AdminSite 属性自定义管理站点文本

正如我们可以使用admin.site属性来自定义 Django 应用程序的文本一样,我们也可以使用AdminSite类公开的属性来自定义这些文本。在练习 10.02覆盖默认管理网站中,我们查看更新了管理网站的site_header属性。同样,还有许多其他属性我们可以修改。以下是一些可以覆盖的属性描述如下:

  • site_header: 在每个管理页面的顶部显示的文本(默认为Django 管理)。

  • site_title: 在浏览器标题栏中显示的文本(默认为Django 管理网站)。

  • site_url: 用于“查看网站”选项的链接(默认为/)。当网站在自定义路径上运行时,此设置会被覆盖,并且重定向应将用户直接带到子路径。

  • index_title: 这是应该在管理应用程序的索引页上显示的文本(默认为站点管理)。

    注意

    关于所有adminsite属性的更多信息,请参阅官方 Django 文档docs.djangoproject.com/en/3.1/ref/contrib/admin/#adminsite-attributes

如果我们想在自定义管理网站上覆盖这些属性,过程非常简单:

class MyAdminSite(admin.AdminSite):
    site_header = "My web application"
    site_title = "My Django Web application"
    index_title = "Administration Panel"

如我们迄今为止所看到的示例,我们已经为 Bookr 创建了一个自定义管理应用,并将其设置为项目的默认管理网站。这里出现了一个有趣的问题。由于我们迄今为止所自定义的属性也可以通过直接使用admin.site对象来自定义,为什么我们要创建一个自定义管理应用?我们难道不能直接修改admin.site的属性吗?

事实上,有人选择自定义管理网站可能有多个原因;例如,他们可能想要更改默认管理网站的布局,以使其与应用程序的整体布局保持一致。这在创建一个对内容同质性要求很高的企业级 Web 应用时非常常见。以下是一个简短的列表,这些要求可能会促使开发者去构建一个自定义管理网站,而不是仅仅修改admin.site变量的属性:

  • 需要覆盖管理界面的索引模板

  • 需要覆盖登录或注销模板

  • 需要在管理界面中添加自定义视图

自定义管理网站模板

就像一些可定制的通用文本,例如出现在管理网站上的site_headersite_title,Django 也允许我们通过在AdminSite类中设置某些属性来自定义模板,这些模板用于在管理网站内部渲染不同的页面。

这些自定义可以包括修改用于渲染索引页面、登录页面、模型数据页面等模板。这些自定义可以通过利用 Django 提供的模板系统轻松完成。例如,以下代码片段展示了我们如何向 Django 管理员仪表板添加一个新的模板:

{% extends "admin/base_site.html" %}
{% block content %}
  <!-- Template Content -->
{% endblock %}

在这个自定义模板中,有几个重要的方面我们需要理解。

当通过修改仪表板内某些页面的外观或向仪表板添加一组新页面来自定义现有的 Django 管理员仪表板时,我们可能不想从头开始重新编写每个 HTML 片段以保持 Django 管理员仪表板的基本外观和感觉。

通常,在自定义管理员仪表板时,我们希望保留 Django 组织仪表板上显示的不同元素的布局,这样我们就可以专注于修改对我们重要的页面部分。这个页面的基本布局,以及常见的页面元素,如页面标题和页面页脚,都在 Django 管理员的基模板中定义,该模板还充当默认 Django 管理员网站内所有页面的主模板。

为了保留 Django 管理员页面内常见元素的组织和渲染方式,我们需要从这个基模板扩展,使得我们的自定义模板页面提供与 Django 管理员仪表板内其他页面一致的用户体验。这可以通过使用模板扩展标签并从 Django 提供的admin模块扩展base_site.html模板来完成:

{% extends "admin/base_site.html" %}

完成此操作后,下一步是定义我们自己的自定义模板内容。Django 提供的base_site.html模板提供了一个基于块的占位符,供开发者为模板添加自己的内容。要添加此内容,开发人员必须在{% block content %}标签内放置他们自己的页面自定义元素逻辑。这本质上覆盖了base_site.html模板内定义的任何{% block content %}标签内容,遵循 Django 模板继承的概念。

现在,让我们看看我们如何自定义用于渲染注销页面的模板,一旦用户在管理员面板中点击“注销”按钮。

练习 10.03:为 Bookr 管理员网站自定义注销模板

在这个练习中,你将自定义一个模板,该模板用于在用户点击管理员网站上的“注销”按钮后渲染注销页面。这种覆盖在银行网站上可能会很有用。一旦用户点击“注销”,银行可能希望向用户展示一个页面,其中包含有关如何确保其银行会话安全关闭的详细说明。

  1. 在你之前章节中必须创建的templates目录下,创建另一个名为admin的目录,该目录将用于存储你自定义管理员网站的模板。

    注意

    在继续之前,请确保将模板目录添加到你的settings.py文件中的DIRS列表(在bookr/项目下)。

  2. 现在,随着目录结构设置完成,并且 Django 配置为加载模板,下一步涉及编写你想要渲染的自定义注销模板。为此,让我们在步骤 1中创建的templates/admin目录下创建一个名为logout.html的新文件,并将以下内容添加到其中:

    {% extends "admin/base_site.html" %}
    {% block content %}
    <p>You have been logged out from the Admin panel. </p>
    <p><a href="{% url 'admin:index' %}">Login Again</a> or   <a href="{{ site_url }}">Go to Home Page</a></p>
    {% endblock %}
    

    在前面的代码片段中,我们做了几件事情。首先,对于我们的自定义注销模板,我们将使用由django.contrib.admin模块提供的相同主布局。所以,考虑以下内容:

    {% extends "admin/base_site.html" %}
    

    当我们编写这段代码时,Django 会尝试在由django.contrib.admin模块提供的templates目录中查找并加载admin/base_site.html模板。

    现在,随着我们的基础模板设置完毕,接下来我们要尝试通过执行以下命令来覆盖内容块的 HTML:

    {% block content %}
    …
    {% endblock %}
    

    admin:indexsite_url的值由AdminSite类自动提供,基于我们定义的设置。

    使用admin:indexsite_url的值,我们创建了一个名为“再次登录”的超链接,当点击时,将用户带回到登录表单,以及一个“返回”主页的链接,这将带用户回到网站的主页。文件现在应该看起来像这样:packt.live/3oIGQPo

  3. 现在,自定义模板已经定义好了,下一步就是在我们自定义的 admin 站点中使用这个自定义模板。为此,让我们打开位于bookr_admin目录下的admin.py文件,并将以下字段添加为BookrAdmin类的最后一个值:

    logout_template = 'admin/logout.html'
    

    保存文件。它应该看起来像这样:packt.live/3oHHsVz

  4. 一旦所有前面的步骤都完成,让我们通过运行以下命令来启动我们的开发服务器:

    python manage.py runserver localhost:8000
    

    然后,我们导航到http://localhost:8000/admin

    一旦你到了那里,尝试登录然后点击“注销”。一旦你注销,你会看到以下页面呈现:

    ![图 10.6:点击注销按钮后向用户呈现的注销视图]

    图片

图 10.6:点击注销按钮后向用户呈现的注销视图

通过这样,我们已经成功覆盖了我们的第一个模板。同样,我们也可以覆盖 Django admin 面板中的其他模板,例如索引视图和登录表单的模板。

向 Admin 站点添加视图

就像 Django 内部的一般应用程序可以有多个与之关联的视图一样,Django 允许开发者向 admin 站点添加自定义视图。这允许开发者扩展 admin 站点界面的功能范围。

将自己的观点添加到管理站点的能力为网站的行政面板提供了很多可扩展性,这可以用于几个额外的用例。例如,正如我们在本章开头所讨论的,一个大组织的 IT 团队可以向管理站点添加一个自定义视图,然后可以使用它来监控组织内不同 IT 系统的健康状况,并为 IT 团队提供快速查看任何需要解决的紧急警报的能力。

现在,我们需要回答的下一个问题是:我们如何向管理站点添加一个自定义视图?

实际上,在管理模板中添加新视图相当简单,遵循了我们创建应用程序视图时使用的方法,尽管有一些小的修改。在下一节中,我们将探讨如何将新视图添加到我们的 Django 管理仪表板。

创建视图函数

向 Django 应用程序添加新视图的第一步是创建一个视图函数,该函数实现了处理视图的逻辑。在前面的章节中,我们在一个名为views.py的单独文件中创建了视图函数,该文件用于存放我们所有的基于方法和类的视图。

当涉及到向 Django 管理仪表板添加新视图时,为了创建新视图,我们需要在我们的自定义AdminSite类中定义一个新的视图函数。例如,为了添加一个渲染组织内不同 IT 系统健康状况的页面的新视图,我们可以在自定义AdminSite类实现中创建一个名为system_health_dashboard()的新视图函数,如下面的代码片段所示:

class SysAdminSite(admin.AdminSite):
    def system_health_dashboard(self, request):
        # View function logic

在视图函数内部,我们可以执行任何我们想要的操作来生成视图,并最终使用该响应来渲染模板。在这个视图函数内部,有一些重要的逻辑我们需要确保正确实现。

第一种方法是在视图函数内部为request字段的current_app属性设置值。这是为了允许 Django 的 URL 解析器在模板中正确解析应用程序的视图函数。为了在刚刚创建的自定义视图函数中设置此值,我们需要按照以下代码片段设置current_app属性:

request.current_app = self.name

self.name字段由 Django 的AdminSite类自动填充,我们不需要显式地初始化它。有了这个,我们的最小自定义视图实现将如以下代码片段所示:

class SysAdminSite(admin.AdminSite):
    def system_health_dashboard(self, request):
        request.current_app = self.name
        # View function logic

访问常用模板变量

当创建自定义视图函数时,我们可能希望访问常用的模板变量,例如site_headersite_title,以便在关联的视图函数模板中正确渲染它们。实际上,通过使用AdminSite类提供的each_context()方法,这相当容易实现。

AdminSite 类的 each_context() 方法接受一个单一参数 request,它是当前请求上下文,并返回要插入到所有管理站点模板中的模板变量。

例如,如果我们想在自定义视图函数内部访问模板变量,我们可以实现类似于以下代码片段的代码:

def system_health_dashboard(self, request):
    request.current_app = self.name
    context = self.each_context(request)
    # view function logic

each_context() 方法返回的值是一个包含变量名称和相关值的字典。

映射自定义视图的 URL

一旦定义了视图函数,下一步就是将该视图函数映射到一个 URL,以便用户可以访问它或允许其他视图链接到它。对于在 AdminSite 内定义的视图,这种 URL 映射由 AdminSite 类实现的 get_urls() 方法控制。get_urls() 方法返回映射到 AdminSite 视图的 urlpatterns 列表。

如果我们想要为我们的自定义视图添加 URL 映射,首选的方法是在我们的自定义 AdminSite 类中覆盖 get_urls() 的实现,并在那里添加 URL 映射。以下代码片段展示了这种方法:

class SysAdminSite(admin.AdminSite):
    def get_urls(self):
        base_urls = super().get_urls(). # Get the existing set of URLs
        # Define our URL patterns for custom views
        urlpatterns = [path("health_dashboard/"),\
                           (self.system_health_dashboard)]
        # Return the updated mapping
        return base_urls + urlpatterns. 

get_urls() 方法通常由 Django 自动调用,因此不需要对其进行任何手动处理。

完成此操作后,最后一步是确保我们的自定义管理视图只能通过管理站点访问,并且非管理员用户不应能够访问它。让我们看看如何实现这一点。

将自定义视图限制在管理站点

如果你仔细阅读了所有前面的部分,你现在应该有一个自定义的 AdminSite 视图可以使用了。然而,有一个小问题。这个视图也可以被任何不在管理站点上的用户直接访问。

为了确保这种情况不会发生,我们需要将此视图限制在管理站点。这可以通过将我们的 URL 路径包裹在 admin_view() 调用中来实现,如下面的代码片段所示:

urlpatterns = [self.admin_view\
               (path("health_dashboard/"),\
               (self.system_health_dashboard))]

admin_view 函数确保传递给它的路径仅限于管理仪表板,并且没有非管理员权限的用户可以访问它。

现在,让我们为我们管理站点添加一个新的自定义视图。

练习 10.04:将自定义视图添加到管理站点

在这个练习中,你将为管理站点添加一个自定义视图,该视图将渲染用户资料,并显示用户修改电子邮件或添加新个人照片的选项。要构建这个自定义视图,请遵循以下步骤描述:

  1. 打开位于 bookr_admin 目录下的 admin.py 文件,并添加以下导入。这些将用于在管理站点应用程序内部构建我们的自定义视图:

    from django.template.response import TemplateResponse
    from django.urls import path
    
  2. 打开位于 bookr_admin 目录下的 admin.py 文件,并在 BookrAdmin 类中创建一个名为 profile_view 的新方法,该方法接受一个 request 变量作为其参数:

    def profile_view(self, request):
    

    接下来,在方法内部,获取当前应用程序的名称并将其设置在request上下文中。为此,你可以使用类的name属性,该属性由 Django 自动填充。要获取此属性并将其设置在您的request上下文中,您需要添加以下行:

    request.current_app = self.name
    

    一旦将应用程序名称填充到请求上下文中,下一步是获取模板变量,这些变量是渲染内容所必需的,例如site_titlesite_header等,在管理模板中。为此,利用AdminSite类的each_context()方法,该方法从类中提供管理站点模板变量的字典:

    context = self.each_context(request)
    

    一旦设置了数据,最后一步是返回一个TemplateResponse对象,当有人访问映射到您自定义视图的 URL 端点时,它将渲染自定义配置文件模板:

    return TemplateResponse(request, "admin/admin_profile.html", \
                            context)
    
  3. 现在已经创建了视图函数,下一步是让AdminSite返回将视图映射到AdminSite内部路径的 URL 映射。为此,您需要创建一个名为get_urls()的新方法,该方法覆盖了AdminSite.get_urls()方法并返回您新视图的映射。这可以通过首先在您为自定义管理站点创建的BookrAdmin类中创建一个名为get_urls()的新方法来完成:

    def get_urls(self):
    

    在此方法中,您需要做的第一件事是获取已映射到管理端点的 URL 列表。这是必需的步骤,否则,您的自定义管理站点将无法加载与模型编辑页面、注销页面等相关联的任何结果,如果这种映射丢失。要获取此映射,请调用从BookrAdmin类派生的基类的get_urls()方法:

    urls = super().get_urls()
    

    一旦捕获了基类的 URL,下一步是创建一个 URL 列表,将我们的自定义视图映射到管理站点的 URL 端点。为此,我们创建一个名为url_patterns的新列表,并将我们的profile_view方法映射到admin_profile端点。为此,我们使用 Django 的path实用函数,该函数允许我们将视图函数与基于字符串的 API 端点路径映射:

    url_patterns = [path("admin_profile", self.profile_view)]
    return urls + url_patterns
    

    保存admin.py文件。它应该看起来像这样:packt.live/38Jlyvz

  4. 现在,已经为新的视图配置了BookrAdmin类,下一步是创建管理配置文件页面的模板。为此,在项目根目录下的templates/admin目录中创建一个名为admin_profile.html的新文件。在这个文件中,首先添加一个extend标签以确保您是从默认的admin模板扩展的:

    {% extends "admin/index.html" %}
    

    此步骤确保您的所有管理模板样式表和 HTML 都可以在自定义视图模板中使用。例如,如果没有这个extend标签,您的自定义视图将不会显示任何已映射到您的管理站点的特定内容,如site_headersite_title或任何注销或转到其他页面的链接。

    一旦添加了扩展标签,添加一个 block 标签并提供内容值。这确保了你在 {% block content %}…{% endblock %} 段落之间添加的代码会覆盖 Django 管理模块预先打包的 index.html 模板中存在的任何值:

    {% block content %}
    

    block 标签内部,添加渲染在练习的第二步中创建的配置文件视图所需的 HTML:

    <p>Welcome to your profile, {{ username }}</p>
    <p>You can do the following operations</p>
    <ul>
        <li><a href="#">Change E-Mail Address</a></li>
        <li><a href="#">Add Profile Picture</a></li>
    </ul>
    {% endblock %}
    

    文件应该看起来像这样:packt.live/2MZhU8d

  5. 现在完成前面的步骤后,通过运行 python manage.py runserver localhost:8000 来重新加载你的应用程序服务器,然后访问 http://localhost:8000/admin/admin_profile

    当页面打开时,你可以期待看到以下截图:

    图 10.7:管理站点中的配置文件页面视图

图 10.7:管理站点中的配置文件页面视图

注意

到目前为止创建的视图将正常渲染,无论用户是否登录到管理应用程序。

为了确保这个视图只能被登录的管理员访问,你需要在练习的第三步中定义的 get_urls() 方法内部进行一个小修改。

get_urls() 方法中,修改 url_patterns 列表,使其看起来像下面这样:

url_patterns = [path("admin_profile", \
                self.admin_view(self.profile_view)),]

在前面的代码中,你将 profile_view 方法包裹在 admin_view() 方法内部。

AdminSite.admin_view() 方法使得视图仅限于已登录的用户。如果一个当前未登录管理站点的用户尝试直接访问 URL,他们将被重定向到登录页面,并且只有在成功登录的情况下,他们才能看到我们自定义页面的内容。

在这个练习中,我们利用了我们对为 Django 应用编写视图的现有理解,并将其与 AdminSite 类的上下文相结合,为我们的管理仪表板构建了一个自定义视图。有了这些知识,我们现在可以继续前进,并为我们的 Django 管理添加有用的功能。

使用模板变量传递额外的键

在管理站点内部,传递给模板的变量值是通过使用模板变量来传递的。这些模板变量是由 AdminSite.each_context() 方法准备并返回的。

现在,如果你想要将某个值传递到你的管理站点上的所有模板,你可以覆盖 AdminSite.each_context() 方法,并将所需的字段添加到 request 上下文中。让我们通过一个例子看看我们如何实现这个结果。

考虑到 username 字段,我们之前将其传递给了我们的 admin_profile 模板。如果我们想将其传递到自定义管理站点内的每个模板,我们首先需要在 BookrAdmin 类内部覆盖 each_context() 方法,如下所示:

def each_context(self, request):
        context = super().each_context(request)
        context['username'] = request.user.username
        return context

each_context()方法接受一个单一参数(这里我们不考虑 self),它使用该参数来评估某些其他值。

现在,在我们的重写each_context()方法中,我们首先调用基类each_context()方法,以便检索管理网站的context字典:

context = super().each_context(request)

完成此操作后,接下来要做的是将我们的username字段添加到context中,并将其值设置为request.user.username字段的值:

context['username'] = request.user.username

完成此操作后,最后剩下的事情就是返回这个修改后的上下文。

现在,每当我们的自定义管理网站渲染模板时,模板将带有这个额外的用户名变量。

活动十点零一:使用内置搜索构建自定义管理仪表板

在这个活动中,您将使用您在创建自定义管理网站的不同方面获得的知识来为 Bookr 构建自定义管理仪表板。在这个仪表板中,您将引入允许用户通过书籍名称或书籍出版社的名称搜索书籍的能力,并允许用户修改或删除这些书籍记录。

以下步骤将帮助您构建自定义管理仪表板,并添加通过出版社名称搜索书籍记录的功能:

  1. 在 Bookr 项目中创建一个新的应用,命名为bookr_admin,如果尚未创建。这将存储我们自定义管理网站的逻辑。

  2. bookr_admin目录下的admin.py文件中,创建一个新的类BookrAdmin,该类继承自 Django 管理模块的AdminSite类。

  3. 步骤 2中新建的BookrAdmin类中,添加对站点标题或其他管理仪表板品牌组件的任何自定义设置。

  4. bookr_admin目录下的apps.py文件中,创建一个新的BookrAdminConfig类,并在新的BookrAdminConfig类中,将默认站点属性设置为我们的自定义管理网站类BookrAdmin的完全限定模块名称。

  5. 在您的 Django 项目的settings.py文件中,将创建于步骤 4BookrAdminConfig类的完全限定路径作为第一个安装的应用程序。

  6. 要在 Bookr 中注册来自reviews应用的Books模型,打开reviews目录下的admin.py文件,并确保使用admin.site.register(ModelClass)将 Books 模型注册到管理网站。

  7. 为了允许根据出版社名称搜索书籍,在reviews应用的admin.py文件中,修改BookAdmin类,并向其中添加一个名为search_fields的属性,该属性包含publisher_name字段。

  8. 为了正确获取search_fields属性的出版社名称,在BookAdmin类中引入一个新的方法get_publisher,该方法将从Book模型返回出版社的名称字段。

  9. 确保使用admin.site.register(Book, BookModel)在我们的 Django 管理仪表板中将BookAdmin类注册为书籍模型的模型管理类。

完成此活动后,一旦您启动应用程序服务器并访问 http://localhost:8000/admin 并导航到书籍模型,您应该能够通过使用出版社的名称来搜索书籍,在搜索成功的情况下,您将看到一个类似于以下截图所示的页面:

![图 10.8:Bookr 管理仪表板内的书籍编辑页面

![img/B15509_10_08.jpg]

图 10.8:Bookr 管理仪表板内的书籍编辑页面

注意

此活动的解决方案可以在packt.live/2Nh1NTJ找到。

摘要

在本章中,我们探讨了 Django 如何允许自定义其管理站点。它是通过为网站的一些更通用的部分提供易于使用的属性来实现的,例如标题字段、标题和主页链接。除此之外,我们还学习了如何通过利用 Python 面向对象编程的概念来创建AdminSite的子类来构建自定义管理站点。

通过实现一个自定义的注销页面模板,我们进一步增强了此功能。我们还学习了如何通过添加一组新的视图来增强我们的管理仪表板。

随着我们进入下一章,我们将通过学习如何为模板创建我们自己的自定义标签和过滤器来构建我们迄今为止所学的内容。此外,通过使用基于类的视图,我们将获得以面向对象风格构建视图的能力。

第十一章:11. 高级模板和基于类的视图

概述

在本章中,你将学习如何使用 Django 的模板 API 创建自定义模板标签和过滤器。你还将编写 基于类的视图,这将帮助你执行 CRUD 操作。到本章结束时,你将清楚地了解 Django 如何处理高级模板,以及你如何构建支持 CRUD 操作的自定义视图。你将能够使用类在 Django 中定义视图,并能够构建自定义标签和过滤器来补充 Django 提供的强大模板引擎。

简介

第三章URL 映射、视图和模板 中,我们学习了如何在 Django 中构建视图和创建模板。然后,我们学习了如何使用这些视图来渲染我们构建的模板。在本章中,我们将通过使用 if-else 条件来构建视图的知识,成功处理不同类型的 HTTP 请求方法。相比之下,基于类的视图允许我们为每个我们想要处理的 HTTP 请求方法定义单独的方法。然后,根据接收到的请求类型,Django 会负责调用基于类的视图中的正确方法。

不仅仅能够根据不同的开发技术构建视图,Django 还内置了一个强大的模板引擎。这个引擎允许开发者为他们的网络应用程序构建可重用的模板。通过使用 模板标签过滤器,这种模板引擎的可重用性得到了进一步增强,它们有助于在模板中轻松实现常用功能,例如遍历数据列表、以特定样式格式化数据、从变量中提取文本以显示,以及覆盖模板特定块的内容。所有这些功能也扩展了 Django 模板的可重用性。

在我们学习本章内容的过程中,我们将探讨如何通过利用 Django 定义自定义模板标签和过滤器的能力来扩展 Django 提供的默认模板过滤器和模板标签集。这些自定义模板标签和过滤器可以以可重用的方式在我们的网络应用程序中实现一些常见功能。例如,在构建可以在网络应用程序的多个位置显示的用户个人资料徽章时,利用编写自定义模板包含标签的能力,只需将徽章模板插入我们想要的任何视图中,而不是重写整个徽章模板的代码或引入模板的额外复杂性,这会更好。

模板过滤器

在开发模板时,开发者通常只想在将模板变量渲染给用户之前更改其值。例如,考虑我们正在为 Bookr 用户构建一个个人资料页面。在那里,我们想显示用户已阅读的书籍数量。下面,我们还想显示一个列出他们已阅读的书籍的表格。

要实现这一点,我们可以从我们的视图传递两个独立的变量到 HTML 模板中。一个可以命名为books_read,表示用户阅读的书籍数量。另一个可以是book_list,包含用户阅读的书籍名称列表,例如:

<span class="books_read">You have read {{ books_read }} books</span>
<ul>
{% for book in book_list %}
<li>{{ book }} </li>
{% endfor %}
</ul>

或者,我们可以使用模板过滤器。Django 中的模板过滤器是简单的基于 Python 的函数,它接受一个变量作为参数(以及变量上下文中的任何附加数据),根据我们的要求更改其值,然后渲染更改后的值。

现在,通过使用 Django 中的模板过滤器,我们可以不使用两个独立的变量就获得前面代码片段的相同结果,如下所示:

<span class="books_read">You have read {{ book_list|length }}</span>
<ul>
{% for book in book_list %}
<li>{{ book }}</li>
{% endfor %}
</ul>

在这里,我们使用了 Django 提供的内置length过滤器。使用此过滤器会导致book_list变量的长度被评估并返回,然后在渲染过程中将其插入到我们的 HTML 模板中。

length一样,Django 还预包装了许多其他模板过滤器,可以直接使用。例如,lowercase过滤器将文本转换为全部小写格式,last过滤器可以用来返回列表中的最后一个项目,而json_script过滤器可以用来输出作为 JSON 值包裹在<script>标签中的传递给模板的 Python 对象。

注意

您可以参考 Django 的官方文档以获取 Django 提供的完整模板过滤器列表:docs.djangoproject.com/en/3.1/ref/templates/builtins/

自定义模板过滤器

Django 为我们提供了许多有用的过滤器,我们可以在项目开发过程中使用。但如果有人想要格式化一段特定的文本并使用不同的字体渲染它?或者如果有人想要根据后端中错误代码的映射将错误代码翻译成用户友好的错误消息。在这些情况下,预定义的过滤器不足以满足需求,我们希望编写自己的过滤器,以便在整个项目中重用。

幸运的是,Django 提供了一个易于使用的 API,我们可以用它来编写自定义过滤器。这个 API 为开发者提供了一些有用的装饰器函数,可以用来快速将 Python 函数注册为自定义模板过滤器。一旦 Python 函数被注册为自定义过滤器,开发者就可以开始在模板中使用该函数。

访问这些过滤器需要一个template库方法的实例。这个实例可以通过从 Django 的template模块中实例化Library()类来创建,如下所示:

from django import template
register = template.Library()

一旦创建了实例,我们现在可以使用模板库实例中的过滤器装饰器来注册我们的过滤器。

模板过滤器

要创建自定义模板过滤器,我们需要采取几个步骤。让我们尝试理解这些步骤以及它们如何帮助我们创建自定义模板过滤器。

设置存储模板过滤器的目录

需要注意的是,在创建自定义模板过滤器或模板标签时,我们需要将它们放在应用程序目录下的名为templatetags的目录中。这个要求是因为 Django 内部配置为在加载 Web 应用程序时寻找自定义模板标签和过滤器。如果目录名称不是templatetags,将导致 Django 无法加载我们创建的自定义模板过滤器和标签。

要创建此目录,首先,导航到您想要创建自定义模板过滤器的应用程序文件夹内,然后在终端中运行以下命令:

mkdir templatetags

一旦创建了目录,下一步是在templatetags目录内创建一个新的文件来存储我们的自定义过滤器的代码。这可以通过在templatetags目录内执行以下命令来完成:

touch custom_filter.py

注意

上述命令在 Windows 上无法工作。然而,您可以使用 Windows Explorer 导航到所需的目录并创建一个新文件。

或者,您可以使用 PyCharm 提供的 GUI 界面来完成此操作。

设置模板库

一旦创建了存储自定义过滤器代码的文件,我们现在可以开始实现我们的自定义过滤器代码。为了在 Django 中使用自定义过滤器,它们需要在模板中使用之前注册到 Django 的模板库中。为此,在上一节中创建的custom_filters.py文件中,我们首先需要从 Django 项目中导入模板模块:

from django import template

一旦导入被解决,下一步是创建模板库的实例,通过添加以下代码行:

register = template.Library()

Django 模板模块中的Library类实现为一个单例类,它在应用程序开始时只初始化一次并返回相同的对象。

一旦设置了模板库实例,我们现在可以继续实现我们的自定义过滤器。

实现自定义过滤器函数

Django 中的自定义过滤器实际上只是简单的 Python 函数,本质上需要以下参数:

  1. 过滤器应用的价值(必需)

  2. 需要传递给过滤器的任何附加参数(零个或多个)(可选)

为了作为模板过滤器工作,这些函数需要使用 Django 模板库实例的filter属性进行装饰。例如,自定义过滤器的通用实现将如下所示:

@register.filter
def my_filter(value, arg):
    # Implementation logic of the filter

通过这些,我们已经学习了如何实现自定义过滤器的基础知识。在我们开始第一个练习之前,让我们快速学习如何使用它们。

在模板中使用自定义过滤器

一旦创建了过滤器,就可以简单地在我们的模板中使用它。为此,过滤器首先需要被导入到模板中。这可以通过在模板文件顶部添加以下行轻松完成:

{% load custom_filter %}

当 Django 的模板引擎解析模板文件时,前一行会自动由 Django 解析以找到在 templatetags 目录下指定的正确模块。因此,custom_filter 模块中提到的所有过滤器都会自动在模板中可用。

在模板中使用我们的自定义过滤器就像添加以下行一样简单:

{{ some_value|generic_filter:"arg" }}

拥有了这些知识,现在让我们创建我们的第一个自定义过滤器。

练习 11.01:创建自定义模板过滤器

在这个练习中,你将编写一个名为 explode 的自定义过滤器,当提供一个字符串和一个用户提供的分隔符时,它会返回一个字符串列表。例如,考虑以下字符串:

names = "john,doe,mark,swain"

你将应用以下过滤器到这个字符串上:

{{ names|explode:"," }}

应用此过滤器后的输出应如下所示:

["john", "doe", "mark", "swain"]
  1. bookr 项目内创建一个新的应用程序,你可以用它来进行演示:

    python manage.py startapp filter_demo
    

    上述命令将在你的 Django 项目中设置一个新的应用程序。

  2. 现在,在 filter_demo 应用程序目录内创建一个名为 templatetags 的新目录来存储你的自定义模板过滤器的代码。为了创建目录,从终端应用或命令提示符中在 filter_demo 目录内运行以下命令:

    mkdir templatetags
    
  3. 一旦创建了目录,就在 templatetags 目录内创建一个名为 explode_filter.py 的新文件。

  4. 打开文件并添加以下行:

    from django import template
    register = template.Library()
    

    上述代码创建了一个 Django 库的实例,可以用来将我们的自定义过滤器注册到 Django 中。

  5. 添加以下代码以实现 explode 过滤器:

    @register.filter
    def explode(value, separator):
        return value.split(separator)
    

    explode 过滤器接受两个参数;一个是 value,即过滤器所使用的值,另一个是从模板传递给过滤器的 separator。过滤器将使用此分隔符将字符串转换为列表。

  6. 自定义过滤器准备就绪后,创建一个模板,以便应用此过滤器。为此,首先在 filter_demo 目录下创建一个名为 templates 的新文件夹,然后在其中创建一个名为 index.html 的新文件,内容如下:

    <html>
    <head>
      <title>Custom Filter Example</title>
    <body>
    explode_filter module so that it can be used inside the templates. To achieve this, Django will look for the explode_filter module under the templatetags directory and if, found, will load it for use.In the next line, you pass the `names` variable passed to the template and apply the `explode` filter to it, while also passing in the comma "`,`" as a separator value to the filter.
    
  7. 现在,模板创建完成后,接下来需要创建一个 Django 视图来渲染这个模板并将 name 变量传递给模板。为此,打开 views.py 文件并添加以下高亮代码:

    from django.shortcuts import render
    render helper from the django.shortcuts module, which helps render the templates. Once the import is complete, it defines a new view function named index(), which renders index.html.
    
  8. 现在,将视图映射到一个 URL,然后可以使用该 URL 在浏览器中渲染结果。为此,在 filter_demo 目录内创建一个名为 urls.py 的新文件,并将以下代码添加到其中:

    from django.urls import path
    from . import views
    urlpatterns = [path('', views.index, name='index')]
    
  9. filter_demo 应用程序添加到项目的 URL 映射中。为此,打开 bookr 项目目录下的 urls.py,并在 urlpatterns 中添加以下高亮行:

    urlpatterns = [path('filter_demo/', include('filter_demo.urls')),\
                   ….] 
    
  10. 最后,在 bookr 项目的 settings.py 中的 INSTALLED_APPS 部分下添加应用程序:

    INSTALLED_APPS = ….,\
                      INSTALLED_APPS section.
    
  11. 要查看自定义过滤器是否工作,请运行以下命令:

    python manage.py runserver localhost:8000
    

    现在,在您的浏览器中导航到以下页面:http://localhost:8000/filter_demo(或使用 localhost 而不是 127.0.0.1)。

    此页面应显示为 图 11.1 所示:

    ![图 11.1:使用爆炸过滤器显示的索引页面图 11.1:使用爆炸过滤器显示的索引页面通过这种方式,我们看到了如何在 Django 中快速创建一个自定义过滤器,然后将其用于我们的模板中。现在,让我们看看另一种类型的过滤器,即字符串过滤器,它仅对字符串类型的值起作用。## 字符串过滤器在 练习 11.01创建自定义模板过滤器 中,我们构建了一个自定义过滤器,它允许我们使用分隔符分割提供的字符串并从中生成一个列表。这个过滤器可以接受任何类型的变量,并根据提供的分隔符将其分割成值的列表。但如果我们想限制我们的过滤器只处理字符串,而不是任何其他类型的值,比如整数呢?要开发仅对 字符串 有效的过滤器,我们可以使用 Django 模板库提供的 stringfilter 装饰器。当使用 stringfilter 装饰器将 Python 方法注册为 Django 中的过滤器时,框架确保在过滤器执行之前将传递给过滤器的值转换为字符串。这减少了当传递非字符串值给我们的过滤器时可能出现的任何潜在问题。在 设置存储模板过滤器目录 部分中我们创建的 custom_filter.py 文件中实现步骤?我们可以在其中添加一个新的 Python 函数,该函数将作为我们的字符串过滤器。在我们能够实现字符串过滤器之前,我们首先需要导入 stringfilter 装饰器,该装饰器将自定义过滤器函数标记为字符串过滤器。我们可以在 custom_filters.py 文件中添加以下 import 语句来实现这一点:pyfrom django.template.defaultfilters import stringfilter现在,要实现我们的自定义字符串过滤器,可以使用以下语法:py@register.filter@stringfilterdef generic_string_filter(value, arg):    # Logic for string filter implementation使用这种方法,我们可以构建尽可能多的字符串过滤器,并像使用任何其他过滤器一样使用它们。# 模板标签模板标签是 Django 模板引擎的一个强大功能。它们允许开发者通过评估某些条件来生成 HTML,从而构建强大的模板,并有助于避免重复编写常见代码。我们可能使用模板标签的一个例子是网站导航栏中的注册/登录选项。在这种情况下,我们可以使用模板标签来评估当前页面的访问者是否已登录。基于这一点,我们可以渲染一个个人横幅或注册/登录横幅。在开发模板时,标签也是一个常见的现象。例如,考虑以下代码行,我们在 练习 11.01创建自定义模板过滤器 中使用它来在我们的模板中导入自定义过滤器:py{% load explode_filter %}这使用了一个名为 load 的模板标签,它负责将 explode 过滤器加载到模板中。与过滤器相比,模板标签功能更强大。虽然过滤器只能访问它们正在操作的价值,但模板标签可以访问整个模板的上下文,因此它们可以用来在模板内构建很多复杂的功能。让我们看看 Django 支持的模板标签的不同类型,以及我们如何构建我们自己的自定义模板标签。## 模板标签类型 Django 主要支持两种类型的模板标签:+ 简单标签:这些标签在提供的变量数据(以及任何附加的变量)上操作,并在它们被调用的同一模板中渲染。例如,一个这样的用例可能包括根据用户的用户名渲染自定义欢迎消息,或者根据用户名显示用户的最后登录时间。+ 包含标签:这些标签接收提供的数据变量,并通过渲染另一个模板来生成输出。例如,标签可以接收一个对象列表,并遍历它们以生成一个 HTML 列表。在接下来的章节中,我们将探讨如何创建这些不同类型的标签,并在我们的应用程序中使用它们。## 简单标签简单标签为开发者提供了一种构建模板标签的方法,这些标签可以从模板中接收一个或多个变量,处理它们,并返回一个响应。从模板标签返回的响应用于替换 HTML 模板内提供的模板标签定义。这类标签可以用来构建多种有用的功能,例如,解析日期,或者显示任何我们想要向用户展示的活跃警报(如果有的话)。简单标签可以通过模板库提供的 simple_tag 装饰器轻松创建,通过装饰应该作为模板标签起作用的 Python 方法。现在,让我们看看如何使用 Django 的模板库实现一个自定义简单标签。## 如何创建一个简单模板标签创建简单的模板标签遵循我们在 自定义模板过滤器 部分讨论的相同约定,有一些细微的差别。让我们回顾一下理解如何为在 Django 模板中使用而创建模板标签的过程。### 设置目录就像自定义过滤器一样,自定义模板标签也需要在同一个 templatetags 目录内创建,以便让 Django 的模板引擎能够发现它们。这个目录可以直接使用 PyCharm 图形界面创建,或者通过在我们要创建自定义标签的应用程序目录内运行以下命令来创建:pymkdir templatetags完成此操作后,我们现在可以创建一个新的文件来存储我们的自定义模板标签的代码,使用以下命令:pytouch custom_tags.py注意上述命令在 Windows 上将无法工作。然而,您可以使用 Windows 资源管理器创建一个新文件。### 设置模板库一旦设置好目录结构并且我们已经有一个文件用于存放自定义模板标签的代码,我们现在可以继续创建我们的模板标签。但在那之前,我们需要设置 Django 模板库的一个实例,就像我们之前做的那样。这可以通过向我们的 custom_tag.py 文件中添加以下几行代码来完成:pyfrom django import templateregister = template.Library()与自定义过滤器一样,模板库实例在这里用于注册自定义模板标签以供 Django 模板使用。### 实现简单模板标签 Django 中的简单模板标签是我们想要的任何数量的参数的 Python 函数。这些 Python 函数需要使用模板库中的 simple_tag 装饰器进行装饰,以便这些函数被注册为简单模板标签。以下代码片段展示了如何实现一个简单模板标签:py@register.simple_tagdef generic_simple_tag(arg1, arg2):    # Logic to implement a generic simple tag### 在模板中使用简单标签在 Django 模板中使用简单标签相当简单。在模板文件中,我们需要首先确保我们已将标签导入到模板中,方法是在模板文件顶部添加以下内容:py{% load custom_tag %}上述语句将加载我们之前定义的 custom_tag.py 文件中的所有标签,并在我们的模板中使它们可用。然后我们可以通过添加以下命令来使用我们的自定义简单标签:py{% custom_simple_tag "argument1" "argument2" %}现在,让我们将这个知识应用到实践中,创建我们的第一个自定义简单标签。## 练习 11.02:创建自定义简单标签在这个练习中,您将创建一个简单标签,它将接受两个参数:第一个将是问候消息,第二个将是用户的姓名。此标签将打印一个格式化的问候消息:1. 根据 练习 11.01 中所示的示例,创建自定义模板过滤器,让我们重用相同的目录结构来存储简单标签的代码。因此,首先,在 filter_demo/template_tags 目录下创建一个名为 simple_tag.py 的新文件。在这个文件中,添加以下代码: py from django import template register = template.Library() @register.simple_tag def greet_user(message, username):     return\     "{greeting_message},\      {user}!!!".format(greeting_message=message, user=username) 在这种情况下,您创建一个新的 Python 方法 greet_user(),它接受两个参数,message 是用于问候的消息,username 是要问候的用户的姓名。然后,该方法使用 @register.simple_tag 装饰器进行装饰,这表示该方法是一个简单标签,可以在模板中使用。1. 现在,创建一个新的模板,该模板将使用您的简单标签。为此,在 filter_demo/templates 目录下创建一个名为 simple_tag_template.html 的新文件,并将以下代码添加到其中: py <html> <head> <title>Simple Tag Template Example</title> </head> <body> {% load simple_tag %} {% greet_user "Hey there" username %} </body> </html> 在前面的代码片段中,你只是创建了一个使用你的自定义简单标签的裸骨 HTML 页面。加载自定义模板标签的语义与加载自定义模板过滤器类似,需要在模板中使用{% load %}标签。这个过程将在templatetags目录下寻找simple_tag.py模块,如果找到,将加载在该模块下定义的标签。 以下行显示了如何使用自定义模板标签: py {% greet_user "Hey there" username %} 在这里,你首先使用了 Django 的标签指定符{% %},在其中,你传递的第一个参数是需要使用的标签的名称,后面跟着第一个参数Hey there,这是问候信息,以及第二个参数username,它将从视图函数传递到模板中。1. 创建模板后,下一步是创建一个视图来渲染你的模板。为此,在filter_demo目录下的views.py文件中添加以下代码: py def greeting_view(request):     return render(request),\                  ('simple_tag_template.html', {'username': 'jdoe'}) 在前面的代码片段中,你创建了一个基于简单函数的视图,它将渲染你在步骤 2中定义的simple_tag_template,并将值'jdoe'传递给名为username的变量。1. 创建视图后,下一步是将它映射到应用中的 URL 端点。为此,打开filter_demo目录下的urls.py文件,并在urlpatterns列表中添加以下内容: py path('greet', views.greeting_view, name='greeting') 通过这种方式,greeting_view现在被映射到你的filter_demo应用的 URL 端点/greet。1. 要看到自定义标签的实际效果,通过运行以下命令启动你的 web 服务器: py python manage.py runserver localhost:8000 在浏览器中访问http://localhost:8000/filter_demo/greet(或使用localhost代替127.0.0.1),你应该看到以下页面: 图 11.2:使用自定义简单标签生成的问候信息

图 11.2:使用自定义简单标签生成的问候信息

通过这种方式,我们已经创建了我们的第一个自定义模板标签,并成功将其用于渲染模板,如图 11.2所示。现在,让我们看看简单标签的另一个重要方面,即与将模板中可用的上下文变量传递给模板标签相关。

在自定义模板标签中传递模板上下文

在上一个练习中,我们创建了一个简单标签,我们向其中传递了两个参数,即问候信息和用户名。但如果我们想向标签传递大量变量呢?或者简单地说,如果我们不想明确将用户的用户名传递给标签呢?

有时候,开发者希望访问模板中所有存在的变量和数据,以便在自定义标签内部可用。幸运的是,这很容易实现。

以我们之前的greet_user标签为例,让我们创建一个新的标签名为contextual_greet_user,并看看我们如何可以直接将模板中可用的数据传递给标签,而不是手动作为参数传递。

我们需要做的第一个修改是修改我们的装饰器,使其看起来如下:

@register.simple_tag(takes_context=True)

通过这种方式,我们告诉 Django,当我们的contextual_greet_user标签被使用时,Django 也应该传递它模板上下文,其中包含从视图传递到模板的所有数据。完成这个添加后,接下来我们需要做的是将我们的contextual_greet_user实现修改为接受添加的上下文作为参数。以下代码显示了修改后的contextual_greet_user标签形式,它使用我们的模板上下文来渲染问候消息:

@register.simple_tag(takes_context=True)
def contextual_greet_user(context, message):
    username = context['username']
    return "{greeting_message},\
            {user}".format(greeting_message=message, user=username)

在前面的代码示例中,我们可以看到contextual_greet_user()方法是如何修改的,以接受传递的上下文作为第一个参数,然后是用户传递的问候消息。

要利用这个修改后的模板标签,我们只需要将filter_demo下的simple_tag_template.html中的contextual_greet_user标签的调用更改如下:

{% contextual_greet_user "Hey there" %}

然后,当我们重新加载我们的 Django Web 应用程序时,http://localhost:8000/filter_demo/greet的输出应该类似于练习 11.02的第5 步中显示的输出,创建自定义简单标签

通过这种方式,我们了解了如何构建一个简单的标签并处理将模板上下文传递给标签。现在,让我们看看如何构建一个包含标签,它可以用来以另一个模板描述的格式渲染数据。

包含标签

简单标签允许我们构建接受一个或多个输入变量的标签,对它们进行一些处理,并返回一个输出。然后,这个输出被插入到简单标签被使用的地方。

但如果我们想构建标签,这些标签不是返回文本输出,而是返回一个 HTML 模板,然后可以用来渲染页面的部分。例如,许多 Web 应用程序允许用户向他们的个人资料添加自定义小部件。这些单独的小部件可以作为包含标签构建并独立渲染。这种方法将基础页面模板和单独的模板的代码分开,因此允许易于重用以及重构。

开发自定义包含标签的过程与我们开发简单标签的过程类似。这涉及到使用模板库提供的inclusion_tag装饰器。因此,让我们看看我们如何做到这一点。

实现包含标签

包含标签是那些在模板内部使用时作为响应渲染模板的标签。这些标签可以以与其他自定义模板标签类似的方式实现,只需进行一些小的修改。

包含标签也是简单的 Python 函数,可以接受多个参数,其中每个参数映射到从调用标签的模板传递的参数。这些标签使用 Django 模板库中的 inclusion_tag 装饰器进行装饰。inclusion_tag 装饰器接受一个参数,即模板的名称,该名称应在包含标签的处理过程中作为响应进行渲染。

包含标签的通用实现将类似于以下代码片段所示:

@register.inclusion_tag('template_file.html')
def my_inclusion_tag(arg):
    # logic for processing
    return {'key1': 'value1'}

注意这个例子中的返回值。包含标签应该返回一个字典,该字典将用于渲染 template_file.html 文件,该文件作为 inclusion_tag 装饰器中的参数指定。

在模板中使用包含标签

包含标签可以轻松地用于模板文件中。为此,首先按照以下方式导入标签:

{% load custom_tags %}

然后通过像使用其他标签一样使用该标签:

{% my_inclusion_tag "argument1" %}

此标签的渲染响应将是一个子模板,它将在包含标签被使用的我们的主模板内进行渲染。

练习 11.03:构建自定义包含标签

在这个练习中,我们将构建一个自定义的 inclusion 标签,该标签将渲染用户阅读的书籍列表:

  1. 对于这个练习,你将继续使用之前练习中相同的演示文件夹。首先,在 filter_demo/templatetags 目录下创建一个名为 inclusion_tag.py 的新文件,并在其中编写以下代码:

    from django import template
    register = template.Library()
    @register.inclusion_tag('book_list.html')
    def book_list(books):
        book_list = [book_name for book_name, \
                     book_author in books.items()]
        return {'book_list': book_list}
    

    使用 @register.inclusion_tag 装饰器来标记方法作为自定义包含标签。此装饰器接受模板名称作为参数,该参数应用于渲染标签函数返回的数据。

    在装饰器之后,您定义一个函数来实现自定义包含标签的逻辑。此函数接受一个名为 books 的单个参数。此参数将从模板文件传递,并包含读者已阅读的书籍列表(以 Python 字典的形式)。在定义内部,您将字典转换为 Pythonic 的书籍名称列表。字典中的键映射到书籍名称,值映射到作者:

    books_list = [book_name for book_name, \
                  book_author in books.items()]
    

    一旦列表形成,以下代码将列表作为上下文返回,传递给包含标签的模板(在本例中为 book_list.html):

    return {'book_list': books_list}
    

    此方法返回的值将被 Django 传递给 book_list.html 模板,然后内容将被渲染。

  2. 接下来,创建实际的模板,它将包含模板标签的渲染结构。为此,在 filter_demo/templates 目录下创建一个名为 book_list.html 的新模板文件,并向其中添加以下内容:

    <ul>
      {% for book in book_list %}
    <li>{{ book }}</li>
      {% endfor %}
    </ul>
    

    在这里,在您创建的新模板文件中,您创建了一个无序列表,该列表将包含用户已阅读的书籍列表。接下来,使用 for 模板标签,您遍历 book_list 中的值,这些值将由自定义模板函数提供:

    {% for book in book_list %}
    

    此迭代会产生几个列表项,如下定义:

    <li>{{ book }}</li>
    

    列表项是通过从book_list传递的内容生成的,该内容被传递到模板中。for标签会根据book_list中项目数量执行多次。

  3. book_list标签定义了模板后,修改现有的问候模板,使其内部可用此标签,并使用它来显示用户已阅读的书籍列表。为此,修改filter_demo/templates目录下的simple_tag_template.html文件,并将代码修改如下:

    <html>
    <head>
      <title>Simple Tag Template Example</title>
    </head>
    <body>
    {% load simple_tag inclusion_tag %}
    {% greet_user "Hey" username %}
      <br />
      <span class="message">You have read the following books     till date</span>
    {% book_list books %}
    </body>
    </html>
    

    在这个片段中,您首先通过编写以下代码加载了inclusion_tag模块:

    {% load simple_tag inclusion_tag %}
    

    标签加载后,您现在可以在模板的任何位置使用它。要使用它,您以以下格式添加了book_list标签:

    {% book_list books %}
    

    此标签接受一个参数,即包含书籍的字典,其中键是书籍标题,键的值是书籍的作者。在此阶段,您甚至可以自定义问候信息;在这个步骤中,我们选择了简单的"Hey"而不是"Hey there"。

  4. 模板现在已修改,最后一步是将所需数据传递给模板。为了实现这一点,修改filter_demo目录下的views.py文件,并将问候视图函数修改如下:

    def greeting_view(request):
        books = {"The night rider": "Ben Author",\
                 "The Justice": "Don Abeman"}
        return render(request),\
                     ('simple_tag_template.html'),\
                     ({'username': 'jdoe', 'books': books})
    

    在这里,您修改了greeting_view函数,添加了书籍及其作者的字典,并将其传递给simple_tag_template上下文。

  5. 在实施上述更改后,现在是时候渲染修改后的模板了。为此,通过运行以下命令重新启动您的 Django 应用程序服务器:

    python manage.py runserver localhost:8080
    

    导航到http://localhost:8080/filter_demo/greet,现在应该会渲染一个类似于以下截图的页面:

    图 11.3:用户访问问候端点时阅读的书籍列表

图 11.3:用户访问问候端点时阅读的书籍列表

页面显示了用户访问问候端点时阅读的书籍列表。您在页面上看到的列表是使用内联标签渲染的。首先单独创建列出这些书籍的模板,然后使用内联标签将其添加到页面上。

注意

我们对filter_demo应用程序的工作已经完成。如果您想进一步定制此应用程序以练习您学到的概念,您可以继续这样做。由于该应用程序仅用于解释自定义模板过滤器和模板标签的概念,并且与我们要构建的bookr应用程序无关,因此您不会在 GitHub 存储库的final/bookr应用程序文件夹中找到它。

有了这个,我们现在有了构建高度复杂的模板过滤器或自定义标签的基础,这些标签可以帮助我们开发想要工作的项目。

现在,让我们重新审视 Django 视图,并深入到一个新的视图领域,称为基于类的视图。由 Django 提供,这些视图帮助我们利用面向对象编程的力量,并允许代码的重用以渲染视图。

Django 视图

回想一下,Django 中的视图是一段 Python 代码,它允许接收请求,根据请求执行操作,然后向用户返回响应,因此构成了我们 Django 应用程序的重要部分。

在 Django 内部,我们有两种不同的方法来构建我们的视图,其中一种我们在前面的示例中已经看到,被称为基于函数的视图,而另一种我们很快就会介绍,被称为基于类的视图:

  • HTTPRequest类型对象作为它们的第一位置参数,并返回一个HTTPResponse类型对象,这对应于视图在处理请求后想要执行的操作。在前面的练习中,index()greeting_view()是 FBV 的例子。

  • 基于类的视图CBV):CBV 是紧密遵循 Python 面向对象原则的视图,并允许在基于类的表示中映射视图调用。这些视图在本质上具有专业性,并且给定的 CBV 执行特定的操作。CBV 提供的优势包括视图的易于扩展性和代码的重用,这在 FBV 中可能是一个复杂的任务。

现在,基本定义已经明确,并且我们已经掌握了 FBV 的知识,让我们来看看 CBV(基于类的视图)并看看它们为我们准备了什么。

基于类的视图

Django 提供了不同的方式,让开发者可以为他们的应用程序编写视图。一种方式是将 Python 函数映射为视图函数以创建 FBV。另一种创建视图的方式是使用基于 Python 类的 Python 对象实例。这些被称为 CBV。一个重要的问题是,当我们已经可以使用 FBV 方法创建视图时,为什么还需要 CBV?

在创建 FBV(基于函数的视图)时,我们的想法是,有时我们可能需要反复复制相同的逻辑,例如处理某些字段或处理某些请求类型的逻辑。尽管创建逻辑上分离的函数来处理特定的逻辑是完全可能的,但随着应用程序复杂性的增加,这项任务变得难以管理。

这就是 CBV(基于类的视图)派上用场的地方,它们抽象出了我们需要编写的常见重复代码的实现,例如模板的渲染。同时,它们也通过使用继承和混入使代码的重用变得容易。例如,以下代码片段显示了 CBV 的实现:

from django.http import HttpResponse
from django.views import View
class IndexView(View):
    def get(self, request):
        return HttpResponse("Hey there!")

在前面的示例中,我们通过继承 Django 提供的内置视图类构建了一个简单的 CBV。

使用这些 CBV(基于类的视图)也很简单。例如,假设我们想在应用程序中将IndexView映射到一个 URL 端点。在这种情况下,我们只需要在应用程序的urls.py文件中的urlpatterns列表中添加以下行:

urlpatterns = [path('my_path', IndexView.as_view(), \
                    name='index_view')]

在这里,正如我们可以观察到的,我们使用了 CBV 的as_view()方法。每个 CBV 都实现了as_view()方法,这使得视图类可以通过返回视图控制器实例来映射到 URL 端点。

Django 提供了一些内置的 CBV,它们提供了许多常见任务的实现,例如如何渲染模板,或者如何处理特定的请求。内置的 CBV 有助于在处理基本功能时避免从头开始重写代码,从而实现代码的可重用性。以下是一些内置视图的示例:

  • 请求方法,例如GETPOSTPUTDELETE,视图将自动根据接收到的请求类型委托给负责处理该请求的方法。

  • TemplateView:一个视图,可以根据调用 URL 中提供的模板数据参数来渲染模板。这允许开发者轻松渲染模板,而无需编写任何与渲染处理相关的逻辑。

  • RedirectView:一个视图,可以根据用户请求自动将用户重定向到正确的资源。

  • DetailView:一个映射到 Django 模型并可以使用选择的模板渲染从模型获取的数据的视图。

前面的视图只是 Django 默认提供的内置视图的一部分,随着我们进入本章,我们将介绍更多。

现在,为了更好地理解 CBV 在 Django 中的工作方式,让我们尝试构建我们的第一个 CBV。

练习 11.04:使用 CBV 创建图书目录

在这个练习中,你将创建一个基于类的表单视图,这将帮助你构建一个图书目录。这个目录将包括书的名称和书的作者名称。

注意

为了理解类视图的概念,我们将在 Bookr 中创建一个单独的应用程序,它有自己的模型和表单,这样我们的现有代码就不会受到影响。就像filter_demo一样,我们不会将此应用程序包含在我们的 GitHub 仓库的final/bookr文件夹中。

  1. 要开始,在我们的bookr项目中创建一个新的应用程序,并将其命名为book_management。这可以通过简单地运行以下命令来完成:

    python manage.py startapp book_management
    
  2. 现在,在构建图书目录之前,你首先需要定义一个 Django 模型,这将帮助你将记录存储在数据库中。为此,打开你刚刚创建的book_management应用下的models.py文件,并定义一个新的名为Book的模型,如下所示:

    from django.db import models
    class Book(models.Model):
        name = models.CharField(max_length=255)
        author = models.CharField(max_length=50)
    

    模型包含两个字段,即书名和作者名。有了模型,你需要将其迁移到数据库中,这样你就可以开始在数据库中存储你的数据。

  3. 一旦完成所有前面的步骤,将你的book_management应用程序添加到INSTALLED_APPS列表中,这样 Django 就可以发现它,你就可以正确使用你的模型。为此,打开bookr目录下的settings.py文件,并在INSTALLED_APPS部分的最后位置添加以下代码:

    INSTALLED_APPS = [….,\
                      'book_management']
    
  4. 通过运行以下两个命令将你的模型迁移到数据库中。这些命令首先创建一个 Django 迁移文件,然后在你的数据库中创建一个表:

    python manage.py makemigrations
    python manage.py migrate
    
  5. 现在,数据库模型已经就绪,让我们创建一个新的表单,我们将使用它来捕捉有关书籍的信息,例如书名、作者和 ISBN。为此,在book_management目录下创建一个名为forms.py的新文件,并在其中添加以下代码:

    from django import forms
    from .models import Book
    class BookForm(forms.ModelForm):
        class Meta:
            model = Book
            fields = ['name', 'author']
    

    在前面的代码片段中,你首先导入了 Django 的表单模块,这将允许你轻松创建表单,并也将提供表单的渲染能力。下一行导入了将存储表单数据的模型:

    from django import forms
    from .models import Book
    

    在下一行中,你创建了一个名为BookForm的新类,它继承自ModelForm。这只是一个将模型的字段映射到表单的类。为了成功实现模型和表单之间的这种映射,你在BookForm类下定义了一个新的子类Meta,并将属性 model 指向Book模型,将属性 fields 设置为要在表单中显示的字段列表:

    class Meta:
        model = Book
        fields = ['name', 'author']
    

    这允许ModelForm在需要时渲染正确的表单 HTML。ModelForm类提供了一个内置的Form.save()方法,当使用时,将表单中的数据写入数据库,从而帮助避免编写冗余代码。

  6. 现在你已经准备好了模型和表单,继续实现一个视图,该视图将渲染表单并接受用户的输入。为此,打开book_management目录下的views.py文件,并向文件中添加以下代码行:

    from django.http import HttpResponse
    from django.views.generic.edit import FormView
    from django.views import View
    from .forms import BookForm
    class BookRecordFormView(FormView):
        template_name = 'book_form.html'
        form_class = BookForm
        success_url = '/book_management/entry_success'
        def form_valid(self, form):
            form.save()
            return super().form_valid(form)
    class FormSuccessView(View):
        def get(self, request, *args, **kwargs):
            return HttpResponse("Book record saved successfully")
    

    在前面的代码片段中,你创建了两个主要视图,一个是BookRecordFormView,它还负责渲染书籍目录表单,另一个是FormSuccessView,你将使用它来渲染如果表单数据成功保存的成功消息。现在让我们分别查看这两个视图,并了解我们在做什么。

    首先,你创建了一个名为BookRecordFormView的新视图 CBV,它继承自FormView

    class BookRecordFormView(FormView)
    

    FormView类允许你轻松创建处理表单的视图。为此类,你需要提供某些参数,例如它将渲染以显示表单的模板名称、它应该用于渲染表单的表单类,以及当表单处理成功时重定向到的成功 URL:

    template_name = 'book_form.html'
    form_class = BookForm
    success_url = '/book_management/entry_success'
    

    FormView类还提供了一个form_valid()方法,当表单成功完成验证时被调用。在form_valid()方法中,我们可以决定要做什么。对于我们的用例,当表单验证成功完成时,我们首先调用form.save()方法,将表单数据持久化到数据库中,然后调用基类的form_valid()方法,如果表单验证成功,这将导致表单视图重定向到成功 URL:

    def form_valid(self, form):
        form.save()
        return super().form_valid(form)
    class FormSuccessView(View)
    

    在这个类中,我们重写了get()方法,当表单成功保存时将调用此方法。在get()方法中,通过返回一个新的HttpResponse来渲染一个简单的成功消息:

        def get(self, request, *args, **kwargs):
            return HttpResponse("Book record saved successfully")
    
  7. 现在,创建一个将用于渲染表单的模板。为此,在book_management目录下创建一个新的templates文件夹,并创建一个名为book_form.html的新文件。在文件内添加以下代码行:

    <html>
      <head>
        <title>Book Record Insertion</title>
      </head>
      <body>
        <form method="POST">
          {% csrf_token %}
          {{ form.as_p }}
          <input type="submit" value="Save record" />
        </form>
      </body>
    </html>
    

    在这个代码片段中,需要讨论两个重要的事情。

    第一点是使用{% csrf_token %}标签。此标签被插入以防止表单遇到csrf_token问题。这是 Django 提供的一个内置模板标签,用于避免此类攻击。它通过为每个渲染的表单实例生成一个唯一的令牌来实现。

    第二点是使用{{ form.as_p }}模板变量。此变量的数据由基于FormView的视图自动提供。as_p调用使得表单字段被渲染在<p></p>标签内。

  8. 现在已经构建了 CBVs(类视图),接下来将它们映射到 URL 上,这样你就可以开始使用它们来添加新的图书记录。为此,在book_management目录下创建一个新的名为urls.py的文件,并将以下代码添加到其中:

    from django.urls import path
    from .views import BookRecordFormView, FormSuccessView
    urlpatterns = [path('new_book_record',\
                   BookRecordFormView.as_view(),\
                   name='book_record_form'),\
                   path('entry_success', FormSuccessView.as_view()),\
                       (name='form_success')]
    

    前面的代码片段的大部分与之前你编写的类似,但在将 CBVs 映射到 URL 模式的方式上有一点不同。当使用 CBVs 时,我们不是直接添加函数名,而是使用类名并使用它的as_view方法,该方法将类对象映射到视图。例如,要将BookRecordFormView映射为一个视图,我们将使用BookRecordFormView.as_view()

  9. 在我们的urls.py文件中添加了 URL 后,接下来要添加我们的应用程序 URL 映射到bookr项目中。为此,打开bookr应用下的urls.py文件,并将以下行添加到urlpatterns中:

    urlpatterns = [path('book_management/',\
                   include('book_management.urls')),\
                   ….]
    
  10. 现在,通过运行以下命令启动你的开发服务器:

    python manage.py runserver localhost:8080
    

    然后,访问http://localhost:8080/book_management/new_book_record(或者使用127.0.0.1代替localhost。)

    如果一切顺利,你将看到如下所示的页面:

    图 11.4:添加新书籍到数据库的视图

图 11.4:添加新书籍到数据库的视图

点击“保存记录”后,您的记录将被写入数据库,并显示以下页面:

图 11.5:记录成功插入时渲染的模板

图 11.5:记录成功插入时渲染的模板

通过这种方式,我们创建了自己的 CBV(类视图),这使得我们能够为新书籍保存记录。带着我们对 CBV 的知识,现在让我们看看如何借助 CBV 执行创建、读取、更新、删除(CRUD)操作。

使用 CBV 进行 CRUD 操作

在与 Django 模型一起工作时,我们遇到的最常见的模式之一涉及在数据库中存储的对象的创建、读取、更新和删除。Django 管理界面使我们能够轻松地实现这些 CRUD 操作,但如果我们想构建自定义视图以获得相同的功能呢?

事实上,Django 的 CBV 允许我们非常容易地实现这一点。我们只需要编写我们的自定义 CBV,并从 Django 提供的内置基类中继承。基于我们现有的书籍记录管理示例,让我们看看如何在 Django 中构建基于 CRUD 的视图。

创建视图

要构建一个帮助进行对象创建的视图,我们需要打开位于book_management目录下的view.py文件,并向其中添加以下代码行:

from django.views.generic.edit import CreateView
from .models import Book
class BookCreateView(CreateView):
model = Book
    fields = ['name', 'author']
    template_name = 'book_form.html'
    success_url = '/book_management/entry_success'

通过这种方式,我们已经为书籍资源创建了CreateView。在我们能够使用它之前,我们需要将其映射到一个 URL。为此,我们可以打开位于book_management目录下的urls.py文件,并在urlpatterns列表下添加以下条目:

urlpatterns = [….,\
               path('book_record_create'),\
                    (BookCreateView.as_view(), name='book_create')]

现在,当我们访问http://127.0.0.1:8000/book_management/book_record_create时,我们将看到以下页面:

图 11.6:基于创建视图插入新书籍记录的视图

图 11.6:基于创建视图插入新书籍记录的视图

这看起来与使用表单视图时得到的结果相似。在填写数据并点击“保存记录”后,Django 会将数据保存到数据库中。

更新视图

在此视图中,我们想要更新特定记录的数据。为此,我们需要打开位于book_management目录下的view.py文件,并向其中添加以下代码行:

from django.views.generic.edit import UpdateView
from .models import Book
class BookUpdateView(UpdateView):
    model = Book
    fields = ['name', 'author']
    template_name = 'book_form.html'
    success_url = '/book_management/entry_success'

在前面的代码片段中,我们使用了内置的UpdateView模板,它允许我们更新存储的记录。这里的字段属性应该接受我们希望允许用户更新的字段名称。

视图创建完成后,下一步是添加 URL 映射。为此,我们可以打开位于book_management目录下的urls.py文件,并向其中添加以下代码行:

urlpatterns = [path('book_record_update/<int:pk>'),\
                   (BookUpdateView.as_view(), name='book_update')]

在此示例中,我们将<int:pk>附加到 URL 字段。这表示我们将要输入的字段以检索记录。在 Django 模型中,Django 插入一个整型主键,用于唯一标识记录。在 URL 映射中,这是我们要求插入的字段。

现在,当我们尝试打开http://127.0.0.1:8000/book_management/book_record_update/1时,它应该显示我们插入到数据库中的第一条记录,并允许我们编辑它:

图 11.7:基于更新视图显示书籍记录更新模板的视图

图 11.7:基于更新视图显示书籍记录更新模板的视图

删除视图

删除视图,正如其名所示,是一个从我们的数据库中删除记录的视图。为了为我们的Book模型实现此类视图,您需要打开book_management目录下的views.py文件,并将其中的以下代码片段添加到该文件中:

from django.views.generic.edit import DeleteView
from .models import Book
class BookDeleteView(DeleteView):
    model = Book
    template_name = 'book_delete_form.html'
    success_url = '/book_management/delete_success

通过这种方式,我们刚刚为我们的书籍记录创建了一个删除视图。正如我们所见,此视图使用了一个不同的模板,我们希望从用户那里确认的是,他们是否真的想要删除记录?为了实现这一点,你可以创建一个新的模板文件book_delete_form.html,并将其中的以下代码添加到该文件中:

<html>
  <head>
    <title>Delete Book Record</title>
  </head>
  <body>
    <p>Delete Book Record</p>
    <form method="POST">
      {% csrf_token %}
      Do you want to delete the book record?
      <input type="submit" value="Delete record" />
    </form>
  </body>
</html>

然后,我们可以通过修改book_management目录下的urls.py文件中的urlpatterns列表来为我们的删除视图添加映射,如下所示:

urlpatterns = [….,\
               path('book_record_delete/<int:pk>'),\
               (BookDeleteView.as_view(), name='book_delete')]

现在,当访问http://127.0.0.1:8000/book_management/book_record_delete/1时,我们应该看到以下页面:

图 11.8:基于删除视图类的删除书籍记录视图

图 11.8:基于删除视图类的删除书籍记录视图

点击“删除记录”按钮后,记录将从数据库中删除,并渲染删除成功页面。

读取视图

在此视图中,我们希望看到数据库中存储的书籍记录列表。为了实现这一点,我们将在book_management目录下的views.py文件中添加以下代码行来构建一个名为DetailView的视图,该视图将渲染我们请求的书籍的详细信息。构建此视图,我们可以在book_management目录下的views.py文件中添加以下代码行:

from django.views.generic import DetailView
class BookRecordDetailView(DetailView):
    model = Book
    template_name = 'book_detail.html'

在前面的代码片段中,我们创建了一个DetailView,它将帮助我们渲染我们请求的书籍 ID 的详细信息。详细视图内部使用我们提供的书籍 ID 查询我们的数据库模型,如果找到记录,则通过将存储在记录中的数据作为对象变量传递到模板上下文中来渲染模板。

完成此操作后,下一步是创建我们的书籍详细信息的模板。为此,我们需要在book_management应用程序的templates目录下创建一个名为book_detail.html的新模板文件,其内容如下:

<html>
  <head>
    <title>Book List</title>
  </head>
  <body>
    <span>Book Name: {{ object.name }}</span><br />
    <span>Author: {{ object.author }}</span>
  </body>
</html>

现在,有了模板,我们最后需要做的是为 Detail 视图添加 URL 映射。这可以通过将以下内容追加到 book_management 应用程序的 urls.py 文件中的 urlpatterns 列表来完成:

path('book_record_detail/<int:pk>'),\
     (BookRecordDetail.as_view(), name='book_detail')

现在,所有这些配置完成后,如果我们现在打开 http://127.0.0.1:8000/book_management/book_record_detail/1,我们将看到有关我们书籍的详细信息,如图所示:

图 11.9:尝试访问先前存储的书籍记录时渲染的视图

图 11.9:尝试访问先前存储的书籍记录时渲染的视图

通过前面的示例,我们只是为我们的 Book 模型启用了 CRUD 操作,而且这一切都是在使用 CBVs(类视图)的同时完成的。

活动 11.01:使用包含标签在用户个人资料页面渲染详细信息

在此活动中,您将创建一个自定义包含标签,该标签有助于开发一个渲染用户详细信息以及他们阅读的书籍列表的用户个人资料页面。

以下步骤应有助于您成功完成此活动:

  1. bookr 项目的 reviews 应用程序下创建一个新的 templatetags 目录,以提供一个可以创建自定义模板标签的地方。

  2. 创建一个名为 profile_tags.py 的新文件,该文件将存储包含标签的代码。

  3. profile_tags.py 文件中,导入 Django 的模板库并使用它来初始化模板库类的实例。

  4. reviews 应用程序导入 Review 模型以获取用户的评论。这将用于过滤当前用户的评论以在用户个人资料页面渲染。

  5. 接下来,创建一个名为 book_list 的新 Python 函数,该函数将包含包含标签的逻辑。此函数应仅接受一个参数,即当前登录用户的用户名。

  6. book_list 函数的主体中,添加获取此用户评论的逻辑并提取此用户阅读的书籍名称。假设用户已经阅读了他们提供评论的所有书籍。

  7. 使用 inclusion_tag 装饰器装饰此 book_list 函数,并为其提供一个模板名称 book_list.html

  8. 创建一个名为 book_list.html 的新模板文件,该文件在 步骤 7 中被指定为包含标签装饰器。在这个文件中,添加代码以渲染书籍列表。这可以通过使用 for 循环结构和为列表中的每个项目渲染 HTML 列表标签来实现。

  9. 修改 templates 目录下现有的 profile.html 文件,该文件将用于渲染用户个人资料。在这个模板文件中,包含自定义模板标签并使用它来渲染用户阅读的书籍列表。

    一旦实现了上述所有步骤,启动应用程序服务器并访问用户个人资料页面应该会渲染一个类似于 图 11.10 中所示的页面:

    图 11.10:用户个人资料页面,列出了用户阅读过的书籍

图 11.10:用户个人资料页面,列出了用户阅读过的书籍

注意

该活动的解决方案可以在packt.live/2Nh1NTJ找到。

摘要

在本章中,我们学习了 Django 的高级模板概念,并了解了我们如何创建自定义模板标签和过滤器来适应各种用例,并支持应用程序中组件的可重用性。然后我们探讨了 Django 如何提供灵活性,使我们能够实现 FBVs(函数视图)和 CBVs 来渲染我们的响应。

在探索 CBVs(类视图)的过程中,我们学习了它们如何帮助我们避免代码重复,以及我们如何利用内置的 CBVs 来渲染保存数据的表单,帮助我们更新现有记录,并在我们的数据库资源上实现 CRUD 操作。

随着我们进入下一章,我们现在将利用我们构建 CBVs 的知识来实施 Django 中的 REST APIs。这将使我们能够在 Bookr 应用程序内部对数据进行定义良好的 HTTP 操作,而不需要在应用程序内部维护任何状态。

第十二章:12. 构建 REST API

概述

本章介绍了REST APIDjango REST 框架DRF)。你将从为 Bookr 项目实现一个简单的 API 开始。接下来,你将学习模型实例的序列化,这是将数据传递到 Django 应用前端的关键步骤。你将探索不同类型的 API 视图,包括函数式和基于类的类型。到本章结束时,你将能够实现自定义 API 端点,包括简单的身份验证。

简介

在上一章中,我们学习了模板和基于类的视图。这些概念极大地帮助我们扩展了提供给用户的前端(即在他们的网络浏览器中)的功能范围。然而,这还不足以构建一个现代 Web 应用程序。Web 应用程序通常使用完全独立的库来构建前端,例如ReactJSAngularJS。这些库提供了构建动态用户界面的强大工具;然而,它们不能直接与我们的后端 Django 代码或数据库通信。前端代码仅在网络浏览器中运行,并且无法直接访问后端服务器上的任何数据。因此,我们需要创建一种方法,让这些应用程序能够“与”我们的后端代码“交流”。在 Django 中实现这一点的最佳方法之一就是使用 REST API。

API代表应用程序编程接口。API 用于促进不同软件组件之间的交互,并且它们使用HTTP超文本传输协议)进行通信。这是服务器和客户端之间通信的标准协议,是网络信息传输的基础。API 以 HTTP 格式接收请求并发送响应。

在本章的使用案例中,API 将帮助我们促进 Django 后端和前端 JS 代码之间的交互。例如,想象一下我们想要创建一个前端应用程序,允许用户向 Bookr 数据库添加新书。用户的网络浏览器会向我们的 API 发送一条消息(一个 HTTP 请求),表示他们想要为新书创建一个条目,并可能在该消息中包含一些关于书籍的细节。我们的服务器会发送一个响应来报告书籍是否成功添加。然后网络浏览器将能够向用户显示他们操作的结果。

REST API

REST代表表示性状态转移。大多数现代 Web API 都可以归类为 REST API。REST API 是一种简单的 API 类型,它专注于在数据库服务器和前端客户端之间通信和同步对象的状态

例如,想象一下你正在更新你登录账户的网站上的详细信息。当你访问账户详情页面时,网络服务器会告诉你的浏览器与你账户相关的各种详细信息。当你更改该页面的值时,浏览器会将更新后的详细信息发送回网络服务器,并告诉它更新数据库中的这些详细信息。如果操作成功,网站将显示确认消息。

这是一个非常简单的例子,说明了前端和后端系统之间所知的 解耦 架构。解耦提供了更大的灵活性,并使得更新或更改架构中的组件变得更加容易。所以,假设你想创建一个新的前端网站。在这种情况下,你根本不需要更改后端代码,只要你的新前端能够构建出与旧的一个相同的 API 请求即可。

REST API 是 无状态的,这意味着客户端和服务器在通信过程中都不会存储任何状态。每次请求时,数据都会被处理,并返回响应,而无需协议本身存储任何中间数据。这意味着 API 正在独立处理每个请求。它不需要存储有关会话本身的信息。这与维护会话信息的 TCP 等有状态协议形成对比。

因此,正如其名所示,RESTful 网络服务是一组用于执行一系列任务的 REST API 的集合。例如,如果我们为 Bookr 应用程序开发一组用于执行特定任务的 REST API,那么我们可以称它为 RESTful 网络服务。

Django REST 框架

Django REST 框架,简称 DRF,是一个开源的 Python 库,可用于为 Django 项目开发 REST API。DRF 内置了大多数必要的功能,以帮助开发任何 Django 项目的 API。在本章中,我们将使用它来为我们的 Bookr 项目开发 API。

安装和配置

在 PyCharm 的虚拟环境设置中安装 djangorestframework。在您的终端应用程序或命令提示符中输入以下代码来完成此操作:

pip install djangorestframework

接下来,打开 settings.py 文件,并将 rest_framework 添加到 INSTALLED_APPS 中,如下面的代码片段所示:

INSTALLED_APPS = ['django.contrib.admin',\
                  'django.contrib.auth',\
                  ‹django.contrib.contenttypes›,\
                  'django.contrib.sessions',\
                  'django.contrib.messages',\
                  'django.contrib.staticfiles',\
                  ‹rest_framework›,\
                  ‹reviews›]

现在,你已经准备好开始使用 DRF 创建你的第一个简单 API。

功能性 API 视图

第三章URL 映射、视图和模板 中,我们学习了简单的功能性视图,它接受一个请求并返回一个响应。我们可以使用 DRF 编写类似的函数视图。然而,请注意,基于类的视图更常用,将在下一章中介绍。一个功能性视图是通过简单地在一个普通视图上添加以下装饰器来创建的,如下所示:

from rest_framework.decorators import api_view
@api_view
def my_view(request):
     ...

这个装饰器将功能视图转换为 DRF 的 APIView 子类。这是一种快速地将现有视图作为 API 部分包含进来的方法。

练习 12.01:创建一个简单的 REST API

在这个练习中,您将使用 DRF 创建您的第一个 REST API,并使用功能视图实现一个端点。您将创建这个端点来查看数据库中的书籍总数:

注意

您需要在本系统上安装 DRF 才能继续此练习。如果您尚未安装,请确保您参考了本章前面标题为 安装和配置 的部分。

  1. bookr/reviews 文件夹中创建 api_views.py

    REST API 视图的工作方式类似于 Django 的传统视图。我们本可以将 API 视图以及其他视图一起添加到 views.py 文件夹中。但是,将我们的 REST API 视图放在单独的文件中将帮助我们保持代码库的整洁。

  2. api_views.py 中添加以下代码:

    from rest_framework.decorators import api_view
    from rest_framework.response import Response
    from .models import Book
    @api_view()
    def first_api_view(request):
        num_books = Book.objects.count()
        api_view decorator, which will be used to convert our functional view into one that can be used with DRF, and the second line imports Response, which will be used to return a response.The `view` function returns a `Response` object containing a dictionary with the number of books in our database (see the highlighted part).Open `bookr/reviews/urls.py` and import the `api_views` module. Then, add a new path to the `api_views` module in the URL patterns that we have developed throughout this course, as follows:
    
    

    从 . 导入 views, api_views

    urlpatterns = [path('api/first_api_view/',)]

    path(api_views.first_api_view)

    ]

    
    Start the Django service with the `python manage.py runserver` command and go to `http://0.0.0.0:8000/api/first_api_view/` to make your first API request. Your screen should appear as in *Figure 12.1*:![Figure 12.1: API view with the number of books    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/webdev-dj-2e/img/B15509_12_01.jpg)Figure 12.1: API view with the number of booksCalling this URL endpoint made a default `GET` request to the API endpoint, which returned a JSON key-value pair `("num_books": 0`). Also, notice how DRF provides a nice interface to view and interact with the APIs.
    
  3. 我们也可以使用 Linux 的 curl(客户端 URL)命令发送 HTTP 请求,如下所示:

    curl http://0.0.0.0:8000/api/first_api_view/
    {"num_books":0}
    

    或者,如果您使用的是 Windows 10,您可以通过命令提示符使用 curl.exe 发送等效的 HTTP 请求,如下所示:

    curl.exe http://0.0.0.0:8000/api/first_api_view/
    

在这个练习中,我们学习了如何使用 DRF 创建 API 视图和使用简单功能视图。现在我们将探讨一种更优雅的方法,使用序列化器在数据库中存储的信息和 API 返回的信息之间进行转换。

序列化器

到现在为止,我们对 Django 在我们的应用程序中处理数据的方式已经非常熟悉。从广义上讲,数据库表中的列是在 models.py 中的一个类中定义的,当我们访问表中的一行时,我们正在处理该类的实例。理想情况下,我们通常只想将此对象传递给我们的前端应用程序。例如,如果我们想构建一个显示 Bookr 应用程序中书籍列表的网站,我们就会调用每个书籍实例的 title 属性,以知道向用户显示什么字符串。然而,我们的前端应用程序对 Python 一无所知,需要通过 HTTP 请求检索这些数据,该请求只返回特定格式的字符串。

这意味着在 Django 和前端(通过我们的 API)之间转换的任何信息都必须通过以 JavaScript 对象表示法JSON)格式表示信息来完成。JSON 对象看起来与 Python 字典相似,但有一些额外的规则限制了确切的语法。在我们的上一个示例 练习 12.01创建一个简单的 REST API 中,API 返回了以下包含我们数据库中书籍数量的 JSON 对象:

{"num_books": 0}

但如果我们想通过我们的 API 返回数据库中实际书籍的完整详细信息呢?DRF 的serializer类帮助将复杂的 Python 对象转换为 JSON 或 XML 等格式,以便可以通过 HTTP 协议在网络上传输。DRF 中执行此转换的部分被称为serializer。序列化器还执行反序列化,这指的是将序列化数据转换回 Python 对象,以便在应用程序中处理。

练习 12.02:创建一个 API 视图以显示书籍列表

在这个练习中,你将使用序列化器创建一个 API,该 API 返回bookr应用程序中所有书籍的列表:

  1. bookr/reviews文件夹中创建一个名为serializers.py的文件。这是我们放置所有 API 序列化代码的文件。

  2. 将以下代码添加到serializers.py中:

    from rest_framework import serializers
    class PublisherSerializer(serializers.Serializer):
        name = serializers.CharField()
        website = serializers.URLField()
        email = serializers.EmailField()
    class BookSerializer(serializers.Serializer):
        title = serializers.CharField()
        publication_date = serializers.DateField()
        isbn = serializers.CharField()
        publisher = PublisherSerializer()
    

    这里,第一行从rest_framework模块导入了序列化器。

    在导入之后,我们定义了两个类,PublisherSerializerBookSerializer。正如其名称所暗示的,它们分别是PublisherBook模型的序列化器。这两个序列化器都是serializers.Serializer的子类,并且我们为每个序列化器定义了字段类型,如CharFieldURLFieldEmailField等。

    查看bookr/reviews/models.py文件中的Publisher模型。Publisher模型有namewebsiteemail属性。因此,为了序列化Publisher对象,我们需要在serializer类中包含namewebsiteemail属性,我们已经在PublisherSerializer中相应地定义了这些属性。同样,对于Book模型,我们在BookSerializer中定义了titlepublication_dateisbnpublisher作为所需的属性。由于publisherBook模型的外键,我们已将PublisherSerializer用作publisher属性的序列化器。

  3. 打开bookr/reviews/api_views.py,删除任何现有的代码,并添加以下代码:

    from rest_framework.decorators import api_view
    from rest_framework.response import Response
    from .models import Book
    from .serializers import BookSerializer
    @api_view()
    def all_books(request):
        books = Book.objects.all()
        book_serializer = BookSerializer(books, many=True)
        return Response(book_serializer.data)
    

    在第二行,我们从serializers模块导入了新创建的BookSerializer

    然后,我们添加一个功能视图all_books(如前一个练习所示)。此视图接受包含所有书籍的查询集,然后使用BookSerializer对它们进行序列化。serializer类还接受一个参数many=True,这表示books对象是一个queryset或多个对象的列表。请记住,序列化将 Python 对象转换为可序列化为 JSON 的格式,如下所示:

    [OrderedDict([('title', 'Advanced Deep Learning with Keras'), ('publication_date', '2018-10-31'), ('isbn', '9781788629416'), ('publisher', OrderedDict([('name', 'Packt Publishing'), ('website', 'https://www.packtpub.com/'), ('email', 'info@packtpub.com')]))]), OrderedDict([('title', 'Hands-On Machine Learning for Algorithmic Trading'), ('publication_date', '2018-12-31'), ('isbn', '9781789346411'), ('publisher', OrderedDict([('name', 'Packt Publishing'), ('website', 'https://www.packtpub.com/'), ('email', 'info@packtpub.com')]))]) …
    
  4. 打开bookr/reviews/urls.py,删除之前的first_api_view示例路径,并添加如下代码中的all_books路径:

    from django.urls import path
    from . import views, api_views
    urlpatterns = [path('api/all_books/'),\
                   path(api_views.all_books),\
                   path(name='all_books')
        …
    ]
    

    新增的路径在遇到 URL 中的api/all_books/路径时会调用视图函数all_books

  5. 一旦添加了所有代码,使用python manage.py runserver命令运行 Django 服务器,并导航到http://0.0.0.0:8000/api/all_books/。你应该会看到类似于图 12.2的内容:![图 12.2:在 all_books 端点显示的书籍列表]

    ![img/B15509_12_02.jpg]

图 12.2:在 all_books 端点显示的书籍列表

前面的截图显示,在调用/api/all_books端点时,返回了所有书籍的列表。就这样,你已经成功使用序列化器在数据库中高效地返回数据,借助 REST API 的帮助。

到目前为止,我们一直专注于功能视图。然而,你现在将了解到在 DRF 中更常用的是基于类的视图,这将使你的生活变得更加轻松。

基于类的 API 视图和通用视图

与我们在第十一章高级模板和基于类的视图中学到的类似,我们也可以为 REST API 编写基于类的视图。对于编写视图来说,基于类的视图是开发者中最受欢迎的方式,因为通过编写很少的代码就能实现很多功能。

就像传统的视图一样,DRF 提供了一套通用视图,使得编写基于类的视图变得更加简单。通用视图是在考虑到创建 API 时所需的一些最常见操作而设计的。DRF 提供的通用视图包括ListAPIViewRetrieveAPIView等等。在练习 12.02创建一个显示书籍列表的 API 视图中,我们的功能视图负责创建对象的queryset并调用序列化器。同样,我们也可以使用ListAPIView来完成相同的事情:

class AllBooks(ListAPIView):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

在这里,对象的queryset被定义为类属性。将queryset传递给serializerListAPIView上的方法处理。

模型序列化器

练习 12.02创建一个显示书籍列表的 API 视图中,我们的序列化器定义如下:

class BookSerializer(serializers.Serializer):
    title = serializers.CharField()
    publication_date = serializers.DateField()
    isbn = serializers.CharField()
    publisher = PublisherSerializer()

然而,我们的Book模型看起来是这样的(注意模型和序列化器的定义看起来多么相似):

class Book(models.Model):
    """A published book."""
    title = models.CharField(max_length=70),\
                            (help_text="The title of the book.")
    publication_date = models.DateField\
                      (verbose_name="Date the book was published.")
    isbn = models.CharField(max_length=20),\
                           (verbose_name="ISBN number of the book.")
    publisher = models.ForeignKey(Publisher),\
                                 (on_delete=models.CASCADE)
    contributors = models.ManyToManyField('Contributor'),\
                                         (through="BookContributor")
    def __str__(self):
        return self.title

我们宁愿不指定标题必须是serializers.CharField()。如果序列化器只需查看模型中title是如何定义的,并能够确定要使用什么序列化器字段,那就更容易了。

这就是模型序列化器发挥作用的地方。它们通过利用模型上字段的定义来提供创建序列化器的快捷方式。我们不需要指定title应该使用CharField进行序列化,我们只需告诉模型序列化器我们想要包含title,它就会使用CharField序列化器,因为模型上的title字段也是一个CharField

例如,假设我们想在models.py中为Contributor模型创建一个序列化器。我们不需要指定每个字段应该使用哪种序列化器的类型,我们可以给它一个字段名称的列表,让它自己决定其他的事情:

from rest_framework import serializers
from .models import Contributor
class ContributorSerializer(serializers.ModelSerializer):
    class Meta:
        model = Contributor
        fields = ['first_names', 'last_names', 'email']

在以下练习中,我们将看到如何使用模型序列化器来避免在前面类中重复代码。

练习 12.03:创建基于类的 API 视图和模型序列化器

在这个练习中,你将创建基于类的视图来显示所有书籍的列表,同时使用模型序列化器:

  1. 打开文件bookr/reviews/serializers.py,删除任何现有的代码,并用以下代码替换:

    from rest_framework import serializers
    from .models import Book, Publisher
    class PublisherSerializer(serializers.ModelSerializer):
        class Meta:
            model = Publisher
            fields = ['name', 'website', 'email']
    class BookSerializer(serializers.ModelSerializer):
        publisher = PublisherSerializer()
        class Meta:
            model = Book
            fields = ['title', 'publication_date', 'isbn', 'publisher']
    

    在这里,我们包含了两个模型序列化类,PublisherSerializerBookSerializer。这两个类都继承自父类serializers.ModelSerializer。我们不需要指定每个字段如何序列化,相反,我们可以简单地传递一个字段名称列表,字段类型将根据models.py中的定义推断。

    虽然在fields中提及字段对于模型序列化器来说是足够的,但在某些特殊情况下,例如这个例子,我们可能需要自定义字段,因为publisher字段是一个外键。因此,我们必须使用PublisherSerializer来序列化publisher字段。

  2. 接下来,打开bookr/reviews/api_views.py,删除任何现有的代码,并添加以下代码:

    from rest_framework import generics
    from .models import Book
    from .serializers import BookSerializer
    class AllBooks(generics.ListAPIView):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
    

    在这里,我们使用 DRF 的基于类的ListAPIView而不是功能视图。这意味着书籍列表被定义为类的属性,我们不需要编写一个直接处理请求并调用序列化器的函数。上一步骤中的书籍序列化器也被导入并分配为这个类的属性。

    打开bookr/reviews/urls.py文件,修改/api/all_books API 路径以包含新的基于类的视图,如下所示:

    urlpatterns = [path('api/all_books/'),\
                   path(api_views.AllBooks.as_view()),\
                   path(name='all_books')]
    

    由于我们使用的是基于类的视图,我们必须使用类名以及as_view()方法。

  3. 完成所有前面的修改后,等待 Django 服务重启,或者使用python manage.py runserver命令启动服务器,然后在网络浏览器中打开http://0.0.0.0:8000/api/all_books/处的 API。你应该会看到类似于图 12.3的内容:![图 12.3:在 all_books 端点显示的书籍列表 图片

图 12.3:在 all_books 端点显示的书籍列表

如同我们在练习 12.02创建一个显示书籍列表的 API 视图中看到的,这是一个显示在书籍评论应用程序中的所有书籍的列表。在这个练习中,我们使用了模型序列化器来简化我们的代码,并使用通用的基于类的ListAPIView来返回数据库中书籍的列表。

活动 12.01:创建一个顶级贡献者页面 API 端点

假设你的团队决定创建一个显示数据库中顶级贡献者(即作者、合著者和编辑)的网页。他们决定聘请外部开发者使用 React JavaScript 创建一个应用程序。为了与 Django 后端集成,开发者需要一个提供以下内容的端点:

  • 数据库中所有贡献者的列表

  • 对于每个贡献者,列出他们所贡献的所有书籍。

  • 对于每个贡献者,列出他们所贡献的书籍数量。

  • 对于他们所贡献的每本书,列出他们在书中的角色。

最终的 API 视图应如下所示:

![图 12.4:顶级贡献者端点图片 B15509_12_04.jpg

图 12.4:顶级贡献者端点

要执行此任务,请执行以下步骤:

  1. Contributor类添加一个方法,该方法返回所做的贡献数量。

  2. 添加ContributionSerializer,它序列化BookContribution模型。

  3. 添加ContributorSerializer,它序列化Contributor模型。

  4. 添加ContributorView,它使用ContributorSerializer

  5. urls.py中添加一个模式以启用对ContributorView的访问。

    注意

    本活动的解决方案可以在packt.live/2Nh1NTJ找到。

视图集

我们已经看到,我们可以如何通过使用基于类的通用视图来优化我们的代码并使其更加简洁。AllBooks视图返回应用程序中所有书籍的列表,而BookDetail视图返回单本书的详细信息。使用视图集,我们可以将这两个类合并成一个。

DRF 还提供了一个名为ModelViewSet的类。这个类不仅结合了前面讨论中提到的两个视图(即列表和详情),还允许您创建、更新和删除模型实例。实现所有这些功能所需的代码可能非常简单,只需指定序列化和queryset即可。例如,一个允许您管理用户模型的所有这些操作的视图可以简洁地定义如下:

class UserViewSet(viewsets.ModelViewSet):
    serializer_class = UserSerializer
    queryset = User

最后,DRF 提供了一个名为ReadOnlyModelViewSet的类。这是先前ModelViewSet的一个更简单的版本。它与前面的版本相同,只是它只允许您列出和检索特定用户。您不能创建、更新或删除记录。

路由器

当与视图集一起使用时,路由器会自动创建视图集所需的 URL 端点。这是因为单个视图集在不同的 URL 上被访问。例如,在先前的UserViewSet中,您可以在 URL /api/users/ 上访问用户列表,并在 URL /api/users/123 上访问特定用户记录,其中123是该用户记录的主键。以下是一个简单的示例,说明您如何在先前定义的UserViewSet的上下文中使用路由器:

from rest_framework import routers
router = routers.SimpleRouter()
router.register(r'users', UserViewSet)
urlpatterns = router.urls

现在,让我们通过一个简单的练习来尝试结合路由器和视图集的概念。

练习 12.04:使用视图集和路由器

在这个练习中,我们将结合现有的视图来创建视图集,并为视图集创建所需的路由:

  1. 打开文件bookr/reviews/serializers.py,删除现有的代码,并添加以下代码片段:

    reviews/serializers.py
    01  from django.contrib.auth.models import User
    02  from django.utils import timezone
    03  from rest_framework import serializers
    04  from rest_framework.exceptions import NotAuthenticated, PermissionDenied
    05
    06  from .models import Book, Publisher, Review
    07  from .utils import average_rating
    08
    09  class PublisherSerializer(serializers.ModelSerializer):
    You can find the complete code snippet at http://packt.live/3osYJli.
    

    在这里,我们向 BookSerializer 添加了两个新字段,即 reviewsrating。这些字段的有趣之处在于,它们背后的逻辑被定义为序列化器本身上的一个方法。这就是为什么我们使用 serializers.SerializerMethodField 类型来设置 serializer 类属性。

  2. 打开文件 bookr/reviews/api_views.py,删除现有的代码,并添加以下内容:

    from rest_framework import viewsets
    from rest_framework.pagination import LimitOffsetPagination
    from .models import Book, Review
    from .serializers import BookSerializer, ReviewSerializer
    class BookViewSet(viewsets.ReadOnlyModelViewSet):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
    class ReviewViewSet(viewsets.ModelViewSet):
        queryset = Review.objects.order_by('-date_created')
        serializer_class = ReviewSerializer
        pagination_class = LimitOffsetPagination
        authentication_classes = []
    

    在这里,我们删除了 AllBookBookDetail 视图,并用 BookViewSetReviewViewSet 替换它们。在第一行,我们从 rest_framework 模块导入 ViewSets 模块。BookViewSet 类是 ReadOnlyModelViewSet 的子类,这确保了视图仅用于 GET 操作。

    接下来,打开 bookr/reviews/urls.py 文件,删除以 api/ 开头的第一个两个 URL 模式,然后添加以下(突出显示)代码:

    all_books and book_detail paths into a single path called books.  We have also added a new endpoint under the path reviews which we will need in a later chapter.We start by importing the `DefaultRouter` class from `rest_framework.routers`. Then, we create a `router` object using the `DefaultRouter` class and then register the newly created `BookViewSet` and `ReviewViewSet`, as can be seen from the highlighted code. This ensures that the `BookViewSet` is invoked whenever the API has the `/api/books` path.
    
  3. 保存所有文件,一旦 Django 服务重启(或者您使用 python manage.py runserver 命令手动启动),请访问 URL http://0.0.0.0:8000/api/books/ 以获取所有书籍的列表。您应该在 API 探索器中看到以下视图:图 12.5:/api/books 路径下的书籍列表

    图 12.5:/api/books 路径下的书籍列表

  4. 您也可以使用 URL http://0.0.0.0:8000/api/books/1/ 访问特定书籍的详细信息。在这种情况下,它将返回具有主键 1 的书籍的详细信息(如果它在您的数据库中存在):图 12.6:“使用 Keras 进行高级深度学习”的书籍详细信息

图 12.6:“使用 Keras 进行高级深度学习”的书籍详细信息

在这个练习中,我们看到了如何使用视图集和路由器将列表视图和详细信息视图合并为一个视图集。使用视图集将使我们的代码更加一致和符合惯例,更容易与其他开发者协作。当与单独的前端应用程序集成时,这一点尤为重要。

身份验证

正如我们在 第九章 中学到的,会话和身份验证,验证我们应用程序的用户非常重要。只允许在应用程序中注册的用户登录并访问应用程序中的信息是一种良好的做法。同样,对于 REST API,我们也需要设计一种方式在传递任何信息之前对用户进行验证和授权。例如,假设 Facebook 的网站通过 API 请求获取一个帖子的所有评论列表。如果他们在这个端点上没有进行身份验证,您就可以用它来程序化地获取任何您想要的帖子的评论。显然,他们不希望允许这样做,因此需要实现某种形式的身份验证。

存在多种认证方案,例如基本认证会话认证令牌认证远程用户认证以及各种第三方认证解决方案。在本章范围内,以及针对我们的 Bookr 应用程序,我们将使用令牌认证

注意

关于所有认证方案的进一步阅读,请参阅官方文档www.django-rest-framework.org/api-guide/authentication

基于令牌的认证

基于令牌的认证通过为用户生成一个唯一的令牌来交换用户的用户名和密码。一旦生成令牌,它将被存储在数据库中以供进一步参考,并在每次登录时返回给用户。

这个令牌对每个用户都是唯一的,用户可以使用这个令牌来授权他们发出的每个 API 请求。基于令牌的认证消除了在每次请求中传递用户名和密码的需要。它更加安全,非常适合客户端-服务器通信,例如,基于 JavaScript 的 Web 客户端通过 REST API 与后端应用程序交互。

例如,一个 ReactJS 或 AngularJS 应用程序通过 REST API 与 Django 后端交互。

如果你正在开发一个与后端服务器通过 REST API 交互的移动应用程序,例如,一个与 Django 后端通过 REST API 交互的 Android 或 iOS 应用程序,可以使用相同的架构。

练习 12.05:为 Bookr API 实现基于令牌的认证

在这个练习中,你将为bookr应用程序的 API 实现基于令牌的认证:

  1. 打开bookr/settings.py文件,并将rest_framework.authtoken添加到INSTALLED_APPS中:

    INSTALLED_APPS = ['django.contrib.admin',\
                      'django.contrib.auth',\
                      ‹django.contrib.contenttypes›,\
                      'django.contrib.sessions',\
                      'django.contrib.messages',\
                      'django.contrib.staticfiles',\
                      ‹rest_framework›,\
                      ‹rest_framework.authtoken›,\
                      ‹reviews›]
    
  2. 由于authtoken应用程序关联了数据库更改,请在命令行/终端中运行以下migrate命令:

    python manage.py migrate
    
  3. 打开bookr/reviews/api_views.py文件,删除任何现有的代码,并用以下内容替换:

    /reviews/api_views.py
    from django.contrib.auth import authenticate
    from rest_framework import viewsets
    from rest_framework.authentication import TokenAuthentication
    from rest_framework.authtoken.models import Token
    from rest_framework.pagination import LimitOffsetPagination
    from rest_framework.permissions import IsAuthenticated
    from rest_framework.response import Response
    from rest_framework.status import HTTP_404_NOT_FOUND, HTTP_200_OK
    from rest_framework.views import APIView
    You can find the complete code for this file at http://packt.live/2JQebbS.
    

    在这里,我们定义了一个名为Login的视图。这个视图的目的是允许用户获取(或创建如果尚未存在)一个令牌,他们可以使用这个令牌来通过 API 进行认证。

    我们覆盖了这个视图的post方法,因为我们想自定义用户发送数据(即他们的登录详情)时的行为。首先,我们使用 Django 的auth库中的authenticate方法来检查用户名和密码是否正确。如果正确,那么我们将有一个user对象。如果不正确,我们返回一个HTTP 404错误。如果我们有一个有效的user对象,那么我们只需获取或创建一个令牌,并将其返回给用户。

  4. 接下来,让我们将认证类添加到我们的BookViewSet中。这意味着当用户尝试访问此视图集时,它将要求他们使用基于令牌的认证进行认证。请注意,可以包含一系列不同的接受认证方法,而不仅仅是其中一个。我们还添加了permissions_classes属性,它仅使用 DRF 内置的类来检查给定用户是否有权限查看此模型中的数据:

    class BookViewSet(viewsets.ReadOnlyModelViewSet):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
        authentication_classes = [TokenAuthentication]
        permission_classes = [IsAuthenticated]
    

    注意

    前面的代码(高亮显示)不会与你在 GitHub 上看到的代码匹配,因为我们将在第 9 步中对其进行修改

  5. 打开bookr/reviews/urls.py文件,并将以下路径添加到 URL 模式中。

    path('api/login', api_views.Login.as_view(), name='login')
    
  6. 保存文件并等待应用程序重新启动,或者使用python manage.py runserver命令手动启动服务器。然后使用 URL http://0.0.0.0:8000/api/login 访问应用程序。你的屏幕应该如下所示:图 12.7:登录页面

    图 12.7:登录页面

    /api/login API 只接受POST请求,因此显示“方法 GET 不允许”。

  7. 接下来,在内容中输入以下片段,然后点击POST

    {
    "username": "Peter",
    "password": "testuserpassword"
    }
    

    你需要将此替换为数据库中你账户的实际用户名和密码。现在你可以看到为用户生成的令牌。这是我们用来访问BookSerializer的令牌:

    图 12.8:为用户生成的令牌

    图 12.8:为用户生成的令牌

  8. 尝试使用我们之前创建的 API 访问书籍列表,API 地址为http://0.0.0.0:8000/api/books/。请注意,你现在不再被允许访问它。这是因为这个视图集现在要求你使用你的令牌进行认证。

    同样的 API 可以使用命令行中的curl访问:

    curl -X GET http://0.0.0.0:8000/api/books/
    {"detail":"Authentication credentials were not provided."}
    

    由于未提供令牌,显示消息“未提供认证凭据”:

    图 12.9:显示认证详情未提供的消息

    curl -X GET http://0.0.0.0:8000/api/books/ -H "Authorization: Token 724865fcaff6d0aace359620a12ec0b5cc6524fl"
     [{"title":"Advanced Deep Learning with Keras","publication_date":"2018-10-31","isbn":"9781788629416","publisher":{"name":"Packt Publishing","website":"https://www.packtpub.com/","email":"info@packtpub.com"},"rating":4,"reviews":[{"content":"A must read for all","date_created":… (truncated)
    

    此操作确保只有应用程序的现有用户才能访问和检索所有书籍的集合。

  9. 在继续之前,将BookViewSet上的认证和权限类设置为空字符串。未来的章节将不会使用这些认证方法,我们将为了简单起见假设我们的 API 可以被未经认证的用户访问。

    class BookViewSet(viewsets.ReadOnlyModelViewSet):
        queryset = Book.objects.all()
        serializer_class = BookSerializer
        authentication_classes = []
        permission_classes = []
    

在这个练习中,我们在 Bookr 应用程序中实现了基于令牌的认证。我们创建了一个登录视图,允许我们检索给定认证用户的令牌。然后,我们通过在请求中将令牌作为头部传递,从命令行进行 API 请求。

摘要

本章介绍了 REST API,这是大多数现实世界 Web 应用的基本构建块。这些 API 促进了后端服务器与 Web 浏览器之间的通信,因此对于你作为 Django Web 开发者的成长至关重要。我们学习了如何在数据库中序列化数据,以便通过 HTTP 请求进行传输。我们还学习了 DRF 为我们提供的各种选项,以简化我们编写的代码,利用模型本身的现有定义。我们还涵盖了视图集和路由器,并看到了如何通过组合多个视图的功能来进一步压缩代码。我们还学习了身份验证和授权,并为书评应用实现了基于令牌的身份验证。在下一章中,我们将通过学习如何生成 CSV、PDF 和其他二进制文件类型来扩展 Bookr 为用户的功能。

第十三章:13. 生成 CSV、PDF 以及其他二进制文件

概述

本章将教会你如何使用 Python 中的一些常用库生成不同数据格式的文件,例如 CSVPDF 以及其他二进制文件格式(例如,兼容 Excel 的文件)。这些知识将帮助你构建允许用户将网站上的记录导出并下载到熟悉的 CSV 或 Excel 格式的 Web 项目。你还将学习如何在 Python 中生成图表,并将其渲染为 HTML,在 Web 应用程序中显示。此外,你将能够构建允许用户以 PDF 格式导出数据的特性。

简介

到目前为止,我们已经学习了 Django 框架的各个方面,并探讨了如何使用 Django 构建具有我们所需的所有功能和自定义的 Web 应用程序。

假设我们在构建 Web 应用程序时需要进行分析并准备一些报告。我们可能需要分析用户的人口统计信息,了解平台的使用情况,或者生成可以输入到机器学习系统中以寻找模式的数据。我们希望我们的网站能够以表格形式显示我们分析的一些结果,并以详细的图表和图形显示其他结果。此外,我们还希望允许我们的用户将报告导出,并在 Jupyter Notebook 和 Excel 等应用程序中进一步查看。

随着我们逐步学习本章内容,我们将了解如何将这些想法付诸实践,并在我们的 Web 应用程序中实现功能,使我们能够通过使用 逗号分隔值CSV)文件或 Excel 文件将记录导出为结构化格式,如表格。我们还将学习如何允许我们的用户生成我们存储在 Web 应用程序中的数据的视觉表示,并将其导出为 PDF 格式,以便可以轻松分发以供快速参考。

让我们从学习如何在 Python 中处理 CSV 文件开始我们的旅程。掌握这项技能将帮助我们创建允许我们的读者将我们的数据导出以进行进一步分析的功能。

在 Python 中处理 CSV 文件

我们可能需要将应用程序中的数据导出出去,原因有很多。其中一个原因可能涉及对该数据进行分析——例如,我们可能需要了解在应用程序上注册的用户的人口统计信息或提取应用程序使用的模式。我们可能还需要了解我们的应用程序对用户的工作情况,以便设计未来的改进。这些用例需要数据以易于消费和分析的格式存在。在这里,CSV 文件格式就派上用场了。

CSV 是一种方便的文件格式,可以用于快速将数据从应用程序中以行和列的格式导出。CSV 文件通常使用简单的分隔符来分隔数据,这些分隔符用于区分一列与另一列,以及换行符,用于在表格内指示新记录(或行)的开始。

Python 的标准库通过csv模块提供了对 CSV 文件的良好支持。这种支持使得读取、解析和写入 CSV 文件成为可能。让我们看看我们如何利用 Python 提供的 CSV 模块来处理 CSV 文件,并从中读取和写入数据。

使用 Python 的 CSV 模块

Python 的csv模块为我们提供了与 CSV 格式文件交互的能力,CSV 格式实际上是一种文本文件格式。也就是说,存储在 CSV 文件中的数据是可读的。

csv模块要求在应用csv模块提供的方法之前打开文件。让我们看看我们如何从读取 CSV 文件的基本操作开始。

从 CSV 文件读取数据

从 CSV 文件读取数据相当简单,包括以下步骤:

  1. 首先,我们打开文件:

    csv_file = open('path to csv file')
    

    这里,我们使用 Python 的open()方法读取文件,然后将要读取数据的文件名传递给它。

  2. 然后,我们使用csv模块的reader方法从file对象中读取数据:

    import csv
    csv_data = csv.reader(csv_file)
    

    在第一行,我们导入了csv模块,它包含处理 CSV 文件所需的方法集:

    import csv
    

    文件打开后,下一步是使用csv模块的reader方法创建一个 CSVreader对象。此方法接受由open()调用返回的file对象,并使用该file对象从 CSV 文件中读取数据:

    csv_reader = csv.reader(csv_file)
    

    通过reader()方法读取的数据以列表的列表形式返回,其中每个子列表是一个新记录,列表中的每个值都是指定列的值。通常,列表中的第一个记录被称为标题,它表示 CSV 文件中存在的不同列,但 CSV 文件中不需要有header字段。

  3. 一旦数据被csv模块读取,我们就可以遍历这些数据以执行我们可能需要的任何操作。这可以按照以下方式完成:

    for csv_record in csv_data:
        # do something
    
  4. 一旦处理完成,我们可以通过在 Python 的文件处理对象中调用close()方法简单地关闭 CSV 文件:

    csv_file.close()
    

现在,让我们看看我们的第一个练习,我们将实现一个简单的模块,帮助我们读取 CSV 文件并将内容输出到屏幕上。

练习 13.01:使用 Python 读取 CSV 文件

在这个练习中,你将使用 Python 的内置csv模块在 Python 中读取并处理 CSV 文件。CSV 文件包含几家纳斯达克上市公司虚构的市场数据:

  1. 首先,通过点击以下链接从本书的 GitHub 存储库下载market_cap.csv文件:packt.live/2MNWzOV

    注意

    CSV 文件由随机生成数据组成,并不对应任何历史市场趋势。

  2. 一旦文件下载完成,打开它并查看其内容。您会发现文件包含一组以逗号分隔的值,每个不同的记录都在自己的行上:![图 13.1:市值 CSV 文件内容 图片

    图 13.1:市值 CSV 文件内容

  3. 一旦文件下载完成,您可以继续编写第一段代码。为此,在 CSV 文件下载的同一目录中创建一个名为 csv_reader.py 的新文件,并在其中添加以下代码:

    import csv
    def read_csv(filename):
        """Read and output the details of CSV file."""
        try:
           with open(filename, newline='') as csv_file:
               csv_reader = csv.reader(csv_file)
               for record in csv_reader:
                   print(record)
        except (IOError, OSError) as file_read_error:
           print("Unable to open the csv file. Exception: {}".format(file_read_error))
    if __name__ == '__main__':
        read_csv('market_cap.csv')
    

    让我们尝试理解您在上面的代码片段中刚刚实现的内容。

    在导入 csv 模块后,为了使代码模块化,您创建了一个名为 read_csv() 的新方法,它接受一个参数,即从其中读取数据的文件名:

    try:
           with open(filename, newline='') as csv_file:
    

    现在,如果您不熟悉前面代码片段中显示的文件打开方法,这也被称为 with 块将有权访问 file 对象,一旦代码退出 with 块的作用域,文件将自动关闭。

    for record in csv_reader:
        print(record)
    

    完成此操作后,您需要编写入口点方法,您的代码将从该方法开始执行,通过调用 read_csv() 方法并传递要读取的 CSV 文件名:

    if __name__ == '__main__':
        read_csv(market_cap.csv')
    
  4. 使用这些步骤,您现在可以解析您的 CSV 文件了。您可以通过在终端或命令提示符中运行您的 Python 文件来完成此操作,如下所示:

    python3 csv_reader.py
    

    注意

    或者,在 Windows 上,如 图 13.2 所示,使用 python csv_reader.py

    代码执行后,您应该会看到以下输出:

    ![图 13.2:CSV 读取程序输出 图片

图 13.2:CSV 读取程序输出

通过这种方式,现在您已经知道了如何读取 CSV 文件内容。同时,如您从 练习 13.01 的输出中看到,使用 Python 读取 CSV 文件,单个行的输出以列表的形式表示。

现在,让我们看看如何使用 Python 的 csv 模块创建新的 CSV 文件。

使用 Python 写入 CSV 文件

在上一节中,我们探讨了如何使用 Python 的 csv 模块读取 CSV 格式文件的 内容。现在,让我们学习如何将 CSV 数据写入文件。

写入 CSV 数据的方法与从 CSV 文件读取类似,但有细微差别。以下步骤概述了将数据写入 CSV 文件的过程:

  1. 以写入模式打开文件:

    csv_file = open('path to csv file', 'w')
    
  2. 获取一个 CSV 写入器对象,这可以帮助我们写入正确格式化的 CSV 格式的数据。这是通过调用 csv 模块的 writer() 方法来完成的,它返回一个 writer 对象,可以用来将 CSV 格式兼容的数据写入 CSV 文件:

    csv_writer = csv.writer(csv_file)
    
  3. 一旦 writer 对象可用,我们就可以开始写入数据。这可以通过 writer 对象的 write_row() 方法来实现。write_row() 方法接收一个值列表,将其写入 CSV 文件。列表本身表示一行,列表内的值表示列的值:

    record = ['value1', 'value2', 'value3']
    csv_writer.writerow(record)
    

    如果你想在单次调用中写入多个记录,你也可以使用 CSV 写入器的 writerows() 方法。writerows() 方法的行为类似于 writerow() 方法,但它接受一个列表的列表,可以一次写入多行:

    records = [['value11', 'value12', 'value13'],\
               ['value21', 'value22', 'value23']]
    csv_writer.writerows(records)
    
  4. 记录写入后,我们可以关闭 CSV 文件:

    csv_file.close()
    

现在,让我们应用我们学到的知识,并实现一个程序,帮助我们向 CSV 文件写入值。

练习 13.02:使用 Python 的 csv 模块生成 CSV 文件

在这个练习中,你将使用 Python 的 csv 模块来创建新的 CSV 文件:

  1. 创建一个名为 csv_writer.py 的新文件,在这个文件中,你需要编写 CSV 写入器的代码。在这个文件中,添加以下代码:

    import csv
    def write_csv(filename, header, data):
        """Write the provided data to the CSV file.
        :param str filename: The name of the file \
            to which the data should be written
        :param list header: The header for the \
            columns in csv file
        :param list data: The list of list mapping \
            the values to the columns
        """
        try:
            with open(filename, 'w') as csv_file:
                csv_writer = csv.writer(csv_file)
                csv_writer.writerow(header)
                csv_writer.writerows(data)
        except (IOError, OSError) as csv_file_error:
            print\
            ("Unable to write the contents to csv file. Exception: {}"\
             .format(csv_file_error))
    

    使用这段代码,你现在应该能够轻松地创建新的 CSV 文件。现在,一步一步地,让我们理解你在这段代码中试图做什么:

    你定义了一个名为 write_csv() 的新方法,它接受三个参数:数据应写入的文件名(filename)、用作标题的列名列表(header),以及最后是一个列表,其中包含需要映射到各个列的数据(data):

     def write_csv(filename, header, data):
    

    现在,参数已经设置好了,下一步是打开需要写入数据的目标文件,并将其映射到一个对象:

    with open(filename, 'w') as csv_file:
    

    文件打开后,你执行三个主要步骤:首先,使用 csv 模块的 writer() 方法获取一个新的 CSV 写入器对象,并将其传递给包含打开文件引用的文件处理器:

    csv_writer = csv.writer(csv_file)
    

    下一步是使用 CSV 写入器的 writerow() 方法将数据集的标题字段写入文件:

    csv_writer.writerow(header)
    

    在写入标题之后,最后一步是将数据写入 CSV 文件,针对现有的各个列。为此,使用 csv 模块的 writerows() 方法一次性写入多行:

    csv_writer.writerows(data)
    

    注意

    我们也可以通过将标题和数据合并为数据列表的第一个元素,并使用数据列表作为参数调用 writerows() 方法,将写入标题和数据的一步合并为单行代码。

  2. 当你创建了可以将提供的数据写入 CSV 文件的方法后,你编写入口点调用的代码,并在其中设置标题、数据和文件名字段的值,最后调用你之前定义的 write_csv() 方法:

    if __name__ == '__main__':
        header = ['name', 'age', 'gender']
        data = [['Richard', 32, 'M'], \
                ['Mumzil', 21, 'F'], \
                ['Melinda', 25, 'F']]
        filename = 'sample_output.csv'
        write_csv(filename, header, data)
    
  3. 现在代码已经就绪,执行你刚刚创建的文件,看看它是否创建了 CSV 文件。要执行,请运行以下命令:

    python3 csv_writer.py
    

    执行完成后,你将看到在执行命令的同一目录下创建了一个新文件。当你打开文件时,内容应该类似于以下图示:

    图 13.3:CSV 写入器示例输出 sample_output.csv

    图 13.3:CSV 写入器示例输出 sample_output.csv

图 13.3:CSV 写入器示例输出 sample_output.csv

现在,你已经具备了读取和写入 CSV 文件内容的能力。

通过这个练习,我们学习了如何将数据写入 CSV 文件。现在,是时候看看一些增强功能,这些功能可以使作为开发者的您在读取和写入 CSV 文件时更加方便。

更好地读取和写入 CSV 文件的方法

现在,有一件重要的事情需要注意。如果您还记得,CSV 读取器读取的数据通常将值映射到列表。现在,如果您想访问单个列的值,您需要使用列表索引来访问它们。这种方式并不自然,并且会导致编写文件和读取文件之间的耦合度更高。例如,如果编写程序打乱了行的顺序怎么办?在这种情况下,您现在必须更新读取程序以确保它能够识别正确的行。因此,问题出现了,我们是否有更好的方法来读取和写入值,而不是使用列表索引,而是使用列名,同时保留上下文?

这个问题的答案是肯定的,解决方案由另一组名为DictReaderDictWriter的 CSV 模块提供,这些模块提供将 CSV 文件中的对象映射到dict的功能,而不是映射到列表。

此接口易于实现。让我们回顾一下您在练习 13.01使用 Python 读取 CSV 文件中编写的代码。如果您想将代码解析为字典,read_csv()方法的实现需要按如下所示更改:

def read_csv(filename):
    """Read and output the details of CSV file."""
    try:
       with open(filename, newline='') as csv_file:
           csv_reader = csv.DictReader(csv_file)
           for record in csv_reader:
               print(record)
    except (IOError, OSError) as file_read_error:
        print\
        ("Unable to open the csv file. Exception: {}"\
        .format(file_read_error))

您将注意到,我们做的唯一改变是将csv.reader()改为csv.DictReader(),这应该将 CSV 文件中的单独行表示为OrderedDict。您也可以通过进行此更改并执行以下命令来验证这一点:

python3 csv_reader.py

这应该会产生以下输出:

图 13.4:使用 DictReader 的输出

图 13.4:使用 DictReader 的输出

如前图所示,单独的行被映射为字典中的键值对。要访问这些单独的行中的字段,我们可以使用以下方法:

print(record.get('stock_symbol'))

这应该会给出我们单个记录中的stock_symbol字段的值。

同样,您也可以使用DictWriter()接口将 CSV 文件操作为字典。为了了解这一点,让我们看看练习 13.02使用 Python 的 csv 模块生成 CSV 文件中的write_csv()方法,并按如下方式修改它:

def write_csv(filename, header, data):
    """Write the provided data to the CSV file.
    :param str filename: The name of the file \
        to which the data should be written
    :param list header: The header for the \
        columns in csv file
    :param list data: The list of dicts mapping \
        the values to the columns
    """
    try:
        with open(filename, 'w') as csv_file:
            csv_writer = csv.DictWriter(csv_file, fieldnames=header)
            csv_writer.writeheader()
            csv_writer.writerows(data)
    except (IOError, OSError) as csv_file_error:
        print\
        ("Unable to write the contents to csv file. Exception: {}"\
        .format(csv_file_error))

在前面的代码中,我们将csv.writer()替换为csv.DictWriter(),它提供了一个类似字典的接口来与 CSV 文件交互。DictWriter()还接受一个fieldnames参数,该参数用于在写入之前将 CSV 文件中的单独列映射。

接下来,为了写入这个标题,调用writeheader()方法,它将fieldname标题写入 CSV 文件。

最后的调用涉及writerows()方法,它接受一个字典列表并将其写入 CSV 文件。为了使代码正确运行,您还需要修改数据列表,使其类似于以下所示:

data = [{'name': Richard, 'age': 32, 'gender': 'M'}, \
        {'name': Mumzil', 'age': 21, 'gender':'F'}, \
        {'name': 'Melinda', 'age': 25, 'gender': 'F'}]

通过这样,你将拥有足够的知识来在 Python 中处理 CSV 文件。

由于我们正在讨论如何处理表格数据,特别是将其读取和写入文件,让我们看看最著名的文件格式之一,由最流行的表格数据编辑器之一——微软 Excel——所使用。

使用 Python 处理 Excel 文件

微软 Excel 是簿记和表格记录管理领域的世界知名软件。同样,随着 Excel 一起引入的 XLSX 文件格式也迅速得到广泛采用,并且现在所有主要产品供应商都支持它。

你会发现微软 Excel 和其 XLSX 格式在许多公司的市场和销售部门中被广泛使用。比如说,对于某家公司的市场部门,你正在使用 Django 构建一个跟踪用户购买产品的网络门户。它还显示了关于购买的数据,例如购买时间和购买地点。市场和销售团队计划使用这些数据来生成潜在客户或创建相关的广告。

由于市场和销售团队大量使用 Excel,我们可能希望将我们网络应用内部的数据导出为 XLSX 格式,这是 Excel 的原生格式。很快,我们将探讨如何使我们的网站与这种 XLSX 格式协同工作。但在那之前,让我们快速了解一下二进制文件格式的用法。

数据导出的二进制文件格式

到目前为止,我们主要处理的是文本数据以及如何从文本文件中读取和写入这些数据。但通常,基于文本的格式是不够的。例如,想象一下你想导出一张图片或一个图表。你将如何用文本表示一张图片或一个图表,以及你将如何读取和写入这些图片?

在这些情况下,二进制文件格式可以为我们提供帮助。它们可以帮助我们读取和写入丰富的数据集。所有商业操作系统都提供对文本和二进制文件格式的原生支持,Python 提供了最灵活的实现之一来处理二进制数据文件,这并不令人惊讶。一个简单的例子是 open 命令,你使用它来指定你想要打开的文件格式:

file_handler = open('path to file', 'rb')

在这里,b 表示二进制。

从本节开始,我们现在将处理如何处理二进制文件,并使用它们来表示和从我们的 Django 网络应用中导出数据。我们将首先查看的是由微软 Excel 使之流行的 XLSX 文件格式。

因此,让我们深入探讨如何使用 Python 处理 XLSX 文件。

使用 XlsxWriter 包处理 XLSX 文件

在本节中,我们将更深入地了解 XLSX 文件格式,并了解我们如何使用 XlsxWriter 包来与之协同工作。

XLSX 文件

XLSX 文件是用于存储表格数据的二进制文件。这些文件可以被任何实现对该格式支持支持的软件读取。XLSX 格式将数据安排为两个逻辑分区:

  • Example_file.xlsx是一个工作簿(1):![图 13.5:Excel 中的工作簿和工作表

    ![img/B15509_13_05.jpg]

图 13.5:Excel 中的工作簿和工作表

  • Sheet1Sheet2是两个工作表(2)

当处理 XLSX 格式时,这两个是我们通常工作的单元。如果你了解关系数据库,你可以将工作簿视为数据库,将工作表视为表。

有了这些,让我们尝试理解如何在 Python 中开始处理 XLSX 文件。

XlsxWriter Python 包

Python 的标准库没有提供对 XLSX 文件的原生支持。但是,多亏了 Python 生态系统中的庞大开发者社区,我们很容易找到许多可以帮助我们管理 XLSX 文件交互的包。在这个类别中,一个流行的包是XlsxWriter

XlsxWriter是由开发者社区积极维护的包,提供与 XLSX 文件交互的支持。该包提供了许多有用的功能,并支持创建和管理工作簿以及单个工作簿中的工作表。您可以通过在终端或命令提示符中运行以下命令来安装它:

pip install XlsxWriter

安装完成后,你可以按照以下方式导入xlsxwriter模块:

import xlsxwriter

因此,让我们看看如何利用XlsxWriter包开始创建 XLSX 文件。

创建工作簿

要开始处理 XLSX 文件,我们首先需要创建它们。XLSX 文件也称为工作簿,可以通过从xlsxwriter模块调用Workbook类来创建,如下所示:

workbook = xlsxwriter.Workbook(filename)

Workbook类的调用打开一个由filename参数指定的二进制文件,并返回一个workbook实例,可以用来进一步创建工作表和写入数据。

创建工作表

在我们开始向 XLSX 文件写入数据之前,我们首先需要创建一个工作表。这可以通过调用我们在上一步中获得的工作簿对象的add_worksheet()方法轻松完成:

worksheet = workbook.add_worksheet()

add_worksheet()方法创建一个新的工作表,将其添加到工作簿中,并返回一个将工作表映射到 Python 对象的映射对象,通过这个对象我们可以将数据写入工作表。

向工作表写入数据

一旦有了工作表的引用,我们可以通过调用worksheet对象的write方法开始向其写入数据,如下所示:

worksheet.write(row_num, col_num, col_value)

如您所见,write()方法接受三个参数:行号(row_num)、列号(col_num)以及属于[row_num, col_num]对的col_value表示的数据。这个调用可以重复进行,以将多个数据项插入到工作表中。

将数据写入工作簿

一旦所有数据都写入,为了最终确定写入的数据集并干净地关闭 XLSX 文件,您需要在工作簿上调用close()方法:

workbook.close()

此方法写入文件缓冲区中可能存在的任何数据,并最终关闭工作簿。现在,让我们利用这些知识来实现我们自己的代码,这将帮助我们向 XLSX 文件写入数据。

进一步阅读

在本章中,无法涵盖XlsxWriter包提供的所有方法和功能。更多信息,您可以阅读官方文档:xlsxwriter.readthedocs.io/contents.html

练习 13.03:在 Python 中创建 XLSX 文件

在这个练习中,您将使用XlsxWriter包创建一个新的 Excel(XLSX)文件,并从 Python 向其中添加数据:

  1. 对于这个练习,您需要在系统中安装XlsxWriter包。您可以通过在终端应用程序或命令提示符中运行以下命令来安装它:

    pip install XlsxWriter
    

    一旦命令执行完毕,您将在系统中安装上该包。

  2. 在安装了包之后,您就可以开始编写创建 Excel 文件的代码了。创建一个名为xlsx_demo.py的新文件,并在其中添加以下代码:

    import xlsxwriter
    def create_workbook(filename):
        """Create a new workbook on which we can work."""
        workbook = xlsxwriter.Workbook(filename)
        return workbook
    

    在前面的代码片段中,您已创建了一个新的函数,该函数将帮助您创建一个新的工作簿,您可以在其中存储数据。一旦创建了新的工作簿,下一步就是创建一个工作表,它为您提供了组织要存储在 XLSX 工作簿中的数据的表格格式。

  3. 在创建工作簿后,通过在您的xlsx_demo.py文件中添加以下代码片段来创建一个新的工作表:

    def create_worksheet(workbook):
        """Add a new worksheet in the workbook."""
        worksheet = workbook.add_worksheet()
        return worksheet
    

    在前面的代码片段中,您已使用XlsxWriter包提供的workbook对象中的add_worksheet()方法创建了一个新的工作表。然后,这个工作表将被用来写入对象的数据。

  4. 下一步是创建一个辅助函数,它可以协助将数据以表格格式写入工作表,该表格格式由行和列编号定义。为此,将以下代码片段添加到您的xlsx_writer.py文件中:

    def write_data(worksheet, data):
        """Write data to the worksheet."""
        for row in range(len(data)):
            for col in range(len(data[row])):
                worksheet.write(row, col, data[row][col])
    

    在前面的代码片段中,您已创建了一个名为write_data()的新函数,该函数接受两个参数:需要写入的worksheet对象和表示为列表的列表的data对象,这些列表需要写入工作表。该函数遍历传递给它的数据,然后将数据写入其所属的行和列。

  5. 现在所有核心方法都已实现,您现在可以添加一个可以帮助干净地关闭workbook对象的方法,这样数据就可以写入文件而不会发生任何文件损坏。为此,在xlsx_demo.py文件中实现以下代码片段:

    def close_workbook(workbook):
        """Close an opened workbook."""
        workbook.close()
    
  6. 练习的最后一步是将你在前几步中实现的所有方法集成在一起。为此,在你的 xlsx_demo.py 文件中创建一个新的入口点方法,如下面的代码片段所示:

    if __name__ == '__main__':
        data = [['John Doe', 38], \
                ['Adam Cuvver', 22], \
                ['Stacy Martin', 28], \
                ['Tom Harris', 42]]
        workbook = create_workbook('sample_workbook.xlsx')
        worksheet = create_worksheet(workbook)
        write_data(worksheet, data)
        close_workbook(workbook)
    

    在前面的代码片段中,你首先创建了一个数据集,你希望将其以列表的形式写入 XLSX 文件。一旦完成,你获得了一个新的 workbook 对象,该对象将用于创建 XLSX 文件。在这个 workbook 对象内部,你创建了一个工作表来以行列格式组织你的数据,然后将数据写入工作表,并关闭工作簿以将数据持久化到磁盘。

  7. 现在,让我们看看你编写的代码是否按预期工作。为此,运行以下命令:

    python3 xlsx_demo.py
    

    一旦命令执行完毕,你将在命令执行的目录中看到一个名为 sample_workbook.xlsx 的新文件被创建。为了验证它是否包含正确的结果,你可以用 Microsoft Excel 或 Google Sheets 打开此文件,查看内容。它应该看起来像这里所示:

    ![图 13.6:使用 xlsxwriter 生成的 Excel 表格]

    ![图片 B15509_13_06.jpg]

图 13.6:使用 xlsxwriter 生成的 Excel 表格

xlsxwriter 模块的帮助下,你还可以将公式应用到你的列上。例如,如果你想添加一行来显示电子表格中人们的平均年龄,你可以通过简单地修改如下的 write_data() 方法来实现:

def write_data(worksheet, data):
    """Write data to the worksheet."""
    for row in range(len(data)):
        for col in range(len(data[row])):
            worksheet.write(row, col, data[row][col])
    worksheet.write(len(data), 0, "Avg. Age") 
    # len(data) will give the next index to write to
    avg_formula = "=AVERAGE(B{}:B{})".format(1, len(data))
    worksheet.write(len(data), 1, avg_formula)

在前面的代码片段中,你向工作表添加了一个额外的 write 调用,并使用了 Excel 提供的 AVERAGE 函数来计算工作表中人们的平均年龄。

通过这种方式,你现在知道了如何使用 Python 生成与 Microsoft Excel 兼容的 XLSX 文件,以及如何导出易于组织内部不同团队消费的表格内容。

现在,让我们来探讨另一个在全球范围内广泛使用的有趣文件格式。

在 Python 中处理 PDF 文件

便携式文档格式PDF是世界上最常见的文件格式之一。你肯定在某些时候遇到过 PDF 文档。这些文档可以包括商业报告、数字书籍等等。

此外,你还记得曾经遇到过有“打印”页面“作为 PDF”按钮的网站吗?许多政府机构的网站都提供了这个选项,允许你直接将网页打印成 PDF。因此,问题来了,我们如何为我们的 Web 应用程序做这件事?我们应该如何添加导出某些内容为 PDF 的选项?

几年来,一个庞大的开发者社区为 Python 生态系统贡献了大量的有用包。其中之一可以帮助我们实现 PDF 文件生成。

将网页转换为 PDF

有时,我们可能会遇到想要将网页转换为 PDF 的情况。例如,我们可能想要打印网页以存储为本地副本。当尝试打印原生显示为网页的证书时,这也很有用。

为了帮助我们进行这样的努力,我们可以利用一个名为 weasyprint 的简单库,该库由一群 Python 开发者维护,并允许快速轻松地将网页转换为 PDF。那么,让我们看看我们如何生成网页的 PDF 版本。

练习 13.04:在 Python 中生成网页的 PDF 版本

在这个练习中,你将使用一个名为 weasyprint 的社区贡献的 Python 模块来生成 PDF 版本的网站。这个模块将帮助你生成 PDF:

  1. 为了使接下来的步骤中的代码正确工作,请在你的系统上安装 weasyprint 模块。为此,运行以下命令:

    pip install weasyprint
    

    注意

    weasyprint 依赖于 cairo 库。如果你还没有安装 cairo 库,使用 weasyprint 可能会引发错误,错误信息为:libcairo-2.dll file not found。如果你遇到这个问题或安装模块时遇到任何其他问题,请使用我们提供的位于 GitHub 仓库中的 requirements.txt 文件,网址为 packt.live/3btLoVV。将文件下载到你的磁盘上,然后在终端、shell 或命令提示符中输入以下命令(你需要 cd 到保存此文件的本地路径):pip install -r requirements.txt。如果这还不行,请按照 weasyprint 文档中提到的步骤进行操作:weasyprint.readthedocs.io/en/stable/install.html

  2. 现在包已经安装,创建一个名为 pdf_demo.py 的新文件,该文件将包含 PDF 生成逻辑。在这个文件中,编写以下代码:

    from weasyprint import HTML
    def generate_pdf(url, pdf_file):
        """Generate PDF version of the provided URL."""
        print("Generating PDF...")
        HTML(url).write_pdf(pdf_file)
    

    现在,让我们尝试理解这段代码的作用。在第一行,你从 weasyprint 包中导入了 HTML 类,这是你在 步骤 1 中安装的:

    from weasyprint import HTML
    

    这个 HTML 类为我们提供了一个机制,通过这个机制,如果我们有网站的 URL,我们可以读取网站的 HTML 内容。

    在下一步中,你创建了一个名为 generate_pdf() 的新方法,它接受两个参数,即用作生成 PDF 的源 URL 的 URL 和 pdf_file 参数,它接受要写入文档的文件名:

    def generate_pdf(url, pdf_file):
    

    接下来,你将 URL 传递给了你之前导入的 HTML 类对象。这导致 URL 被由 weasyprint 库解析,并读取其 HTML 内容。完成此操作后,你调用了 HTML 类对象的 write_pdf() 方法,并提供了要写入内容的文件名:

    HTML(url).write_pdf(pdf_file)
    
  3. 然后,编写入口点代码来设置 URL(在这个练习中,我们将使用 generate_pdf() 方法的文本版本来生成内容):

    if __name__ == '__main__':
        url = 'http://text.npr.org'
        pdf_file = 'demo_page.pdf'
        generate_pdf(url, pdf_file)
    
  4. 现在,要查看代码的实际效果,请运行以下命令:

    python3 pdf_demo.py
    

    一旦命令执行完成,您将在执行命令的同一目录下获得一个名为demo_page.pdf的新 PDF 文件。当您打开文件时,它应该看起来像这里所示:

    ![图 13.7:使用 weasyprint 转换的网页

    ![图片 B15509_13_07.jpg]

图 13.7:使用 weasyprint 转换的网页

在生成的 PDF 文件中,我们可以看到内容似乎缺少了实际网站所具有的格式。这是因为weasyprint包读取了 HTML 内容,但没有解析页面附带的 CSS 样式表,因此页面格式丢失了。

weasyprint还使得更改页面格式变得非常简单。这可以通过向write_pdf()方法引入样式表参数来完成。接下来将描述对generate_pdf()方法的简单修改:

from weasyprint import CSS, HTML
def generate_pdf(url, pdf_file):
    """Generate PDF version of the provided URL."""
    print("Generating PDF...")
    css = CSS(string='body{ font-size: 8px; }')
    HTML(url).write_pdf(pdf_file, stylesheets=[css])

现在,当执行前面的代码时,我们将看到页面的 HTML 内容体内的所有文本的字体大小在打印的 PDF 版本中为8px

注意

weasyprint中的HTML类也能够接受任何本地文件以及原始 HTML 字符串内容,并可以使用这些文件生成 PDF。有关更多信息,请访问weasyprint文档,链接为weasyprint.readthedocs.io

到目前为止,我们已经学习了如何使用 Python 生成不同类型的二进制文件,这可以帮助我们以结构化的方式导出我们的数据,或者帮助我们打印页面的 PDF 版本。接下来,我们将看到如何使用 Python 生成数据的图表表示。

在 Python 中玩转图表

图表是可视化特定维度内变化数据的绝佳方式。我们在日常生活中经常遇到图表,无论是每周的天气图表,股市走势,还是学生成绩报告单。

类似地,当我们在处理我们的 Web 应用程序时,图表也可以非常有用。对于 Bookr,我们可以使用图表作为视觉媒介来向用户展示他们每周阅读的书籍数量。或者,我们可以根据特定时间有多少读者在阅读指定的书籍来展示书籍随时间的变化趋势。现在,让我们看看我们如何使用 Python 生成图表,并在我们的网页上显示它们。

使用 plotly 生成图表

当尝试可视化我们应用程序维护的数据中的模式时,图表非常有用。有许多 Python 库可以帮助开发者生成静态或交互式图表。

对于这本书,我们将使用plotly,这是一个社区支持的 Python 库,它生成图表并在网页上渲染。plotly因其与 Django 集成的简便性而对我们特别有趣。

要在您的系统上安装它,您可以在命令行中输入以下命令:

pip install plotly

现在已经完成了,让我们看看如何使用 plotly 生成图形可视化。

设置图形

在我们开始生成图形之前,首先需要初始化一个 plotly Figure 对象,它本质上是一个用于我们的图形的容器。plotlyFigure 对象初始化非常简单;可以通过以下代码片段完成:

from plotly.graph_objs import graphs
figure = graphs.Figure()

plotly 库的 graph_objs 模块的 Figure() 构造函数返回 Figure 图形容器的实例,在其中可以生成图形。一旦 Figure 对象就位,下一步需要做的就是生成图形。

生成图形

图形是数据集的视觉表示。这个图形可以是散点图、折线图、图表等等。例如,要生成散点图,可以使用以下代码片段:

scatter_plot = graphs.Scatter(x_axis_values, y_axis_values)

Scatter 构造函数接收 X 轴和 Y 轴的值,并返回一个可以用来构建散点图的对象。一旦生成了 scatter_plot 对象,下一步就是将其添加到我们的 Figure 中。这可以通过以下方式完成:

figure.add_trace(scatter_plot)

add_trace() 方法负责将绘图对象添加到图形中,并在图形内生成其可视化。

在网页上渲染图形

一旦图形被添加到图形中,就可以通过调用 plotly 库的 offline 绘图模块中的 plot 方法在网页上渲染它。以下是一个代码片段示例:

from plotly.offline import plot
visualization_html = plot(figure, output_type='div')

plot 方法接受两个主要参数:第一个是需要渲染的图形,第二个是包含图形 HTML 的容器的 HTML 标签。plot 方法返回可以嵌入任何网页或作为模板的一部分以渲染图形的完整集成 HTML。

现在,了解了图形绘图的工作原理后,让我们尝试一个动手练习来为我们的样本数据集生成图形。

练习 13.05:在 Python 中生成图形

在这个练习中,你将使用 Python 生成一个图形图。它将是一个散点图,用于表示二维数据:

  1. 对于这个练习,你将使用 plotly 库。要使用这个库,首先需要在系统上安装它。为此,运行以下命令:

    pip install plotly
    

    注意

    你可以使用我们提供的 GitHub 仓库中的 requirements.txt 文件安装 plotly 和其他依赖项:packt.live/38y5OLR

  2. 现在库已经安装,创建一个名为 scatter_plot_demo.py 的新文件,并在其中添加以下 import 语句:

    from plotly.offline import plot
    import plotly.graph_objs as graphs
    
  3. 在导入排序完成后,创建一个名为 generate_scatter_plot() 的方法,它接受两个参数,即 X 轴的值和 Y 轴的值:

    def generate_scatter_plot(x_axis, y_axis):
    
  4. 在这个方法中,首先创建一个作为图形容器的对象:

        figure = graphs.Figure()
    
  5. 一旦设置了图形的容器,创建一个新的 Scatter 对象,包含 X 轴和 Y 轴的值,并将其添加到图形 Figure 容器中:

        scatter = graphs.Scatter(x=x_axis, y=y_axis)
        figure.add_trace(scatter)
    
  6. 一旦散点图准备就绪并添加到图形中,最后一步是生成 HTML,这可以用来在网页中渲染此图形。为此,调用 plot 方法并将图形容器对象传递给它,然后在 HTML div 标签内渲染 HTML:

        return plot(figure, output_type='div')
    

    完整的 generate_scatter_plot() 方法现在应该看起来像这样:

    def generate_scatter_plot(x_axis, y_axis):
        figure = graphs.Figure()
        scatter = graphs.Scatter(x=x_axis, y=y_axis)
        figure.add_trace(scatter)
        return plot(figure, output_type='div')
    
  7. 一旦生成了图形的 HTML,它需要被渲染到某个地方。为此,创建一个名为 generate_html() 的新方法,它将接受图形 HTML 作为其参数,并渲染一个包含图形的 HTML 文件:

    def generate_html(plot_html):
        """Generate an HTML page for the provided plot."""
        html_content = "<html><head><title>Plot       Demo</title></head><body>{}</body></html>".format(plot_html)
        try:
            with open('plot_demo.html', 'w') as plot_file:
                plot_file.write(html_content)
        except (IOError, OSError) as file_io_error:
            print\
            ("Unable to generate plot file. Exception: {}"\
            .format(file_io_error))
    
  8. 一旦设置了方法,最后一步是调用它。为此,创建一个脚本入口点,该入口点将设置 X 轴列表和 Y 轴列表的值,然后调用 generate_scatter_plot() 方法。使用方法返回的值,调用 generate_html() 方法,该方法将创建一个包含散点图的 HTML 页面:

    if __name__ == '__main__':
        x = [1,2,3,4,5]
        y = [3,8,7,9,2]
        plot_html = generate_scatter_plot(x, y)
        generate_html(plot_html)
    
  9. 在代码就绪后,运行文件并查看生成的输出。要运行代码,执行以下命令:

    python3 scatter_plot_demo.py
    

    一旦执行完成,将在脚本执行的同一目录中创建一个新的 plot_demo.html 文件。打开文件后,你应该看到以下内容:

    图 13.8:使用 plotly 在浏览器中生成的图形

图 13.8:使用 plotly 在浏览器中生成的图形

通过这种方式,我们已经生成了第一个散点图,其中不同的点通过线条连接。

在这个练习中,你使用了 plotly 库来生成一个可以在浏览器中渲染的图形,以便你的读者可视化数据。

现在,你已经知道了如何在 Python 中处理图形以及如何从它们生成 HTML 页面。

但作为一个网页开发者,你如何在 Django 中使用这些图形呢?让我们来找出答案。

将 plotly 与 Django 集成

使用 plotly 生成的图形很容易嵌入到 Django 模板中。由于 plot 方法返回一个完整的 HTML,可以用来渲染图形,因此我们可以将返回的 HTML 作为 Django 模板变量传递,并保持原样。然后,Django 模板引擎将负责在浏览器显示之前将生成的 HTML 添加到最终的模板中。

下面展示了进行此操作的示例代码:

def user_profile(request):
    username = request.user.get_username()
    scatter_plot_html = scatter_plot_books_read(username)
    return render(request, 'user_profile.html'),\
                 (context={'plt_div': scatter_plot_html})

以下代码将导致模板内使用的 {{ plt_div }} 内容被存储在 scatter_plot_demo 变量中的 HTML 替换,并渲染每周阅读书籍数量的散点图。

将可视化与 Django 集成

在前面的章节中,你已经学习了如何以不同的格式读取和写入数据,以满足用户的不同需求。但我们如何将所学知识与 Django 集成呢?

例如,在 Bookr 中,我们可能希望允许用户导出他们已阅读的书籍列表或可视化他们一年的阅读活动。这该如何实现?本章的下一个练习将专注于这个方面,你将学习如何将我们迄今为止看到的组件集成到 Django 网络应用程序中。

练习 13.06:在用户个人资料页面上可视化用户的阅读历史

在这个练习中,你的目标是修改用户的个人资料页面,以便用户在访问 Bookr 上的个人资料页面时可以可视化他们的阅读历史。

让我们看看如何实现这一点:

  1. 要开始集成可视化用户阅读历史的功能,你首先需要安装 plotly 库。为此,请在您的终端中运行以下命令:

    pip install plotly
    

    注意

    您可以使用我们提供的 GitHub 仓库中的 requirements.txt 文件安装 plotly 和其他依赖项:packt.live/3scIvPp

  2. 库安装完成后,下一步是编写代码,以获取用户阅读的总书籍数量以及按月阅读的书籍。为此,在 bookr 应用程序目录下创建一个名为 utils.py 的新文件,并添加所需的导入,这些导入将用于从 reviews 应用程序的 Review 模型中获取用户的阅读历史:

    import datetime
    from django.db.models import Count
    from reviews.models import Review
    
  3. 接下来,创建一个名为 get_books_read_by_month() 的新实用方法,该方法接收需要获取阅读历史的用户名。

  4. 在该方法内部,我们查询 Review 模型,并按月返回用户阅读的书籍字典:

    def get_books_read_by_month(username):
        """Get the books read by the user on per month basis.
        :param: str The username for which the books needs to be returned
        :return: dict of month wise books read
        """
        current_year = datetime.datetime.now().year
        books = Review.objects.filter\
                (creator__username__contains=username),\
                (date_created__year=current_year)\
                .values('date_created__month')\
                .annotate(book_count=Count('book__title'))
        return books
    

    现在,让我们检查以下查询,该查询负责按月获取今年阅读的书籍结果:

    Review.objects.filter(creator__username__contains=username,date_created__year=current_year).values('date_created__month').annotate(book_count=Count('book__title'))
    

    此查询可以分解为以下组件:

    通过添加 __year,可以轻松访问我们的 date_created 字段中的 year 字段。

    使用 values() 调用来选择将要执行分组操作的 Review 模型的 date_created 属性中的 month 字段。

    annotate 方法应用于 values() 调用返回的 QuerySet 实例。

  5. 一旦放置好实用文件,下一步就是编写视图函数,这将有助于在用户的个人资料页面上显示每月阅读的书籍图表。为此,打开 bookr 目录下的 views.py 文件,并首先添加以下导入:

    from plotly.offline import plot
    import plotly.graph_objects as graphs
    from .utils import get_books_read_by_month
    
  6. 完成这些导入后,接下来要做的就是修改渲染个人资料页面的视图函数。目前,个人资料页面是由 views.py 文件中的 profile() 方法处理的。修改该方法,使其类似于以下所示:

    @login_required
    def profile(request):
        user = request.user
        permissions = user.get_all_permissions()
        # Get the books read in different months this year
        books_read_by_month = get_books_read_by_month(user.username)
        """
        Initialize the Axis for graphs, X-Axis is months, 
        Y-axis is books read
        """
        months = [i+1 for i in range(12)]
        books_read = [0 for _ in range(12)]
        # Set the value for books read per month on Y-Axis
        for num_books_read in books_read_by_month:
            list_index = num_books_read['date_created__month'] - 1
            books_read[list_index] = num_books_read['book_count']
        # Generate a scatter plot HTML
        figure = graphs.Figure()
        scatter = graphs.Scatter(x=months, y=books_read)
        figure.add_trace(scatter)
        figure.update_layout(xaxis_title="Month"),\
                            (yaxis_title="No. of books read")
        plot_html = plot(figure, output_type='div')
        # Add to template
          return render(request, 'profile.html'),\
                       ({'user': user, 'permissions': permissions,\
                       'books_read_plot': plot_html})
    

    在这个方法中,你做了几件事情。首先,你调用了get_books_read_by_month()方法,并提供了当前登录用户的用户名。该方法返回给定用户在当前年度按月阅读的书籍列表:

    books_read_by_month = get_books_read_by_month(user.username)
    

    接下来,你预先初始化了图表的X轴和Y轴的一些默认值。对于这个可视化,使用X轴显示月份,使用Y轴显示阅读的书籍数量。

    现在,既然你已经知道一年只有 12 个月,预先初始化X轴的值在112之间:

    months = [i+1 for i in range(12)]
    

    对于已阅读的书籍,将Y轴初始化为所有12个索引都设置为0,如下所示:

    books_read = [0 for _ in range(12)]
    

    现在,在完成预初始化后,为每月阅读的书籍填充一些实际值。为此,遍历get_books_read_by_month(user.username)调用结果得到的列表,从中提取月份和该月的书籍数量。

    一旦提取了书籍数量和月份,下一步是将book_count值分配给books_read列表的月份索引:

        for num_books_read in books_read_by_month:
            list_index = num_books_read['date_created__month'] - 1
            books_read[list_index] = num_books_read['book_count']
    

    现在,在设置了轴的值后,使用plotly库生成散点图:

    figure = graphs.Figure()
    scatter = graphs.Scatter(x=months, y=books_read)
    figure.add_trace(scatter)
    figure.update_layout(xaxis_title="Month", \
                         yaxis_title="No. of books read")
    plot_html = plot(figure, output_type='div')
    

    一旦生成了绘图 HTML,请使用render()方法将其传递给模板,以便在个人资料页面上进行可视化:

    return render(request, 'profile.html',
           {'user': user, 'permissions': permissions,\
            'books_read_plot': plot_html}
    
  7. 视图函数完成后,下一步是修改模板以渲染此图表。为此,打开templates目录下的profile.html文件,并在最后一个{% endblock %}语句之前添加以下突出显示的代码:

    {% extends "base.html" %}
    {% block title %}Bookr{% endblock %}
    {% block heading %}Profile{% endblock %}
    {% block content %}
      <ul>
          <li>Username: {{ user.username }} </li>
          <li>Name: {{ user.first_name }} {{ user.last_name }}</li>
          <li>Date Joined: {{ user.date_joined }} </li>
          <li>Email: {{ user.email }}</li>
          <li>Last Login: {{ user.last_login }}</li>
          <li>Groups: {{ groups }}{% if not groups %}None{% endif %} </li>
      </ul>
    books_read_plot variable passed in the view function to be used inside our HTML template. Also note that autoescape is set to off for this variable. This is required because this variable contains HTML generated by the plotly library and if you allow Django to escape the HTML, you will only see raw HTML in the profile page and not a graph visualization.With this, you have successfully integrated the plot into the application.
    
  8. 要尝试可视化,运行以下命令,然后通过访问http://localhost:8080导航到您的用户个人资料:

    python manage.py runserver localhost:8080
    

    你应该会看到一个类似于下面显示的页面:

    ![图 13.9:用户书籍阅读历史散点图]

    ![img/B15509_13_09.jpg]

图 13.9:用户书籍阅读历史散点图

在前面的练习中,你看到了如何将绘图库与 Django 集成以可视化用户的阅读历史。同样,Django 允许你将任何通用 Python 代码集成到 Web 应用程序中,唯一的限制是,集成产生的数据应转换为有效的 HTTP 响应,以便任何标准 HTTP 兼容的工具可以处理,例如 Web 浏览器或命令行工具,如 CURL。

活动 13.01:导出用户阅读的书籍为 XLSX 文件

在这个活动中,你将在 Bookr 中实现一个新的 API 端点,允许用户导出并下载他们已阅读的书籍列表作为 XLSX 文件:

  1. 安装XlsxWriter库。

  2. bookr应用程序下创建的utils.py文件中,创建一个新的函数,该函数将帮助获取用户已阅读的书籍列表。

  3. bookr目录下的views.py文件中,创建一个新的视图函数,允许用户以 XLSX 文件格式下载他们的阅读历史。

  4. 要在视图函数中创建 XLSX 文件,首先创建一个基于BytesIO的内存文件,该文件可以用来存储来自XlsxWriter库的数据。

  5. 使用临时文件对象的getvalue()方法读取内存文件中存储的数据。

  6. 最后,创建一个新的HttpResponse实例,并设置'application/vnd.ms-excel'内容类型头,然后将步骤 5 中获得的数据写入响应对象。

  7. 准备好响应对象后,从视图函数返回响应对象。

  8. 视图函数准备就绪后,将其映射到用户可以访问以下载他们的书籍阅读历史的 URL 端点。

一旦您已将 URL 端点映射,启动应用程序并使用您的用户账户登录。完成后,访问您刚刚创建的 URL 端点,如果在访问 URL 端点时您的浏览器开始下载 Excel 文件,则表示您已成功完成该活动。

注意

该活动的解决方案可在packt.live/2Nh1NTJ找到。

摘要

在本章中,我们探讨了如何处理二进制文件,以及 Python 的标准库,它预装了必要的工具,可以让我们处理常用的文件格式,如 CSV。然后,我们学习了如何使用 Python 的 CSV 模块在 Python 中读取和写入 CSV 文件。后来,我们使用了XlsxWriter包,它为我们提供了在 Python 环境中直接生成与 Microsoft Excel 兼容的文件的能力,无需担心文件的内部格式。

本章的后半部分致力于学习如何使用weasyprint库生成 HTML 页面的 PDF 版本。当我们想为用户提供一个方便的选项,让他们打印带有我们选择的任何 CSS 样式的 HTML 页面时,这项技能会很有用。本章的最后部分讨论了如何使用plotly库在 Python 中生成交互式图表,并将它们渲染为可以在浏览器中查看的 HTML 页面。

在下一章中,我们将探讨如何测试我们在前几章中实现的不同组件,以确保代码更改不会破坏我们网站的 功能。

第十四章:14. 测试

概述

本章向您介绍了测试 Django Web 应用程序的概念。您将了解测试在软件开发中的重要性,尤其是在构建 Web 应用程序方面。您将为 Django 应用程序的组件编写单元测试,例如视图模型端点。完成本章后,您将具备为 Django Web 应用程序编写测试用例的技能。这样,您可以确保您的应用程序代码按预期工作。

简介

在前面的章节中,我们通过编写不同的组件,如数据库模型、视图和模板,来专注于构建我们的 Django Web 应用程序。我们这样做是为了提供一个交互式应用程序,让用户可以创建个人资料并为他们读过的书籍撰写评论。

除了构建和运行应用程序之外,确保应用程序代码按预期工作还有一个重要的方面。这是通过一种称为测试的技术来保证的。在测试中,我们运行 Web 应用程序的不同部分,并检查执行组件的输出是否与预期的输出匹配。如果输出匹配,我们可以说该组件已成功测试,如果输出不匹配,我们则说该组件未能按预期工作。

在本章中,随着我们浏览不同的部分,我们将了解测试的重要性,了解测试 Web 应用程序的不同方法,以及我们如何构建一个强大的测试策略,以确保我们构建的 Web 应用程序是健壮的。让我们从了解测试的重要性开始我们的旅程。

测试的重要性

确保应用程序按预期设计的方式工作是开发工作的重要方面,否则,我们的用户可能会不断遇到奇怪的行为,这通常会驱使他们远离与应用程序的互动。

我们在测试上投入的努力帮助我们确保我们打算解决的问题确实被正确解决。想象一下,一个开发者正在构建一个在线活动调度平台。在这个平台上,用户可以根据他们的本地时区在日历上安排活动。现在,如果在这个平台上,用户可以按预期安排活动,但由于一个错误,活动被安排在了错误的时间区域?这类问题往往会驱使许多用户离开。

正因如此,许多公司花费大量资金确保他们构建的应用程序已经经过彻底的测试。这样,他们可以确保不会发布有缺陷的产品或远未满足用户需求的产品。

简而言之,测试帮助我们实现以下目标:

  • 确保应用程序的组件按规范工作

  • 确保与不同基础设施平台的互操作性:如果一个应用程序可以部署在不同的操作系统上,例如 Linux、Windows 等

  • 在重构应用程序代码时降低引入错误的可能性

现在,许多人关于测试的常见假设是他们必须手动测试所有组件,以确保每个组件按照其规范工作,每次更改或向应用程序添加新组件时都重复此操作。虽然这是真的,但这并不提供完整的测试图景。随着时间的推移,测试作为一种技术已经变得非常强大,作为开发者,你可以通过实现自动化测试用例来减少大量的测试工作。那么,这些自动化测试用例是什么?或者说,什么是自动化测试?让我们来了解一下。

自动化测试

当单个组件被修改时,重复测试整个应用程序可能是一项具有挑战性的任务,尤其是如果该应用程序包含大量的代码库。代码库的大小可能是由于功能数量庞大或解决的问题的复杂性。

随着我们开发应用程序,确保对这些应用程序所做的更改可以轻松测试非常重要,这样我们就可以验证是否有破坏性的东西。这就是自动化测试概念派上用场的地方。自动化测试的重点是将测试编写为代码,这样应用程序的各个组件就可以在隔离状态下以及它们相互交互的情况下进行测试。

从这个角度来看,现在对我们来说,定义可以为应用程序执行的不同类型的自动化测试变得很重要。

自动化测试可以大致分为五种不同类型:

  • 单元测试:在这种测试类型中,代码的各个独立单元被单独测试。例如,单元测试可以针对单个方法或单个独立的 API。这种测试的目的是确保应用程序的基本单元按照其规范工作。

  • 集成测试:在这种测试类型中,代码的各个独立单元被合并成一个逻辑分组。一旦形成这种分组,就会对这个逻辑组进行测试,以确保该组按预期的方式工作。

  • 功能测试:在这种测试中,测试应用程序不同组件的整体功能。这可能包括不同的 API、用户界面等。

  • 冒烟测试:在这种测试中,测试已部署应用程序的稳定性,以确保应用程序在用户与其交互时继续保持功能,而不会导致崩溃。

  • 回归测试:这种测试是为了确保对应用程序所做的更改不会降低应用程序先前构建的功能。

如我们所见,测试是一个庞大的领域,需要时间来掌握,关于这个主题已经写出了整本书。为了确保我们突出测试的重要方面,我们将在本章中专注于单元测试的方面。

Django 中的测试

Django 是一个功能丰富的框架,旨在使 Web 应用程序开发快速。它提供了一种全面的方式来测试应用程序。它还提供了一个良好集成的模块,允许应用程序开发者为其应用程序编写单元测试。此模块基于大多数 Python 发行版附带的 Python unittest库。

让我们开始了解如何在 Django 中编写基本的测试用例,以及如何利用框架提供的模块来测试我们的应用程序代码。

实现测试用例

当你在实现测试代码的机制时,首先需要理解的是如何逻辑地分组这种实现,以便相互紧密相关的模块可以在一个逻辑单元中进行测试。

这可以通过实现一个测试用例来简化。测试用例不过是一个逻辑单元,它将逻辑上相似的测试组合在一起,这样所有用于初始化测试用例环境的公共逻辑都可以组合在同一个地方,从而在实现应用程序测试代码时避免重复工作。

Django 中的单元测试

现在,随着我们对测试的基本理解已经清楚,让我们看看我们如何在 Django 中进行单元测试。在 Django 的上下文中,一个单元测试由两个主要部分组成:

  • 一个TestCase类,它封装了为给定模块分组的不同测试用例

  • 需要执行以测试特定组件流程的实际测试用例

实现单元测试的类应该继承自 Django 的test模块提供的TestCase类。默认情况下,Django 在应用程序目录中提供了一个tests.py文件,可以用来存储应用程序模块的测试用例。

一旦编写了这些单元测试,它们也可以通过直接运行manage.py中提供的test命令来轻松执行,如下所示:

python manage.py test

利用断言

编写测试的一个重要部分是验证测试是否通过或失败。通常,为了在测试环境中实现这样的决策,我们使用一种称为断言的东西。

断言是软件测试中的一个常见概念。它们接受两个操作数,并验证左边的操作数(LHS)的值是否与右边的操作数(RHS)的值匹配。如果左边的值与右边的值匹配,则认为断言成功,而如果值不同,则认为断言失败。

一个评估为False的断言实际上会导致测试用例被评估为失败,然后报告给用户。

Python 中的断言实现相当简单,它们使用一个简单的关键字assert。例如,以下代码片段展示了一个非常简单的断言:

assert 1 == 1

前面的断言接受一个表达式,该表达式评估为True。如果这个断言是测试用例的一部分,那么测试就会成功。

现在,让我们看看如何使用 Python 的unittest库实现测试用例。这样做相当简单,可以按照以下几个易于遵循的步骤完成:

  1. 导入unittest模块,它允许我们构建测试用例:

    import unittest
    
  2. 一旦模块被导入,你可以创建一个以Test开头的新类,该类继承自unittest模块提供的TestCase类:

    class TestMyModule(unittest.TestCase):
        def test_method_a(self):
            assert <expression>
    

    只有当TestMyModule类继承自TestCase类时,Django 才能自动运行它,并且与框架完全集成。一旦类被定义,我们就可以在类内部实现一个新的方法,命名为test_method_a(),该方法验证断言。

    注意

    这里需要注意的一个重要部分是测试用例和测试函数的命名方案。所实现的测试用例应该以test为前缀,这样测试执行模块可以检测它们作为有效的测试用例并执行它们。同样的规则也适用于测试方法的命名。

  3. 一旦编写了测试用例,就可以简单地通过运行以下命令来执行它:

    python manage.py test
    

    现在,随着我们对实现测试用例的基本理解已经明确,让我们编写一个非常简单的单元测试来查看单元测试框架在 Django 中的行为。

练习 14.01:编写简单的单元测试

在这个练习中,你将编写一个简单的单元测试来了解 Django 单元测试框架的工作方式,并使用这些知识来实现你的第一个测试用例,该测试用例验证几个简单的表达式。

  1. 要开始,打开Bookr项目下reviews应用的tests.py文件。默认情况下,该文件将只包含一行,导入 Django 的TestCase类。如果文件已经包含几个测试用例,你可以删除文件中除导入TestCase类的行之外的所有行,如下所示:

    from django.test import TestCase
    
  2. 在你刚刚打开的tests.py文件中添加以下代码行:

    class TestSimpleComponent(TestCase):
        def test_basic_sum(self):
            assert 1+1 == 2
    

    在这里,你创建了一个名为 TestSimpleComponent 的新类,它继承自 Django 的 test 模块提供的 TestCase 类。assert 语句将比较左侧的表达式(1 + 1)与右侧的表达式(2)。

  3. 一旦你编写了测试用例,导航回项目文件夹,并运行以下命令:

    python manage.py test
    

    应该生成以下输出:

    % ./manage.py test
    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    .
    ----------------------------------------------------------------------
    Ran 1 test in 0.001s
    OK
    Destroying test database for alias 'default'...
    

    前面的输出表明 Django 的测试运行器执行了一个测试用例,该测试用例成功通过了评估。

  4. 在确认测试用例正常工作并通过测试后,现在尝试在 test_basic_sum() 方法的末尾添加另一个断言,如下面的代码片段所示:

        assert 1+1 == 3
    
  5. tests.py 文件中添加了 assert 语句后,现在可以从项目文件夹中运行以下命令来执行测试用例:

    python manage.py test
    

在这一点上,你会注意到 Django 报告测试用例的执行失败了。

通过这种方式,你现在已经了解了如何在 Django 中编写测试用例以及如何使用断言来验证测试方法调用生成的输出是否正确。

断言类型

练习 14.01编写简单的单元测试 中,当我们遇到以下 assert 语句时,我们对断言有了一个简短的接触:

assert 1+1 == 2

这些断言语句很简单,使用了 Python 的 assert 关键字。在使用 unittest 库进行单元测试时,有几种不同的断言类型可以进行测试。让我们看看那些:

  • assertIsNone:这个断言用于检查一个表达式是否评估为 None。例如,这种类型的断言可以在查询数据库返回 None 的情况下使用,因为没有找到指定过滤条件下的记录。

  • assertIsInstance:这个断言用于验证提供的对象是否评估为提供的类型的实例。例如,我们可以验证方法返回的值是否确实为特定的类型,如列表、字典、元组等。

  • assertEquals:这是一个非常基础的函数,它接受两个参数并检查提供给它的参数是否在值上相等。这在你计划比较那些不保证排序的数据结构的值时可能很有用。

  • assertRaises:这个方法用于验证当调用它时,提供给它的方法名称是否引发了指定的异常。这在编写测试用例时很有用,其中需要测试引发异常的代码路径。例如,这种断言在确保执行数据库查询的方法(例如,让我们知道数据库连接是否尚未建立)引发异常时可能很有用。

这些只是我们可以在测试用例中做出的一小部分有用的断言。Django 测试库建立在 unittest 模块之上,它提供了更多可以进行测试的断言。

在每个测试用例运行后执行预测试设置和清理

在编写测试用例时,有时我们可能需要执行一些重复性任务;例如,设置一些测试所需的变量。一旦测试完成,我们希望清理所有对测试变量的更改,以便任何新的测试都从一个全新的实例开始。

幸运的是,unittest库提供了一种有用的方法,通过它可以自动化我们在每个测试用例运行之前设置环境以及在测试用例完成后清理环境时的重复性工作。这是通过以下两个方法实现的,我们可以在TestCase中实现这些方法。

setUp():此方法在TestCase类中每个test方法执行之前调用。它实现了在测试执行之前设置测试用例环境的代码。此方法可以是一个设置任何本地数据库实例或测试变量的好地方,这些变量可能对测试用例是必需的。

注意

setUp()方法仅适用于在TestCase类内部编写的测试用例。

例如,以下示例说明了如何在TestCase类内部使用setUp()方法的一个简单定义:

class MyTestCase(unittest.TestCase):
    def setUp(self):
        # Do some initialization work
    def test_method_a(self):
        # code for testing method A
    def test_method_b(self):
        # code for testing method B

在前面的示例中,当我们尝试执行测试用例时,我们定义的setUp()方法将在每次test方法执行之前调用。换句话说,setUp()方法将在test_method_a()调用之前调用,然后它将在test_method_b()调用之前再次调用。

tearDown():此方法在test函数执行完毕后调用,并在测试用例执行完毕后清理变量及其值。无论测试用例评估结果为True还是False,都会执行此方法。下面将展示如何使用tearDown()方法的示例:

class MyTestCase(unittest.TestCase):
    def setUp(self):
        # Do some initialization work
    def test_method_a(self):
        # code for testing method A
    def test_method_b(self):
        # code for testing method B
    def tearDown(self):
        # perform cleanup

在前面的示例中,tearDown()方法将在每次test方法执行完毕时调用,即test_method_a()执行完毕后,再次在test_method_b()执行完毕后。

现在,我们已经了解了编写测试用例的不同组件。让我们看看如何使用提供的测试框架来测试 Django 应用程序的不同方面。

测试 Django 模型

Django 中的模型是数据将在应用程序数据库中存储的对象表示。它们提供了可以帮助我们验证给定记录提供的数据输入的方法,以及在数据插入数据库之前对数据进行任何处理的方法。

就像在 Django 中创建模型一样容易,测试它们也同样简单。现在,让我们看看如何使用 Django 测试框架来测试 Django 模型。

练习 14.02:测试 Django 模型

在这个练习中,您将创建一个新的 Django 模型,并为它编写测试用例。这个测试用例将验证您的模型是否能够正确地将数据插入和从数据库中检索。这类在数据库模型上运行的测试用例在团队开发大型项目时可能非常有用,因为同一个数据库模型可能会随着时间的推移被多个开发者修改。为数据库模型实现测试用例允许开发者预先识别他们可能在不经意间引入的潜在破坏性更改:

注意

为了确保我们能够熟练地从零开始在新创建的应用程序上运行测试,我们将创建一个新的应用程序,名为 bookr_test。这个应用程序的代码与主 bookr 应用程序独立,因此,我们不会将这个应用程序的文件包含在 final/bookr 文件夹中。完成本章内容后,我们建议您通过为 bookr 应用程序的各种组件编写类似的测试来练习您所学的知识。

  1. 创建一个新的应用程序,您将使用它来完成本章的练习。为此,运行以下命令,这将为您的情况设置一个新的应用程序:

    python manage.py startapp bookr_test
    
  2. 为了确保 bookr_test 应用程序的行为与 Django 项目中的任何其他应用程序相同,将此应用程序添加到 bookr 项目的 INSTALLED_APPS 部分中。为此,打开您的 bookr 项目的 settings.py 文件,并将以下代码追加到 INSTALLED_APPS 列表中:

    INSTALLED_APPS = [….,\
                      ….,\
                      'bookr_test']
    
  3. 现在,随着应用程序设置的完成,创建一个新的数据库模型,您将使用它来进行测试。对于这个练习,您将创建一个名为 Publisher 的新模型,该模型将存储有关书籍出版商的详细信息。要创建模型,打开 bookr_test 目录下的 models.py 文件,并将以下代码添加到其中:

    from django.db import models
    class Publisher(models.Model):
        """A company that publishes books."""
        name = models.CharField\
               (max_length=50,\
                help_text="The name of the Publisher.")
        website = models.URLField\
                  (help_text="The Publisher's website.")
        email = models.EmailField\
                (help_text="The Publisher's email address.")
        def __str__(self):
            return self.name
    

    在前面的代码片段中,您创建了一个名为 Publisher 的新类,该类继承自 Django 的 models 模块中的 Model 类,将类定义为 Django 模型,该模型将用于存储有关出版商的数据:

    class Publisher(models.Model)
    

    在这个模型内部,您添加了三个字段,这些字段将作为模型的属性:

    name: 出版商的名称

    website: 出版商的网站

    email: 出版商的电子邮件地址

    完成此操作后,您创建了一个类方法 __str__(),它定义了模型字符串表示的形式。

  4. 现在,模型已经创建,您首先需要迁移此模型,然后才能在它上面运行测试。为此,运行以下命令:

    python manage.py makemigrations
    python manage.py migrate
    
  5. 现在模型已经设置好了,编写测试用例来测试在 步骤 3 中创建的模型。为此,打开 bookr_test 目录下的 tests.py 文件,并将以下代码添加到其中:

    from django.test import TestCase
    from .models import Publisher
    class TestPublisherModel(TestCase):
        """Test the publisher model."""
        def setUp(self):
            self.p = Publisher(name='Packt', \
                               website='www.packt.com', \
                               email='contact@packt.com')
        def test_create_publisher(self):
            self.assertIsInstance(self.p, Publisher)
        def test_str_representation(self):
            self.assertEquals(str(self.p), "Packt")
    

    在前面的代码片段中,有几个值得探讨的地方。

    在开始时,在从 Django 的test模块导入TestCase类之后,你从bookr_test目录导入了Publisher模型,该模型将被用于测试。

    在导入所需的库之后,你创建了一个名为TestPublisherModel的新类,它继承自TestCase类,并用于组织与Publisher模型相关的单元测试:

    class TestPublisherModel(TestCase):
    

    在这个类中,你定义了一些方法。首先,你定义了一个名为setUp()的新方法,并在其中添加了Model对象创建的代码,这样每次在这个测试用例中执行新的test方法时,都会创建一个Model对象。这个Model对象被存储为类成员,这样就可以在其他方法中无问题地访问它:

    def setUp(self):
        self.p = Publisher(name='Packt', \
                           website='www.packt.com', \
                           email='contact@packt.com')
    

    第一个测试用例验证Publisher模型的Model对象是否创建成功。为此,你创建了一个名为test_create_publisher()的新方法,在其中检查创建的模型对象是否指向Publisher类型的对象。如果这个Model对象没有成功创建,你的测试将失败:

        def test_create_publisher(self):
            self.assertIsInstance(self.p, Publisher)
    

    如果你仔细检查,你在这里使用的是unittest库的assertIsInstance()方法来断言Model对象是否属于Publisher类型。

    下一个测试验证模型的字符串表示是否与预期相同。从代码定义来看,Publisher模型的字符串表示应该输出出版者的名称。为了测试这一点,你创建了一个名为test_str_representation()的新方法,并检查生成的模型字符串表示是否与预期匹配:

    def test_str_representation(self):
        self.assertEquals(str(self.p), "Packt")
    

    为了执行这个验证,你使用unittest库的assertEquals方法,该方法验证提供的两个值是否相等。

  6. 现在测试用例已经就绪,你可以运行它们来检查会发生什么。要运行这些测试用例,请运行以下命令:

    python manage.py test
    

    一旦命令执行完成,你将看到类似于以下输出的输出(你的输出可能略有不同):

    % python manage.py test
    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    ..
    ----------------------------------------------------------------------
    Ran 2 tests in 0.002s
    OK
    Destroying test database for alias 'default'...
    

    如前所述的输出所示,测试用例执行成功,从而验证了诸如创建新的Publisher对象及其在检索时的字符串表示等操作是否正确执行。

通过这个练习,我们看到了如何轻松编写 Django 模型的测试用例并验证其功能,包括对象的创建、检索和表示。

此外,在这个练习的输出中还有一行重要的内容需要注意:

"Destroying test database for alias 'default'..."

这是因为当存在需要将数据持久化存储在数据库中的测试用例时,Django 不会使用生产数据库,而是为测试用例创建一个新的空数据库,它使用这个数据库来持久化测试用例的值。

测试 Django 视图

Django 中的视图控制用户基于在 Web 应用程序中访问的 URL 渲染 HTTP 响应。在本节中,我们将了解如何测试 Django 中的视图。想象一下,您正在开发一个需要大量应用程序编程接口API)端点的网站。一个有趣的问题可能是,您将如何验证每个新的端点?如果手动完成,每次添加新端点时,您都必须首先部署应用程序,然后在浏览器中手动访问端点以验证其是否正常工作。当端点数量较少时,这种方法可能可行,但如果端点有数百个,这种方法可能会变得极其繁琐。

Django 提供了一种非常全面的测试应用程序视图的方法。这是通过使用 Django 的test模块提供的测试客户端类来实现的。这个类可以用来访问映射到视图的 URL,并捕获访问 URL 端点时生成的输出。然后我们可以使用捕获的输出来测试 URL 是否生成了正确的响应。此客户端可以通过从 Django 的test模块导入Client类,然后按照以下代码片段初始化它来使用:

from django.test import Client
c = Client()

客户端对象支持多种方法,这些方法可以用来模拟用户可以发起的不同 HTTP 调用,例如,GETPOSTPUTDELETE等。发起此类请求的示例将如下所示:

response = c.get('/welcome')

视图生成的响应随后被客户端捕获,并作为response对象暴露出来,然后可以查询以验证视图的输出。

带着这些知识,现在让我们看看我们如何为我们的 Django 视图编写测试用例。

练习 14.03:为 Django 视图编写单元测试

在这个练习中,您将使用 Django 测试客户端编写针对您的 Django 视图的测试用例,该视图将被映射到特定的 URL。这些测试用例将帮助您验证当使用其映射的 URL 访问视图函数时,是否生成了正确的响应:

  1. 对于这个练习,您将使用在练习 14.02步骤 1中创建的bookr_test应用程序,即测试 Django 模型。要开始,打开 bookr_test 目录下的views.py文件,并将以下代码添加到其中:

    from django.http import HttpResponse
    def greeting_view(request):
        """Greet the user."""
        return HttpResponse("Hey there, welcome to Bookr!")\
                           ("Your one stop place")\
                           ("to review books.")
    

    在这里,您创建了一个简单的 Django 视图,该视图将在用户访问映射到提供的视图的端点时,用欢迎消息问候用户。

  2. 一旦创建了此视图,您需要将其映射到 URL 端点,然后可以在浏览器或测试客户端中访问它。为此,打开bookr_test目录下的urls.py文件,并将高亮代码添加到urlpatterns列表中:

    from django.urls import path
    from . import views
    urlpatterns = [greeting_view to the 'test/greeting' endpoint for the application by setting the path in the urlpatterns list.
    
  3. 一旦设置了此路径,你需要确保它也被你的项目识别。为此,你需要将此条目添加到bookr项目的 URL 映射中。为了实现这一点,打开bookr目录下的urls.py文件,并将以下突出显示的行追加到urlpatterns列表的末尾,如下所示:

    urlpatterns = [….,\
                   ….,\
                   urls.py file should look like this now: http://packt.live/3nF8Sdb.
    
  4. 一旦设置好视图,请验证其是否正确工作。通过运行以下命令来完成此操作:

    python manage.py runserver localhost:8080
    

    然后在你的网络浏览器中访问http://localhost:8080/test/greeting。一旦页面打开,你应该看到以下文本,这是你在步骤 1中添加到问候视图中的,并在浏览器中显示:

    Hey there, welcome to Bookr! Your one stop place to review books.
    
  5. 现在,你准备好为greeting_view编写测试用例。在这个练习中,你将编写一个测试用例,检查在访问/test/greeting端点时,你是否得到一个成功的结果。为了实现这个测试用例,打开bookr_test目录下的tests.py文件,并在文件末尾添加以下代码:

    from django.test import TestCase, Client
    class TestGreetingView(TestCase):
        """Test the greeting view."""
        def setUp(self):
            self.client = Client()
        def test_greeting_view(self):
            response = self.client.get('/test/greeting')
            self.assertEquals(response.status_code, 200)
    

    在前面的代码片段中,你定义了一个测试用例,有助于验证问候视图是否正常工作。

    这是通过首先导入 Django 的测试客户端来完成的,它允许通过对其调用并分析生成的响应来测试映射到 URL 的视图:

    from django.test import TestCase, Client
    

    完成导入后,你现在创建一个名为TestGreetingView的新类,该类将分组与你在步骤 2中创建的问候视图相关的测试用例:

    class TestGreetingView(TestCase):
    

    在此测试用例中,你定义了两个方法,setUp()test_greeting_view()test_greeting_view()方法实现了你的测试用例。在这个方法中,你首先对映射到问候视图的 URL 进行 HTTP GET调用,然后将视图生成的响应存储在创建的response对象中:

    response = self.client.get('/test/greeting')
    

    一旦这个调用完成,你将在response变量内部获得其 HTTP 响应代码、内容和头信息。接下来,使用以下代码,你将进行断言以验证调用生成的状态码是否与成功 HTTP 调用(HTTP 200)的状态码匹配:

    self.assertEquals(response.status_code, 200)
    

    通过这种方式,你现在可以运行测试了。

  6. 编写测试用例后,让我们看看运行测试用例时会发生什么:

    python manage.py test
    

    一旦命令执行,你可以预期看到以下片段所示的输出:

    % python manage.py test
    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    ...
    ----------------------------------------------------------------------
    Ran 3 tests in 0.006s
    OK
    Destroying test database for alias 'default'...
    

    如输出所示,你的测试用例执行成功,从而验证了greeting_view()方法生成的响应符合你的预期。

在这个练习中,你学习了如何为 Django 视图函数实现测试用例,并使用 Django 提供的TestClient断言视图函数生成的输出与开发者应看到的输出相匹配。

使用身份验证测试视图

在上一个示例中,我们探讨了如何在 Django 中测试视图。关于这个视图的一个重要方面是,我们创建的视图可以被任何人访问,并且没有任何身份验证或登录检查来保护。现在想象一个场景,其中视图只有在用户登录时才可访问。例如,想象实现一个视图函数,用于渲染我们网络应用的注册用户的个人资料页面。为了确保只有登录用户可以查看其账户的个人资料页面,您可能希望将视图限制为仅对登录用户开放。

通过这种方式,我们现在有一个重要的问题:我们如何测试需要认证的视图?

幸运的是,Django 的测试客户端提供了这项功能,我们可以通过它登录到我们的视图并对其运行测试。这可以通过使用 Django 的测试客户端 login() 方法来实现。当此方法被调用时,Django 的测试客户端会对服务执行身份验证操作,如果身份验证成功,它将在内部存储登录 cookie,然后可以使用它进行进一步的测试运行。以下代码片段显示了如何设置 Django 的测试客户端来模拟已登录用户:

login = self.client.login(username='testuser', password='testpassword')

login 方法需要测试用户的用户名和密码,正如将在下一个练习中展示的那样。因此,让我们看看如何测试需要用户身份验证的流程。

练习 14.04:编写测试用例以验证已认证用户

在这个练习中,您将编写测试用例来测试需要用户进行身份验证的视图。作为这部分,您将验证当未登录用户尝试访问页面以及当登录用户尝试访问映射到视图函数的页面时,视图方法生成的输出。

  1. 对于这个练习,您将使用在 练习 14.02步骤 1 中创建的 bookr_test 应用程序。要开始,打开 bookr_test 应用程序下的 views.py 文件,并向其中添加以下代码:

    from django.http import HttpResponse
    from django.contrib.auth.decorators import login_required
    

    一旦添加了前面的代码片段,请在文件的末尾创建一个新的函数 greeting_view_user(),如下面的代码片段所示:

    @login_required
    def greeting_view_user(request):
        """Greeting view for the user."""
        user = request.user
        return HttpResponse("Welcome to Bookr! {username}"\
                            .format(username=user))
    

    通过这种方式,您已经创建了一个简单的 Django 视图,该视图将在用户访问映射到提供的视图的端点时,用欢迎信息问候登录用户。

  2. 一旦创建了此视图,您需要将其映射到可以在浏览器或测试客户端中访问的 URL 端点。为此,打开 bookr_test 目录下的 urls.py 文件,并向其中添加以下突出显示的代码:

    from django.urls import path
    from . import views
    urlpatterns = [greeting_view_user to the 'test/greet_user' endpoint for the application by setting the path in the urlpatterns list. If you have followed the previous exercises, this URL should already be set up for detection in the project and no further steps are required to configure the URL mapping.
    
  3. 一旦设置了视图,接下来您需要做的就是验证它是否正确工作。为此,运行以下命令:

    python manage.py runserver localhost:8080
    

    然后在您的网页浏览器中访问 http://localhost:8080/test/greet_user

    如果您尚未登录,通过访问前面的 URL,您将被重定向到项目的登录页面。

  4. 现在,为greeting_view_user编写测试用例,该测试用例检查在访问/test/greet_user端点时是否得到成功的结果。为了实现此测试用例,打开bookr_test目录下的tests.py文件,并向其中添加以下代码:

    from django.contrib.auth.models import User
    class TestLoggedInGreetingView(TestCase):
        """Test the greeting view for the authenticated users."""
        def setUp(self):
            test_user = User.objects.create_user\
                        (username='testuser', \
                         password='test@#628password')
            test_user.save()
            self.client = Client()
        def test_user_greeting_not_authenticated(self):
            response = self.client.get('/test/greet_user')
            self.assertEquals(response.status_code, 302)
        def test_user_authenticated(self):
            login = self.client.login\
                    (username='testuser', \
                     password='test@#628password')
            response = self.client.get('/test/greet_user')
            self.assertEquals(response.status_code, 200)
    

    在前面的代码片段中,您实现了一个测试用例,该用例检查在内容可见之前是否启用了视图的认证。

    因此,您首先导入了将用于定义测试用例和初始化测试客户端的必需类和方法:

    from django.test import TestCase, Client
    

    您接下来需要的是 Django auth模块中的User模型:

    from django.contrib.auth.models import User
    

    此模型是必需的,因为对于需要认证的测试用例,您需要初始化一个新的测试用户。接下来,您创建了一个名为TestLoggedInGreetingView的新类,该类封装了与greeting_user视图(需要认证)相关的测试。在这个类内部,您定义了三个方法,分别是:setUp()test_user_greeting_not_authenticated()test_user_authenticated()setUp()方法用于首先初始化一个测试用户,您将使用它进行认证。这是一个必需的步骤,因为 Django 测试环境是一个完全隔离的环境,它不使用生产应用程序中的数据,因此所有必需的模型和对象都必须在测试环境中单独实例化。

    然后,您使用以下代码创建了测试用户并初始化了测试客户端:

    test_user = User.objects.create_user\
                (username='testuser', \
                 password='test@#628password')
    test_user.save()
    self.client = Client()
    

    接下来,您为用户未认证时的greet_user端点编写了测试用例。在这个测试用例中,您应该期望 Django 将用户重定向到登录端点。可以通过检查响应的 HTTP 状态码来检测此重定向,状态码应设置为HTTP 302,表示重定向操作:

    def test_user_greeting_not_authenticated(self):
        response = self.client.get('/test/greet_user')
        self.assertEquals(response.status_code, 302)
    

    接下来,您又编写了另一个测试用例,以检查当用户认证时greet_user端点是否成功渲染。为了认证用户,您首先调用测试客户端的login()方法,并通过提供在setUp()方法中创建的测试用户的用户名和密码进行认证,如下所示:

    login = self.client.login\
            (username='testuser', \
             password='test@#628password')
    

    登录完成后,您向greet_user端点发送一个HTTP GET请求,并通过检查返回响应的 HTTP 状态码来验证端点是否生成正确的结果:

    response = self.client.get('/test/greet_user')
    self.assertEquals(response.status_code, 200)
    
  5. 测试用例编写完成后,是时候检查它们如何运行了。为此,运行以下命令:

    python manage.py test
    

    执行完成后,您应该会看到以下类似响应:

    % python manage.py test
    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    .....
    ----------------------------------------------------------------------
    Ran 5 tests in 0.366s
    OK
    Destroying test database for alias 'default'...
    

    如前所述的输出所示,我们的测试用例已成功通过,验证了我们创建的视图在用户未认证时重定向用户到网站,并在用户认证时允许用户查看页面。

在这个练习中,我们只是实现了一个测试用例,我们可以测试视图函数生成的关于用户认证状态的输出。

Django 请求工厂

到目前为止,我们一直在使用 Django 的测试客户端来测试我们为应用程序创建的视图。测试客户端类模拟了一个浏览器,并使用这种模拟来调用所需的 API。但如果我们不想使用测试客户端及其作为浏览器的相关模拟,而是想直接通过传递请求参数来测试视图函数,我们应该怎么做?

为了帮助我们处理这种情况,我们可以利用 Django 提供的RequestFactory类。RequestFactory类帮助我们提供request对象,我们可以将其传递给我们的视图函数以评估其工作。可以通过以下方式创建RequestFactory的实例:

factory = RequestFactory()

因此创建的factory对象仅支持 HTTP 方法,如get()post()put()等,以模拟对任何 URL 端点的调用。让我们看看我们如何修改我们在练习 14.04编写测试用例以验证已认证用户中编写的测试用例,以使用RequestFactory

练习 14.05:使用请求工厂测试视图

在这个练习中,你将使用请求工厂来测试 Django 中的视图函数:

  1. 对于这个练习,你将使用在练习 14.04编写测试用例以验证已认证用户的第 1 步中创建的现有的greeting_view_user视图函数,如下所示:

    @login_required
    def greeting_view_user(request):
        """Greeting view for the user."""
        user = request.user
        return HttpResponse("Welcome to Bookr! {username}"\
                            .format(username=user))
    
  2. 接下来,修改在bookr_test目录下的tests.py文件中定义的现有测试用例TestLoggedInGreetingView。打开tests.py文件并做出以下更改。

    首先,你需要添加以下导入以在测试用例中使用RequestFactory

    from django.test import RequestFactory
    

    下一步你需要的是从 Django 的auth模块导入AnonymousUser类和从views模块导入的greeting_view_user视图方法。这是为了测试使用模拟未登录用户的视图函数所必需的。这可以通过添加以下代码来完成:

    from django.contrib.auth.models import AnonymousUser
    from .views import greeting_view_user
    
  3. 一旦添加了import语句,修改TestLoggedInGreetingView类的setUp()方法,并更改其内容以类似于以下所示:

    def setUp(self):
        self.test_user = User.objects.create_user\
                         (username='testuser', \
                          password='test@#628password')
        self.test_user.save()
        self.factory = RequestFactory()
    

    在这个方法中,你首先创建了一个user对象,并将其存储为类成员,以便你可以在测试中稍后使用它。一旦创建了user对象,然后实例化一个新的RequestFactory类的新实例,以便用于测试我们的视图函数。

  4. 现在定义了setUp()方法后,修改现有的测试以使用RequestFactory实例。对于对视图函数的非认证调用测试,将test_user_greeting_not_authenticated方法修改为以下内容:

    def test_user_greeting_not_authenticated(self):
        request = self.factory.get('/test/greet_user')
        request.user = AnonymousUser()
        response = greeting_view_user(request)
        self.assertEquals(response.status_code, 302)
    

    在这个方法中,你首先使用在setUp()方法中定义的RequestFactory实例创建了一个request对象。一旦完成,你将一个AnonymousUser()实例分配给request.user属性。将AnonymousUser()实例分配给该属性使得视图函数认为发起请求的用户未登录:

    request.user = AnonymousUser()
    

    一旦完成,你调用了greeting_view_user()视图方法,并将你创建的request对象传递给它。一旦调用成功,你使用以下代码在response变量中捕获方法的输出:

    response = greeting_view_user(request)
    

    对于未认证的用户,你期望得到一个重定向响应,可以通过检查响应的 HTTP 状态码来测试,如下所示:

    self.assertEquals(response.status_code, 302)
    
  5. 一旦完成这个步骤,继续修改其他方法,例如test_user_authenticated(),同样使用RequestFactory实例,如下所示:

    def test_user_authenticated(self):
        request = self.factory.get('/test/greet_user')
        request.user = self.test_user
        response = greeting_view_user(request)
        self.assertEquals(response.status_code, 200)
    

    如你所见,大部分代码与你在test_user_greeting_not_authenticated方法中编写的代码相似,只是有一点小的变化:在这个方法中,我们不是使用AnonymousUser来设置request.user属性,而是使用你在setUp()方法中创建的test_user

    request.user = self.test_user
    

    完成这些更改后,是时候运行测试了。

  6. 要运行测试并验证请求工厂是否按预期工作,请运行以下命令:

    python manage.py test
    

    命令执行后,你可以期待看到类似于以下输出的结果:

    % python manage.py test   
    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    ......
    ----------------------------------------------------------------------
    Ran 6 tests in 0.248s
    OK
    Destroying test database for alias 'default'...
    

    从输出中我们可以看到,我们编写的测试用例已经成功通过,从而验证了RequestFactory类的行为。

通过这个练习,我们学习了如何利用RequestFactory编写测试用例,并将request对象直接传递给视图函数,而不是使用测试客户端方法模拟 URL 访问,从而允许更直接的测试。

测试基于类的视图

在上一个练习中,我们看到了如何测试定义为方法的视图。但对于基于类的视图呢?我们该如何测试它们?

结果表明,测试基于类的视图相当简单。例如,如果我们有一个名为ExampleClassView(View)的基于类的视图,要测试这个视图,我们只需要使用以下语法:

response = ExampleClassView.as_view()(request)

就这么简单。

Django 应用程序通常由几个不同的组件组成,这些组件可以独立工作,例如模型,以及一些需要与 URL 映射和其他框架部分交互才能工作的其他组件。测试这些不同的组件可能需要一些只有这些组件共有的步骤。例如,在测试模型时,我们可能首先想要在开始测试之前创建某些Model类的对象,或者对于视图,我们可能首先想要使用用户凭据初始化测试客户端。

事实上,Django 还提供了一些基于TestCase类的其他类,可以用来编写关于所使用组件类型的特定类型的测试用例。让我们看看 Django 提供这些不同的类。

Django 中的测试用例类

除了 Django 提供的基类TestCase,它可以用来为不同的组件定义多种测试用例之外,Django 还提供了一些从TestCase类派生的专用类。这些类根据它们提供给开发者的功能,用于特定类型的测试用例。

让我们快速看一下它们。

SimpleTestCase

这个类是从 Django 的test模块提供的TestCase类派生出来的,应该用于编写测试视图函数的简单测试用例。通常,当你的测试用例涉及进行数据库查询时,不推荐使用这个类。该类还提供了许多有用的功能,例如以下功能:

  • 检查由视图函数引发的异常的能力

  • 测试表单字段的能力

  • 内置的测试客户端

  • 验证视图函数引起的重定向的能力

  • 匹配由视图函数生成的两个 HTML、JSON 或 XML 输出的相等性

现在,让我们先了解一下SimpleTestCase是什么,然后尝试理解另一种类型的测试用例类,它有助于编写涉及与数据库交互的测试用例。

TransactionTestCase

这个类是从SimpleTestCase类派生出来的,应该在编写涉及数据库交互的测试用例时使用,例如数据库查询、模型对象创建等。

该类提供了以下附加功能:

  • 在测试用例运行之前将数据库重置到默认状态的能力

  • 根据数据库功能跳过测试 - 如果用于测试的数据库不支持生产数据库的所有功能,这个功能可能会很有用

LiveServerTestCase

这个类类似于TransactionTestCase类,但有一个小的区别,即该类中编写的测试用例使用 Django 创建的实时服务器(而不是使用默认的测试客户端)。

当编写测试用例以测试渲染的网页及其任何交互时,这种运行实时服务器进行测试的能力会很有用,这是在使用默认测试客户端时无法实现的。

这样的测试用例可以利用像Selenium这样的工具,它可以用来构建通过与之交互来修改渲染页面状态的交互式测试用例。

测试代码模块化

在前面的练习中,我们已经看到了如何为项目中的不同组件编写测试用例。但需要注意的一个重要方面是,到目前为止,我们一直在单个文件中为所有组件编写测试用例。当应用程序没有很多视图和模型时,这种方法是可以的。但随着应用程序的增长,这可能会变得有问题,因为现在我们的单个 tests.py 文件将难以维护。

为了避免遇到此类场景,我们应该尝试模块化我们的测试用例,使得模型测试用例与视图相关的测试用例等保持分离。为了实现这种模块化,我们只需要执行两个简单的步骤:

  1. 通过运行以下命令,在您的应用程序目录内创建一个名为 tests 的新目录:

    mkdir tests
    
  2. 通过运行以下命令,在您的测试目录中创建一个名为 __init__.py 的新空文件:

    touch __init__.py
    

    这个 __init__.py 文件是 Django 所需的,以便正确检测我们创建的 tests 目录作为一个模块而不是普通目录。

完成前面的步骤后,您可以继续为应用程序中的不同组件创建新的测试文件。例如,要为您的模型编写测试用例,您可以在测试目录中创建一个名为 test_models.py 的新文件,并将与模型测试相关的任何代码添加到此文件中。

此外,您不需要采取任何其他额外步骤来运行您的测试。相同的命令将完美适用于您的模块化测试代码库:

python manage.py test

通过这种方式,我们现在已经了解了如何为我们的项目编写测试用例。那么,我们何不通过为正在进行的 Bookr 项目编写测试用例来评估我们的知识呢?

活动 14.01:在 Bookr 中测试模型和视图

在这个活动中,您将为 Bookr 项目实现测试用例。您将实现验证 reviews 应用程序中创建的模型的测试用例,然后您将实现一个简单的测试用例来验证 reviews 应用程序。

以下步骤将帮助您完成这个活动:

  1. reviews 应用程序目录中创建一个名为 tests 的目录,以便我们可以将所有针对 reviews 应用程序的测试用例进行模块化。

  2. 创建一个空的 __init__.py 文件,这样目录就被视为不是普通目录,而是一个 Python 模块目录。

  3. 创建一个新文件,名为 test_models.py,用于实现测试模型的代码。在此文件中,导入您想要测试的模型。

  4. test_models.py 中,创建一个新的类,该类继承自 django.tests 模块的 TestCase 类,并实现验证 Model 对象创建和读取的方法。

  5. 要测试视图函数,在 tests 目录中(在步骤 1 中创建的)创建一个名为 test_views.py 的新文件。

  6. test_views.py 文件中,从 django.tests 模块导入测试 Client 类,以及从 reviews 应用程序的 views.py 文件中导入 index 视图函数。

  7. 步骤 5 中创建的 test_views.py 文件中,创建一个新的 TestCase 类,并实现方法来验证索引视图。

  8. 步骤 7 中创建的 TestCase 类中,创建一个新的函数 setUp(),在其中你应该初始化一个 RequestFactory 实例,该实例将用于创建一个可以直接传递给视图函数进行测试的 request 对象。

  9. 完成前面的步骤并编写了测试用例后,通过执行 python manage.py test 来运行测试用例,以验证测试用例是否通过。

完成此活动后,所有测试用例都应成功通过。

注意

此活动的解决方案可以在 packt.live/2Nh1NTJ 找到。

摘要

在本章中,我们探讨了如何使用 Django 为我们的 Web 应用程序项目编写不同组件的测试用例。我们了解了为什么测试在任何 Web 应用程序的开发中都起着至关重要的作用,以及行业中采用的不同测试技术,以确保他们发布的应用程序代码是稳定且无错误的。

我们接着探讨了如何使用 Django 的 test 模块提供的 TestCase 类来实现我们的单元测试,这些测试可以用来测试模型以及视图。我们还探讨了如何使用 Django 的 test 客户端来测试需要或不需要用户认证的视图函数。我们还简要介绍了使用 RequestFactory 来测试方法视图和基于类的视图的另一种方法。

我们通过了解 Django 提供的预定义类以及它们应该在哪里使用,并探讨了如何模块化我们的测试代码库,使其看起来更整洁,来结束本章。

随着我们进入下一章,我们将尝试理解如何通过将第三方库集成到我们的项目中来使我们的 Django 应用程序更加强大。然后,我们将使用此功能将第三方身份验证集成到我们的 Django 应用程序中,从而允许用户使用 Google Sign-In、Facebook 登录等流行服务登录应用程序。

第十五章:15. Django 第三方库

概述

本章向您介绍 Django 第三方库。您将使用dj-database-urls通过 URL 配置您的数据库连接,并使用django-crispy-forms检查和调试您的应用程序,您将增强表单的外观,并通过使用crispy模板标签减少您需要编写的代码量。我们还将介绍django-allauth库,它允许您使用第三方提供者对用户进行身份验证。在最后的活动中,我们将使用django-crispy-forms增强 Bookr 的表单。

简介

由于 Django 自 2007 年以来一直存在,因此有一个丰富的第三方库生态系统,可以将其连接到应用程序以提供额外功能。到目前为止,我们已经学到了很多关于 Django 的知识,并使用了其许多功能,包括数据库模型、URL 路由、模板、表单等等。我们直接使用这些 Django 工具来构建 Web 应用程序,但现在我们将探讨如何利用他人的工作来快速为我们自己的应用程序添加更多高级功能。我们提到了用于存储文件的程序(在第五章服务静态文件中,我们提到了一个程序,django-storages,可以将我们的静态文件存储在 CDN 上),但除了文件存储之外,我们还可以使用它们连接到第三方身份验证系统、集成支付网关、自定义设置构建方式、修改图像、更轻松地构建表单、调试我们的网站、使用不同类型的数据库等等。可能性很大,如果您想添加某个功能,可能已经存在一个应用程序可以做到这一点。

由于 Django 自 2007 年以来一直存在,因此有一个丰富的第三方库生态系统,可以将其连接到应用程序以提供额外功能。到目前为止,我们已经学到了很多关于 Django 的知识,并使用了其许多功能,包括数据库模型、URL 路由、模板、表单等等。我们直接使用这些 Django 工具来构建 Web 应用程序,但现在我们将探讨如何利用他人的工作来快速为我们自己的应用程序添加更多高级功能。我们提到了用于存储文件的程序(在第五章服务静态文件中,我们提到了一个程序,django-storages,可以将我们的静态文件存储在 CDN 上),但除了文件存储之外,我们还可以使用它们连接到第三方身份验证系统、集成支付网关、自定义设置构建方式、修改图像、更轻松地构建表单、调试我们的网站、使用不同类型的数据库等等。可能性很大,如果您想添加某个功能,可能已经存在一个应用程序可以做到这一点。

对于这些库中的每一个,我们将涵盖安装和基本设置以及使用方法,主要针对 Bookr 应用。它们还有更多的配置选项,以便进一步自定义以适应您的应用程序。这些应用程序都可以使用pip安装。

我们还将简要介绍django-allauth,它允许 Django 应用程序对第三方提供者(如 Google、GitHub、Facebook 和 Twitter)进行用户身份验证。我们不会详细涵盖其安装和设置,但会提供一些示例来帮助您进行配置。

环境变量

当我们创建一个程序时,我们通常希望用户能够配置其行为的一些方面。例如,假设你有一个连接到数据库并将找到的所有记录保存到文件的程序。通常它可能只会向终端打印出 成功 消息,但你可能还希望以 调试模式 运行它,这样它也会打印出它正在执行的 SQL 语句。

配置这样的程序有许多方法。例如,你可以让它从配置文件中读取。但在某些情况下,用户可能希望快速运行 Django 服务器并使用特定的设置(比如,调试模式),然后再关闭该设置重新运行服务器。每次都更改配置文件可能不太方便。在这种情况下,我们可以从 环境变量 中读取。环境变量是在操作系统中设置的键/值对,然后程序可以读取它们。它们可以通过几种方式设置:

  • 你的 shell(终端)在启动时可以读取配置脚本中的变量,然后每个程序都将能够访问这些变量。

  • 你可以在终端内设置一个变量,然后它将对随后启动的任何程序可用。在 Linux 和 macOS 上,这通过 export 命令完成;Windows 使用 set 命令。以这种方式设置的任何变量都将覆盖配置脚本中的变量,但仅限于当前会话。当你关闭终端时,变量就会丢失。

  • 你可以在终端运行命令的同时设置环境变量。这些变量将只对正在运行的程序持续存在,并且它们会覆盖已导出的环境变量和从配置脚本中读取的变量。

  • 你可以在运行中的程序内设置环境变量,并且它们只会在程序内部(或程序启动的程序)中可用。以这种方式设置的环境变量将覆盖我们刚刚设置的所有其他方法。

这些可能听起来很复杂,但我们将通过一个简短的 Python 脚本来解释它们,并展示如何以最后三种方式(第一种方法取决于你使用的 shell)设置变量。脚本还将展示如何读取环境变量。

在 Python 中,可以使用 os.environ 变量访问环境变量。这是一个类似于字典的对象,可以用来通过名称访问环境变量。最安全的方法是使用 get 方法来访问值,以防它们未设置。它还提供了一个 setdefault 方法,允许只在未设置值的情况下设置值(即,它不会覆盖现有的键)。

这里是一个读取环境变量的示例 Python 脚本:

import os
# This will set the value since it's not already set
os.environ.setdefault('UNSET_VAR', 'UNSET_VAR_VALUE')
# This value will not be set since it's already passed
# in from the command line
os.environ.setdefault('SET_VAR', 'SET_VAR_VALUE')
print('UNSET_VAR:' + os.environ.get('UNSET_VAR', ''))
print('SET_VAR:' + os.environ.get('SET_VAR', ''))
# All these values were provided from the shell in some way
print('HOME:' + os.environ.get('HOME', ''))
print('VAR1:' + os.environ.get('VAR1', ''))
print('VAR2:' + os.environ.get('VAR2', ''))
print('VAR3:' + os.environ.get('VAR3', ''))
print('VAR4:' + os.environ.get('VAR4', ''))

我们通过设置一些变量来设置我们的 shell。在 Linux 或 macOS 上,我们使用 export(注意这些命令没有输出):

$ export SET_VAR="Set Using Export"
$ export VAR1="Set Using Export"
$ export VAR2="Set Using Export"

在 Windows 中,我们会在命令行中使用 set 命令如下:

set SET_VAR="Set Using Export"
set VAR1="Set Using Export"
set VAR2="Set Using Export"

在 Linux 和 macOS 上,我们也可以通过在命令之前设置环境变量来提供环境变量(实际命令仅为 python3 env_example.py):

$ VAR2="Set From Command Line" VAR3="Also Set From Command Line" python3 env_example.py 

注意

注意,上述命令在 Windows 上将无法工作。对于 Windows,环境变量必须在执行之前设置,并且不能同时传递。

该命令的输出如下:

UNSET_VAR:UNSET_VAR_VALUE
SET_VAR:Set Using Export
HOME:/Users/ben
VAR1:Set Using Export
VAR2:Set From Command Line
VAR3:Also Set From Command Line
VAR4:
  • 当脚本运行 os.environ.setdefault('UNSET_VAR', 'UNSET_VAR_VALUE') 时,由于 shell 没有为 UNSET_VAR 设置值,因此值是在脚本内部设置的。输出的值是脚本本身设置的值。

  • 当执行 os.environ.setdefault('SET_VAR', 'SET_VAR_VALUE') 时,由于 shell 已经提供了一个值,因此该值没有被设置。这是通过 export SET_VAR="Set Using Export" 命令设置的。

  • HOME 的值没有被运行的任何命令设置——这是由 shell 提供的。它是用户的主目录。这只是一个示例,说明 shell 通常提供的环境变量。

  • VAR1 是通过 export 设置的,在执行脚本时没有被覆盖。

  • VAR2 是通过 export 设置的,但在执行脚本时被覆盖了。

  • VAR3 仅在执行脚本时设置。

  • VAR4 从未设置——我们使用 get 方法来访问它以避免 KeyError

现在已经介绍了环境变量,我们可以回到讨论需要修改 manage.py 以支持 django-configurations 的更改。

django-configurations

将 Django 应用程序部署到生产环境时的主要考虑之一是如何配置它。正如您在本章中看到的,settings.py 文件是定义所有 Django 配置的地方。甚至第三方应用程序的配置也在这个文件中。您已经在 第十二章构建 REST API 中看到过这一点,当时您正在使用 Django REST 框架。

在 Django 中提供不同的配置并在它们之间切换有许多方法。如果您已经开始在一个已经存在并且已经具有在开发和生产环境中切换配置的特定方法的现有应用程序上工作,那么您可能应该继续使用该方法。

当我们将 Bookr 部署到产品 web 服务器时,在 第十七章Django 应用程序的部署(第一部分 – 服务器设置) 中,我们需要切换到生产配置,那时我们将使用 django-configurations

要安装 django-configurations,请使用以下 pip3

pip3 install django-configurations

注意

对于 Windows,您可以在前面的命令中使用 pip 而不是 pip3

输出将如下所示:

Collecting django-configurations
  Using cached https://files.pythonhosted.org/packages/96/ef/bddcce16f3cd36f03c9874d8ce1e5d35f3cedea27b7d8455265e79a77c3d/django_configurations-2.2-py2.py3-none-any.whl
Requirement already satisfied: six in /Users/ben/.virtualenvs/bookr/lib/python3.6/site-packages (from django-configurations) (1.14.0)
Installing collected packages: django-configurations
Successfully installed django-configurations-2.2

django-configurations更改settings.py文件,使得所有设置都从您定义的类中读取,该类将是configurations.Configuration的子类。设置不再是settings.py内部的全局变量,而是您定义的类上的属性。通过使用基于类的这种方法,我们可以利用面向对象范式,特别是继承。在类中定义的设置可以继承另一个类中的设置。例如,生产设置类可以继承开发设置类,并仅覆盖一些特定的设置——例如在生产中强制DEBUGFalse

我们可以通过仅显示文件中的前几个设置来展示需要对设置文件进行的更改。标准的 Django settings.py文件通常是这样开始的(已删除注释行):

import os
BASE_DIR =os.path.dirname\
          (os.path.dirname(os.path.abspath(__file__)))
SECRET_KEY =\
'y%ux@_^+#eahu3!^i2w71qtgidwpvs^o=w2*$=xy+2-y4r_!fw'
DEBUG = True
…
# The rest of the settings are not shown

要将设置转换为django-configurations,首先从configurations导入Configuration。然后定义一个Configuration子类。最后,将所有要设置的设置缩进到类下。在 PyCharm 中,这就像选择所有设置并按Tab键将它们全部缩进一样简单。

完成这些操作后,您的settings.py文件将看起来像这样:

import os

from configurations import Configuration
class Dev(Configuration):
    BASE_DIR = os.path.dirname\
               (os.path.dirname(os.path.abspath(__file__)))
    SECRET_KEY = \
    'y%ux@_^+#eahu3!^i2w71qtgidwpvs^o=w2*$=xy+2-y4r_!fw'
    DEBUG = True
    …
    # All other settings indented in the same manner

要有不同的配置(不同的设置集),您只需扩展您的配置类并覆盖应该不同的设置。

例如,在生产环境中需要覆盖的一个变量是DEBUG:它应该是False(出于安全和性能原因)。可以定义一个扩展Dev并设置DEBUGProd类,如下所示:

class Dev(Configuration):
    DEBUG = True
    …
    # Other settings truncated

class Prod(Dev):
    DEBUG = False
    # no other settings defined since we're only overriding DEBUG

当然,您也可以覆盖其他生产设置,而不仅仅是DEBUG。通常,出于安全考虑,您还可能需要重新定义SECRET_KEYALLOWED_HOSTS;并且为了配置 Django 使用您的生产数据库,您还需要设置DATABASES值。任何 Django 设置都可以按您选择的方式进行配置。

如果您现在尝试执行 runserver(或其他管理命令),您将得到一个错误,因为 Django 不知道如何找到这样的settings.py文件:

django.core.exceptions.ImproperlyConfigured: django-configurations settings importer wasn't correctly installed. Please use one of the starter functions to install it as mentioned in the docs: https://django-configurations.readthedocs.io/

manage.py文件开始工作之前,我们需要对其进行一些更改。但在我们进行更改之前,我们将简要讨论环境变量,以防您之前没有使用过它们。

manage.py 更改

manage.py中需要添加/更改两行以启用django-configurations。首先,我们需要定义一个默认环境变量,告诉 Django Configuration 它应该加载哪个Configuration类。

这行应该在main()函数中添加,以设置DJANGO_CONFIGURATION环境变量的默认值:

os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')

这将默认设置为Dev——我们定义的类的名称。正如我们在示例脚本中看到的那样,如果这个值已经定义,它不会被覆盖。这将允许我们通过环境变量在配置之间切换。

第二个更改是将 execute_from_command_line 函数与 django-configurations 提供的函数交换。考虑以下行:

from django.core.management import execute_from_command_line

这行代码的更改如下:

from configurations.management import execute_from_command_line

从现在起,manage.py 将像以前一样工作,只是在启动时它会打印出它正在使用的 Configuration 类 (图 15.1):

图 15.1:django-configurations 正在使用配置 Dev

图 15.1:django-configurations 正在使用配置 Dev

在第二行中,你可以看到 django-configurations 输出正在使用 Dev 类进行设置。

从环境变量配置

除了使用环境变量在 Configuration 类之间切换之外,django-configurations 还允许我们使用环境变量为单个设置提供值。它提供了 Value 类,这些类将自动从环境读取值。如果没有提供值,我们可以定义默认值。由于环境变量始终是字符串,因此不同的 Value 类用于将字符串转换为指定的类型。

让我们通过几个示例来实际看看这个。我们将允许 DEBUGALLOWED_HOSTSTIME_ZONESECRET_KEY 使用以下环境变量进行设置:

from configurations import Configuration, values
class Dev(Configuration):
    DEBUG = values.BooleanValue(True)
    ALLOWED_HOSTS = values.ListValue([])
    TIME_ZONE = values.Value('UTC')
    SECRET_KEY =\
    'y%ux@_^+#eahu3!^i2w71qtgidwpvs^o=w2*$=xy+2-y4r_!fw'
    …
    # Other settings truncated
class Prod(Dev):
    DEBUG = False
    SECRET_KEY = values.SecretValue()
    # no other settings are present

我们将逐个解释设置:

  • Dev 中,DEBUG 从环境变量中读取并转换为布尔值。值 yesytrue1 转换为 True;值 nonfalse0 转换为 False。这允许我们在开发机器上关闭 DEBUG,这在某些情况下可能很有用(例如,测试自定义异常页面而不是 Django 的默认页面)。在 Prod 配置中,我们不希望 DEBUG 不小心变为 True,因此我们将其静态设置。

  • ALLOWED_HOSTS 在生产中是必需的。它是 Django 应该接受请求的主机列表。

  • ListValue 类将逗号分隔的字符串转换为 Python 列表。

  • 例如,字符串 www.example.com,example.com 转换为 ["www.example.com", "example.com"]

  • TIME_ZONE 只接受字符串值,因此它使用 Value 类设置。这个类只是读取环境变量,并不对其进行任何转换。

  • SECRET_KEYDev 配置中静态定义;它不能通过环境变量更改。在 Prod 配置中,它使用 SecretValue 设置。这类似于 Value,因为它只是一个字符串设置;然而,它不允许设置默认值。如果设置了默认值,则会引发异常。这是为了确保你永远不会将秘密值放入 settings.py,因为它可能会意外共享(例如,上传到 GitHub)。请注意,由于我们不在生产中使用 DevSECRET_KEY,所以我们不关心它是否泄露。

默认情况下,django-configurations期望每个环境变量都有DJANGO_前缀。例如,要设置DEBUG,使用DJANGO_DEBUG环境变量;要设置ALLOWED_HOSTS,使用DJANGO_ALLOWED_HOSTS,依此类推。

现在我们已经介绍了django-configurations以及需要对该项目进行的更改以支持它,让我们将其添加到 Bookr 并做出这些更改。在下一个练习中,你将在 Bookr 中安装和设置django-configurations

练习 15.01:Django 配置设置

在这个练习中,你将使用pip安装django-configurations,然后更新settings.py以添加DevProd配置。然后,你将对manage.py进行必要的更改以支持新的配置样式,并测试一切是否仍然正常工作:

  1. 在终端中,确保你已经激活了bookr虚拟环境,然后运行以下命令使用pip3安装django-configurations

    pip3 install django-configurations
    

    注意

    对于 Windows,你可以在前面的命令中使用pip而不是pip3

    安装过程将运行,你应该会有像图 15.2这样的输出:

    图 15.2:使用 pip 安装 django-configurations

    图 15.2:使用 pip 安装 django-configurations

  2. 在 PyCharm 中,打开bookr包内的settings.py。在现有的os导入下面,从configurations导入Configurationvalues,如下所示:

    from configurations import Configuration, values
    
  3. 在导入之后但在你的第一个设置定义之前(设置BASE_DIR值的行),添加一个新的Configuration子类,称为Dev

    class Dev(Configuration):
    
  4. 现在我们需要将所有现有的设置移动,使它们成为Dev类的属性而不是全局变量。在 PyCharm 中,这就像选择所有设置,然后按Tab键缩进它们一样简单。完成此操作后,你的设置应该如下所示:图 15.3:新的 Dev 配置

    图 15.3:新的 Dev 配置

  5. 在缩进设置后,我们将更改一些设置,使其从环境变量中读取。首先,将DEBUG更改为以BooleanValue读取。它应该默认为True。考虑以下行:

        DEBUG = True
    

    然后将其更改为:

        DEBUG = values.BooleanValue(True)
    

    这将自动从DJANGO_DEBUG环境变量中读取DEBUG并将其转换为布尔值。如果环境变量未设置,则默认为True

  6. 还将ALLOWED_HOSTS转换为从环境变量中读取,使用values.ListValue类。它应该默认为[](空列表)。考虑以下行:

        ALLOWED_HOSTS = []
    

    然后将其更改为:

        ALLOWED_HOSTS = values.ListValue([])
    

    ALLOWED_HOSTS将从DJANGO_ALLOWED_HOSTS环境变量中读取,并默认为空列表。

  7. 到目前为止,你所做的一切都是在Dev类上添加/更改属性。现在,在同一文件的末尾,添加一个从Dev继承的Prod类。它应该定义两个属性,DEBUG = TrueSECRET_KEY = values.SecretValue()。完成的类应该看起来像这样:

    class Prod(Dev):
        DEBUG = False
        SECRET_KEY = values.SecretValue()
    

    保存settings.py

  8. 如果我们现在尝试运行任何管理命令,我们将收到一个错误,表明django-configurations设置不正确。我们需要对manage.py做一些更改才能使其再次工作。在bookr项目目录中打开manage.py

    考虑以下行:

    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bookr.settings')
    

    在它下面,添加以下行:

    os.environ.setdefault('DJANGO_CONFIGURATION', 'Dev')
    

    这将默认配置设置为Dev类。它可以由设置DJANGO_CONFIGURATION环境变量来覆盖(例如,设置为Prod)。

  9. 在上一步的下一行,你已经有以下import语句:

    from django.core.management import execute_from_command_line
    

    将其更改为:

    from manage.py script use Django Configuration's execute_from_command_line function, instead of the Django built-in one.Save `manage.py`.
    
  10. 启动 Django 开发服务器。如果它没有错误地开始,你可以确信你做的更改已经生效。为了确保这一点,检查页面是否在浏览器中加载。打开http://127.0.0.1:8000/并尝试浏览网站。一切应该看起来和感觉都像之前一样:图 15.4:Bookr 网站应该看起来和感觉都像之前

图 15.4:Bookr 网站应该看起来和感觉都像之前

在这个练习中,我们安装了django-configurations并重构了我们的settings.py文件,使用其Configuration类来定义我们的设置。我们添加了DevProd配置,并使DEBUGALLOWED_HOSTSSECRET_KEY可以通过环境变量设置。最后,我们更新了manage.py以使用 Django Configuration 的execute_from_command_line函数,这使得使用新的settings.py格式成为可能。

在下一节中,我们将介绍dj-database-url,这是一个使您能够使用 URL 配置 Django 数据库设置的包。

dj-database-url

dj-database-url是另一个帮助配置 Django 应用程序的 app。具体来说,它允许您使用 URL 而不是配置值的字典来设置数据库(您的 Django 应用程序连接到)。正如您在现有的settings.py文件中可以看到的,DATABASES设置包含几个条目,当使用具有更多配置选项的不同数据库(例如用户名、密码等)时,它会变得更加详细。我们可以从 URL 设置这些值,该 URL 可以包含所有这些值。

根据您是使用本地 SQLite 数据库还是远程数据库服务器,URL 的格式会有所不同。要使用磁盘上的 SQLite(如 Bookr 目前所做的那样),URL 是这样的:

sqlite:///<path>

注意这里有三个斜杠。这是因为 SQLite 没有主机名,所以这就像一个 URL 是这样的:

<protocol>://<hostname>/<path>

即,URL 有一个空的主机名。因此,所有三个斜杠都在一起。

要为远程数据库服务器构建一个 URL,其格式通常是这样的:

<protocol>://<username>:<password>@<hostname>:<port>/<database_name>

例如,要连接到主机db.example.com上的名为bookr_django的 PostgreSQL 数据库,端口为5432,用户名为bookr,密码为b00ks,URL 将是这样的:

postgres://bookr:b00ks@db.example.com:5432/bookr_django

现在我们已经看到了 URL 的格式,让我们看看如何在 settings.py 文件中实际使用它们。首先,必须使用 pip3 安装 dj-database-url

pip3 install dj-database-url

注意

对于 Windows,你可以在前面的命令中使用 pip 而不是 pip3

输出如下:

Collecting dj-database-url
  Downloading https://files.pythonhosted.org/packages/d4/a6/4b8578c1848690d0c307c7c0596af2077536c9ef2a04d42b00fabaa7e49d/dj_database_url-0.5.0-py2.py3-none-any.whl
Installing collected packages: dj-database-url
Successfully installed dj-database-url-0.5.0

现在,可以将 dj_database_url 导入到 settings.py 中,并使用 dj_database_url.parse 方法将 URL 转换为 Django 可以使用的字典。我们可以使用它的返回值来设置 DATABASES 字典中的 default(或其它)项:

import dj_database_url
DATABASES = {'default':dj_database_url.parse\
             ('postgres://bookr:b00ks@db.example.com:5432/\
               bookr_django')}

或者,对于我们的 SQLite 数据库,我们可以利用已经存在的 BASE_DIR 设置,并将其包含在 URL 中:

import dj_database_url
DATABASES = {'default': dj_database_url.parse\
             ('sqlite:///{}/db.sqlite3'.format(BASE_DIR))}

解析后,DATABASES 字典与我们之前定义的类似。它包括一些不适用于 SQLite 数据库的冗余项(如 USERPASSWORDHOST 等),但 Django 会忽略它们:

DATABASES = {'default': \
             {'NAME': '/Users/ben/bookr/bookr/db.sqlite3',\
              'USER': '',\
              'PASSWORD': '',\
              'HOST': '',\
              'PORT': '',\
              'CONN_MAX_AGE': 0,\
              'ENGINE': 'django.db.backends.sqlite3'}}

这种设置数据库连接信息的方法并不那么有用,因为我们仍然在 settings.py 中静态定义数据。唯一的区别是我们使用 URL 而不是字典。dj-database-url 还可以自动从环境变量中读取 URL。这将允许我们通过在环境中设置它们来覆盖这些值。

要从环境中读取数据,使用 dj_database_url.config 函数,如下所示:

import dj_database_url
DATABASES = {'default': dj_database_url.config()}

URL 会自动从 DATABASE_URL 环境变量中读取。

我们可以通过向 config 函数提供 default 参数来改进这一点。这是在没有在环境变量中指定的情况下默认使用的 URL:

import dj_database_url
DATABASES = {'default':dj_database_url.config\
             (default='sqlite:///{}/db.sqlite3'\
              .format(BASE_DIR))}

以这种方式,我们可以在生产环境中指定一个可以被环境变量覆盖的默认 URL。

我们还可以通过传递 env 参数来指定读取 URL 的环境变量——这是第一个位置参数。这样,你可以为不同的数据库设置读取多个 URL:

import dj_database_url
DATABASES = {'default':dj_database_url.config\
             (default='sqlite:///{}/db.sqlite3'\
                      .format(BASE_DIR)),\
             'secondary':dj_database_url.config\
                         ('DATABASE_URL_SECONDARY'\
                          default=\
                          'sqlite:///{}/db-secondary.sqlite3'\
                          .format(BASE_DIR)),}

在这个例子中,default 项的 URL 是从 DATABASE_URL 环境变量中读取的,而 secondary 是从 DATABASE_URL_SECONDARY 中读取的。

django-configurations 还提供了一个与 dj_database_url: DatabaseURLValue 协同工作的配置类。它与 dj_database_url.config 略有不同,因为它生成包括 default 项在内的整个 DATABASES 字典。例如,考虑以下代码:

import dj_database_url
DATABASES = {'default': dj_database_url.config()}

这段代码等同于以下代码:

from configurations import values
DATABASES = values.DatabaseURLValue()

不要这样写 DATABASES['default'] = values.DatabaseURLValue(),因为你的字典将会嵌套两层。

如果你需要指定多个数据库,你需要直接回退到使用 dj_database_url.config 而不是使用 DatabaseURLValue

与其他 values 类一样,DatabaseURLValue 将默认值作为其第一个参数。你可能还希望使用 environment_prefix 参数并将其设置为 DJANGO,以便其环境变量名称与其他设置保持一致。因此,使用 DatabaseURLValue 的完整示例将如下所示:

DATABASES = values.DatabaseURLValue\
            ('sqlite:///{}/db.sqlite3'.format(BASE_DIR),\
              environment_prefix='DJANGO')

通过这样设置 environment_prefix,我们可以使用 DJANGO_DATABASE_URL 环境变量(而不是仅 DATABASE_URL)来设置数据库 URL。这意味着它与以 DJANGO 开头的其他环境变量设置保持一致,例如 DJANGO_DEBUGDJANGO_ALLOWED_HOSTS

注意,尽管我们没有在 settings.py 中导入 dj-database-url,但 django-configurations 会内部使用它,因此它仍然必须被安装。

在下一个练习中,我们将配置 Bookr 以使用 DatabaseURLValue 来设置其数据库配置。它将能够从环境变量中读取,并在我们指定的默认值回退。

练习 15.02:dj-database-url 和设置

在这个练习中,我们将使用 pip3 安装 dj-database-url。然后,我们将更新 Bookr 的 settings.py 文件以使用 URL 配置 DATABASE 设置,该 URL 从环境变量中读取:

  1. 在终端中,确保你已经激活了 bookr 虚拟环境,然后运行以下命令使用 pip3 安装 dj-database-url

    pip3 install dj-database-url
    

    安装过程将运行,你应该有类似的输出:

    图 15.5:使用 pip 安装 dj-database-url

    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/webdev-dj-2e/img/B15509_15_05.jpg)

    图 15.5:使用 pip 安装 dj-database-url

  2. 在 PyCharm 中,打开 bookr 包目录中的 settings.py 文件。向下滚动以找到定义 DATABASES 属性的位置。将其替换为 values.DatabaseURLValue 类。第一个参数(默认值)应该是 SQLite 数据库的 URL:'sqlite:///{}/db.sqlite3'.format(BASE_DIR)。同时传递 environ_prefix,设置为 DJANGO。完成此步骤后,你应该像这样设置属性:

    DATABASES = values.DatabaseURLValue\
                ('sqlite:///{}/db.sqlite3'.format(BASE_DIR),\
                  environ_prefix='DJANGO')
    

    保存 settings.py

  3. 启动 Django 开发服务器。与 练习 15.01Django 配置设置 一样,如果启动正常,你可以确信你的更改是成功的。为了确保,请在浏览器中打开 http://127.0.0.1:8000/ 并检查一切看起来和之前一样。你应该访问一个查询数据库的页面(例如 Books List 页面),并检查是否显示了一本书的列表:图 15.6:Bookr 页面中的数据库查询仍然有效

    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/webdev-dj-2e/img/B15509_15_06.jpg)

图 15.6:Bookr 页面中的数据库查询仍然有效

在这个练习中,我们更新了我们的 settings.py 文件,使其从环境变量中指定的 URL 确定其 DATABASES 设置。我们使用了 values.DatabaseURLValue 类来自动读取值,并提供了默认 URL。我们还设置了 environ_prefix 参数为 DJANGO,以便环境变量名称为 DJANGO_DATABASE_URL,这与其他设置保持一致。

在下一节中,我们将游览 Django 调试工具栏,这是一个通过浏览器帮助你调试 Django 应用程序的应用程序。

Django 调试工具栏

Django 调试工具栏是一个应用,它直接在你的浏览器中显示有关网页的调试信息。它包括有关生成页面的 SQL 命令、请求和响应头信息、页面渲染所需时间等信息。这些信息在以下情况下可能很有用:

  • 页面加载时间过长 – 可能是因为运行了太多的数据库查询。你可以查看是否运行了相同的查询多次,在这种情况下,你可以考虑使用缓存。否则,通过在数据库中添加索引,一些查询可能会加快速度。

  • 你想要确定为什么页面返回错误信息。你的浏览器可能发送了你没有预料到的头信息,或者可能是 Django 的一些头信息不正确。

  • 你的页面运行缓慢,因为它在非数据库代码中花费了时间 – 你可以分析页面以查看哪些函数耗时最长。

  • 页面看起来不正确。你可以看到 Django 渲染了哪些模板。可能有一个意外的第三方模板正在被渲染。你还可以检查所有正在使用的设置(包括我们未设置的内置 Django 设置)。这有助于确定错误的设置,导致页面行为不正确。

我们将解释如何使用 Django 调试工具栏来查看这些信息。在深入探讨如何设置 Django 调试工具栏以及如何使用它之前,让我们快速看一下它。工具栏显示在浏览器窗口的右侧,可以切换打开和关闭以显示信息:

图 15.7:Django 调试工具栏已关闭

图 15.7:Django 调试工具栏已关闭

前面的图显示了 Django 调试工具栏在关闭状态下的样子。注意窗口右上角的切换栏。点击工具栏可以打开它:

图 15.8:Django 调试工具栏已打开

图 15.8:Django 调试工具栏已打开

图 15.8显示了 Django 调试工具栏已打开。

使用pip安装 Django 调试工具栏:

pip3 install django-debug-toolbar

注意

对于 Windows,你可以在前面的命令中使用pip而不是pip3

然后有一些步骤来设置它,主要是通过修改settings.py

  1. debug_toolbar添加到INSTALLED_APPS设置列表中。

  2. debug_toolbar.middleware.DebugToolbarMiddleware添加到MIDDLEWARE设置列表中。应尽可能早地进行此操作;对于 Bookr,它可以是此列表中的第一项。这是所有请求和响应都必须通过的中介。

  3. '127.0.0.1'添加到INTERNAL_IPS设置列表中(此设置可能需要创建)。Django 调试工具栏将只显示在此列表中列出的 IP 地址。

  4. 将 Django 调试工具栏的 URL 添加到基本urls.py文件中。我们只想在DEBUG模式下添加此映射:

    path('__debug__/', include(debug_toolbar.urls))
    

在下一个练习中,我们将详细讲解这些步骤。

一旦安装并设置了 Django 调试工具栏,您访问的任何页面都会显示 DjDT 侧边栏(您可以使用 DjDT 菜单打开或关闭它)。当它打开时,您将能够看到另一组可以点击以获取更多信息的部分。

每个面板旁边都有一个复选框,这允许您启用或禁用该指标的收集。收集的每个指标都会略微减慢页面加载速度(尽管通常这是不明显的感觉)。如果您发现某个指标的收集速度较慢,您可以在这里将其关闭:

  1. 我们将逐一介绍每个面板。第一个是版本,它显示了正在运行的 Django 版本。您可以点击它打开一个大的版本显示,它还将显示 Python 版本和 Django 调试工具栏(图 15.9):![图 15.9: DjDT 版本面板(为了简洁,截图已裁剪) 图片

    图 15.9: DjDT 版本面板(为了简洁,截图已裁剪)

  2. 第二个面板是时间,它显示了处理请求所需的时间。它被细分为系统时间和用户时间(图 15.10):![图 15.10: DjDT 时间面板 图片

    图 15.10: DjDT 时间面板

    这些设置之间的区别超出了本书的范围,但基本上,系统时间是花费在内核中的时间(例如,进行网络或文件读写操作),而用户时间是位于操作系统内核之外(这包括你在 Django、Python 等中编写的代码)。

    还显示了在浏览器中花费的时间,例如获取请求所需的时间和渲染页面所需的时间。

  3. 第三部分,设置,显示了应用程序使用的所有设置(图 15.11):![图 15.11: DjDT 设置面板 图片

    图 15.11: DjDT 设置面板

    这很有用,因为它显示了来自settings.py的设置和默认的 Django 设置。

  4. 第四个面板是标头图 15.12)。它显示了浏览器发出的请求的标头以及 Django 发送的响应标头:![图 15.12: DjDT 标头面板 图片

    图 15.12: DjDT 标头面板

  5. 第五部分,请求,显示了生成响应的视图以及调用时使用的参数和关键字参数(图 15.13)。您还可以看到在其 URL 映射中使用的 URL 名称:![图 15.13: DjDT 请求面板(为了简洁,一些面板未显示) 图片

    图 15.13: DjDT 请求面板(为了简洁,一些面板未显示)

    它还显示了请求的 cookie、存储在会话中的信息(会话在第八章媒体服务和文件上传中介绍)以及request.GETrequest.POST数据。

  6. 第六部分,SQL,显示了在构建响应时正在执行的 SQL 数据库查询(图 15.14):![图 15.14: DjDT SQL 面板 图片

    图 15.14: DjDT SQL 面板

    您可以看到每个查询执行所需的时间以及它们的执行顺序。它还会标记相似和重复的查询,以便您可能重构代码以删除它们。

    每个SELECT查询显示两个操作按钮,Sel,代表选择,和Expl,代表解释。这些按钮对于INSERTUDPATEDELETE查询不会显示。

    Sel按钮显示了执行的SELECT语句以及查询检索的所有数据(图 15.15):

    ![图 15.15:DjDT SQL 选择面板

    ![img/B15509_15_15.jpg]

    图 15.15:DjDT SQL 选择面板

    Expl按钮显示了SELECT查询的EXPLAIN查询(图 15.16):

    ![图 15.16:DjDT SQL 解释面板(部分面板因简洁性未显示)]

    ![img/B15509_15_16.jpg]

    图 15.16:DjDT SQL 解释面板(部分面板因简洁性未显示)

    EXPLAIN查询超出了本书的范围,但它们基本上显示了数据库如何尝试执行SELECT查询,例如,使用了哪些数据库索引。您可能会发现查询没有使用索引,因此您可以通过添加一个索引来获得更快的性能。

  7. 第七个面板是“静态文件”,显示了在此请求中加载的静态文件(图 15.17)。它还显示了所有可用的静态文件以及它们如何被加载(即哪个静态文件查找器找到了它们)。静态文件面板的信息类似于您可以从findstatic管理命令中获得的信息:![图 15.17:DjDT 静态面板

    ![img/B15509_15_17.jpg]

    图 15.17:DjDT 静态面板

  8. 第八个面板是“模板”,显示了已渲染的模板信息(图 15.18):![图 15.18:DjDT 模板面板

    ![img/B15509_15_18.jpg]

    图 15.18:DjDT 模板面板

    它显示了模板加载的路径和继承链。

  9. 第九个面板是“缓存”,显示了从 Django 缓存中检索的数据信息(图 15.19):![图 15.19:DjDT 缓存面板(部分面板因简洁性未显示)

    ![img/B15509_15_19.jpg]

    图 15.19:DjDT 缓存面板(部分面板因简洁性未显示)

    由于我们在 Bookr 中没有使用缓存,因此本节为空白。如果我们使用缓存,我们将能够看到对缓存发出的请求数量,以及成功检索到项目请求的数量。我们还将看到添加到缓存中的项目数量。这可以给您一个关于您是否有效地使用缓存的线索。如果您添加了大量项目到缓存但未检索任何内容,那么您应该重新考虑您要缓存的哪些数据。相反,如果您有很多“缓存未命中”(未命中是指请求不在缓存中的数据),那么您应该比现在缓存更多的数据。

  10. 第十个面板是“信号”,显示了有关 Django 信号的信息(图 15.20):![图 15.20:DjDT 信号面板(部分面板因简洁性未显示)

    ![img/B15509_15_20.jpg]

    图 15.20:DjDT 信号面板(为简洁起见,一些面板未显示)

    尽管我们在这本书中没有涵盖信号,但它们类似于你可以挂钩的事件,当 Django 执行某些操作时可以执行函数;例如,如果创建了一个用户,可以发送欢迎邮件。本节显示了哪些信号被发送以及哪些函数接收了它们。

  11. 第十一个面板“日志”显示了由你的 Django 应用程序生成的日志消息(如图 15.21 所示):![图 15.21:DjDT 日志面板 img/B15509_15_21.jpg

    图 15.21:DjDT 日志面板

    由于此请求没有生成日志消息,此面板为空。

    下一个选项“拦截重定向”不是一个包含数据的部分。相反,它允许你切换重定向拦截。如果你的视图返回重定向,它将不会跟随。相反,会显示一个类似于图 15.22的页面:

    ![图 15.22:DjDT 拦截的重定向 img/B15509_15_22.jpg

    图 15.22:DjDT 拦截的重定向

    这允许你为生成重定向的视图打开 Django 调试工具栏——否则,你只能看到你被重定向到的视图的信息。

  12. 最后一个面板是“分析”。默认情况下它是关闭的,因为分析可能会大大减慢你的响应速度。一旦打开,你必须刷新页面以生成分析信息(如图 15.23 所示):![图 15.23:DjDT 分析面板 img/B15509_15_23.jpg

图 15.23:DjDT 分析面板

这里显示的信息是关于你的响应中每个函数调用所花费时间的分解。页面左侧显示了所有执行的调用堆栈跟踪。右侧是包含时间数据的列。列包括:

  • CumTime:在函数及其调用的任何子函数中花费的总时间

  • Count)

  • TotTime:在此函数中花费的时间(不包括它调用的任何子函数)

  • Count)

  • 调用次数:此函数的调用次数

这些信息可以帮助你确定在哪里加快你的应用程序。例如,优化一个被调用 1000 次的小函数可能比优化一个只被调用一次的大函数更容易。关于如何加快代码的更深入的建议超出了本书的范围。

练习 15.03:设置 Django 调试工具栏

在这个练习中,你将通过修改INSTALLED_APPSMIDDLEWAREINTERNAL_IPS设置来添加 Django 调试工具栏设置。然后你将添加debug_toolbar.urls映射到bookr包的urls.py。然后你将在浏览器中加载一个带有 Django 调试工具栏的页面并使用它:

  1. 在终端中,确保您已激活了bookr虚拟环境,然后运行以下命令使用pip3安装 Django 调试工具栏:

    pip3 install django-debug-toolbar
    INSTALLED_APPS = […\
                      'debug_toolbar']
    

    这将允许 Django 找到 Django 调试工具栏的静态文件。

  2. debug_toolbar.middleware.DebugToolbarMiddleware添加到MIDDLEWARE设置中——它应该是列表中的第一项:

    MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware',\
                  …]
    

    这将使请求和响应通过DebugToolbarMiddleware路由,允许 Django 调试工具栏检查请求并将其 HTML 插入到响应中。

  3. 需要添加的最后一个设置是将地址127.0.0.1添加到INTERNAL_IPS。您可能还没有定义INTERNAL_IPS设置,所以添加如下设置:

    INTERNAL_IPS = ['127.0.0.1']
    

    这将使 Django 调试工具栏仅在开发者的计算机上显示。现在您可以保存settings.py

  4. 现在,我们需要添加 Django 调试工具栏的 URL。在bookr包目录中打开urls.py。我们已经有了一个检查DEBUG模式的if条件,然后添加了媒体 URL,如下所示:

    if settings.DEBUG:
        urlpatterns += static(settings.MEDIA_URL,\
                              document_root=settings.MEDIA_ROOT)
    

    我们还将在这个if语句内添加debug_toolbar.urlsinclude,但是我们将将其添加到urlpatterns的开始处,而不是追加到末尾。在if语句内添加以下代码:

        import debug_toolbar
        urlpatterns = [path\
                       ('__debug__/',\
                        include(debug_toolbar.urls)),] + urlpatterns
    

    保存urls.py

  5. 如果 Django 开发服务器尚未运行,请启动它并导航到http://127.0.0.1:8000。您应该看到 Django 调试工具栏已打开。如果没有打开,请点击右上角的DjDT切换按钮打开它:![图 15.25:DjDT 切换按钮显示在角落 图片

    图 15.25:DjDT 切换按钮显示在角落

  6. 尝试浏览一些面板并访问不同的页面,看看您可以找到哪些信息。还可以尝试开启拦截重定向,然后创建一个新的书评。提交表单后,您应该看到被拦截的页面而不是被重定向到新的评论页面(图 15.26):![图 15.26:提交新评论后的重定向拦截页面 图片

    图 15.26:提交新评论后的重定向拦截页面

    您可以点击位置链接跳转到它被重定向到的页面。

  7. 您还可以尝试开启性能分析并查看哪些函数被频繁调用以及哪些函数占据了大部分渲染时间。

  8. 一旦您完成对 Django 调试工具栏的实验,请关闭拦截重定向性能分析

在这个练习中,我们通过添加设置和 URL 映射来安装和设置 Django 调试工具栏。然后我们看到了它的实际应用,并检查了它可以给我们提供的有用信息,包括如何处理重定向和查看性能分析信息。

在下一节中,我们将查看django-crispy-forms应用程序,它将使我们减少编写表单所需的代码量。

django-crispy-forms

在 Bookr 中,我们使用 Bootstrap CSS 框架。它提供了一些 CSS 类,可以应用于表单。由于 Django 独立于 Bootstrap,当我们使用 Django 表单时,它甚至不知道我们正在使用 Bootstrap,因此不知道应该将哪些类应用于表单小部件。

django-crispy-forms在 Django 表单和 Bootstrap 表单之间充当中间件。它可以接受 Django 表单并将其渲染为正确的 Bootstrap 元素和类。它不仅支持 Bootstrap,还支持其他框架,如crispy-forms-foundation)。

它的安装和设置相当简单。再次强调,它是通过pip3安装的:

pip3 install django-crispy-forms 

注意

对于 Windows,你可以在前面的命令中使用pip而不是pip3

然后,只需进行几项设置更改。首先,将crispy_forms添加到你的INSTALLED_APPS中。然后,你需要告诉django-crispy-forms你正在使用哪个框架,以便它加载正确的模板。这是通过CRISPY_TEMPLATE_PACK设置完成的。在我们的例子中,它应该设置为bootstrap4

CRISPY_TEMPLATE_PACK = 'bootstrap4'

django-crispy-forms有两种主要的工作模式,要么作为过滤器,要么作为模板标签。前者更容易插入到现有的模板中。后者提供了更多的配置选项,并将更多的 HTML 生成移动到Form类中。我们将依次查看这两种方法。

Crispy 过滤器

使用django-crispy-forms渲染表单的第一种方法是使用crispy模板。首先,必须在模板中加载过滤器。库的名称是crispy_forms_tags

{% load crispy_forms_tags %}

然后,不要使用as_p方法(或其他方法)来渲染表单,而是使用crispy过滤器。考虑以下行:

{{ form.as_p }}

并将其替换为:

{{ form|crispy }}

这里是一个快速展示“创建回顾”表单的前后对比。除了表单的渲染之外,HTML 的其他部分都没有改变。图 15.27显示了标准的 Django 表单:

![图 15.27:带有默认样式的创建回顾表单图 15.27:带有默认样式的创建回顾表单

图 15.27:带有默认样式的创建回顾表单

图 15.28显示了在django-crispy-forms添加 Bootstrap 类之后的表单:

![图 15.28:通过 django-crispy-forms 添加 Bootstrap 类后的创建回顾表单图 15.28:通过 django-crispy-forms 添加 Bootstrap 类后的创建回顾表单

图 15.28:通过 django-crispy-forms 添加 Bootstrap 类后的创建回顾表单

当我们将django-crispy-forms集成到 Bookr 中时,我们不会使用这种方法,然而,由于它很容易插入到现有的模板中,所以了解这一点是很有价值的。

Crispy 模板标签

使用django-crispy-forms渲染表单的另一种方法是使用crispy模板标签。要使用它,必须首先将crispy_forms_tags库加载到模板中(就像我们在上一节中所做的那样)。然后,表单可以这样渲染:

{% crispy form %}

这与crispy过滤器有何不同?crispy模板标签也会为您渲染<form>元素和{% csrf_token %}模板标签。例如,你可以这样使用它:

<form method="post">
  {% csrf_token %}
  {% crispy form %}
</form>

对于这个输出,结果如下:

<form method="post" >
<input type="hidden" name="csrfmiddlewaretoken" value="…">
<form method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="…">
    … form fields …
</form>
</form>

即,表单和 CSRF 令牌字段被重复了。为了自定义生成的 <form> 元素,django-crispy-forms 提供了一个 FormHelper 类,它可以被设置为 Form 实例的 helper 属性。它是 FormHelper 实例,crispy 模板标签使用它来确定 <form> 应该具有哪些属性。

让我们看看添加了辅助器的 ExampleForm。首先,导入所需的模块:

from django import forms
from crispy_forms.helper import FormHelper

接下来,定义一个表单:

class ExampleForm(forms.Form):
example_field = forms.CharField()

我们可以实例化一个 FormHelper 实例,然后将其设置为 form.helper 属性(例如,在一个视图中),但通常在表单的 __init__ 方法中创建和分配它更有用。我们还没有创建一个带有 __init__ 方法的表单,但它与其他 Python 类没有区别:

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

接下来,我们设置辅助器和辅助器的 form_method(它随后将在表单 HTML 中渲染):

self.helper = FormHelper()
self.helper.form_method = 'post'

可以在辅助器上设置其他属性,例如 form_actionform_idform_class。在 Bookr 中我们不需要使用这些。我们也不需要手动设置表单或其辅助器的 enctype,因为 crispy 表单标签会自动将其设置为 multipart/form-data,如果表单包含文件上传字段。

如果我们现在尝试渲染表单,我们将无法提交它,因为没有提交按钮(记住我们手动添加了提交按钮到我们的表单中,它们不是 Django 表单的一部分)。django-crispy-forms 还包括可以添加到表单中的布局辅助器。它们将在其他字段之后渲染。我们可以这样添加一个提交按钮——首先,导入 Submit 类:

from crispy_forms.layout import Submit

注意

django-crispy-forms 并不完全支持使用 <button> 输入来提交表单,但就我们的目的而言,<input type="submit"> 在功能上是相同的。

我们然后实例化它,并在一行中将其添加到辅助器的输入中:

self.helper.add_input(Submit("submit", "Submit"))

Submit 构造函数的第一个参数是其 name,第二个参数是其 label

django-crispy-forms 知道我们正在使用 Bootstrap,并将自动使用 btn btn-primary 类渲染按钮。

使用 crispy 模板标签和 FormHelper 的优点是,这意味着只有一处定义了属性和表单的行为。我们已经在 Form 类中定义了所有表单字段;这允许我们在同一位置定义表单的其他属性。我们可以轻松地将表单从 GET 提交更改为 POST 提交。然后,FormHelper 实例将自动知道在渲染时需要将其 CSRF 令牌添加到其 HTML 输出中。

我们将在下一个练习中将所有这些应用到实践中,在那里你将安装 django-crispy-forms,然后更新 SearchForm 以利用表单辅助器,然后使用 crispy 模板标签渲染它。

练习 15.04:使用 Django Crispy Forms 与 SearchForm

在这个练习中,您将安装 django-crispy-forms,然后将 SearchForm 转换为可以使用 crispy 模板标签的形式。这将通过添加一个 __init__ 方法和在其中构建 FormHelper 实例来完成:

  1. 在终端中,确保您已经激活了 bookr 虚拟环境,然后运行此命令使用 pip3 安装 django-crispy-forms

    pip3 install django-crispy-forms
    INSTALLED_APPS = […\
                      'reviews',\
                      'debug_toolbar',\
                      'crispy_forms'\]
    

    这将允许 Django 找到所需的模板。

  2. settings.py 中,为 CRISPY_TEMPLATE_PACK 添加一个新的设置 - 其值应该是 bootstrap4。这应该作为 Dev 类的一个属性添加:

    CRISPY_TEMPLATE_PACK = 'bootstrap4'
    

    这让 django-crispy-forms 知道在渲染表单时应该使用为 Bootstrap 版本 4 设计的模板。您现在可以保存并关闭 settings.py

  3. 打开 reviews 应用的 forms.py 文件。首先,我们需要在文件顶部添加两个导入:从 crispy_forms.helper 导入 FormHelper,从 crispy_forms.layout 导入 Submit

    from crispy_forms.helper import FormHelper
    from crispy_forms.layout import Submit
    
  4. 接下来,向 SearchForm 添加一个 __init__ 方法。它应该接受 *args**kwargs 作为参数,然后使用它们调用超类的 __init__ 方法:

    class SearchForm(forms.Form):
    …
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
    

    这将简单地传递给超类构造函数提供的任何参数。

  5. 仍然在 __init__ 方法中,将 self.helper 设置为 FormHelper 的一个实例。然后设置 helper 的 form_methodget。最后,创建一个 Submit 的实例,将空字符串作为名称(第一个参数),将 Search 作为按钮标签(第二个参数)。使用 add_input 方法将此添加到 helper 中:

    self.helper = FormHelper()
    self.helper.form_method = "get"
    self.helper.add_input(Submit("", "Search"))
    

    您现在可以保存并关闭 forms.py

  6. reviews 应用程序的 templates 目录中,打开 search-results.html 文件。在文件开始处,在 extends 模板标签之后,使用 load 模板标签加载 crispy_forms_tags

    {% load crispy_forms_tags %}
    
  7. 在模板中定位现有的 <form> 元素。它应该看起来像这样:

    <form>
        {{ form.as_p }}
    <button type="submit" class="btn btn-primary">Search</button>
    </form>
    

    您可以删除输入的 <form> 元素,并用 crispy 模板标签替换它:

    {% crispy form %}
    

    这将使用 django-crispy-forms 库渲染表单,包括 <form> 元素和提交按钮。在此更改后,模板的这一部分应该看起来像 图 15.30

    图 15.30:将  替换为 crispy 表单渲染器后的 search-results.html

    图 15.32:使用更新后的搜索表单进行搜索

    图 15.30:将

    替换为 crispy 表单渲染器后的 search-results.html

    您现在可以保存 search-results.html

  8. 如果尚未运行,请启动 Django 开发服务器并转到 http://127.0.0.1:8000/book-search/。您应该看到与 图 15.31 一样的图书搜索表单:图 15.31:使用 django-crispy-forms 渲染的图书搜索表单

    图 15.31:与之前相同的方式使用表单

图 15.31:使用 django-crispy-forms 渲染的图书搜索表单

您应该能够以与之前相同的方式使用该表单(图 15.32):

图 15.32:使用更新后的搜索表单进行搜索

图 15.32:与之前相同的方式使用表单

图 15.32:使用更新后的搜索表单进行搜索

尝试在您的网络浏览器中查看页面的源代码以查看渲染输出。您会看到<form>元素已使用method="get"属性渲染,正如我们在步骤 5中指定给FormHelper的。注意,django-crispy-forms没有插入 CSRF 令牌字段——它知道在通过GET提交的表单中不需要。

在这个练习中,我们使用pip3(Windows 上的pip)安装了django-crispy-forms,然后在settings.py中通过将其添加到INSTALLED_APPS并定义我们想要使用的CRISPY_TEMPLATE_PACK(在我们的情况下是bootstrap4)来配置它。然后,我们更新了SearchForm类,使用FormHelper实例来控制表单上的属性,并使用Submit类添加了一个提交按钮。最后,我们将search-results.html模板更改为使用crispy模板标签来渲染表单,这使得我们可以移除之前使用的<form>元素,并通过将所有与表单相关的代码移动到 Python 代码中(而不是部分在 HTML 中,部分在 Python 中)来简化表单生成。

django-allauth

在浏览网站时,您可能已经看到了允许您使用其他网站的凭据登录的按钮。例如,使用您的 GitHub 登录:

图 15.33:带有使用 Google 或 GitHub 登录选项的登录表单

图 15.33:带有使用 Google 或 GitHub 登录选项的登录表单

在我们解释这个过程之前,让我们介绍我们将要使用的术语:

  • 请求网站:用户试图登录的网站。

  • 身份验证提供者:用户正在验证的第三方提供者(例如,Google、GitHub 等)。

  • 身份验证应用程序:这是请求网站创建者在身份验证提供者处设置的东西。它决定了请求网站将拥有哪些权限。例如,请求应用程序可以获取您的 GitHub 用户名,但不会获得写入您仓库的权限。用户可以通过禁用对身份验证应用程序的访问来阻止请求网站访问身份验证提供者中的信息。

无论您选择哪种第三方登录选项,流程通常都是相同的。首先,您将被重定向到身份验证提供者网站,并要求允许身份验证应用程序访问您的账户(图 15.34):

图 15.34:身份验证提供者授权屏幕

图 15.34:身份验证提供者授权屏幕

在您授权身份验证应用程序后,身份验证提供者将重定向回请求的网站。您将被重定向到的 URL 将包含一个秘密令牌,请求的网站可以使用该令牌在后台请求您的用户信息。这允许请求的网站通过直接与身份验证提供者通信来验证您的身份。在验证您的身份后,请求的网站可以将其重定向到您的页面。此流程在 图 15.35 中的序列图中表示:

![图 15.35:第三方身份验证流程图片 B15509_15_35.jpg

图 15.35:第三方身份验证流程

现在我们已经介绍了使用第三方服务进行身份验证,我们可以讨论 django-allauthdjango-allauth 是一个应用程序,它可以将您的 Django 应用程序轻松地连接到第三方身份验证服务,包括 Google、GitHub、Facebook、Twitter 等。实际上,在撰写本文时,django-allauth 支持超过 75 个身份验证提供者。

当用户首次在您的网站上进行身份验证时,django-allauth 将为您创建一个标准的 Django User 实例。它还知道如何解析身份验证提供者在最终用户授权身份验证应用程序后加载的回调/重定向 URL。

django-allauth 为您的应用程序添加了三个模型:

  • SocialApplication:此模型存储用于识别您的身份验证应用程序的信息。您输入的信息将取决于提供者,提供者将为您提供 客户端 ID、密钥 和(可选的)一个 密钥。请注意,这些是 django-allauth 用于这些值的名称,并且它们将根据提供者而有所不同。我们将在本节稍后提供一些这些值的示例。SocialApplicationdjango-allauth 模型中唯一一个您需要自己创建的,其他模型在用户进行身份验证时由 django-allauth 自动创建。

  • SocialApplicationToken:此模型包含用于将 Django 用户识别给身份验证提供者的值。它包含一个 令牌 和(可选的)一个 令牌密钥。它还包含一个引用创建它的 SocialApplication 和它应用的 SocialAccount

  • SocialAccount:此模型将 Django 用户与提供者(例如,Google 或 GitHub)相关联,并存储提供者可能提供的额外信息。

由于存在如此多的身份验证提供者,我们不会涵盖如何设置它们的所有内容,但我们将提供简短的设置说明以及如何将提供者的身份验证令牌映射到 SocialApplication 中的正确字段。我们将为此章节中提到的两个身份验证提供者进行此操作:Google 和 GitHub。

django-allauth 安装和设置

与本章中的其他应用程序一样,django-allauth 使用 pip3 进行安装:

pip3 install django-allauth

注意

对于 Windows,您可以在前面的命令中使用 pip 而不是 pip3

我们接下来需要做一些设置更改。django-allauth 需要运行 django.contrib.sites 应用,因此需要将其添加到 INSTALLED_APPS。然后需要添加一个新的设置来定义我们站点的 SITE_ID。我们可以在 settings.py 文件中将此设置为 1

INSTALLED_APPS = [# this entry added
                  'django.contrib.sites',\
                  'django.contrib.admin',\
                  'django.contrib.auth',\
                  # the rest of the values are truncated]
SITE_ID = 1

注意

有可能将单个 Django 项目托管在多个主机名下,并且使其在每个主机名上表现不同——但也可以在所有站点之间共享内容。我们不需要在我们的项目中其他任何地方使用 SITE_ID,但必须在此处设置。您可以在 docs.djangoproject.com/en/3.0/ref/contrib/sites/ 中了解更多关于 SITE_ID 设置的信息。

我们还需要将 allauthallauth.socialaccount 添加到 INSTALLED_APPS

INSTALLED_APPS = [# the rest of the values are truncated
                  'allauth',\
                  'allauth.socialaccount',]

然后,我们想要支持的每个提供者也必须添加到 INSTALLED_APPS 列表中;例如,考虑以下片段:

INSTALLED_APPS = [# the rest of the values are truncated
                  'allauth.socialaccount.providers.github',\
                  'allauth.socialaccount.providers.google',]

在完成所有这些之后,我们需要运行 migrate 管理命令,以创建 django-allauth 模型:

python3 manage.py migrate

一旦完成,您可以通过 Django 管理界面添加新的社交应用(图 15.36):

![图 15.36:添加社交应用]

![img/B15509_15_36.jpg]

图 15.36:添加社交应用

要添加社交应用,选择一个 Provider(此列表将仅显示 INSTALLED_APPS 列表中的那些),输入一个名称(它可以与 Provider 相同),并输入提供者网站上的 Client ID(我们很快会详细介绍这一点)。您可能还需要 Secret keyKey。选择它应该应用的站点。(如果您只有一个 Site 实例,那么其名称无关紧要,只需选择它即可。站点名称可以在 Django 管理的 Sites 部分中更新。您也可以在那里添加更多站点。)

我们现在将查看我们三个示例提供者使用的令牌。

GitHub 身份验证设置

您可以在您的 GitHub 个人资料下设置一个新的 GitHub 应用程序。在开发期间,您应用的回调 URL 应设置为 http://127.0.0.1:8000/accounts/github/login/callback/,并在您部署到生产时更新为真实的主机名。创建应用后,它将提供 Client IDClient Secret。这些就是 django-allauth 中的 Client idSecret key

Google 身份验证设置

创建 Google 应用程序是通过您的 Google 开发者控制台完成的。在开发期间,授权的重定向 URI 应设置为 http://127.0.0.1:8000/accounts/google/login/callback/,并在生产部署后更新。应用的 Client ID 也是在 django-allauth 中的 Client id,而应用的 Client secretSecret key

使用 django-allauth 启动身份验证

要通过第三方提供者启动身份验证,您首先需要在您的 URL 映射中添加 django-allauth 的 URL。在您的 urlpatterns 中的某个地方,有一个 urls.py 文件,包含 allauth.urls

urlpatterns = [path('allauth', include('allauth.urls')),]

然后,您可以使用类似http://127.0.0.1:8000/allauth/github/login/?process=loginhttp://127.0.0.1:8000/allauth/google/login/?process=login的 URL 启动登录,等等。django-allauth将为您处理所有重定向,并在用户返回网站时创建/验证 Django 用户。您可以在登录页面上添加带有“使用 GitHub 登录”或“使用 Google 登录”等文本的按钮,这些按钮链接到这些 URL。

其他 django-allauth 功能

除了使用第三方提供者进行身份验证之外,django-allauth还可以添加一些 Django 本身没有的实用功能。例如,您可以配置它要求用户提供一个电子邮件地址,并在用户登录之前通过点击他们收到的确认链接来验证他们的电子邮件地址,django-allauth还可以处理为用户生成一个通过电子邮件发送的密码重置 URL。您可以在django-allauth.readthedocs.io/en/stable/overview.html找到解释这些功能以及更多内容的django-allauth文档。

现在我们已经深入探讨了前四个第三方应用程序,并对django-allauth进行了简要概述,您可以为本章的活动承担任务。在这个活动中,您将重构我们正在使用的ModelForm实例,以使用CrispyFormHelper类。

活动十五.01:使用 FormHelper 更新表单

在这个活动中,我们将更新ModelForm实例(PublisherFormReviewFormBookMediaForm)以使用CrispyFormHelper类。使用FormHelper,我们可以在Form类内部定义Submit按钮的文本。然后,我们可以将<form>渲染逻辑从instance-form.html模板中移除,并用crispy模板标签替换它。

这些步骤将帮助您完成活动:

  1. 创建一个InstanceForm类,该类是forms.ModelForm的子类。这将是现有ModelForm类的基类。

  2. InstanceForm__init__方法中,为self设置一个FormHelper实例。

  3. FormHelper添加一个Submit按钮。如果表单使用instance实例化,则按钮文本应为“保存”,否则应为“创建”。

  4. 将 PublisherForm、ReviewForm 和 BookMediaForm 更新为从InstanceForm扩展。

  5. 更新instance-form.html模板,以便使用crispy模板标签渲染form。其余的<form>可以删除。

  6. book_media视图中,不再需要is_file_upload上下文项。

完成后,您应该会看到使用 Bootstrap 主题渲染的表单。图 15.37显示了“新出版商”页面:

图 15.37:新出版商页面

图 15.37:新出版商页面

图 15.38显示了“新评论”页面:

图 15.38:新评论表单

图 15.38:新评论表单

最后,图书媒体页面在图 15.39中显示:

![图 15.39:图书媒体页面图片

图 15.39:书籍媒体页面

您应该注意到表单仍然表现良好,并允许文件上传。django-crispy-forms 已自动将 enctype="multipart/form-data" 属性添加到 <form> 中。您可以通过查看页面源代码来验证这一点。

注意

本活动的解决方案可以在 packt.live/2Nh1NTJ 找到。

摘要

在本章中,我们介绍了五个可以增强您网站功能的第三方 Django 应用程序。我们安装并设置了 django-configurations,这使得我们能够轻松地在不同的设置之间切换,并使用环境变量来更改它们。dj-database-url 也帮助处理设置,允许我们通过使用 URL 来进行数据库设置更改。我们看到了 Django 调试工具栏如何帮助我们了解我们的应用程序正在做什么,并帮助我们调试与之相关的问题。django-crispy-forms 不仅可以使用 Bootstrap CSS 渲染我们的表单,还允许我们通过将它们的行性行为定义为表单类本身的一部分来节省代码。我们简要地了解了 django-allauth,并看到了它如何集成到第三方身份验证提供商中。在本章的活动部分,我们将我们的 ModelForm 实例更新为使用 django-crispy-formsFormHelper,并通过使用 crispy 模板标签从模板中移除一些逻辑。

在下一章中,我们将探讨如何将 React JavaScript 框架集成到 Django 应用程序中。

第十六章:16. 使用前端 JavaScript 库与 Django 结合

概述

本章介绍了 JavaScript 的基础知识,并以使用 React JavaScript 框架为 Bookr 构建交互式网页前端结束。你将学习如何在 Django 模板中包含 React JavaScript 框架,以及如何构建 React 组件。本章还包括了 fetch JavaScript 函数的介绍,该函数用于从 REST API 获取信息。在章节的末尾,你将了解到 Django {% verbatim %} 模板标签,它用于在 Django 模板中包含未解析的数据。

简介

Django 是构建应用程序后端的一个优秀工具。你已经看到了设置数据库、路由 URL 和渲染模板是多么容易。然而,如果不使用 JavaScript,当这些页面渲染到浏览器时,它们是静态的,并且不提供任何形式的交互。通过使用 JavaScript,你的页面可以变成在浏览器中完全交互的应用程序。

本章将简要介绍 JavaScript 框架及其与 Django 的使用方法。虽然它不会深入探讨如何从头开始构建一个完整的 JavaScript 应用程序(那将是一本自己的书),但我们将提供足够的介绍,以便你可以在自己的 Django 应用程序中添加交互式组件。在本章中,我们将主要使用 React 框架。即使你没有 JavaScript 经验,我们也会介绍足够的内容,以便在本章结束时,你将能够舒适地编写自己的 React 组件。在 第十二章构建 REST API 中,你为 Bookr 构建了一个 REST API。我们将使用 JavaScript 与该 API 交互以检索数据。我们将通过在主页上显示一些动态加载并可分页的评论预览来增强 Bookr。

注意

本章练习和活动的代码可以在本书的 GitHub 仓库中找到,网址为 packt.live/3iasIMl

JavaScript 框架

现在,实时交互是网络应用程序的基本组成部分。虽然可以不使用框架添加简单的交互(不使用框架的开发通常被称为 Vanilla JS),但随着你的网络应用程序的增长,使用框架进行管理会容易得多。没有框架,你需要自己完成所有这些事情:

  • 手动定义数据库模式。

  • 将 HTTP 请求中的数据转换为原生对象。

  • 编写表单验证。

  • 编写 SQL 查询以保存数据。

  • 构建 HTML 来显示响应。

将此与 Django 提供的功能进行比较。它的ORM对象关系映射)、自动表单解析和验证以及模板化大大减少了你需要编写的代码量。JavaScript 框架为 JavaScript 开发带来了类似的时间节省增强。没有它们,你将不得不手动更新浏览器中的 HTML 元素,以适应数据的变化。让我们用一个简单的例子来说明:显示按钮被点击的次数。没有框架,你必须做以下事情:

  1. 为按钮点击事件分配处理程序。

  2. 增加存储计数的变量。

  3. 定位包含点击计数显示的元素。

  4. 将元素的文本替换为新的点击计数。

当使用框架时,按钮计数变量绑定到显示(HTML),因此你需要编写的代码过程如下:

  1. 处理按钮点击。

  2. 增加变量。

框架负责自动重新渲染数字显示。这只是一个简单的例子;随着你的应用程序的增长,两种方法之间的复杂性差异会扩大。有几个 JavaScript 框架可供选择,每个框架都有不同的功能,其中一些被大公司支持和使用。其中一些最受欢迎的是 React (reactjs.org)、Vue (vuejs.org)、Angular (angularjs.org)、Ember (emberjs.com) 和 Backbone.js (backbonejs.org)。

在本章中,我们将使用 React,因为它很容易集成到现有的网络应用中,并允许渐进增强。这意味着你不必从头开始构建你的应用程序,针对 React,你只需将其应用于 Django 生成的 HTML 的某些部分;例如,一个自动解释 Markdown 并显示结果的文本字段,而无需重新加载页面。我们还将介绍 Django 提供的一些功能,这些功能可以帮助集成多个 JavaScript 框架。

JavaScript 可以在多个不同的层级被整合到网络应用中。图 16.1 展示了我们的当前堆栈,其中不包含 JavaScript(注意以下图表没有显示对服务器的请求):

![图 16.1:当前堆栈

![img/B15509_16_01.jpg]

图 16.1:当前堆栈

你可以使用Node.js(一个服务器端 JavaScript 解释器)将整个应用程序基于 JavaScript 构建,这将取代堆栈中的 Python 和 Django。图 16.2 展示了这可能看起来是什么样子:

![图 16.2:使用 Node.js 生成 HTML

![img/B15509_16_02.jpg]

图 16.2:使用 Node.js 生成 HTML

或者,你可以将你的前端和模板完全用 JavaScript 编写,只需使用 Django 作为 REST API 来提供渲染所需的数据。图 16.3 展示了这个堆栈:

![图 16.3:从 Django 发送 JSON 并在浏览器中渲染

![img/B15509_16_03.jpg]

图 16.3:从 Django 发送 JSON 并在浏览器中渲染它

最后一种方法是渐进增强,正如之前提到的,这是我们将会使用的方法。这样,Django 仍然生成 HTML 模板,React 则位于其上以添加交互性:

![图 16.4:使用 Django 生成的 HTML,React 提供渐进增强

![img/B15509_16_04.jpg]

图 16.4:使用 Django 生成的 HTML,React 提供渐进增强

注意,通常会将多种技术结合使用。例如,Django 可能会生成初始 HTML,React 在浏览器中应用。然后浏览器可以查询 Django 以获取要渲染的 JSON 数据,使用 React。

JavaScript 简介

在本节中,我们将简要介绍一些基本的 JavaScript 概念,例如变量和函数。在介绍它们时,我们将涵盖不同的运算符。

加载 JavaScript

JavaScript 可以是 HTML 页面内的内联代码,也可以是从单独的 JavaScript 文件中包含的。两种方法都使用 <script> 标签。对于内联 JavaScript,JavaScript 代码直接写在 HTML 文件中的 <script> 标签内;例如,像这样:

<script>
    // comments in JavaScript can start with //
    /* Block comments are also supported. This comment is multiple
      lines and doesn't end until we use a star then slash:
    */
    let a = 5; // declare the variable a, and set its value to 5
    console.log(a); // print a (5) to the browser console
</script>

注意,console.log 函数将数据打印到浏览器控制台,这在浏览器的开发者工具中是可见的:

![图 16.5:console.log(a) 调用的结果——5 被打印到浏览器控制台

![img/B15509_16_05.jpg]

图 16.5:console.log(a) 调用的结果——5 被打印到浏览器控制台

我们也可以将代码放入自己的文件中(在独立文件中我们不会包含 <script> 标签)。然后我们使用 <script> 标签的 src 属性将其加载到页面中,就像我们在 第五章服务静态文件 中看到的那样:

<script src="img/{% static 'file.js' }"></script>

不论是内联还是包含,源代码都会在浏览器加载 <script> 标签时立即执行。

变量和常量

与 Python 不同,JavaScript 中的变量必须使用 varletconst 关键字进行声明:

var a = 1; // variable a has the numeric value 1
let b = 'a'; // variable b has the string value 'a'
const pi = 3.14; // assigned as a constant and can't be redefined

尽管如此,与 Python 一样,变量的类型不需要声明。你会注意到代码行以分号结尾。JavaScript 不需要以分号结束行——这是可选的。然而,一些样式指南强制使用它们。你应该尝试为任何项目坚持使用单一约定。

你应该使用 let 关键字来声明变量。变量声明是有范围的。例如,在 for 循环内部使用 let 声明的变量在循环外部不会被定义。在这个例子中,我们将遍历并计算 10 到 90 的倍数之和,然后将结果打印到 console.log。你会注意到我们可以在 for 循环内部访问在函数级别声明的变量,但反之则不行:

let total = 0;
for (let i = 0; i< 10; i++){  // variable i is scoped to the loop
    let toAdd = i * 10;  // variable toAdd is also scoped
    total += toAdd;  // we can access total since it's in the outer scope
}
console.log(total);  // prints 450
console.log(toAdd);  /* throws an exception as the variable is not   declared in the outer scope */
console.log(i);  /* this code is not executed since an exception was   thrown the line before, but it would also generate the same     exception */

const 用于常量数据且不能被重新定义。但这并不意味着它指向的对象不能被改变。例如,你不能这样做:

const pi = 3.1416;
pi = 3.1;  /* raises exception since const values can't be   reassigned */

var 关键字是旧版浏览器所必需的,这些浏览器不支持 letconst。如今只有 1% 的浏览器不支持这些关键字,所以在本章的其余部分,我们只会使用 letconst。与 let 一样,使用 var 声明的变量可以被重新分配;然而,它们仅在函数级别有作用域。

JavaScript 支持多种不同类型的变量,包括字符串、数组、对象(类似于字典)和数字。现在我们将单独介绍数组和对象。

数组

数组定义的方式与 Python 中的定义类似,使用方括号。它们可以包含不同类型的数据,就像 Python 一样:

const myThings = [1, 'foo', 4.5];

使用 const 需要记住的另一件事是,它防止重新分配常量,但不会阻止更改所指向的变量或对象。例如,我们不允许这样做:

myThings = [1, 'foo', 4.5, 'another value'];

然而,你可以通过使用 push 方法(类似于 Python 的 list.append)来更新 myThings 数组的内容,添加一个新项目:

myThings.push('another value');

对象

JavaScript 对象类似于 Python 字典,提供键值存储。声明它们的语法也类似:

const o = {foo: 'bar', baz: 4};

注意,与 Python 不同,JavaScript 对象/字典的键在创建时不需要引号 – 除非它们包含特殊字符(空格、破折号、点等)。

o 中获取值可以使用项目访问或属性访问:

o.foo; // 'bar'
o['baz']; // 4

还要注意,由于 o 被声明为常量,我们无法重新分配它,但我们可以更改对象的属性:

o.anotherKey = 'another value'  // this is allowed

函数

在 JavaScript 中定义函数有几种不同的方法。我们将探讨三种。你可以使用 function 关键字来定义它们:

function myFunc(a, b, c) {
  if (a == b)
    return c;
  else if (a > b)
    return 0;
  return 1;
}

在 JavaScript 中,所有函数的参数都是可选的;也就是说,你可以像这样调用前面的函数:myFunc(),而不会引发错误(至少在调用时不会)。变量 abc 都将是特殊类型 undefined。这可能会在函数的逻辑中引起问题。undefined 在 Python 中类似于 None – 尽管 JavaScript 也有 null,它更类似于 None。函数也可以通过将它们分配给变量(或常量)来定义:

const myFunc = function(a, b, c) {
    // function body is implemented the same as above
}

我们还可以使用箭头语法来定义函数。例如,我们也可以这样定义 myFunc

const myFunc = (a, b, c) => {
    // function body as above
}

当将函数作为对象的一部分定义时,这种情况更为常见,例如:

const o = {
myFunc: (a, b, c) => {
    // function body
    }
}

在这种情况下,它将这样调用:

o.myFunc(3, 4, 5);

在介绍类之后,我们将回到使用箭头函数的原因。

类和方法

类使用 class 关键字定义。在类定义内部,方法定义时不使用 function 关键字。JavaScript 解释器可以识别这种语法,并知道它是一个方法。以下是一个示例类,它通过 toAdd 参数接收一个数字,当实例化时。这个数字将被添加到传递给 add 方法的任何内容上,并返回结果:

class Adder {
    // A class to add a certain value to any number
    // this is like Python's __init__ method
    constructor (toAdd) {
        //"this" is like "self" in Python
        //it's implicit and not manually passed into every method
        this.toAdd = toAdd;
    }
    add (n) {
        // add our instance's value to the passed in number
        return this.toAdd + n;
    }
}

类使用 new 关键字实例化。除此之外,它们的用法与 Python 中的类非常相似:

const a = new Adder(5);
console.log(a.add(3)); // prints "8"

箭头函数

现在我们已经介绍了 this 关键字,我们可以回到箭头函数的目的。它们不仅更易于编写,而且还能保留 this 的上下文。与 Python 中的 self 不同,self 总是指向特定的对象,因为它被传递到方法中,而 this 指向的对象可以根据上下文而变化。通常,这是由于函数的嵌套,这在 JavaScript 中很常见。

让我们看看两个例子。首先,一个名为 outer 的函数对象。这个 outer 函数包含一个 inner 函数。我们在 innerouter 函数中都引用了 this

备注

下一个代码示例涉及 window 对象。在 JavaScript 中,window 是一个特殊的全局变量,存在于每个浏览器标签中,并代表该标签的信息。它是 window 类的一个实例。window 具有的属性示例包括 document(存储当前的 HTML 文档)、location(在标签的地址栏中显示的当前位置)以及 outerWidthouterHeight(分别代表浏览器窗口的宽度和高度)。例如,要将当前标签的位置打印到浏览器控制台,你会写 console.log(window.location)

const o1 = {
    outer: function() {
        console.log(this);  // "this" refers to o1
        const inner = function() {
            console.log(this);  // "this" refers to the "window"               object
        }
        inner();
    }
}

outer 函数内部,this 指的是 o1 本身,而在 inner 函数内部,this 指的是窗口(一个包含有关浏览器窗口信息的对象)。

将此与使用箭头语法定义内部函数进行比较:

const o2 = {
    outer: function() {
        console.log(this);  // refers to o2
        const inner = () => {
            console.log(this);  // also refers to o2
        }
        inner();
    }
}

当我们使用箭头语法时,this 在两种情况下都是一致的,并指向 o2。现在我们已经对 JavaScript 有了一个非常简要的介绍,让我们来介绍 React。

进一步阅读

覆盖 JavaScript 的所有概念超出了本书的范围。对于一门完整的、动手实践的 JavaScript 课程,你总是可以参考 The JavaScript Workshopcourses.packtpub.com/courses/javascript

React

React 允许你使用组件来构建应用程序。每个组件都可以通过生成要插入页面的 HTML 来渲染自己。

一个组件也可能跟踪其自身的状态。如果它跟踪自己的状态,当状态发生变化时,组件将自动重新渲染自己。这意味着如果你有一个更新组件状态变量的操作方法,你不需要再确定组件是否需要重绘;React 会为你完成这项工作。一个 Web 应用应该跟踪其自身的状态,这样它就不需要查询服务器以了解如何更新以显示数据。

数据通过属性或简称为 props 的属性在组件之间传递。传递属性的方法看起来有点像 HTML 属性,但有一些区别,我们将在本章后面讨论。属性通过一个单独的 props 对象被组件接收。

以一个例子来说明,你可能使用 React 来构建一个购物清单应用。你将会有一个用于列表容器的组件(ListContainer),以及一个用于列表项的组件(ListItem)。ListItem 将会被实例化多次,每次对应购物清单上的一个项目。容器将包含一个状态,其中包含项目名称的列表。每个项目名称都会作为 prop 传递给 ListItem 实例。每个 ListItem 将在其自己的状态中存储项目名称和一个 isBought 标志。当你点击一个项目来标记它从列表中移除时,isBought 将被设置为 true。然后 React 会自动调用该 ListItemrender 方法来更新显示。

使用 React 与你的应用程序结合使用有几种不同的方法。如果你想构建一个深度和复杂的 React 应用程序,你应该使用 npm (<script> 标签:

<script crossorigin src="img/react.development.js"></script>
<script crossorigin src="img/react-dom.development.js"></script>

注意

crossorigin 属性是为了安全考虑,意味着不能将 cookie 或其他数据发送到远程服务器。当使用公共 CDN,如 unpkg.com/ 时,这是必要的,以防有人在那里托管了恶意脚本。

这些应该放置在你想要添加 React 的页面上,在关闭 </body> 标签之前。将标签放在这里而不是页面的 <head> 中,原因可能是脚本可能需要引用页面上的 HTML 元素。如果我们把脚本标签放在 <head> 中,它将在页面元素可用之前执行(因为它们在后面)。

注意

可以在 reactjs.org/docs/cdn-links.html 找到指向最新 React 版本的链接。

组件

在 React 中构建组件有两种方式:使用函数或使用类。无论采用哪种方法,要显示在页面上,组件必须返回一些 HTML 元素来显示。一个函数式组件是一个返回元素的单一函数,而基于类的组件将从其 render 方法返回元素。函数式组件无法跟踪自己的状态。

React 与 Django 类似,它会自动转义从 render 返回的字符串中的 HTML。要生成 HTML 元素,你必须使用它们的标签、它们应该有的属性/属性以及它们的内容来构建它们。这是通过 React.createElement 函数完成的。一个组件将返回一个 React 元素,该元素可能包含子元素。

让我们看看同一组件的两个实现,首先是作为函数,然后是作为类。函数式组件接受 props 作为参数。这是一个包含传递给它的属性的对象。以下函数返回一个 h1 元素:

function HelloWorld(props) {
return React.createElement('h1', null, 'Hello, ' +   props.name + '!');
}

注意,函数的名称通常以大写字母开头。

虽然函数式组件是一个生成 HTML 的单个函数,但基于类的组件必须实现一个render方法来完成这个任务。render方法中的代码与函数式组件中的代码相同,只有一个区别:基于类的组件在其构造函数中接受props对象,然后render(或其他)方法可以使用this.props来引用props。以下是将相同的HelloWorld组件实现为类的示例:

class HelloWorld extends React.Component {
render() {
return React.createElement('h1', null, 'Hello, ' +   this.props.name + '!');
  }
}

当使用类时,所有组件都扩展自React.Component类。基于类的组件比函数式组件有优势,即它们封装了处理动作/事件和它们自己的状态。对于简单的组件,使用函数式风格意味着更少的代码。有关组件和属性的更多信息,请参阅reactjs.org/docs/components-and-props.html

无论你选择哪种方法来定义组件,它们的使用方式都是相同的。在本章中,我们只将使用基于类的组件。

要将此组件放入 HTML 页面,我们首先需要为 React 添加一个渲染位置。通常,这是使用具有id属性的<div>来完成的。例如:

<div id="react_container"></div>

注意,id不必是react_container,它只需要在页面上是唯一的。然后,在 JavaScript 代码中,在定义了所有组件之后,它们使用ReactDOM.render函数在页面上进行渲染。这个函数接受两个参数:根 React 元素(不是组件)和它应该渲染的 HTML 元素。

我们将像这样使用它:

const container = document.getElementById('react_container');
const componentElement = React.createElement(HelloWorld, {name:   'Ben'});
ReactDOM.render(componentElement, container);

注意,HelloWorld组件(类/函数)本身并没有传递给render函数,它是被React.createElement调用封装的,以实例化它并将其转换为元素。

从其名称中你可能已经猜到了,document.getElementById函数在文档中定位一个 HTML 元素,并返回对其的引用。

当组件被渲染时,在浏览器中的最终输出如下所示:

<h1>Hello, Ben!</h1>

让我们看看一个更高级的示例组件。请注意,由于React.createElement是一个非常常用的函数,通常将其别名到更短的名字,例如e:这就是这个示例的第一行所做的事情。

此组件显示一个按钮,并有一个内部状态来跟踪按钮被点击的次数。首先,让我们看一下组件类的整体结构:

const e = React.createElement;
class ClickCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { clickCount: 0 };
  }
  render() {
    return e(
      'button',  // the element name
      {onClick: () => this.setState({ 
       clickCount: this.state.clickCount + 1 }) },//element props
       this.state.clickCount  // element content
    );
  }
}

关于ClickCounter类的几点注意事项:

  • props参数是一个对象(字典),它包含在组件在 HTML 中使用时传递给它的属性值。例如:

    <ClickCounter foo="bar" rex="baz"/>
    

    props字典将包含键foo,其值为bar,以及键rex,其值为baz

  • super(props)调用超类的constructor方法,并传递props变量。这类似于 Python 中的super()方法。

  • 每个 React 类都有一个state变量,它是一个对象。constructor可以初始化它。应该使用setState方法来更改状态,而不是直接操作。当状态改变时,render方法将自动被调用以重新绘制组件。

render方法返回一个新的 HTML 元素,使用React.createElement函数(记住,e变量被重命名为这个函数)。在这种情况下,React.createElement的参数将返回一个带有点击处理程序和文本内容this.state.clickCount<button>元素。本质上,它将返回如下元素(当clickCount0时):

<button onClick="this.setState(…)">
  0
</button>

onClick函数被设置为箭头语法匿名函数。这类似于以下函数(尽管并不完全相同,因为它处于不同的上下文中):

const onClick = () => {
this.setState({clickCount: this.state.clickCount + 1})
}

由于函数只有一行,我们也可以去掉一组括号,最终得到如下:

{ onClick: () => this.setState({clickCount:   this.state.clickCount + 1}) }

我们在本节前面介绍了如何将ClickCounter放置到页面上,大致如下:

ReactDOM.render(e(ClickCounter), document.getElementById   ('react_container'));

下图截图显示了页面加载时按钮中的计数器:

注意

在以下图中,DjDt指的是我们在第十五章的“Django 调试工具栏”部分中学习的调试工具栏。

![图 16.6:计数为 0 的按钮img/B15509_16_06.jpg

图 16.6:计数为 0 的按钮

点击按钮几次后,按钮看起来如图 16.7 所示:

![图 16.7:点击七次后的按钮img/B15509_16_07.jpg

图 16.7:点击七次后的按钮

现在,为了演示如何编写render函数,我们将看看如果我们只是像这样返回 HTML 字符串会发生什么:

render() {
    return '<button>' + this.state.clickCount + '</button>'
}

现在渲染的页面看起来如图 16.8 所示:

![图 16.8:作为字符串返回的 HTMLimg/B15509_16_08.jpg

图 16.8:作为字符串返回的 HTML

这展示了 React 自动转义 HTML 的功能。现在我们已经对 JavaScript 和 React 有了简要的介绍,让我们在 Bookr 中添加一个示例页面,以便您可以看到它的实际应用。

练习 16.01:设置 React 示例

在这个练习中,我们将创建一个示例视图和模板,用于与 React 一起使用。然后我们将实现ClickCounter组件。在练习结束时,您将能够通过ClickCounter按钮与之交互:

  1. 在 PyCharm 中,进入项目static目录下的New -> File。将新文件命名为react-example.js

  2. 在其中,放入以下代码,这将定义 React 组件,然后将其渲染到我们将要创建的react_container <div>中:

    const e = React.createElement;
    class ClickCounter extends React.Component {
      constructor(props) {
        super(props);
        this.state = { clickCount: 0 };
      }
      render() {
        return e(
          'button',
          { onClick: () => this.setState({ 
               clickCount: this.state.clickCount + 1 
               }) 
    },
          this.state.clickCount
        );
      }
    }
    ReactDOM.render(e(ClickCounter), document.getElementById   ('react_container'))
    

    您现在可以保存react-example.js

  3. 进入项目templates目录下的New -> HTML File:![图 16.9:创建一个新的 HTML 文件 img/B15509_16_09.jpg

    图 16.9:创建一个新的 HTML 文件

    将新文件命名为react-example.html

    ![图 16.10:命名文件 react-example.html img/B15509_16_10.jpg

    图 16.10:将文件命名为 react-example.html

    你可以在 <title> 元素内部更改标题为 React Example,但这对于这个练习不是必需的。

  4. react-example.html 是使用一些之前看到的 HTML 模板创建的。在关闭 </body> 标签之前添加以下 <script> 标签来包含 React:

    <script crossorigin src="img/react.development.js"></script>
    <script crossorigin src="img/react-dom.development.js"></script>
    
  5. react-example.js 文件将通过 <script> 标签被包含,我们需要使用 static 模板标签生成脚本路径。首先,在文件的开始处通过在第二行添加以下内容来 load 静态模板库:

    {% load static %}
    

    你文件的前几行看起来像 图 16.11

    ![图 16.11:包含静态模板标签的加载 图片

    <script src="img/{% static 'react-example.js' %}"></script>
    
  6. 我们现在需要添加 React 将要渲染的包含 <div>。在打开 <body> 标签之后添加此元素:

    <div id="react_container"></div>
    

    你可以保存 react-example.html

  7. 现在我们将添加一个视图来渲染模板。打开 reviews 应用程序的 views.py 文件,并在文件末尾添加一个 react_example 视图:

    def react_example(request):
        return render(request, "react-example.html")
    

    在这个简单的视图中,我们只是渲染了 react-example.html 模板,没有上下文数据。

  8. 最后,我们需要将一个 URL 映射到新的视图。打开 bookr 包的 urls.py 文件。将此映射添加到 urlpatterns 变量中:

    path('react-example/', reviews.views.react_example)
    

    你可以保存并关闭 urls.py

  9. 如果它还没有运行,请启动 Django 开发服务器,然后转到 http://127.0.0.1:8000/react-example/。你应该会看到像 图 16.12 中所示的 ClickCount 按钮被渲染:![图 16.12:ClickCount 按钮 图片

图 16.12:ClickCount 按钮

尝试点击按钮几次,并观察计数器的增加。

在这个例子中,我们创建了我们的第一个 React 组件,然后添加了一个模板和视图来渲染它。我们从 CDN 中包含了 React 框架源。在下一节中,我们将介绍 JSX,这是一种将模板和代码合并到单个文件中的方法,可以简化我们的代码。

JSX

使用 React.createElement 函数定义每个元素可能会相当冗长——即使我们将其别名到更短变量名。当我们开始构建更大的组件时,冗长性会加剧。

当使用 React 时,我们可以使用 JSX 来构建 HTML 元素。JSX 代表 JavaScript XML——因为 JavaScript 和 XML 都写在同一文件中。例如,考虑以下代码,我们使用 render 方法创建了一个按钮:

return React.createElement('button', { onClick: … },   'Button Text')

而不是这样做,我们可以直接返回其 HTML,如下所示:

return <button onClick={…}>Button Text</button>;

注意,HTML 没有被引号括起来并作为字符串返回。也就是说,我们不是这样做:

return '<button onClick={…}>Button Text</button>';

由于 JSX 是一种不寻常的语法(HTML 和 JavaScript 在单个文件中的组合),在使用它之前,我们需要包含另一个 JavaScript 库:Babel (babeljs.io)。这是一个可以在不同版本的 JavaScript 之间 转换 代码的库。您可以使用最新的语法编写代码,并将其 转换(翻译和编译)为旧浏览器可以理解的代码版本。

Babel 可以通过以下 <script> 标签包含:

<script crossorigin src="img/  babel.min.js"></script>

这应该在您的其他与 React 相关的脚本标签之后,但在包含任何包含 JSX 的文件之前。

任何包含 JSX 的 JavaScript 源代码都必须添加 type="text/babel" 属性:

<script src="img/file.js" type="text/babel"></script>

这样做是为了让 Babel 知道解析文件而不是仅仅将其视为纯 JavaScript。

注意

注意,以这种方式使用 Babel 对于大型项目来说可能会很慢。它被设计为作为 npm 项目构建过程的一部分使用,并且要在编译前将 JSX 文件转换为代码(而不是像我们现在这样实时转换)。npm 项目的设置超出了本书的范围。就我们的目的而言,以及我们使用的少量 JSX,使用 Babel 将是合适的。

JSX 使用大括号在 HTML 中包含 JavaScript 数据,类似于 Django 模板中的双大括号。大括号内的 JavaScript 将被执行。我们现在将看看如何将我们的按钮创建示例转换为 JSX。我们的 render 方法可以更改为以下内容:

render() {
    return <button onClick={() =>this.setState({ 
            clickCount: this.state.clickCount + 1 
          })
    }>
    {this.state.clickCount}
</button>;
  }

注意,onClick 属性的值周围没有引号;相反,它被括号包围。这是将定义在行内的 JavaScript 函数传递给组件。它将在传递给 constructor 方法的组件的 props 字典中可用。例如,假设我们像这样传递它:

onClick="() =>this.setState…"

在这种情况下,它将以字符串值的形式传递给组件,因此不会工作。

我们还在 button 的内容中渲染了 clickCount 的当前值。JavaScript 也可以在这些大括号内执行。要显示点击次数加一,我们可以这样做:

{this.state.clickCount + 1}

在下一个练习中,我们将 Babel 包含到我们的模板中,然后转换我们的组件以使用 JSX。

练习 16.02:JSX 和 Babel

在这个练习中,我们想在组件中实现 JSX 以简化我们的代码。为此,我们需要对 react-example.js 文件和 react-example.html 文件进行一些更改,以切换到 JSX 来渲染 ClickCounter

  1. 在 PyCharm 中,打开 react-example.js 并将 render 方法更改为使用 JSX,通过替换以下代码。您可以参考 练习 16.01 中的 步骤 2设置 React 示例,其中我们定义了此方法:

    render() {
    return <button onClick={() => this.setState({ 
           clickCount: this.state.clickCount + 1 
           })
        }>
        {this.state.clickCount}
    </button>;  }
    
  2. 现在,我们可以将 ClickCounter 视为一个元素本身。在文件末尾的 ReactDOM.render 调用中,您可以替换第一个参数,e(ClickCounter),为 <ClickCounter/> 元素,如下所示:

    ReactDOM.render(<ClickCounter/>, document.getElementById   ('react_container'));
    
  3. 由于我们不再使用在练习 16.01步骤 2中创建的React.create函数,我们可以删除我们创建的别名;删除第一行:

    const e = React.createElement;
    

    你可以保存并关闭文件。

  4. 打开react-example.html模板。你需要包含 Babel 库 JavaScript。在 React script元素和react-example.js元素之间添加以下代码:

    <script crossorigin src="img/babel.min.js"></script>
    
  5. react-example.html<script>标签中添加一个type="text/babel"属性:

    <script src="img/{% static 'react-example.js' %}" type="text/babel"></script>
    

    保存react-example.html

  6. 如果 Django 开发服务器尚未运行,请启动它并转到http://127.0.0.1:8000/react-example/。你应该看到我们之前有的相同按钮(图 16.12)。当你点击按钮时,你应该看到计数增加。

在这个练习中,我们没有改变ClickCounter React 组件的行为。相反,我们重构了它以使用 JSX。这使得直接将组件的输出作为 HTML 编写变得更容易,并且减少了我们需要编写的代码量。在下一节中,我们将探讨如何向 JSX React 组件传递属性。

JSX 属性

基于 JSX 的 React 组件上的属性设置方式与标准 HTML 元素上的属性设置方式相同。重要的是要记住,你是将它们作为字符串还是 JavaScript 值来设置的。

让我们通过使用ClickCounter组件来查看一些示例。假设我们想要扩展ClickCounter,以便可以指定一个target数字。当达到目标时,按钮应该被替换为文本Well done, <name>!。这些值应该作为属性传递给ClickCounter

当使用变量时,我们必须将它们作为 JSX 值传递:

let name = 'Ben'
let target = 5;
ReactDOM.render(<ClickCounter name={name} target={target}/>,   document.getElementById('react_container'));

我们也可以混合匹配传递值的方法。这也是有效的:

ReactDOM.render(<ClickCounter name="Ben" target={5}/>,   document.getElementById('react_container'));

在下一个练习中,我们将更新ClickCounter以从属性中读取这些值并更改其在达到目标时的行为。我们将从 Django 模板中传递这些值。

练习 16.03:React 组件属性

在这个练习中,你将修改ClickCounter以从其props中读取targetname的值。你将从 Django 视图传递这些值,并使用escapejs过滤器使name值在 JavaScript 字符串中使用时安全。完成之后,你将能够点击按钮直到它达到目标,然后看到Well done消息:

  1. 在 PyCharm 中,打开reviews应用的views.py。我们将修改react_example视图的render调用,传递一个包含nametarget的上下文,如下所示:

    return render(request, "react-example.html", {"name": "Ben", \
                                                  "target": 5})
    

    如果你喜欢,你可以使用自己的名字并选择不同的目标值。保存views.py

  2. 打开react-example.js文件。我们将更新constructor方法中的state设置,从props中设置名称和目标,如下所示:

    constructor(props) {
        super(props);
        this.state = { clickCount: 0, name: props.name, target:       props.target
        };
    }
    
  3. render方法的行为更改为在达到target后返回Well done, <name>!。在render方法中添加此if语句:

    if (this.state.clickCount === this.state.target) {
        return <span>Well done, {this.state.name}!</span>;
    }
    
  4. 要传递值,请将 ReactDOM.render 调用移动到模板中,以便 Django 可以渲染这段代码。从 react-example.js 的末尾剪切此 ReactDOM.render 行:

    ReactDOM.render(<ClickCounter/>, document.getElementById   ('react_container'));
    

    我们将在 第 6 步 中将其粘贴到模板文件中。现在 react-example.js 应该只包含 ClickCounter 类。保存并关闭文件。

  5. 打开 react-example.html。在所有现有的 <script> 标签之后(但在关闭的 </body> 标签之前),添加带有 type="text/babel" 属性的打开和关闭 <script> 标签。在里面,我们需要将传递给模板的 Django 上下文值分配给 JavaScript 变量。总共,你应该添加以下代码:

    <script type="text/babel">
    let name = "{{ name|escapejs }}";
    let target = {{ target }};
    </script>
    

    第一个将 name 变量与 name 上下文变量赋值。我们使用 escapejs 模板过滤器;否则,如果我们的名字中包含双引号,我们可能会生成无效的 JavaScript 代码。第二个值 targettarget 赋值。这是一个数字,所以它不需要转义。

    注意

    由于 Django 对 JavaScript 的值进行了转义,name 不能直接像这样传递到组件属性中:

    <ClickCounter name="{{ name|escapejs }}"/>

    JSX 不会正确地取消转义值,你最终会得到转义序列。

    然而,你可以像这样传递数值 target

    <ClickCounter target="{ {{ target }} }"/>

    此外,请注意 Django 大括号和 JSX 大括号之间的间距。在这本书中,我们将坚持首先将所有属性分配给变量,然后传递给组件,以保持一致性。

  6. 在这些变量声明下面,粘贴从 react-example.js 复制的 ReactDOM.render 调用。然后,向 ClickCounter 添加 target={ target }name={ name } 属性。记住,这些是正在传递的 JavaScript 变量,而不是 Django 上下文变量——它们只是碰巧有相同的名字。现在的 <script> 块应该看起来像这样:

    <script type="text/babel">
        let name = "{{ name|escapejs }}";
        let target = {{ target }};
        ReactDOM.render(<ClickCounter name={ name }       target={ target }/>, document.getElementById         ('react_container'));
    </script>
    

    你可以保存 react-example.html

  7. 如果 Django 开发服务器尚未运行,请先启动它,然后转到 http://127.0.0.1:8000/react-example/。尝试点击按钮几次——它应该增加,直到你点击它 target 次数。然后,它将被替换为 Well done, <name>! 文本。见 图 16.13 了解点击足够次数后的样子:图 16.13:完成信息

图 16.13:完成信息

在这个练习中,我们使用 props 将数据传递给 React 组件。我们在将数据分配给 JavaScript 变量时使用了 escapejs 模板过滤器进行了转义。在下一节中,我们将介绍如何使用 JavaScript 通过 HTTP 获取数据。

进一步阅读

对于一个更详细、更实用的 React 课程,你可以随时参考 The React Workshopcourses.packtpub.com/courses/react

JavaScript Promises

为了防止在长运行操作上阻塞,许多 JavaScript 函数都是异步实现的。它们的工作方式是立即返回,然后在结果可用时调用回调函数。这些类型函数返回的对象是Promise。通过调用其then方法,向Promise对象提供回调函数。当函数运行完成后,它将要么解析Promise(调用success函数)要么拒绝它(调用failure函数)。

我们将通过一个假设的长运行函数来展示错误和正确使用 Promise 的方式,该函数执行一个大的计算,称为getResult。它不是返回结果,而是返回一个Promise。你不会像这样使用它:

const result = getResult();
console.log(result);  // incorrect, this is a Promise

而应该像这样调用,将回调函数传递给返回的Promise上的then。我们将假设getResult永远不会失败,所以我们只为解析情况提供一个success函数:

const promise = getResult();
promise.then((result) => {
    console.log(result);  /* this is called when the Promise       resolves*/
});

通常,你不会将返回的Promise赋值给变量。相反,你会将then调用链接到函数调用上。我们将在下一个示例中展示这一点,包括一个失败回调(假设getResult现在可能失败)。我们还将添加一些注释来说明代码执行的顺序:

getResult().then( 
(result) => {
        // success function
        console.log(result);  
// this is called 2nd, but only on success
}, 
    () => {
        // failure function
        console.log("getResult failed");
        // this is called 2nd, but only on failure
})
// this will be called 1st, before either of the callbacks
console.log("Waiting for callback");

现在我们已经介绍了 Promise,我们可以看看fetch函数,它用于发起 HTTP 请求。它是异步的,通过返回 Promise 来工作。

fetch

大多数浏览器(95%)支持一个名为fetch的函数,允许你发起 HTTP 请求。它使用带有 Promise 的异步回调接口。

fetch函数接受两个参数。第一个是要发起请求的 URL,第二个是一个包含请求设置的(字典)对象。例如,考虑以下内容:

const promise = fetch("http://www.google.com", {…settings});

设置包括以下内容:

  • method:请求的 HTTP 方法(GETPOST等)。

  • headers:另一个要发送的 HTTP 头部的(字典)对象。

  • body:要发送的 HTTP 正文(用于POST/PUT请求)。

  • credentials:默认情况下,fetch不会发送任何 cookie。这意味着你的请求将表现得像你没有认证。为了使其在请求中设置 cookie,这应该设置为same-origininclude的值。

让我们用一个简单的请求来实际看看:

fetch('/api/books/', {
    method: 'GET',
    headers: {
        Accept: 'application/json'
    }
}).then((resp) => {
    console.log(resp)
})

这段代码将从/api/book-list/获取数据,然后调用一个函数,使用console.log将请求记录到浏览器的控制台。

图 16.14显示了 Firefox 中前一个响应的控制台输出:

![图 16.14:控制台中的响应输出图 16.14:控制台中的响应输出

图 16.14:控制台中的响应输出

如您所见,输出的信息并不多。在我们能够处理它之前,我们需要解码响应。我们可以使用响应对象的json方法来解码响应体到一个 JSON 对象。这也返回一个Promise,所以我们将请求获取 JSON,然后在回调中处理数据。完成这个操作的完整代码块如下所示:

fetch('/api/books/', {
    method: 'GET',
    headers: {
        Accept: 'application/json'
    }
}).then((resp) => {
    return resp.json(); // doesn't return JSON, returns a Promise
}).then((data) => {
    console.log(data);
});

这将在浏览器控制台中记录解码后的 JSON 格式的对象。在 Firefox 中,输出看起来像图 16.15

![图 16.15:解码后的书单输出到控制台图片

图 16.15:解码后的书单输出到控制台

练习 16.04获取和渲染书籍中,我们将编写一个新的 React 组件,该组件将获取书籍列表并将其渲染为列表项(<li>)。在此之前,我们需要了解 JavaScript 的map方法以及如何使用它来构建 React 中的 HTML。

JavaScript 的 map 方法

有时候我们想要对同一块代码(JavaScript 或 JSX)进行多次执行,针对不同的输入数据。在本章中,这将最有用,用于生成具有相同 HTML 标签但不同内容的 JSX 元素。在 JavaScript 中,map方法遍历目标数组,然后对数组中的每个元素执行一个回调函数。然后,这些元素被添加到一个新数组中,该数组随后被返回。例如,这个简短片段使用mapnumbers数组中的每个数字翻倍:

const numbers = [1, 2, 3];
const doubled = numbers.map((n) => {
    return n * 2;
});

doubled数组现在包含值[2, 4, 6]

我们也可以使用这种方法创建一个 JSX 值的列表。需要注意的是,列表中的每个项目都必须设置一个唯一的key属性。在接下来的这个简短示例中,我们将一个数字数组转换成<li>元素。然后我们可以将它们放在<ul>内部。以下是一个示例render函数来完成这个操作:

render() {
    const numbers = [1, 2, 3];
    const listItems = numbers.map((n) => {
      return <li key={n}>{n}</li>;
      });
    return <ul>{listItems}</ul>
}

当渲染时,这将生成以下 HTML:

<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>

在下一个练习中,我们将构建一个 React 组件,其中包含一个按钮,当点击时将从 API 获取书籍列表。然后显示书籍列表。

练习 16.04:获取和渲染书籍

在这个练习中,你将创建一个名为BookDisplay的新组件,该组件在<ul>内部渲染一个书籍数组。这些书籍将通过fetch获取。为此,我们将 React 组件添加到react-example.js文件中。然后,我们将书籍列表的 URL 传递到 Django 模板中的组件内部:

  1. 在 PyCharm 中,打开react-example.js,这是你在练习 16.03React 组件属性步骤 9 中使用的。你可以删除整个ClickCounter类。

  2. 创建一个名为BookDisplay的新类,该类从React.Component扩展。

  3. 然后,添加一个constructor方法,它接受props作为参数。它应该调用super(props),然后设置其状态如下:

    this.state = { books: [], url: props.url, fetchInProgress:   false };
    

    这将初始化books为一个空数组,从传入的属性url中读取 API URL,并将fetchInProgress标志设置为false。你的constructor方法的代码应该如下所示:

    constructor(props) {
      super(props);
      this.state = { books: [], url: props.url, fetchInProgress:   false };
    }
    
  4. 接下来,添加一个doFetch方法。你可以复制并粘贴以下代码来创建它:

    doFetch() {
      if (this.state.fetchInProgress)
          return;
    this.setState({ fetchInProgress: true })
      fetch(this.state.url, {
          method: 'GET',
          headers: {
              Accept: 'application/json'
          }
      }
      ).then((response) => {
          return response.json();
      }).then((data) => {
    this.setState({ fetchInProgress: false, books: data })
      })
    }
    

    首先,使用if语句检查是否已经启动了获取操作。如果是,我们从函数中返回。然后,我们使用setState来更新状态,将fetchInProgress设置为true。这将同时更新我们的按钮显示文本并阻止同时运行多个请求。然后,我们fetch``this.state.url(我们将在练习的稍后通过模板传递)。使用GET方法检索响应,我们只想Accept JSON 响应。在获取响应后,我们使用json方法返回其 JSON。这返回一个Promise,因此我们使用另一个then来处理当 JSON 被解析时的回调。在那个最后的回调中,我们设置组件的状态,将fetchInProgress恢复为false,并将books数组设置为解码后的 JSON 数据。

  5. 接下来,创建render方法。你也可以复制并粘贴以下代码:

    render() {
      const bookListItems = this.state.books.map((book) => {
          return <li key={ book.pk }>{ book.title }</li>;
      })
      const buttonText = this.state.fetchInProgress  ? 
      'Fetch in Progress' : 'Fetch';
      return <div>
    <ul>{ bookListItems }</ul>
    <button onClick={ () =>this.doFetch() } 
            disabled={ this.state.fetchInProgress }>
              {buttonText}
    </button>
    </div>;
    }
    

    这使用map方法遍历state中的书籍数组。我们为每本书生成一个<li>,使用书的pk作为列表项的key实例。<li>的内容是书的标题。我们定义一个buttonText变量来存储(并更新)按钮将显示的文本。如果我们当前有一个正在运行的fetch操作,那么这将显示为获取中。否则,它将是获取。最后,我们返回一个包含我们想要的所有数据的<div><ul>的内容是bookListItems变量(<li>实例的数组)。它还包含一个以类似方式添加的<button>实例。onClick方法调用类的doFetch方法。如果有一个获取操作正在进行,我们可以使按钮disabled(即用户不能点击按钮)。我们将按钮文本设置为之前创建的buttonText变量。现在你可以保存并关闭react-example.js

  6. 打开react-example.html。我们需要用BookDisplay渲染(来自练习 16.03React 组件属性)替换ClickCounter渲染。删除nametarget变量定义。我们将渲染<BookDisplay>。将url属性设置为字符串,并传入书籍列表 API 的 URL,使用{% url %}模板标签生成它。ReactDOM.render调用应该如下所示:

    ReactDOM.render(<BookDisplay url="{% url 'api:book-list' %}" />,  document.getElementById('react_container'));
    

    现在,你可以保存并关闭react-example.html

  7. 如果 Django 开发服务器尚未运行,请启动它,然后访问http://127.0.0.1:8000/react-example/。你应该在页面上看到一个单独的Fetch按钮(图 16.16):![图 16.16:获取书籍按钮 图 16.16:获取书籍按钮

图 16.16:获取书籍按钮

点击“获取”按钮后,它应该变为禁用状态,并更改其文本为“获取中”,正如我们在这里看到的:

![图 16.17:获取中图片

图 16.17:获取中

获取完成后,你应该看到以下渲染的书籍列表:

![图 16.18:书籍获取完成图片

图 16.18:书籍获取完成

这个练习是整合 React 与你在第十二章“构建 REST API”中构建的 Django REST API 的机会。我们创建了一个新的组件(BookDisplay),通过调用fetch来获取书籍列表。我们使用 JavaScript 的map方法将书籍数组转换为一些<li>元素。正如我们之前看到的,我们使用button在点击时触发fetch。然后,我们将书籍列表 API URL 提供给 Django 模板中的 React 组件。后来,我们在 Bookr 中看到了使用 REST API 动态加载的书籍列表。

在我们进入本章的活动之前,我们将讨论与其他 JavaScript 框架一起使用 Django 时的注意事项。

原文模板标签

我们已经看到,当使用 React 时,我们可以在 Django 模板中使用 JSX 插值值。这是因为 JSX 使用单大括号进行插值,而 Django 使用双大括号。只要 JSX 和 Django 大括号之间有空格,它应该可以正常工作。

其他框架,如 Vue,也使用双大括号进行变量插值。这意味着如果你在模板中有 Vue 组件的 HTML,你可能尝试像这样进行插值:

<h1>Hello, {{ name }}!</h1>

当然,当 Django 渲染模板时,它会在 Vue 框架有机会渲染之前先插值name值。

我们可以使用verbatim模板标签让 Django 按模板中显示的原始数据输出,而不进行任何渲染或变量插值。与前面的示例一起使用它很简单:

{% verbatim %}
<h1>Hello, {{ name }}!</h1>
{% endverbatim %}

现在当 Django 渲染模板时,模板标签之间的 HTML 将按原样输出,允许 Vue(或另一个框架)接管并自行插值变量。许多其他框架将它们的模板分离到自己的文件中,这不应与 Django 的模板冲突。

有许多 JavaScript 框架可供选择,你最终决定使用哪个将取决于你自己的意见或你公司/团队使用的框架。如果你遇到冲突,解决方案将取决于你的特定框架。本节中的示例应该能帮助你找到正确的方向。

我们现在已经涵盖了你需要将 React(或其他 JavaScript 框架)与 Django 集成的多数内容。在下一个活动中,你将应用这些知识来获取 Bookr 上最新的评论。

活动十六点零一:预览评论

在这个活动中,我们将更新 Bookr 主页面以获取最近的六条评论并显示它们。用户将能够点击按钮前进到下一条六条评论,然后返回到之前的评论。

这些步骤将帮助你完成活动:

  1. 首先,我们可以清理一些之前练习中的代码。如果你喜欢,可以备份这些文件以备将来参考。或者,你也可以使用 GitHub 版本,以备将来参考。删除react_example视图、react-example URL、react-example.html模板和react-example.js文件。

  2. 创建一个recent-reviews.js静态文件。

  3. 创建两个组件,一个用于显示单个评论数据的ReviewDisplay组件,另一个用于获取评论数据并显示ReviewDisplay组件列表的RecentReviews组件。

    首先,创建ReviewDisplay类。在其构造函数中,你应该读取通过props传递的review并将其分配给状态。

  4. ReviewDisplayrender方法应返回类似以下的 JSX HTML:

    <div className="col mb-4">
    <div className="card">
    <div className="card-body">
    <h5 className="card-title">{ BOOK_TITLE }
    <strong>({ REVIEW_RATING })</strong>
    </h5>
    <h6 className="card-subtitle mb-2 text-muted">CREATOR_EMAIL</h6>
    <p className="card-text">REVIEW_CONTENT</p>
    </div>
    <div className="card-footer">
    <a href={'/books/' + BOOK_ID` + '/' } className="card-link">  View Book</a>
    </div>
    </div>
    </div>
    

    然而,你应该用组件获取的review中的适当值替换BOOK_TITLEREVIEW_RATINGCREATOR_EMAILREVIEW_CONTENTBOOK_ID占位符。

    注意

    注意,当使用 JSX 和 React 时,元素的class是通过className属性设置的,而不是class。当它被渲染为 HTML 时,它变为class

  5. 创建另一个名为RecentReviews的 React 组件。它的constructor方法应使用以下键/值设置state

    reviews: [](空列表)

    currentUrl: props.url

    nextUrl: null

    previousUrl: null

    loading: false

  6. 实现一个从 REST API 下载评论的方法。命名为fetchReviews。如果state.loadingtrue,则应立即返回。然后,应将stateloading属性设置为true

  7. 以与练习 16.04中相同的方式实现fetch。它应遵循请求state.currentUrl并从响应中获取 JSON 数据的相同模式。然后,在state中设置以下值:

    loading: false

    reviews: data.results

    nextUrl: data.next

    previousUrl: data.previous

  8. 实现一个componentDidMount方法。这是一个当 React 将组件加载到页面上时被调用的方法。它应该调用fetchReviews方法。

  9. 创建一个loadNext方法。如果state中的nextUrl为 null,则应立即返回。否则,应将state.currentUrl设置为state.nextUrl,然后调用fetchReviews

  10. 类似地,创建一个loadPrevious方法;然而,这个方法应将state.currentUrl设置为state.previousUrl

  11. 实现渲染方法。如果状态为加载中,则应在<h5>元素内返回文本Loading…

  12. 创建两个变量来存储previousButtonnextButton的 HTML。它们都应该有btn btn-secondary类,并且下一个按钮也应该有float-right类。如果相应的previousUrlnextUrl属性是null,则它们的disabled属性应设置为true。它们的onClick属性应设置为调用loadPreviousloadNext方法。按钮文本应为“上一页”或“下一页”。

  13. 使用map方法遍历评论并将结果存储到一个变量中。每个review应该由一个具有key属性设置为评论的pkreview设置为Review类的ReviewDisplay组件表示。如果没有评论(reviews.length === 0),则该变量应是一个包含内容没有评论可显示<h5>元素。

  14. 最后,将所有内容包裹在<div>元素中,如下所示:

    <div>
    <div className="row row-cols-1 row-cols-sm-2 row-cols-md-3">
          { reviewItems }
    </div>
    <div>
          {previousButton}
          {nextButton}
    </div>
    </div>
    

    我们在这里使用的className将根据屏幕大小显示每个评论预览为一列、两列或三列。

  15. 接下来,编辑base.html。你将在content块内添加所有新内容,这样它就不会在覆盖此块的非主页面上显示。添加一个包含内容Recent Reviews<h4>元素。

  16. 为 React 渲染添加一个<div>元素。确保你给它一个唯一的id

  17. 包含<script>标签以包含 React、React DOM、Babel 和recent-reviews.js文件。这四个标签应该与你在练习 16.04获取和渲染书籍中使用的类似。

  18. 需要添加的最后一件事是另一个包含ReactDOM.render调用代码的<script>标签。正在渲染的根组件是RecentReviews。它应该有一个url属性设置为url="{% url 'api:review-list' %}?limit=6"的值。这会对ReviewViewSet进行 URL 查找,然后追加一个页面大小参数6,将检索到的评论数量限制在最多6条。

完成这些步骤后,你应该能够导航到http://127.0.0.1:8000/(主 Bookr 页面)并看到如下页面:

![图 16.19:完成的评论预览]

![图片 B15509_16_19.jpg]

图 16.19:完成的评论预览

在截图上,页面已经滚动以显示“上一页”/“下一页”按钮。注意“上一页”按钮已被禁用,因为我们处于第一页。

如果你点击“下一页”,你应该看到下一页的评论。如果你多次点击“下一页”(取决于你有多少评论),你最终会到达最后一页,然后“下一页”按钮将被禁用:

![图 16.20:下一页按钮禁用]

![图片 B15509_16_20.jpg]

图 16.20:下一页按钮禁用

如果你没有评论,你应该看到消息“没有评论可显示”:

![图 16.21:没有评论可显示。文本]

![图片 B15509_16_21.jpg]

图 16.21:没有评论可显示。文本

当页面正在加载评论时,你应该看到文本 正在加载...;然而,由于数据是从你的电脑上加载的,它可能只会显示一秒钟:

![图 16.22:加载文本图片 B15509_16_22.jpg

图 16.22:加载文本

注意

该活动的解决方案可以在 packt.live/2Nh1NTJ 找到。

摘要

在本章中,我们介绍了 JavaScript 框架,并描述了它们如何与 Django 一起工作以增强模板并添加交互性。我们介绍了 JavaScript 语言及其主要特性、变量类型和类。然后,我们介绍了 React 背后的概念以及它是如何通过使用组件来构建 HTML 的。我们仅使用 JavaScript 和 React.createElement 函数构建了一个 React 组件。之后,我们介绍了 JSX 以及它如何通过允许你在 React 组件中直接编写 HTML 来简化组件的开发。我们介绍了 promisesfetch 函数的概念,并展示了如何使用 fetch 从 REST API 获取数据。本章以一个练习结束,该练习使用 REST API 从 Bookr 获取评论,并将它们渲染到页面的交互式组件中。

在下一章中,我们将探讨如何将我们的 Django 项目部署到生产型网络服务器。你可以从本书的 GitHub 仓库下载该章节,网址为 packt.live/2Kx6FmR

posted @ 2025-09-18 12:47  绝不原创的飞龙  阅读(22)  评论(0)    收藏  举报