成为-Django-企业级开发者-全-
成为 Django 企业级开发者(全)
原文:
zh.annas-archive.org/md5/c25a1eb02aa86674f6515ab39c915c55译者:飞龙
前言
Django 是一个内置了前端工具的后端框架,旨在帮助开发者快速轻松地构建应用。它旨在减少 Web 开发的繁琐工作,以便开发者可以更多地关注他们正在构建的功能,而不是他们面临的问题。Django 内置了数十种工具,并与无数的第三方 Python 包相结合,提供了许多开箱即用的功能和组件。
Django 框架在设计时考虑了可扩展性和多功能性。随着需求和流量的增长,您的项目也可以相应增长。这使得开发者可以轻松地构建到现有系统中。安全性也是 Django 非常重视的方面,它通过在其框架中直接构建许多不同的安全措施来帮助开发者避免在安全性方面犯常见错误。它甚至拥有自己的用户认证系统,以帮助管理您的用户。
在本书中,我们将学习 Django 框架的基本组件以及它与 Web 开发的关系。从小型网站到大型企业级应用,本书将深入探讨构建任何规模网站或应用的基本组件。
本书面向的对象
本书专注于全栈企业级应用开发。如果您想构建一个 Web 应用、API 或网站或维护现有项目,这本书适合您。本书假设您对 Python 编程语言有中级水平的知识,并且已经为那些刚开始接触 Django 框架的人精心调整。无论您是 Web 开发的新手还是有多年使用其他技术的经验,这本书都适合您。
本书涵盖的内容
第一章,启动一个大型项目,为您讲解如何为大项目做准备。
第二章,项目配置,介绍了虚拟环境、托管和部署。
第三章,模型、关系和继承,介绍了数据库表结构。
第四章,URL、视图和模板,介绍了如何使用 Django 渲染 HTML。
第五章,Django 表单,介绍了如何使用 Django 渲染 HTML 表单。
第六章,探索 Django 管理站点,探讨了 Django 内置的管理站点。
第七章,使用消息、电子邮件通知和 PDF 报告工作,介绍了如何使用 Django 发送电子邮件和创建文档。
第八章,使用 Django REST 框架工作,介绍了如何使用 Django 构建 API。
第九章,Django 测试,介绍了如何使用 Django 编写测试脚本。
第十章,数据库管理,涵盖了优化数据库查询。
为了充分利用本书
您需要 Python 的最新版本。本书中的所有代码示例均已在 Windows 11 上使用 Django 4.0 测试过,Python 版本为 3.9。然而,它们也应该适用于未来的版本发布。

随着本书的进展,每个章节将提供额外的安装和配置说明,例如在 第二章**,项目配置 中,当我们安装可选的集成开发环境软件套件时,或者在 第九章**,Django 测试 中,当我们安装生产力和测试工具时。
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中提供链接)获取代码。这样做将帮助您避免与代码复制粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 在 https://github.com/PacktPublishing/Becoming-an-Enterprise-Django-Developer 下载本书的示例代码文件。如果代码有更新,它将在 GitHub 仓库中更新。
我们还在 https://github.com/PacktPublishing/ 提供了其他丰富的代码包。请查看它们!
本书 GitHub 仓库中提供的代码包括每一章的每个示例。大部分代码都被注释掉了,除了每章的第一个练习。如果您使用本书提供的代码,它旨在随着您在书中的进度注释和取消注释代码。如果您跳过前面,可能需要取消注释跳过的必要代码,以便项目能够运行。每个章节都已被组织到整个项目中的单独章节应用中。项目应用将在 第二章**,项目配置 中介绍和讨论。
代码在行动
本书“代码在行动”视频可在 bit.ly/3HQDP9Z 查看。
下载彩色图像
我们还提供了一份包含本书中使用的截图和图表彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781801073639_ColorImages.pdf。
使用的约定
本书使用了一些文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“确保也将此应用包含在 settings.py 文件中的 INSTALLED_APPS 变量中。”
代码块设置如下:
# /becoming_a_django_entdev/chapter_5/forms.py
from django.forms
import Form
class ContactForm(Form):
pass
当我们希望引起您对代码块中特定部分的注意时,相关的行或项目将以粗体显示:
# /becoming_a_django_entdev/chapter_5/forms.py
from django.forms
import Form, ModelForm
class VehicleForm(ModelForm):
pass
任何命令行输入或输出都按照以下方式编写:
RuntimeError: Conflicting 'vehicle' models in application 'chapter_3':
粗体: 表示新术语、重要单词或屏幕上出现的单词。例如,菜单或对话框中的单词会以粗体显示。以下是一个例子:“我们可以在前面的屏幕截图中看到chapter_3_engine和chapter_3_practice_engine表。”
小贴士或重要注意事项
看起来是这样的。
联系我们
我们始终欢迎读者的反馈。
一般反馈: 如果您对本书的任何方面有疑问,请通过 customercare@packtpub.com 给我们发邮件,并在邮件主题中提及书名。
作者: 如果您想直接联系作者,您可以在 LinkedIn 上找到并给他发消息,链接如下www.linkedin.com/in/mikedinder/。
勘误: 尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这个错误。请访问www.packtpub.com/support/errata并填写表格。
盗版: 如果您在互联网上以任何形式遇到我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过版权@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者: 如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
一旦您阅读了《成为企业级 Django 开发者》,我们很乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
第一部分 – 开始一个项目
在这部分,你将开始学习企业开发是什么,了解需求收集过程,并使用 Heroku 创建一个免费托管计划。你将为代码创建本地、开发、预发布和生产环境,以便代码在其中运行。然后,你将创建一个 Django 项目并将代码推送到这些环境。
我们将讨论配置项目文件和设置以与 Heroku 托管的应用程序一起工作。我们还将创建和配置一个数据库,以便与我们所构建的项目一起使用。然后,我们将构建与该数据库中的表相关的模型。本书的其余部分不需要 Heroku。本书的大部分内容可以在你的机器上本地运行。
本部分包括以下章节:
-
第一章,承担一个庞大的项目
-
第二章,项目配置
-
第三章,模型、关系和继承
第一章:第一章:承担一个庞大的项目
考虑到如今应用程序和网站日益增加的复杂性,本章将向您介绍应对自己庞大项目所需的因素。我们将探讨企业级开发的概念,然后窥视我们可能采取的许多不同路径。我们将讨论帮助我们为项目制定计划的方法和工具,本质上是在构建我们开始所需的蓝图。每个项目也需要硬件来运行其软件,因此我们将探讨提供所需硬件的服务选项。在提供的选项中,我们将选择一个托管提供商,并在本书中展示如何与该提供商合作。
项目本身可以与任何托管提供商一起使用,甚至可以在自建的服务器上运行;然而,请注意,某些设置可能需要针对我们将要使用的托管服务器进行特定配置。在本章结束时,我们将为所选提供商创建一个托管账户,并选择最简单、免费的计划用于本书。我们还将创建和配置多个工作环境,以便代码可以在这些环境中运行。我们还将将该托管计划中的每个环境连接到一个远程仓库,以确保代码的安全存储。
在本章中,我们将涵盖以下主题:
-
建立企业
-
设计和规划
-
托管和部署
大多数人建议在处理 Django 时使用基于 UNIX 或 Linux 的操作系统,例如 Ubuntu 或 macOS。Django 被构建得非常灵活,本书中将要展示的概念和代码示例可以在所有三个主要平台(Windows、Mac 和 Linux)以及更多平台上运行。我个人自从开始学习和直接使用这个框架以来,一直在 Windows 系统上使用 Django。我这样做是因为我来自 Windows 背景;大约一半的工作提供了设备或要求在基于 Windows 的机器上使用某些软件。后来,越来越多的公司开始让开发者选择在他们最舒适的机器上工作。我继续选择 Windows,因为我已经熟悉它了。
技术要求
无论您使用的是 Windows、Mac 还是 Ubuntu 系统,Python都需要安装。Python 3.9.0 是本书编写时的最新版本。很可能此代码将仍然与 Python 的未来版本兼容,但无法保证所有代码都将继续工作,并且未来版本可能会有一些小问题。您可以在他们的网站上找到所有平台的 Python 安装文件:www.python.org/downloads/。对于非 Windows 用户,使用Homebrew安装 Python 是最直接的方法,它会为您创建一个指向已安装包的符号链接。
对于 Windows 用户来说,通过 Microsoft Store 安装 Python 是一种简单的方法。在列表中搜索Python并选择3.9。在安装过程中,如果您看到将 Python 添加到环境变量选项,请选中复选框以包含它!这将在您的开发机器上设置一个指向 Python 包/库的全局库的路径。这些与包含在您的项目中的包不同,我们将在第二章 项目配置中讨论。
需要一个命令行工具来执行与 Django 交互和使用的命令。PowerShell是 Windows 今天标准提供的常见命令行外壳。这个命令行实用程序集成了来自其他现有外壳的一些最酷的功能,全部合并为一个。它也适用于 Mac 和 Linux 操作系统。然而,iTerm2或内置的终端应用是大多数开发者倾向于使用的。无论如何,PowerShell 已经成为许多开发者今天用于自动化管理各种系统的流行工具。
在未来,您还需要安装pip,以便继续使用本书中讨论的其余代码和概念。然而,本书中讨论的并非每个第三方包都保证未来会得到该供应商的支持。无论如何,本书教授了如何使用第三方包以及如何在项目中配置和使用它们,这些知识可以帮助您找到适合自己的包。
小贴士
无论您在哪个操作系统上工作,如果在本书的任何命令中遇到错误消息,例如pip/python 不是内部或外部命令,这意味着您需要在系统上配置环境变量以指向相应的文件目录。要手动配置任何三大平台上的环境变量,请访问以下链接:
• Windows: phoenixnap.com/kb/windows-set-environment-variable
• macOS: phoenixnap.com/kb/set-environment-variable-mac
• Ubuntu: help.ubuntu.com/community/EnvironmentVariables
如果一个命令不被识别为内部或外部命令,你可能需要添加该项目安装在你机器上的路径。在 Windows 中,这通常是Path变量。例如,如果python不被识别为命令,请将 Python 安装在你机器上的路径添加到 Windows 的全局Path变量中。同样的情况也适用于 Linux 或 Mac,但最常见的问题是在 Windows 上。
本书创建和使用的所有代码都可以在这里找到:github.com/PacktPublishing/Becoming-an-Enterprise-Django-Developer。本章实际上并没有深入到任何代码中。然而,对于那些已经熟悉 Django 或者对于 Django 新手来说,已经阅读过第二章、“项目配置”,并返回本章,本书附带了一个名为chapter_1的应用程序,以展示本章标题为“实体关系图”的子节中引入的绘图包。
查看以下视频,看看代码的实际应用:bit.ly/3OfagBj。
构建企业
构建企业级应用软件(EAS)不是一项容易的任务。这项任务需要许多不同的技术专家共同协作,进行高度的合作和预先规划才能完成。未能进行充分的规划可能导致项目花费的时间远超过预期,花费的金钱也更多。你的商业模式所依赖的重要功能可能会被忽略,当新系统启动时,这可能导致日常流程的中断。
企业级软件旨在满足整个企业的需求。EAS 将组织的所有业务逻辑整合到一个系统中,这个系统被视为由许多较小的子系统组成的集合。该软件消除了纸质文件的需求,减少了完成任务所需的步骤,并为当今世界各种问题提供自我自动化甚至人工智能解决方案。网站本身只是整个系统实际组成部分的一小部分。
选择企业级应用的理由
企业级软件通常被认为是针对已经建立系统并需要改进的组织的一种解决方案。无论该系统是数字化的还是手工的,例如在文件柜中的纸张,公司总是在寻找简化日常任务的方法。因此,企业级软件可以由一个或多个不同的消费级应用程序组成。如果你需要的不仅仅是网站,还需要一个同时承担你的业务管理任务的系统,你可能会需要升级到企业级。然而,Django 仍然可以用于最简单的网站以及超出你想象的大型项目。应用程序也可以分解为单个 Django 项目。
假设你有一个拥有许多部门和许多用户的组织,所有这些部门和用户都由不同的权限角色组成。你需要一种方式将他们全部连接起来,以便他们可以生成和共享报告,发送通知,存储和管理客户/用户数据,创建发票,并与其他系统协同工作。也许你发现你的组织需要连接在家远程工作的员工,以便他们在工作中更加高效和高效。想法是每种用户类型将以完全不同的方式与系统交互,甚至拥有不同的权限级别,使他们能够访问你系统的不同部分。
一般而言,当人们想到企业级时,他们也会想到一些独特且定制的产品。这类软件被称为专有软件或闭源软件,并且不打算以任何方式向公众重新分发。然而,并非所有 EAS 都必须是专有的;分发许可证可以设置为任何你希望的方式。例如,主系统可能为一家母公司品牌化,而系统的某些部分可能为其子公司品牌化,或者可以根据你的许可协议分发和重新品牌化副本。如果您的项目(s)极其复杂,与技术律师交谈以帮助编写您的软件许可协议也是一个好主意。在项目开始时邀请技术律师的帮助是预防诉讼的好方法。
接下来,我们将讨论一些主要的企业系统类型。
企业系统类型
企业级系统有很多不同的种类,但我们可以将它们归纳为以下六个主要类别:
-
客户关系管理 (CRM)
-
企业内容管理 (ECM)
-
企业资产管理 (EAM)
-
企业资源计划 (ERP)
-
供应链管理 (SCM)
-
企业信息系统 (EIS)
在这本书中,我们实际上不会完整地构建这些系统。相反,我们将涉及关键的 Django 编程概念,当它们结合在一起时,将帮助您走向终点。并非每个概念都能融入这本书,但 Django 完全能够处理这些系统类型所包含的所有功能。真正取决于您的团队去承担这本书中讨论的其余概念,并在构建系统时将它们作为工具箱中的另一个工具使用。让我们简要地讨论一下这六种主要的企业级系统类型及其主要用途。
客户关系管理
CRM 系统通常可以被视为潜在客户管理、市场营销沟通、销售和库存控制、零售管理以及更多。这些系统可以被视为除了实际销售产品和服务的所有事物。它们甚至可以进一步包括客户支持和数据分析。这些系统旨在与您的商业伙伴、客户、潜在客户以及方程中的任何其他人建立更好的关系。
企业内容管理
一个 ECM 系统最好被描述为为在处理创意和其他知识产权的领域中工作的人提供的一个系统。今天的报纸、杂志和其他新闻公司每天都会在互联网上提供大量内容。一个内容管理系统(CMS)提供了一种更不技术化和快速的方式来构建新页面并将内容上传到网络。企业级只是意味着您正在为您的组织添加越来越多的工具到工具箱中。
企业资产管理
在一个 EAM 系统中,各种产品和库存可以像 CMS 一样输入到系统中。这通常被称为电子商务或购物车网站。这是您使您的实体商品和资产在网上可用的地方。这些系统允许跟踪库存、项目管理以及文档控制,如合同和其他法律文件。这些系统甚至可能包括物理资产,如房地产、汽车和音乐唱片。
企业资源规划
企业资源规划(ERP)系统通常被认为是管理公司员工,即通常所说的 人力资源(HR)部门的一种方式。该系统可以处理入职和离职程序,并存储所有人员记录。它可以作为项目管理、风险评估和记录保存的工具。它甚至可以作为一个知识库,例如以 常见问题解答(FAQs)区域的形式。知识库通常用于引导人们找到常见的问题和答案,以减轻员工的工作负担。这些也可以用于培训目的,例如生成测验或提出趣味问题,并播放教程。
供应链管理
供应链管理(SCM)系统类似于客户关系管理(CRM)和企业资产管理(EAM)系统。这些系统管理供应链中其开发的各个方面。这些系统在国家级甚至全球范围内管理库存。它们与供应商沟通,连接到买家,跟踪包裹,预测未来的供应订单,甚至可以自动下订单。看待 CRM 和 SCM 系统之间差异的一种方式是,CRM 主要用于销售和营销,而 SCM 主要用于生产和分销。两者都涉及产品,但以不同的方式,大型公司或企业集团都需要两者。
企业信息系统
企业信息系统(EIS)是一种通常将客户关系管理(CRM)和供应链管理(SCM)系统结合起来以处理更广泛业务需求的系统。EIS 甚至可能集成 ERP 系统的一部分或全部,并作为一个巨大的中枢神经系统。根据需求,这可能会包括多个数据库甚至多个开发项目,共同工作,构成了整个系统的“大脑”。这些系统以其存储和处理大量数据以及将许多不同的系统连接在一起形成一个整体而闻名。
现在,我们将探讨为什么人们在构建这些企业级系统时倾向于使用 Python 和 Django。我们将看到为什么它适合各种项目,以及它最著名的特性是什么。
为什么选择 Python/Django?
Python 语言是在 1991 年由吉多·范罗苏姆(Guido van Rossum)创建的。他在阅读了 《蒙提·派森飞行马戏团》 的剧本后想出了 Python 这个名字。这种语言主要是为了代码的可读性和可靠性而创建的。Python 作为首选的后端框架已经越来越受欢迎好几年了。Python 在今天的一些非常流行的网站上扮演着重要角色,例如 Google、NASA、Netflix、Spotify、Uber 和 Dropbox,仅举几例。Python 语言因其易用性、学习速度快以及代码的整体适应性,受到了许多开发者的青睐。
Django 于 2003 年底在堪萨斯州劳伦斯市的《劳伦斯日报-世界报》出版商的推动下诞生。与 Python 类似,它有一个目标:以简单易读的方式创建复杂的数据库驱动网站。除了可读性,它还考虑了快速开发和不要重复自己(DRY)的代码结构。可读性是选择 Python 作为 Django 框架基础的原因。由于 Django 在构建和管理数据库方面的作用,它最好被称为后端框架。Django 内置了许多关于网站架构的概念,这使得它对大多数 Web 开发者都有吸引力。
今天,许多人使用 Django 作为他们的后端;但这个框架也可以作为您的前端。如果您曾经使用过 Shopify 的 Liquid 模板语言,也称为 Liquid 语法,或者甚至 ASP.NET 的 Razor 语法,以及它是如何与 C# .NET 或 Visual Basic .NET 结合使用的,您会注意到与Django 模板语言的相似之处,这是 Django 使用 Python 构建 HTML 文件的方法。Django 在页面渲染时使用上下文来提供用户生成的内容或数据库驱动的数据到页面上。模板语言语法相对容易阅读且易于学习。一些使用 Django 的网站示例包括 Bitbucket、Instagram、国家地理、Spotify、《华盛顿邮报》和 YouTube。
Django 的特性包括以下内容:
-
缓存框架
-
数据序列化
-
可扩展性
-
表单处理和验证
-
可伸缩性
-
安全性
-
模板语言
-
测试框架
在这些特性中,可扩展性意味着 Django 被构建成允许轻松集成其他第三方服务。您的开发者可以花更多的时间专注于项目的具体需求,而不是试图弄清楚如何将项目连接到第三方服务提供商的系统。Django 使得连接到今天几乎任何存在的东西变得容易。如果一个功能还没有内置,通常有一个 pip 包可以提供。
Django 不需要仅用于构建网站。它可以用来构建一个作为任何事物中心枢纽的 API,与 表示状态转移(REST)框架进行通信,这是一个在互联网上计算机系统间标准化通信的常见架构。从这里,你可以使用各种常见的现有工具来构建适用于 iPhone 的 iOS 应用或 Android 应用。根据你特定的业务需求,决定利用智能手机可以以多种方式带来好处。如今,大约有 85% 的美国人拥有智能手机(www.pewresearch.org/internet/fact-sheet/mobile/),一些公司可能希望构建一个用户可以在他们的个人手机上安装的应用程序,而不是公司需要购买额外的硬件来分发给所有员工。API 还可以用于许多其他事情,从微服务到大规模的外部网络系统和数据库。
个人而言,当涉及到同时使用 Django 模板语言和基于 JavaScript 的框架,如 React 或 Angular 时,我建议不要这样做,因为这可能会变得过于复杂而难以处理。如果你使用 React、Angular、Vue.js 或 Handlebars.js 等作为你的前端,那么 Django 就只是作为你的后端,通过你的 API 提供上下文。你仍然可以将你的前端和后端文件放在同一个 Django 项目文件夹中。
我有一些推荐可以帮助你选择。如果你想构建 Android 或 iOS 应用或带有 jQuery 的 .html 文件,我将在 第四章 “URLs、视图和模板”中讨论如何做。许多人更喜欢使用纯 JavaScript,这样他们就可以编写简单的任务而无需加载整个 jQuery 库。当涉及到在事物的前端添加装饰时,在我所参与的所有项目中,我发现我几乎可以用 Django 模板语言和 jQuery 完成我需要做的所有事情。我可能在这方面有所偏见,但使用它确实非常简单。然而,使用 ECMAScript 的好处很多,实际上它是一套用于构建和编译 JavaScript 的国际标准,如 ES8、ES9 或 ES10。在项目中更多地关注使用 ECMAScript 是我应该关注的。
小贴士
当使用 Django 和 Python 进行工作时,请要求您的开发人员采用 Python 增强提案 8 (PEP-8) 风格指南来编写 Python 代码。这是一份官方采用的格式化指南,用于保持开发者之间代码的一致性。这可以是严格的或宽松的。我个人喜欢选择一个更宽松的版本,这样可以使事物更加细化且易于阅读。如果您正在构建专有产品,用于内部使用,则不一定需要完全遵循此指南,您可能更喜欢选择一个团队会喜欢的风格。如果您正在构建一个将公开与任何人共享的包,我建议您严格遵循 PEP-8 格式。实际上,这可能是在 Python 包索引 (PyPI) 库中提交包的要求之一。您可以在以下链接中了解更多关于官方风格指南的信息:www.python.org/dev/peps/pep-0008/。
PyPI 是第三方 Python 包库的官方仓库,无论是公开的还是私有的。当寻找可以尝试的新包时,它是一个极好的资源,并且还提供了如何与代码一起工作的信息:pypi.org/。
一个名为 Black 的包可以用作代码检查器,帮助开发者确保他们遵循 PEP-8 风格格式。您可以在以下链接中了解更多关于 Black 的信息:pypi.org/project/black/。
接下来,让我们探讨不同类型的 应用程序编程接口 (API) 是什么,以及为什么在规划项目时我们可能需要了解这些信息。
API 类型
API 是两个系统通过所谓的端点或 URL 进行相互通信的一种方式。
我们可以将 API 分为三个主要类别:

图 1.1 – API 类型
在接下来的章节中,我们将讨论每种 API 类型是什么,以及它们用于什么。根据项目的需求,我们可能需要选择其中一种作为我们想要构建的系统类型。
开放 API
来自和前往您系统的 GET、POST、PUT、PATCH 和 DELETE 请求。
合作 API
合作伙伴 API通常在商业对商业关系中找到。公众无法获得访问权限,只有需要使用你的 API 与你进行业务往来的战略合作伙伴才会被授予许可。可以根据与每一方的协议定义限制。在当今世界,当发生企业合并并且你的团队被要求使两个外部系统相互通信时,这些相当常见。有时,你可能需要出于各种原因(例如,你的商业模式基于授予公司访问你的 API 以便在平台上销售商品)在两个现有系统之间建立一个中央数据库。一个常见的例子是亚马逊的销售合作伙伴 API(SP-API),它用于在亚马逊市场上销售商品。
私有 API
私有 API是最安全的;这些 API 被锁定,因为它们仅打算由该公司或组织内部使用。大型金融机构或企业零售实体可能使用这些 API 来管理其内部功能的任何方面。除非有特定的需求,否则公众和其他外部来源可能无法获得访问权限。常见的例子是政府机构使用 API 连接到保存法律记录和文件的系统。另一个例子可能是大学授予教育部门访问学生和课程记录的权限。
到现在为止,我们已经了解了企业级软件是什么,包括目前存在哪些类型的企业级系统以及我们如何对它们进行分类。我们还讨论了 Python 和 Django 在企业级软件中的作用。现在我们已经学习了这些各种概念,让我们开始设计和规划我们自己的项目。
设计和规划
任何项目,无论大小,都需要一个清晰的计划来明确其目标和构建方式。项目越大,在开发准备阶段投入的工作就越多。企业级开发在真正开始之前也需要大量的前期工作,这并不奇怪。无论你是为公司工作还是作为公司向客户提供解决方案,都应该有一个明确的行动计划。这取决于诸如成本、开发者短缺和截止日期等因素,有很大的灵活性。尽可能做好准备,并尽量遵守设定的时间表,以确保你能够按计划完成。记住,规划不足可能会在以后给你带来麻烦。
开发者应该尽可能多地获得信息,以帮助他们理解他们正在构建的内容。如果你的开发者没有提供足够的文档、蓝图和其他材料,那么他们就会自己做出假设,这些假设后来在开发和质量保证(QA)阶段的测试中作为应用程序中的错误被发现。当这种情况发生时,你可能会发现某个特定功能需要重新构建,这需要重大的基础性变更,这将花费大量时间进行重构。如果我们暂时不考虑编程以外的其他事情,比如建造房屋,我们都知道在团队可以建造房屋框架之前,需要先建造地基。相应地,这个地基需要完成,然后团队才能建造屋顶,安装电线,安装管道等等。没有框架,你不能开始建造屋顶,没有可以放置框架的地基,你不能建造框架。
让我们讨论如何收集你们项目的需求。
需求收集
收集需求对于帮助记录构建过程以及允许双方,即软件开发者和软件所有者,在开发过程中随时参考它来说非常重要。这种能力对于确保事情按计划进行直到完成,并且按时完成也是必不可少的。应该有一个初步的头脑风暴阶段,以了解项目的范围。一个很好的技巧是让所有利益相关者聚集在一起,辩论系统的需求,同时记录辩论中提出的任何关键点,并将它们纳入你的需求发现中。你应该始终从提问开始,并且你应该为不同的人群提出不同的问题。在与利益相关者交谈之后,继续与总监、项目经理、开发人员和员工,即最终用户交谈。尽可能多地采访不同类型的用户。对于极其庞大的实体,你可以创建一份问卷,将问卷分发给许多用户,然后根据结果得出结论。
如果目前有一个遗留系统正在运行,即使它是一种手动与数字流程相结合的过程,你也应该尝试了解它是如何工作的,并确定该流程中的任何痛点。我最喜欢的策略是用户观察,即我观察用户完成日常任务的流程,然后尝试识别可能使他们减速的事情。接下来,我会尝试角色扮演,这是一种你跳入并像用户一样执行任务的策略。你也可以让两种不同类型的用户交换位置,询问他们完成对方任务容易或困难的地方,或者他们认为可以改进工作流程的地方。肯定存在某种类型的瓶颈,否则就没有必要构建更好的东西。这些都是拖慢你日常任务的事情,最终导致公司在时间和资源上花费大量金钱。你需要保持警觉,并识别出你的客户自己无法识别的痛点,或者他们将与你的沟通困难。你的客户不一定知道问题的最佳解决方案,他们甚至可能不知道某个特定问题是一个问题,直到你揭示了一种改善该流程的方法。
研究和发现
查找这将是一个内部、合作伙伴还是公开项目。如果是合作伙伴项目,你可能会有基于合作伙伴特定需求的某些限制。这时,我们就进入了业务需求和功能需求之间的差异。功能需求包括确定要居住在哪个托管计划和服务器的服务器,后端框架,前端框架和一组页面。另一方面,业务需求涵盖了整个企业的愿景、目标和目标。它们是特定于组织或他们与之合作的合作伙伴的需求。一组网页或 API 端点的结构可能是由该公司的商业模式定义的,而不是其他可能被选择的某些逻辑原因。你的利益相关者是否有任何目标、建议或请求需要你考虑?以下是一些可以帮助你制定问题集的常见问题。你不应该仅限于这些问题本身;用于头脑风暴:
-
你是否有特定的业务需求?
-
你为什么需要一个新系统?
-
你的当前系统阻止了你做什么;是否存在任何瓶颈?
-
你想添加哪些新功能;你需要任何改进吗?
-
你想保留哪些旧功能,或者要移除哪些?
-
你将在系统中与谁互动;有哪些类型的用户和角色?
-
你需要报告、电子邮件消息或其他类型的通知系统吗?
-
系统将以任何方式与任何第三方或合作伙伴系统连接吗?
-
我们预测的服务器上的流量或负载是什么样的?
-
这个新系统何时需要投入运行?
-
您为完成此项目分配了多少预算?
-
是否需要将数据从旧系统迁移到新系统?
-
开发团队成员之间的发展如何分配?
-
开发团队有哪些技能;团队的优势和劣势是什么?
-
用户界面(UI)的流程应该如何工作;应该是多页还是单页?
决策
从之前列出的常见问题中,您可以制定数十个甚至数百个其他问题来满足您独特的需求集。然后,这些问题可以分组到几个开发类别中,每个类别都有独特的一套要求和指南。您将非常重视可用性、数据迁移、性能、安全性、可扩展性和可靠性。提前了解这些信息是很好的,这样您就可以选择最佳的方向,让您的开发继续前进。
这些决策可能包括您将选择哪个前端框架,您将向谁寻求托管服务,您是构建自己的服务器还是在云数据中心租用空间,以及您的服务器将如何配置。有无数的可能性需要考虑。当涉及到 UI 时,有许多关于表单字段布局、表单字段验证(服务器端、客户端或两者都进行)、占位符、标签位置以及从开始到结束的流程的问题需要询问。这里的流程指的是用户在继续之前是否应该完成部分或全部表单,以及这些部分是否应该包含在单独的表单中。
请记住,当涉及到表单字段验证时,Django 只会在服务器端验证您的数据。您的项目不需要同时进行服务器端和客户端的表单字段验证。然而,一个健康的网站将实施两者。因此,当发生回发错误时,会出现异常,例如,如果您的表单在页面首次加载时不可见,例如,当用户需要向下滚动很远或执行几个操作才能使表单对用户可见时,您的表单可能不会显示或出现字段错误。客户端表单字段验证通过在将数据发送到服务器之前检查数据是否有效来解决此问题,这被称为数据完整性。这种验证还可以减少服务器处理的工作量,并提供了事件处理,让您可以编写函数来帮助您在页面上格式化 HTML 和 CSS。在决策方面,您可以选择是否在客户端或服务器端进行字段验证,或者两者都进行。如果将在客户端进行验证,那么您可以选择使用哪些工具,这些工具通常基于 JavaScript。
例如,考虑输入属性,如required、minlength和maxlength,这些可以在你的 HTML 中存在。这些通常是由 Django 在渲染具有特定字段参数的表单字段时生成的,如下面的示例所示。我们将在第五章中详细讨论渲染表单,Django 表单:
# Demo Code
<input type="text" name="field" id="field-id" class="form-input" maxlength="150" minlength="5" required="">
大多数浏览器会默认限制用户提交表单,如果这些属性存在且数据不满足它们的要求。所有浏览器也会以不同的方式处理和样式化这些错误状态,比如 Mac 与 Windows 或 Chrome 与 Safari 之间的差异。这是因为它们是由不同的实体开发的,它们在市场上相互竞争,因此有不同的品牌。这种差异阻碍了 Django 显示你代码中为该字段定义的错误消息的 postback 功能。如果由于某种原因,用户能够提交包含无效数据的表单,那么如果,正如我之前提到的,表单在页面加载时被隐藏,那么 postback 可能不会显示表单。这就是为什么你的项目可能需要客户端和服务器端表单验证的原因。
在你的服务器端代码(你定义表单字段的地方)和客户端实现之间处理错误状态消息可能也很棘手,这意味着你必须在源代码中的两个不同位置存储相同的错误消息:一个用于服务器端验证,一个用于客户端验证。随着时间的推移和许多不同的开发者,这会变得非常混乱,尤其是在有人记得更改其中一个但忘记更改另一个时,这些消息被编辑、添加或删除。如果你有严格的必要性确保它们措辞完全相同,可能需要创建一个数据字典,你的源文件可以访问它,这样你就可以把所有的错误消息放在一个地方。如何做到这一点需要一些思考。这个文件也需要通过 Python 和 JavaScript 都可以访问。
可视化和解释
当涉及到 UI 的实际设计时,涉及的因素有很多。大型企业可能有一些特定的品牌指南,它们出于营销和其他法律原因强制执行,这可能限制了你的前端整体设计。有时,这些公司可能有一个内部创意和营销部门,或者他们可能外包给第三方创意公司,以制作一系列 Illustrator 或 Photoshop 文档,以帮助你的前端开发者完成工作。对于较小的项目,你可以自由地设计一些东西,但这通常需要时间,开发者们在被要求设计某物而不是构建某物时,经常会遇到一种写作障碍。
关于前端开发者的一大误解是,人们普遍认为他们都是设计师,这并不总是事实。就像建筑工人阅读蓝图来建造房屋一样,其他人通常为他们绘制蓝图。因此,您可以使用来自在线供应商(如 Envato Market,原名 ThemeForest (themeforest.net/))或 Nicepage (nicepage.com/html-templates)等处的开源模板和脚手架,这些模板可以是 HTML、CSS/SCSS,甚至是 JavaScript。在我自己的 Django 项目中,我之前也求助于这些来源的设计模板。这些模板和模板提供商各不相同。有些是免费的,而有些则需要付费使用,并且使用许可证各不相同。在决定这些来源中的任何一个是否适合您之前,您需要进行独立研究。这些设计模板可能还需要一些挖掘,以确保它们与您的项目很好地配合,但它们仍然可以节省大量时间,并提供一个看起来很时尚的网站,这可能比那些缺乏从无到有设计创意的人为您创造的更好。
许多这些 HTML、CSS 和 JavaScript 模板可能会使用Node 包管理器(NPM)来将源文件构建成生产就绪文件。类似于 PyPI,NPM 用于存储和分发开发中使用的 JavaScript 库。它们依赖于 Node.js 来运行。甚至还有可以在您的 Django 项目中使用的 pip 包,以帮助您使用 NPM 包构建源文件。我将在第二章,项目配置中进一步讨论管理 pip 包和依赖关系。有许多 Python 包可以帮助您转换 SCSS、自动添加前缀、打包和压缩文件。我尝试过很多不同的 Python 包,最终只发现少数几个在最后阶段会使用 NPM 来完成繁重的工作。这意味着,作为您项目的要求,您的开发人员可能需要在他们的机器上甚至服务器上安装 NPM,具体取决于您如何使用 Node.js。对于本书中的示例,我将尽可能使用 Python 包,您可以根据自己的需要将这些包集成到您的项目中。我会尽量避免涉及 NPM 包的代码示例,但我鼓励您在开发环境中使用这些包。
小贴士
Node.js 和 NPM 的最新和最稳定版本可以在这里找到:nodejs.org/en/download/. 对于 Windows 用户,有一个易于使用的安装文件,可以为您安装 Node.js 和 NPM。
你可以在这里找到 Gulp 安装指南:gulpjs.com/docs/en/getting-started/quick-start/。Gulp 要求首先安装 Gulp 命令行工具(CLI),然后安装 Gulp 本身。Gulp 被认为是一个任务运行器,有助于自动化大多数开发任务,如 SCSS 转译、CSS 代码检查、供应商前缀、压缩和打包;ECMAScript 编译;以及其他代码检查。
设计并不意味着项目应该如何看起来;过程也应该关注它将如何工作,或者更确切地说,是引擎的螺丝和螺母。在设计项目时,尽可能多地使用图表来可视化每个过程。可视化可以分为两大类:行为图表和结构图表。一旦你创建了一套图表,就可以用来与利益相关者协作,确保你拥有所需的一切。你的开发者也会将这些作为他们将要构建的蓝图。
在统一建模语言(UML)中有很多不同的图表类型,如以下所示:
-
活动图表
-
类图表
-
通信图表
-
组件图表
-
组合图表
-
部署图表
-
实体关系图表
-
流程图
-
交互图表
-
对象图表
-
包图表
-
配置图表
-
序列图表
-
状态图表
-
时间图表
-
用例图表
深入讨论这些图表可能会相当冗长。在以下小节中,我们将仅讨论目前最常用的六种图表以及它们如何帮助你构建任何规模和类型的项目。
类图表
类图表用于说明系统中的不同类或组件以及它们之间的关系。类最著名的是在系统中具有共享或相似角色的对象集合。类似于实体关系图(ERD),类图表描绘了在数据库中将是表的对象,可能发生的交互,以及系统中的任何其他主要元素。此图表通常以这样的方式构建,即顶部部分是类名,中间部分包含所有属性,也称为字段,底部部分显示可能发生的任何函数或操作。
下图显示了用户、团队和奖项之间的关系。类图表显示了团队可以拥有用户集合,一个团队也可能拥有授予他们的奖项集合。在这个例子中,奖项是授予团队的,而不是授予个人用户。Team模型对象可以有一个名为getAwards()的函数,它将获取团队所赢得所有奖项的集合:

图 1.2 – 类图表
部署图表
部署图用于高级规划。这是开发者之间如何协作以及代码将在不同环境之间更新的方式。网络工程师将使用该图来绘制其配置中将使用的物理节点。开发者将使用它来更好地理解代码如何在不同的环境之间更新,以及当需要更新时,他们可能需要将代码推送到或从何处拉取。部署图的主要组件包括工件、设备和节点:
-
工件是一种数字资产,如文件或某种可执行脚本的类型。
-
设备是一个代表计算资源(如应用服务器或域名服务器)的节点。
-
节点是一个执行组件、子系统或过程的物理实体。节点可以包括物理硬件组件或基于虚拟云的组件。
实体关系图
ERD 可视化系统内对象之间的关系。它最适合映射数据库中不同表之间的链接,有时在数据库中建模关系后被称为实体关系模型。这些由您的后端用于帮助创建数据库的结构以及应包含哪些字段。另一方面,这些可以通过访问现有数据库来创建,以帮助映射系统的当前结构,并帮助您看到如何最好地重建它。这就像访问现有建筑的蓝图。自动生成这些可以意味着它们非常准确,甚至可以告诉您在那些初始蓝图首次起草之后对那座建筑进行的翻新。
自动生成实体关系图(ERD)有许多方法。当您阅读并跟随本章内容时,我将与您分享我最喜欢的两种方法,假设您已经有一个 Django 项目正在运行。如果没有,我将在下一章解释如何从头开始创建 Django 项目,以及如何安装 pgAdmin 工具,第二章,项目配置。第一种方法是使用 pgAdmin,这是 Django 开发者今天用于与 PostgreSQL 数据库一起工作的流行数据库工具。如果您使用的是 pgAdmin 的最新版本,这将非常简单;旧版本没有这个功能。截至本书编写时,pgAdmin 的当前版本是 v5.6。只需右键单击您想要生成图例的数据库,然后点击生成 ERD。

图 1.3 – 使用 pgAdmin 生成 ERD
第二种方法是使用一个流行的命令行工具,名为 django-extensions。这是一个例子,我为已经熟悉 Django 的人提供了一个名为 chapter_1 的 Django 应用程序,并附上了本书提供的代码。你可以在现有的项目中运行这些示例。对于 Django 新手,建议跳到本章的下一小节,标题为 流程图,然后在完成 第二章 中提供的示例后,再回来练习使用这个包来首次配置你的项目。
要在现有的 Django 项目上安装 django-extensions 包,请按照以下步骤操作:
-
运行以下命令:
PS C:\Your_Project_Folder> pip install django-extensions -
在你的
settings.py文件中,将此应用程序添加到你的INSTALLED_APPS变量中:# /becoming_a_django_entdev/settings.py INSTALLED_APPS = ( ... 'django_extensions', ... ) -
你还需要安装一个图表生成器,它会为你绘制图表。这是通过
pydotpluspip 包完成的:PS C:\Your_Project_Folder> pip install pydotplus -
现在,你可以运行以下命令来生成所有表格:
PS C:\Your_Project_Folder> python manage.py graph_models -a -o diagrams/chapter_1/all_models.png
可以针对特定的模型,或者可以针对一组模型,通过逗号分隔,没有空格。在以下示例中,我们针对的是 User、Team 和 Award 模型:
PS C:\Your_Project_Folder> python manage.py graph_models -a -I User,Team,Award -o diagrams/chapter_1/team_models.png
以下图表是通过运行最后一个命令自动生成的,生成了 User、Team 和 Award 模型及其相互关系:

图 1.4 – 来自图模型的 ERD
要详细了解如何使用 Graph Models 插件,请访问 django-extensions.readthedocs.io/en/latest/graph_models.html。
注意
对于 Windows 用户,你还需要在你的计算机上安装 GraphViz 应用程序,以便 graph_models 命令能够工作。在安装过程中,请选择 将 Graphviz 添加到系统 PATH 以供所有或当前用户使用:graphviz.org/download/。
还有适用于 Linux、Mac 和 Solaris 操作系统的安装程序。在这些操作系统上生成这些图表可能也需要 GraphViz。如果你在 Linux、Mac 或 Solaris 上运行前面的命令时遇到错误,请尝试在你的系统上安装 GraphViz。
接下来,让我们讨论流程图及其用途。
流程图
流程图表示系统内部的数据流。它们提供了一种逐步解决问题的方法。这些图表被开发者用来理解在编写代码时适用哪些规则。规则可以包括在进入下一步之前的数据验证场景等逻辑。流程图可以是简单的,也可以相当复杂,通常还会提供用户可以在途中做出的决策。这种图表类型描述了用户在特定流程或一系列流程中的可能交互。
如果您有一个单页表单,该表单被分成多个部分,用户必须完成这些部分才能导航到新页面,那么流程图可以是前端开发者或甚至设计师理解用户如何在表单中从一步到另一步,以及如何在网站的更大流程中从一个页面到另一个页面的有用工具。根据需要,这些图可以分解成更小、更细粒度或更明确的流程。
以下图是一个小示例,展示了用户登录系统的流程。页面是否需要用户登录?如果是,必须做出决定:用户是否已登录?如果没有,提示登录界面;如果是,则必须做出新的决定:用户是否有权限查看页面?

图 1.5 – 流程图
状态图
状态图显示了系统内对象的行为。该图显示了某个特定时间点可能的状态或条件,例如,用户是登录还是注销;订单是已接收、正在处理还是待发货,或者订单是已履行或已退货。这些图非常适合展示特定行为的变化与可做出的决策。它们甚至可能描绘出某些触发器,这些触发器会在操作通过生命周期向完成移动时导致状态变化。
用例图
用例图表示用户和系统的行为。这些图与流程图相似,但通常更关注整体情况。这些图被创意部门、利益相关者、项目经理和导演等团队使用,以帮助理解特定流程或用例将执行的概念。

图 1.6 – 用例图
到目前为止,我们已经介绍了今天人们规划和管理网络应用的一些常见方法。使用这些概念尽可能好地规划您的项目。在下一节中,我们将讨论托管您的应用程序以及不同的部署方法。
域名托管和部署
所有网站和应用程序都需要一个物理位置来存放所有文件;这也被称为域名托管。域名托管是一种服务,它为文件提供物理硬件,并处理信息。托管计划提供由操作系统(如 Linux)组成的解决方案,并包括网络系统的实际硬件。服务器将为您安装某种类型的网络服务器,例如 NGINX 或 Apache 网络服务器。
网络服务器,也称为 超文本传输协议 (HTTP) 服务器,是一种软件,可以发送和接收 HTTP 请求,这些请求基本上是通过互联网发送的消息。网络服务器可以被视为在操作系统上使用 HTTP 运行的软件,HTTP 是一种用于在互联网上分发您的网站或应用程序的标准网络协议。随着今天所有浏览器使用的 HTTP,用户可以通过在浏览器的地址栏中导航到您的域名来访问您的网站。网络服务器可以帮助您进行负载均衡和缓存,并作为您的反向代理服务器,使您的文件通过互联网对全世界可用。托管服务通常会为您提供选项,在您的网站经历用户流量和数据访问增加时进行扩展,此时您需要更多的处理能力或存储空间。
大型项目往往会倾向于使用像 Amazon Web Services (AWS) 或 Microsoft Azure 以及甚至 Heroku 这样的服务。这些是云基础托管服务,意味着您的网站文件将存储在物理硬件上,这些硬件可能与其他客户共享。然而,从这些提供商中的任何一个都可以以一定的价格获得拥有专用且更安全的服务器空间的选择。托管计划还提供了高级选项,使您能够安装和配置自己的操作系统和/或自己的网络服务器。当与 Django 项目一起工作时,NGINX 是首选选择,因为网络服务器以其比 Apache 安装表现更好的声誉,并且每秒可以处理更多的 HTTP 请求。当考虑到性能时,许多 Django 项目使用 NGINX 作为其网络服务器并不令人惊讶。如果您确实需要配置自己的网络服务器,可以从官方 NGINX 安装手册开始,该手册可在以下位置找到:www.nginx.com/resources/wiki/start/topics/tutorials/install/。Apache 网络服务器也可以与 Django 一起使用,并且是可取的,因为它比 NGINX 安装更容易安装。如果您需要走 Apache 的路线,请从阅读官方 Django 文档开始,该文档介绍了如何与 Apache 网络服务器一起工作,可在以下位置找到:docs.djangoproject.com/en/4.0/howto/deployment/wsgi/modwsgi/。
创建和配置 Heroku 计划
由于本书提供的示例,我将使用 Heroku 提供的免费计划,因为该服务的易用性、零成本功能和日益增长的受欢迎程度。Heroku 被称为平台即服务(PaaS),这意味着它使开发者能够在云中完全构建、运行和部署网站和应用。使用此选项,您可以在几分钟内启动并运行,这可以减少雇佣网络工程师团队为您运行系统的成本。为了跟随这个练习,请访问 Heroku 网站,并在www.heroku.com/上注册一个免费账户。然后,一旦您验证了您的电子邮件地址并登录到您的账户,导航到您的仪表板dashboard.heroku.com/apps,然后点击新建 | 创建新应用,然后填写页面,如图所示:
![Figure 1.7 – 创建新的 Heroku 应用
![img/Figure_1.07_B17243.jpg]
图 1.7 – 创建新的 Heroku 应用
为此应用输入一个名称。名称仅用于内部使用,不需要非常精确。请注意,由于应用名称必须在所有 Heroku 应用中是唯一的,因此您需要选择与演示中提供的名称becoming-an-entdev不同的名称。另外,目前不必担心流水线。如果我们需要或想要的话,我们可以在本章的高级部署子节中稍后讨论。如果您不在美国,可能需要将您的区域更改为更靠近您的区域。
由于我们正在构建一个 Django 项目,我们需要选择 Python 构建包。构建包是 Heroku 使用脚本来自动在 Heroku 上构建和编译各种类型应用的方式。从您刚刚创建的 Heroku 应用的仪表板向下滚动到构建包部分,并选择添加构建包。以下截图展示了接下来应该出现的弹出窗口:
![Figure 1.8 – Heroku 构建包选择
![img/Figure_1.08_B17243.jpg]
图 1.8 – Heroku 构建包选择
您可以添加任何与项目需求相关的构建包,但对于与本书相关的内容,python是唯一需要的构建包。如果您选择其他构建包,可能需要额外的配置步骤,这些步骤在本书中未提供,因此请谨慎使用。
接下来,我们将讨论环境,在这个上下文中,环境指的是测试和开发阶段,我们将展示代码在不同开发阶段的运行情况。环境也可以用于其他上下文,例如网络服务器或操作系统。在编程的世界里,“环境”可能有多种含义,通常指的是你正在使用、在或其中的某种配置、设置或结构。
配置 Heroku 环境
在其最基本的形式中,一个 Heroku 应用程序将至少包含两个环境,第一个是生产环境,您的网站或应用程序将在这里被公众和用户访问,第二个是您的本地机器,这是您和您的开发团队进行所有编码以及本地运行网站的地方。当创建应用程序时,Heroku 将默认使用 Heroku Git,它使用 Heroku CLI 将您本地机器上的更改提交到生产环境。在您的 Heroku 账户仪表板 dashboard.heroku.com/apps 上,点击 部署 选项卡,您将看到以下截图所示的选项:

图 1.9 – Heroku 部署方法
使用 Heroku CLI
您需要在 Mac、Windows 或 Linux 上安装 Heroku CLI,通过在此处下载适当的安装程序来完成:devcenter.heroku.com/articles/heroku-cli#download-and-install。
对于 Windows 用户,当您在安装过程中被提示时,请记住选择标记为 将 PATH 设置为 heroku 的复选框。
接下来,导航到您想在本地机器上存放项目的文件夹,然后在那个目录内打开一个新的终端或命令行窗口。您需要使用以下命令登录 Heroku:
PS C:\Projects\Packt\Repo> heroku login
请记住,每次您打开一个新的终端或命令行窗口并执行任务时,都需要登录。它将提示您打开一个新的浏览器标签页进行登录。一旦完成,您应该会看到如下消息:
heroku: Press any key to open up the browser to login or q to exit:
Opening browser to https://cli-auth.heroku.com/auth/cli/browser/03be4a46-28f4-479a-bc10-8bd1bdcdd12b?requestor={{ ... }}
Logging in... done
Logged in as {{ youremail@yourdomain.com }}
PS C:\Projects\Packt\Repo>
如果您看到前面的消息,那么您已成功登录您的账户,可以开始使用 Heroku 命令。
小贴士
在 Windows 中,以管理员身份运行 PowerShell 的简单方法是在文件资源管理器窗口中导航到您希望在其中运行命令的文件夹,然后点击 文件 | 打开 Windows PowerShell | 以管理员身份打开 Windows PowerShell。这将在此目录中启动命令行,减少了通过输入一系列更改目录命令来导航到该目录的步骤。
点击 图 1.10 中的选项将在以下目录中打开命令行:
PS C:\Projects\Packt\Repo>

图 1.10 – 以管理员身份打开 Windows PowerShell
接下来,让我们首次初始化我们的本地 Git 仓库。
初始化 Git 仓库并提交更改
要说您正在初始化本地机器上的 Git 仓库,这意味着您正在选择目录中生成一个 .git 文件夹。.git 文件夹中的配置文件负责在您的本地仓库和远程仓库之间建立通信线路。我们将要链接的远程仓库是 Heroku 应用的位置。
按照以下步骤配置您的 Git 设置:
-
执行以下命令,首先登录您的 Heroku 账户,然后初始化一个本地 Git 仓库,将您的本地仓库与 Heroku 应用程序链接起来:
PS C:\Projects\Packt\Repo> heroku login PS C:\Projects\Packt\Repo> git init PS C:\Projects\Packt\Repo> heroku git:remote -a {{ your_project_name }}
此目录是您的 Django 项目源代码文件所在的位置。在 第二章 项目配置 中,我们将解释如何创建您的第一个 Django 项目。现在,我们只需要执行我们的第一个提交,以便与每个远程环境建立适当的通信线路。为了 Heroku 能够接受提交,Heroku 系统需要检测到它是一个有效的应用程序正在被提交。某些文件必须存在才能通过此测试。第一个文件不一定必需,但我建议您仍然包含它;这是一个使用 Markdown 语言 的 README.md 文件,Markdown 语言是一种轻量级的 标记语言,用于存储诸如构建过程或开发者如何首次启动的说明等信息。它通常用于网页上,以便快速轻松地格式化纯文本,以保留今天可以通过许多不同的基于网页的文本编辑器查看的重要笔记。当此文件在网页浏览器中查看时,它将以易于阅读的格式进行查看、格式化和样式化。
-
要这样做,请从本地仓库的根目录运行以下
touch命令创建一个文件:PS C:\Projects\Packt\Repo> touch README.md
要了解如何使用 Markdown 语言来格式化您的 README 文档的完整指南,请访问 guides.github.com/features/mastering-markdown/。
小贴士
如果您尚未安装,Windows 用户可能需要通过 NPM 安装 touch-cli。您需要在使用前面的 touch 命令之前完成此操作。或者,您可以在文件资源管理器中右键单击并选择 新建 | 文本文档,使用鼠标代替。
要安装 touch-cli,请使用 -g 参数运行以下命令,表示这是一个全局包,用于您的开发机器,而不是仅限于本项目:
PS C:\Projects\Packt\Repo> npm install touch-cli -g
- 请将您喜欢的内容添加到 README 文件中,并准备好将您的第一个提交到 Heroku Git 仓库。
我们还需要创建一个额外的文件,以便在 Heroku 上成功提交,那就是一个requirements.txt文件。否则,当 Heroku 看到这个文件不存在时,会给出错误。错误信息将显示为“在您的仓库根目录中不存在requirements.txt文件”。如果您愿意,这些文件现在都可以保持空白,但requirements.txt文件至少必须存在。
-
运行以下命令从本地仓库的根目录创建您的
requirements.txt文件:PS C:\Projects\Packt\Repo> touch requirements.txt -
要执行您的提交,只需运行以下命令:
PS C:\Projects\Packt\Repo> git add . PS C:\Projects\Packt\Repo> git commit -am "Created Blank README and Requirements Files" PS C:\Projects\Packt\Repo> git push heroku main
-am选项执行将所有被跟踪的已修改文件暂存的操作,并允许我们同时添加一个个人提交信息。
接下来,我们将克隆现有的 Heroku 仓库。
克隆现有的 Heroku Git 仓库
如果您已经完成了前一小节中的步骤,即您已经有了项目的现有仓库,那么现在您需要另一位开发者克隆其副本以开始工作。接下来需要两个命令。如果您还没有登录,请不要忘记先登录:
PS C:\Projects\Packt\Repo> heroku login
PS C:\Projects\Packt\Repo> git init
PS C:\Projects\Packt\Repo> heroku git:remote -a {{ your_project_name }}
然后,运行以下命令来克隆仓库:
PS C:\Projects\Packt\Repo> heroku git:clone -a {{ your_project_name }}
要运行任何标准 Git 命令,例如push或pull,请使用以下命令,根据需要将pull更改为push:
PS C:\Projects\Packt\Repo> git pull heroku main
在 Heroku 中管理环境
通常来说,只用两个基本环境(标准生产和本地环境)来管理大型项目是不明智的。在您的软件开发生命周期(SDLC)中拥有许多环境的目的,是为了在交付时尽可能减少提供给客户的应用程序中的错误。利用这些环境在过程中尽可能过滤掉尽可能多的错误。每个从开始到结束测试应用程序的环境或团队都充当一个过滤器,沿途清除不同的问题。
您可以在您的部署策略中实施尽可能多或尽可能少的 环境,根据您的需要。大多数项目除了前两个基线环境(生产环境和本地环境)外,至少还包括一个开发和测试环境。开发环境将由您的开发者使用,作为他们自己的测试方式,只是为了第一次在其他计算机上运行他们的项目,看看项目会做什么。在这里可以发现常见的构建问题,否则这些问题只会浪费团队测试数据和流程错误的时间。然后,当开发者对开发环境中的代码运行满意时,它可以被推送到测试环境。在这里,不同的测试团队可以审查应用程序,通过流程并寻找故意破坏系统的方法。然后,当他们满意时,代码可以被推送到您的生产环境。理论上,这个环境中不应该存在任何错误;然而,这不是一个完美的世界,所以我们只想确保当项目进入生产环境时,错误尽可能少。
打开你的终端或命令行窗口,导航到你的本地机器上仓库的目录。你可以运行以下代码块中显示的两个命令,为你的应用创建开发和预发布环境。如果你还没有这样做,请确保你已经登录到你的 Heroku 账户:
PS C:\Projects\Packt\Repo> heroku login
PS C:\Projects\Packt\Repo> heroku create --remote development
PS C:\Projects\Packt\Repo> heroku create --remote staging
你应该会看到以下消息,表明操作成功:
Creating app... done, pure-atoll-19670
https://pure-atoll-19670.herokuapp.com/ | https://git.heroku.com/pure-atoll-19670.git
在这个过程中,你基本上是在创建一个新的 Heroku 应用,这个环境将生活在这个应用中。这会自动为你完成。
现在,我们可以使用之前创建的两个文件,README.md和requirements.txt,这些文件应该仍然在你的文件夹中,并将它们推送到开发和预发布环境。在这个 SDLC(软件开发生命周期)的阶段,我们可以确信所有三个环境都是完全相同的。我们通过执行以下两个命令,将我们的代码推送到这些环境来确保这一点:
PS C:\Projects\Packt\Repo> git push development main
PS C:\Projects\Packt\Repo> git push staging main
接下来,让我们讨论使用自定义第三方仓库,例如 GitHub。
自定义仓库
你不仅限于使用 Heroku 来存储你的文件;你欢迎使用基于云的仓库托管服务,例如 GitHub 或 Bitbucket,来存储它们。有许多原因决定使用自定义仓库,除了 Heroku 提供的仓库位置之外。许多人只是喜欢保持事物有序,因为他们已经在另一个仓库中有一个账户,他们可能希望将所有项目放在一起。如果你决定使用 GitHub 来存储你的项目文件,你将选择GitHub | 连接到 GitHub按钮,如图图 1.9所示,该图在本书的配置 Heroku 环境子节中较早出现。请记住,如果你想创建额外的环境,你应该首先完成本节之前的所有步骤。每个环境都将链接到 GitHub 账户中该仓库的 Git 远程。
当你选择通过 GitHub 连接时,你的浏览器将弹出一个窗口,要求你登录你的 GitHub 账户。在这个例子中,从我自己的 GitHub 账户中,我创建了一个名为becoming-an-entdev的私有仓库。在你的 Heroku 账户中,通过搜索你创建的仓库名称,将你的 GitHub 仓库链接到这个 Heroku 应用。点击连接,如果一切顺利,你应该会看到这个部分发生变化,显示一条消息已连接到您的仓库位置。

图 1.11 – 将外部 GitHub 仓库链接到 Heroku
自动部署
接下来,你可以选择在检测到链接到你的 Heroku 应用的仓库的特定分支有更改时启用自动部署。在以下部分,在你的 Heroku 仪表板上同一页面上,从以下下拉菜单中选择分支,然后点击启用自动部署:

图 1.12 – 自动部署
有一个选项可以与 持续集成(CI)工具一起工作,这对于大型项目也很有帮助。如果您需要您的 CI 工具在允许部署之前通过测试,请简单地勾选标记为 在部署前等待 CI 通过 的复选框。CI 和交付可能会变得非常复杂,但它们用于自动化所有贡献者工作的集成,例如运行测试脚本或构建过程。Heroku 的 CI 会定期执行此操作,有时一天内会多次执行。
如果您的自动部署已成功链接到您的 GitHub 仓库,您将看到该仓库的新 webhook。webhook 是网络开发中的一个术语,用来描述当某个事件或触发器发生时,向另一个系统上的监听器发送的信号。例如,当您将更改推送到 GitHub 仓库时,会发送一个信号到 Heroku,触发一个脚本自动抓取所有最新更改并将其合并到 Heroku 应用程序中。Webhooks 有时被称为 反向 API,因为它们只是发送信号;没有请求发送信号然后响应,这是任何标准 API 请求的默认行为。您可以在 GitHub 账户中仓库的设置中找到 webhooks。如果您使用的是不同的服务,只需寻找类似 webhook 或反向 API 的东西。

图 1.13 – GitHub Heroku webhook
配置远程仓库
现在,您已经创建了一个 GitHub 仓库并将其链接到您的生产 Heroku 应用程序,您也已经设置了自动部署,您需要在您的本地 Git 仓库中指定您的远程仓库以将您的本地仓库链接到生产环境。首先,通过使用您账户中提供的 Git URL 创建一个链接到您的 GitHub 仓库的 git-production 远程仓库。您可以将其命名为任何您想要的名称。
按照以下步骤配置您的远程仓库:
-
运行以下命令以创建您的远程仓库:
PS C:\Projects\Packt\Repo> git remote add git-production https://github.com/{{ your_username }}/becoming-an-entdev.git
上述命令仅在您已经运行了 git init 命令以首先创建您的本地仓库的情况下才会运行。
-
接下来,为这个远程仓库创建一个
main分支:PS C:\Projects\Packt\Repo> git branch -M main
我将分支命名为 main,以保持与 Heroku 应用程序上的分支的一致性。
-
现在,您可以通过运行以下命令将您的前两个文件推送到这个远程分支。如果您对这些文件进行了新的更改,请记住在推送之前将它们暂存和提交:
PS C:\Projects\Packt\Repo> git push -u git-production main -
现在我们有了在生产环境中工作的主仓库,我们需要包括其他环境,称为开发和预发布。虽然 Heroku 将环境完全分开为不同的应用程序,但你可以在
git-production上创建新的分支并称它们为开发和预发布,或者进入你的 GitHub 账户创建全新的仓库并将它们链接起来。对于这本书,我们将为了演示和实践将它们分开到全新的仓库中。我提前在我的个人 GitHub 账户中创建了两个新的仓库,分别命名为becoming-an-entdev-dev和becoming-an-entdev-staging。 -
我们将使用以下示例命令将它们链接起来:
-
对于开发环境,使用以下命令:
PS C:\Projects\Packt\Repo> git remote add git-development https://github.com/{{ your_username }}/becoming-an-entdev-dev.git PS C:\Projects\Packt\Repo> git branch -M main PS C:\Projects\Packt\Repo> git push -u git-development main -
对于预发布环境,使用以下命令:
PS C:\Projects\Packt\Repo> git remote add git-staging https://github.com/{{ your_username }}/becoming-an-entdev-staging.git PS C:\Projects\Packt\Repo> git branch -M main PS C:\Projects\Packt\Repo> git push -u git-staging main
-
接下来,我们将配置我们的 Git 分支。
配置分支
提供的示例中所有的远程都包含一个名为main的分支,它作为在向前移动时创建的所有子分支和分叉的父分支;有些人也称其为master。对于本地工作,你需要创建设置为跟踪你的远程分支之一的本地分支。在 Git 术语中,跟踪简单意味着一个本地分支被映射到位于某处的远程仓库或分支。
类似于我们之前运行的git branch -M main命令,我们这样做是为了创建一个仅用于远程版本的main分支,在本地上,我们将运行以下命令以向我们的 Git 配置文件中添加一个新的本地分支,这有助于跟踪或将其映射到我们设置的环境:
PS C:\Projects\Packt\Repo> git branch production git-production/main
PS C:\Projects\Packt\Repo> git branch development git-development/main
PS C:\Projects\Packt\Repo> git branch staging git-staging/main
如果你使用 Sourcetree 等图形用户界面与 Git 仓库进行交互,你应该在应用程序的左侧侧边栏中看到如下截图所示的内容:
![Figure 1.14 – Sourcetree 侧边栏 – 分支和远程]
![img/Figure_1.14_B17243.jpg]
图 1.14 – Sourcetree 侧边栏 – 分支和远程
Heroku 为你要工作的应用程序的生产环境提供的默认名称是heroku。如果你展开列表中的所有远程仓库,你会看到它们都有一个main分支,如前一个截图所示,位于名为development的仓库下。现在,从你硬盘上的一个单独文件夹中,你可以导航到并从所有这些环境,也称为你的应用程序的版本,进行所需的工作。
在这里提供了一些简单且有用的命令:
-
使用以下命令切换分支:
PS C:\Projects\Packt\Repo> git checkout development
此命令切换到development远程。
-
使用以下命令暂存所有内容并包含一条提交更改的消息:
PS C:\Projects\Packt\Repo> git commit -am "Added notes to README File" -
使用以下命令指定HEAD位置推送代码:
PS C:\Projects\Packt\Repo> git push development main
如果你尝试运行前面的命令时遇到错误,显示error: src refspec main does not match any,请尝试以下命令:
PS C:\Projects\Packt\Repo> git push development HEAD:main
到目前为止,我们创建的每个远程仓库只包含一个分支。你的团队将需要其他分支,这些分支在创建时大部分会从main分支分叉出来。有时,出于各种原因,你可能还想从子分支分叉出来。许多人甚至可能更喜欢一个仓库,其中每个环境都是一个不同的分支。我不会深入探讨所有不同的组织和使用特定 Git 工作流程的方法,因为这个主题非常复杂。你必须采用最适合你需求的方法,但一个好的开始是回顾这个指南或关于 Git 工作流程的任何内容:backlog.com/git-tutorial/branching-workflows/。
你可能需要按功能分离分支。我最喜欢的做法是为每个开发者提供一个以他们名字命名的分支,他们可以在每天结束时将他们所有的日常工作提交到这个分支上。稍后,指定的人可以在整个软件开发生命周期(SDLC)中指定的时间间隔内将所有的pull请求合并到main分支。一个选项是将所有这些分支存储在你的开发环境中,然后在你的预发布和生产环境中,你只需保留一个分支,称为main分支。然后,当需要从开发中将某些内容推送到预发布时,只需将这些更改推送到预发布的main分支。选择几乎是无限的,而且没有绝对正确或错误的方法来做这件事。
如果你想的话,可以在其他环境中创建分支。只需注意确保你的应用程序版本之间不要出现不一致。想法是,只要这样做有意义,并且在合并时不会造成大量浪费的时间和头疼,你就走在正确的道路上。在任何策略中,当开发者工作时,通常明智的做法是尽可能频繁地将main分支拉入当前的工作分支,以保持代码与其他已经将代码合并到main的开发者同步。这样,开发者可以在自己的机器上解决他们知道是特定于自己的冲突,从而在稍后处理可能出现在将这个工作分支推送到main时的其他合并冲突时节省时间。很容易落后于其他开发者所做的更改,而且如果代码冲突的数量很多,稍后合并代码到main可能会变得困难。
高级部署
当然,并非所有事情都必须完全自动化。一个较小的团队或项目可以通过指派一个人手动合并分支、编辑代码冲突以及在推送到测试或生产环境之前构建所有资产文件来完成任务。如果您的团队正在构建企业级软件,那么您肯定需要尽可能自动化这一生产步骤。由于有这么多方法可以实现这一点,本书的其余部分将超出范围。您甚至可以雇佣一个专门的人或团队,他们的唯一工作就是构建和管理部署策略、编写构建脚本、处理拉取请求、解决代码冲突以及测试错误。
构建脚本可以用来编译 ES6 到 JavaScript,甚至可以将 SCSS 转换为 CSS,在一个较小的团队中,当合并分支时可能会手动完成这些操作。测试脚本可以帮助您运行测试用例。您可以测试每个方法或类,甚至可以通过测试输入到系统中的特定数据达到细粒度级别。根据您项目的需求,考虑在部署时编写和运行基于 Node.js 和 Python 的构建脚本。
值得您探索的两个更多主题是流水线和容器,它们为项目增加了额外的增强层。我们将在下一节讨论它们。
流水线
软件开发中的流水线通常是指一系列相互连接的对象,其中前一个对象的输出作为后一个对象的输入。这最好描述为将您的开发环境与预发布环境连接起来,然后再将预发布环境与生产环境连接起来。这里可以插入许多工具,以帮助您在每一步审查您的代码。像 Heroku 和 GitHub 这样的服务提供了一种方式,允许您的开发人员创建拉取请求,这是一种将您的代码提交到共享开发项目的方法。该方法甚至提供了创建工单的方式,这些工单用于跟踪和管理在您的应用程序中发现的错误或问题。它们可以被分配给一个人或多人,并且可以分配状态,为团队领导或项目经理提供一种了解已完成的工作和正在处理的工作的方法。
如果你决定跟随本章内容并使用 Heroku,他们的服务提供了许多不同的方式来工作和配置管道。如果需要,它们可以变得非常复杂。Heroku 管理管道的一个非常酷的功能是,当开发者创建一个拉取请求时,会自动创建一个新的审查应用程序。拉取请求是一种提交请求将代码合并到共享项目中的方式。请求被发送给审查者,他们将合并代码并批准或拒绝更改。如果审查者在合并代码时遇到冲突,他们可能会拒绝代码并将其退回给开发者进行修改。这为审查者提供了一个非常简单的方式来启动包含在拉取请求中的代码的应用程序,以查看它是否正常工作。
换句话说,你可以运行任何版本或任何状态的应用程序,而不会以任何方式影响你的当前开发、预发布或生产环境。当通过拉取请求创建新应用程序时,该新应用程序不会出现在你的主仪表板上,而是被视为你正在审查的环境或 Heroku 应用程序的审查应用程序。Heroku 还会在审查者批准特定拉取请求中包含的更改时,自动将更改部署到管道队列中下一个的环境的main分支。
容器
Python 中的虚拟环境最好描述为一种封装所有项目所依赖的包或依赖项的方式。通常所说的容器,或称为容器化,则是一个更高级的步骤,因为它封装了整个操作系统、你的 Django 项目、它自己的虚拟环境以及所有其包和依赖项。Docker 容器用于创建与生产环境完全相同的镜像,并在应用程序的开发生命周期中的每个阶段被开发者使用。这样做的好处是,无论你的开发者正在使用什么操作系统,他们都将虚拟地运行相同的设置,并且应该面临很少或没有问题。
根据我的经验,开发者之间经常使用“在我的电脑上运行正常”这个短语。这通常不是一个真正的问题,但确实存在这样的情况,即在一个 Windows 机器上运行的项目与在 Mac 或 Linux 机器上运行的相同项目表现不同,反之亦然。容器允许你的开发者继续使用他们在职业生涯中变得最舒适和熟悉的操作系统的环境。它们确保开发中的每个移动部分的一致性。
使用容器的一个好处是,它们可以帮助新加入的开发者或新员工在尽可能短的时间内将项目本地运行起来。容器仓库类似于传统的仓库,但用于存储容器镜像的集合。这些可以是完全不同的项目,或者同一应用程序的不同版本或环境的集合。无论哪种方式,容器仓库都用于存储和分发这些镜像给需要它们的开发者。如果你预测在项目生命周期中会有许多开发者加入和离开,我强烈建议花时间为你的团队构建一个容器镜像。
如果你正在使用 Heroku 配合这本书,请选择如图 1.9 所示的容器注册库 | 使用 Heroku CLI按钮,该图在本书的配置 Heroku 环境部分中较早出现。从这里,你可以将你的容器镜像集成到部署中。该镜像可以包括任何附加组件和应用程序变量,甚至可以与你的管道和审查应用程序一起工作。Heroku 和 Docker 还与第三方 CI 服务(如 Codefresh、CodeShip 和 CircleCI)合作。为了使用这些第三方服务,还需要进行相当多的配置,但你可以通过查看它们的文档来帮助您开始。
域名系统
当创建一个环境或审查应用程序时,Heroku 会根据你创建的应用程序名称生成一个新的 URL。由于 Heroku 的系统设置为将你的应用程序名称用作其 URL 结构的子域名,因此你的应用程序名称必须是唯一的。例如,这本书在 Heroku 上的主要项目名为becoming-an-entdev,生成的 URL 为becoming-an-entdev.herokuapp.com/。为这本书创建的开发应用程序自动生成 URL mighty-sea-09431.herokuapp.com/,而预发布环境生成的 URL 为pure-atoll-19670.herokuapp.com/。mighty-sea-09431和pure-atoll-19670是 Heroku 在创建这些环境时自动创建的应用程序名称。
应用程序配置的最后一步是将 Heroku URL 链接到www.your-domain.com。你必须将你的应用程序链接到这个电话簿注册库,这样公众就可以通过你的域名而不是 Heroku 提供的 URL 来访问你的网站或 API。
普遍的注册商,如 Domain.com、Bluehost、GoDaddy 和 Namecheap,允许你注册一个域名,它看起来像www.your-domain.com或www.your-domain.custom.tld/,其中.tld指的是 1500 多个.edu、.gov或.food等顶级域名中的任何一个。一些顶级域名仅限于特定实体,可能不是每个人都可用。你可以通过两种方式将你的 Heroku 应用链接到 DNS。第一种方式是使用www.your-domain.com指向你的 Heroku 应用位置,这样用户在浏览器的地址栏将不再看到你的域名。相反,他们将在地址栏看到becoming-an-entdev.herokuapp.com。如果你想用户在他们的浏览器地址栏中继续看到www.your-domain.com,同时你的网站作为正文内容,你需要设置使用文档的<head>部分的转发。
使用遮罩的转发通常在 SEO 方面被认为是不好的,SEO 是提高网站在搜索引擎如 Google、Bing 和 Yahoo 等搜索结果中可见性的方法。通常,搜索引擎很难或更复杂地将来自一个站点的信息通过 IP 地址链接并映射到你的物理域名,因此,你可能会因为这一点而得到更低的搜索排名。可能搜索引擎正在努力改进这一点,或者至少正在努力不惩罚使用转发和重定向的网站。目前,如果你的项目对 SEO 有很高的要求,你不应该考虑这一点。
使用域名转发之外的替代方案是将你的域名映射到提供你的应用的系统 DNS 服务器。无论你使用哪种方法,如果你使用的不是标准 Heroku 域名,你首先需要让你的 Heroku 应用对你的选择域名可用。首先,通过运行以下命令将你的域名添加到 Heroku 应用中。如果你还没有登录,请先登录:
PS C:\Projects\Packt\Repo> heroku login
PS C:\Projects\Packt\Repo> heroku domains:add www.your-domain.com --app becoming-an-entdev
要使用子域名来作为你的不同环境,请运行以下两个命令:
PS C:\Projects\Packt\Repo> heroku domains:add dev.your-domain.com --app mighty-sea-09431
PS C:\Projects\Packt\Repo> heroku domains:add staging.your-domain.com --app pure-atoll-19670
在这里,我们添加了--app属性设置,并指定了我们想要链接的应用。由于我们创建了不同的环境,我们需要包含这个指定,否则我们会得到错误。dev和staging子域名在先前的命令中用作我们网站的环境特定域名。如果命令成功执行,它将打印出你的 DNS 目标,但你始终可以运行此命令来列出所有域名,它也会为你打印出 DNS 目标:
PS C:\Projects\Packt\Repo> heroku domains --app {{ your_project_name }}
=== {{ your_project_name }} Heroku Domain
{{ your_project_name }}.herokuapp.com
=== {{ your_project_name }} Custom Domains
Domain Name DNS Record Type DNS Target SNI Endpoint
www.your-domain.com CNAME {{ randomly_generated_name }}.herokudns.com undefined
然后,你需要使用提供的 DNS 目标来配置你的域名注册商账户中的 DNS 记录。如果你使用域名转发,许多注册商都有一个创建转发的选项,并会问你几个问题。对于手动将域名映射到你的域名服务器,这通常是通过编辑 DNS 设置中的A-Record来完成的,提供一个名为@的名称,并给它一个之前提供的 DNS 目标值。
你甚至可以为不同的项目、部门、你的应用程序或环境的版本设置自定义子域名,例如dev.your-domain.com或staging.your-domain.com。可以通过创建一个新的CNAME-Record,使用名称dev或staging,并给记录提供之前为相应应用程序提供的 DNS 目标值来链接子域名。对于不接受非数字 IP 地址值的注册商,你必须使用像 Cloudflare 这样的网络基础设施和安全公司,它会在中间充当你的域名服务器。它充当内容分发网络和你的域名服务器,然后你可以使用 Cloudflare 提供的域名服务器设置在你的注册商那里配置它。域名服务器记录在注册商的 DNS 设置中被称为NS-Record。
摘要
很容易有人陷入如何做某事而不是真正去做某事的困境。过度思考在程序员中是非常常见的事情。有时,接受一定水平的工作量以满足你的工作流程,然后在需要时根据需求扩展和扩展,这是一种明智的做法。虽然收集需求对于获得对你试图实现的事情的清晰理解很重要,但本章中讨论的许多步骤和概念可以被视为你工具箱中的另一个工具,或者换句话说,适合的工具有助于完成合适的工作。
将项目容器化(例如使用 Docker)等事情可以很容易地添加到你的工作流程和部署常规中。同样,如果你后来需要添加额外的环境到你的工作流程中,也是如此。新开发者可以很容易地分配一个新的分支,因为需求也会随之产生。那些指派一个人手动运行、构建和测试任务的团队,当工作开始堆积时,总是可以在之后自动化他们的任务。即使你已经开发了一段时间的项目,也可以创建管道。然而,其他决策,比如在开发生命周期的中途决定使用哪个前端框架,如果改变技术,可能会证明是灾难性的。
被称为敏捷开发的是一种不断变化且始终流动的开发环境的过程。这就是许多不同的合作者分享想法,而这些想法通常会随着时间的推移而改变,这意味着项目的范围也会发生变化。如今,许多项目都是在敏捷环境中构建的。利用你的初始需求收集来探索未来可能的变化,并尽可能与尽可能多的开放机会保持一致。记住,一个强大的房屋总是需要一个强大的地基来支撑;一个薄弱的地基会导致你的房屋倒塌到地上。
本章重点介绍了如何为房屋打地基。在下一章,第二章,项目配置中,我们将构建可以被认为是坐落在地基上的房屋框架。这个房屋的框架将包括建立一个项目、虚拟环境和开发所需的数据库。
第二章:第二章: 项目配置
源代码在任何软件中都被认为是肉和骨头,或者说是房屋的框架。在本章中,我们将构建一个包含源代码所在文件的工程。我们将讨论一些在开发者直接与源代码工作时会派上用场的工具。当使用 Django 时,虽然任何工具都可以用来编辑源代码,但有些工具比其他工具更高效。在本章中,我们将探索一些无数的现有工具,并讨论为什么集成开发环境(IDE)也可能被使用。
我们还将了解与项目中的 Django settings.py 文件(夹)一起工作的意义。当然,软件还需要一个数据库来存储和检索用户输入和创建的数据,我们将为项目的每个环境安装本地和远程数据库。我们将介绍可用的各种数据库类型,然后专注于本书中示例所使用的最流行的类型。不需要使用我们将要使用的相同数据库类型,但如果您使用不同的类型,可能会遇到一些细微的差异;请谨慎行事。在阅读本书的其余部分之前,阅读本章至关重要。
在本章中,我们将介绍以下内容:
-
选择开发工具
-
启动项目
-
创建虚拟环境
-
项目配置
-
使用基本数据库设置
-
为 Heroku 准备 PostgreSQL
技术要求
要在本章中处理代码,以下工具需要在您的本地机器上安装:
-
Python 版本 3.9 – 作为项目的底层编程语言
-
Django 版本 4.0 – 作为项目的后端框架
-
pip 软件包管理器 – 用于管理第三方 Python/Django 软件包
接下来,您需要一种方法来编辑本章以及本书其余部分中将要编写的代码。本章的第一部分将向您提供从文本编辑器到 IDE 的几个开发工具选择。相同的 IDE 将用于演示一些操作,但并不需要使用相同的工具。您可以使用您喜欢的任何 IDE,或者根本不使用 IDE,而是使用终端或命令行窗口。还需要一个数据库,本章的第三部分将提供几个选项供您选择。任何数据库类型都可以与您的项目一起工作,并将提供配置示例,但我们将仅使用 PostgreSQL 来与我们的项目和 Heroku 一起工作。
本书将专注于 Django 和企业开发的概念,而不是指导您如何使用 Git 操作。如果您想快速学习如何使用 Git 操作,可以观看以下视频:www.packtpub.com/product/git-and-github-the-complete-git-and-github-course-video/9781800204003。
本章中创建的所有代码都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Becoming-an-Enterprise-Django-Developer。本章中使用的代码将与项目的核心中的每个文件相关。从第三章“模型、关系和继承”开始,每一章的代码都将位于其自己的 Django 应用文件夹中。请参阅本章标题为“创建 Django 应用”的小节,以了解更多相关信息。
查看以下视频以查看代码的实际应用:bit.ly/3NqNuFG
选择开发工具
配置我们的项目指的是我们将如何构建和安排构成应用程序的文件。这也指的是我们如何在协作团队中共享这些文件。一些工具可以创建可以在团队成员之间共享的文件,例如与开发和调试功能相关的预配置设置。这些文件有时被称为配置或解决方案文件。这意味着您可以预先配置一组开发工具,以帮助团队快速上手。确保所有成员使用类似工具可以使调试和查看非自己编写的代码变得容易得多。这种一致性也使得团队成员在同步工作流程时的口头和书面沟通更加高效。
虽然共享项目配置文件有其好处,但并不是团队中的每个人都必须使用相同的工具。实际上,甚至可以在同一个仓库中创建多种不同配置文件,以适应各种开发工具,为开发者提供多种预配置的项目文件供选择。即使我们在仓库中提供了配置文件,开发者仍然可以使用基本的文本编辑器,通过终端或命令行窗口进行编辑并本地运行项目。让我们比较几种这些文本编辑器工具,并讨论它们提供的某些好处。然后,我们将讨论 IDE 是什么以及它们与文本编辑器的区别。
文本编辑器
文本编辑器非常简单,正如其名称所示,是一种编辑文本的方式,或者在我们的情况下,是源代码。由于一些流行编辑器包含了许多功能,因此有时将这些编辑器称为轻量级 IDE。
很可能,今天被认为最流行的三个文本编辑器如下:
-
Atom
-
Notepad++
-
Sublime Text
Atom
Atom 旨在成为一个完全可编辑和可定制的文本编辑器。该工具的官方网站在描述他们的软件时使用了“可黑客化”这个术语。此编辑器允许您安装帮助增强您代码编写能力的包。它具有智能代码自动完成和代码样式,让您输入更少,看到更多。此工具具有内置的文件浏览器,允许您在您选择的文件夹和文件中查找和替换代码。Atom 也适用于所有三个主要操作系统,Windows、Mac 和 Linux。它是免费使用的,也是开源的,允许您修改编辑器的源代码。要下载 Atom 并开始使用它,请访问atom.io/。
Notepad++
Notepad++是另一个免费的开源编辑器,它配备了时尚的代码语法高亮显示器,并提供代码自动完成建议。它是为了与运行大量不同的编程语言而构建的。它甚至允许您直接从编辑器编写宏和运行自定义脚本。像 Atom 一样,它也有安装插件的能力。它非常轻量级,可以设置为操作系统的默认编辑器。要下载 Notepad++,请访问notepad-plus-plus.org/downloads/。
Sublime Text
Sublime Text 是另一个受欢迎的选择。此编辑器允许您创建项目文件,并包括构建各种编程语言的功能。一个酷炫的功能是它将使用您在.html文档内部存在的<script type="text/javascript"></script>来以不同的方式样式化和显示,与该文件中找到的 HTML 不同。
Sublime 家族中的另一个应用称为 Sublime Merge,它是一种简单的方式来合并代码并执行 Git 操作。我最喜欢的功能是使用它来查看历史状态和提交日志。Sublime Text 可以免费评估一段时间,但最终,在试用期满后,它会提示您购买许可证。要开始使用 Sublime Text,请访问www.sublimetext.com/。
如果不是几十个,也有可能是数百个其他文本编辑器可以选择,但本章没有提及。不要将自己限制在提供的流行选择中,并自由探索许多其他工具。一些文本编辑器提供的功能和能力有时可以被称为轻量级 IDE;接下来让我们讨论一下什么是 IDE。
集成开发环境
IDE 是我们所说的将许多不同的编程工具组合成一个单一的桌面应用程序的软件,开发者可以使用它来构建其他应用程序。它是你的团队可以使用并从中受益以保持生产力的开发工具。IDE 主要包含一种查看和编辑源代码的方式,自动化本地构建过程,并提供帮助你调试和分析代码的方式。该工具包含样式化和格式化代码的方式,在你键入时显示错误,并提供代码完成建议以减少你工作时的按键次数。这些环境还提供了搜索项目中包含的其他代码文件的方式,以及将你的代码推送到外部仓库的方式。例如,如果使用具有内置 Git 功能的 IDE,那么在第一章中提到的 Sourcetree 应用程序将不再需要。
同样适用于具有 Git 功能的文本编辑器选择。类似于一些具有花哨功能的文本编辑器,IDE 将创建可以与你的团队共享的常见配置文件。其中一些文件我们不想共享,例如存储断点和其他仅针对该开发者和本地实例的特定本地调试设置的文件。然而,与你的团队共享配置设置可以让成员们更容易、更快地将他们的 IDE 设置在他们的机器上,所有工具都准备好了或几乎准备好了可以使用。
一个简单的文本编辑器就足够了。一个项目可以从这种方式开始,然后你的团队可以在之后引入 IDE 的使用。有时,在使用 IDE 时,你可以在子目录中组织你的项目文件,这些目录在其他情况下可能不存在。如果你已经知道你的团队从一开始就需要这些生产力工具,你可以从 IDE 开始,并通过 IDE 本身创建项目,让 IDE 以对 IDE 自然的方式组织你的文件。这种方式有时更容易,因为一些 IDE 可能配置的文件结构可能与你仅使用终端或命令行窗口中的 Django startproject命令创建的结构略有不同。
让我们讨论一下开发者今天与 Django 一起使用的流行 IDE 选择,然后从中选择一个来展示使用 IDE 的概念。本章将展示在 IDE 和终端中与 Django 管理命令一起工作的方法,以展示使用一个与另一个相比的优势。在本章中,使用 IDE 演示的每个操作,都将提供该操作的命令行驱动的命令等效。这将还允许你在需要时完全绕过 IDE。从现在开始,所有未来的章节都只提供标准的命令行驱动的 Django 管理命令,继续使用 IDE 将是可选的。
PyDev with Eclipse
PyDev 实际上是一个针对 Eclipse IDE 的插件,但它也可以作为一个独立的 IDE 使用。PyDev 可以直接下载并安装,因为它会预装 LiClipse,这是 Eclipse IDE 的轻量级版本。Eclipse IDE 提供了一个完全集成的开发体验。它允许以多种方式调试代码。性能分析只是其中之一,这是一种帮助开发者理解事件时间、内存使用、磁盘使用或其他诊断的工具。CPU 性能分析器这个术语常用于讨论可以帮助你找出哪个特定进程拖累了系统的工具。它会告诉你它挂起的时间有多长,并给你一些关于如何修复它的想法。所有之前提到的文本编辑器都附带在 Eclipse 中。PyDev 拥有大量的库,可供选择用于今天存在的许多不同语言。Eclipse 和 PyDev 都支持所有主流操作系统,如 Windows、Mac 和 Linux。要下载 Eclipse,请访问 www.eclipseclp.org/download.html。
Eclipse 和 PyDev 都是免费使用的,并且都是开源许可,允许你修改 IDE 软件。与今天的其他桌面应用程序相比,Eclipse IDE 的安装稍微困难一些。它的安装程序需要你阅读很多内容,才能知道如何开始安装它。Eclipse 还需要你的机器上安装并运行 Java。Java 是一种高级编程语言和计算平台,通常不需要与 Django 和 Python 一起工作,除非你正在使用 Eclipse。因此,我们不会在本章中使用这个 IDE。要下载 PyDev,请访问 www.pydev.org/download.html。
PyCharm
PyCharm 可能是当今 Python 和 Django 开发者中最受欢迎的选择。它易于使用,比 PyDev 更容易安装,因为它提供了适用于 Windows、Mac 或 Linux 机器的简单可执行文件。它还提供了免费社区版和付费专业版。付费版提供了更多功能,并提供了更多专业的科学和 Web 开发工具,例如直接在 IDE 中进行高级数据库集成和数据库开发,以及其他调试和性能分析工具。免费社区版对于大多数 Django 开发者来说已经足够了。这个版本允许开发者与 Django 项目集成,并在连接到远程 Git 仓库的同时运行虚拟环境。要下载和安装 PyCharm,请访问 www.jetbrains.com/pycharm/download/。
虽然这可能是 Django 开发者中最受欢迎的 IDE,但 Visual Studio 可能是任何语言或框架开发者中最受欢迎的 IDE。因此,我们将使用 Visual Studio 在本章中演示示例。
Visual Studio
Visual Studio 已经是许多 .NET、C#、C++ 和其他开发者过去 20 多年来的首选工具。它是一个非常强大的 IDE,内置了您能想到的所有工具,甚至更多。它有多种不同的版本和风味,并且多年来它只以付费形式提供。然后,大约在 2013 年,微软开始向公众免费提供 Visual Studio Community Edition。对于一些人来说,缺点是 Visual Studio 直到 2017 年才在 Mac 平台上可用,在此之前它只适用于 Windows 平台。目前,Linux 平台不受支持。然而,轻量级的 IDE Visual Studio Code 可在 Windows、Mac 和 Linux 三种平台上使用。
Visual Studio Code 可能已经成为史上最受欢迎的开发者工具。Visual Studio 和 Visual Studio Code 都支持使用 Python 和 Django。在安装这些工具中的任何一个之后,您需要选择要包含在安装中的相关 Python/Django 软件包,或者单独安装它们。对于在 Linux 机器上工作或不想使用 IDE 的您,请参考每个创建项目涉及的动作的 IDE 展示之后的管理命令。如果您在 Windows 或 Mac 系统上,并希望使用 IDE 进行操作,请下载此处提供的 Visual Studio 2019 – 社区版 安装程序:visualstudio.microsoft.com/downloads/。在安装过程中,请确保选择它提供的任何 Python 开发扩展,如下面的截图所示:

图 2.1 – Visual Studio – Python 开发扩展
您可以在该界面的 单独组件 选项卡下找到其他有用的工具。在继续安装之前,包括您想要包含的任何其他工具。现在我们已经安装了 Visual Studio 2019 – 社区版,让我们为可以在仓库中与其他开发者共享的项目构建一个解决方案文件。
注意
虽然 Visual Studio 2019 是微软提供的最新产品,但 Visual Studio 2022 将在本书出版时大约同期发布。如果您使用的是较新版本的 Visual Studio,您应该能够执行本章中描述的所有相同操作。Visual Studio IDE 的截图可能不会完全相同,并且可能还需要进行一些代码调整。
开始一个项目
有两种启动项目的方式,本章将允许你选择想要遵循的方法。我们鼓励你使用 IDE,因为熟练使用这个工具对你的团队来说在长远来看是有益的。然而,如果你的团队使用的是除 Visual Studio 之外的 IDE,或者你只使用文本编辑器与代码一起工作,每个步骤的命令行等效操作也提供给了任何人,以便他们可以完成本章。本书中的其他所有章节都将仅关注代码,这些代码可以与或无需 IDE 一起使用。
使用 IDE
打开 Visual Studio IDE,选择django关键词,并在结果列表中选择空白 Django Web 项目,如图所示:
![Figure 2.2 – Visual Studio – Create a new project
![img/Figure_2.02_B17243.jpg]
Figure 2.2 – Visual Studio – Create a new project
在下一屏中,输入becoming_a_django_entdev作为manage.py文件,这是项目的根目录。这将使使用 IDE 内提供的终端等工具变得稍微容易一些:
![Figure 2.3 – Visual Studio – Creating a Django project
![img/Figure_2.03_B17243.jpg]
Figure 2.3 – Visual Studio – Creating a Django project
注意
当使用 Visual Studio IDE 创建项目时,位于/becoming_a_django_entdev/becoming_a_django_entdev/文件夹中的文件,例如settings.py和urls.py文件,将自动使用 Django 2.1.2 版本生成。这些文件在使用 Django 的后续版本时仍然有效。此外,当我们进入本章的创建虚拟环境部分时,我们将实际安装 Django 4.0 包,这是本书中使用的版本。尽管最初创建一些项目文件时使用了 2.1.2 版本,但项目将始终使用 4.0 版本并成功运行。当使用终端或命令行窗口创建项目时,这种情况不会发生。稍后,在第九章 Django 测试中,你将学习如何验证实际安装和使用的版本。
使用命令行
Django 终端或命令行窗口启动 Django 项目的等效命令是startproject。使用此命令创建项目有两种方式。第一种方法是使用您机器上全局安装的 Django 版本创建项目,然后接下来构建您的虚拟环境。
另一种方式是首先创建虚拟环境,然后激活你的环境,安装你想要的 Django 版本,然后使用虚拟环境中安装的 Django 版本构建项目。IDE 为我们做的第一件事是使用 IDE 提供的 Django 版本创建项目,然后当我们创建虚拟环境时,Django 的版本更新到了requirements.txt文件中指定的版本。
当虚拟环境中某个包更新时,旧版本会被卸载,新版本会被全新安装。为了这个练习,我们将卸载可能存在的任何版本的 Django,然后安装本书编写时最新的版本。然后,我们将在下一节使用命令行创建虚拟环境,尽可能接近使用 IDE 提供的示例。
按照以下步骤创建您的项目文件:
-
打开您的终端或命令行窗口,导航到在第一章“承担一个巨大的项目”中创建的本地仓库文件夹。确保您此时不在虚拟环境中。然后,执行以下命令以卸载任何现有的 Django 版本,然后安装我们将要在机器上全局使用的正确版本的 Django:
PS C:\Projects\Packt\Repo> pip uninstall django PS C:\Projects\Packt\Repo> pip install django==4.0 -
执行基于 Django 4.0 版本的 Django 命令,该命令将创建一个项目以及所有必要的核心文件,以便与 Django 一起工作。我们将把这个项目命名为与 IDE 示例中相同:
becoming_a_django_entdev。startproject命令创建一个manage.py、wsgi.py、asgi.py文件以及几个其他作为所有 Django 项目基础的样板文件。becoming_a_django_entdev选项是项目的名称和项目将被放置的文件夹名称。执行以下命令以创建项目:PS C:\Projects\Packt\Repo> python -m django startproject becoming_a_django_entdev
前面的命令是一个经过测试并证明在 Windows 机器上可以正常工作的友好命令。传统上,开发者会使用以下命令来启动项目;然而,这个命令在 Windows 操作系统上无法工作:
PS C:\Projects\Packt\Repo> django-admin startproject becoming_a_django_entdev
接下来,让我们创建和配置一个虚拟环境,这对于使用项目中包含的任何第三方包是必需的。
创建虚拟环境
目前我们不应该为我们的项目有一个虚拟环境。如果您已经有了,请继续并忽略它,为这个接下来的练习创建一个新的虚拟环境。无论您是使用 Visual Studio IDE 创建项目,还是在前面的练习中使用终端或命令行窗口中的 Django 命令,您的仓库中的文件结构应该看起来像以下树结构:
├── .git
├── readme.md
├── requirements.txt
├── becoming_a_django_entdev
│ ├── .vs
│ ├── becoming_a_django_entdev.sln
│ ├── db.sqlite3
│ ├── manage.py
│ ├── obj
│ ├── requirements.txt
│ ├── staticfiles
│ └── becoming_a_django_entdev
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
现在我们有两个 requirements.txt 文件和两个名为 /becoming_a_django_entdev/becoming_a_django_entdev/ 的文件夹,其中存放着各种文件。我们将保持文件夹结构不变,稍后配置额外的设置以允许 Heroku 与此文件夹结构协同工作。我们这样做是因为通过 IDE 创建项目只会产生这种结果,请注意,其他 IDE 或甚至轻量级 IDE 文本编辑器可能创建的文件夹结构可能与前面的树形结构不同。如果您决定使用 Django 命令创建新项目,您确实有选项指定一个额外的选项以防止在子目录中创建项目。这将把 manage.py 文件放在存储库根目录中,并导致只有一个 requirements.txt 文件。这样做将需要使用以下命令示例:
PS C:\Projects\Packt\Repo> python -m django startproject becoming_a_django_entdev ./
在这里,我们在前面的命令末尾添加了额外的 ./ 选项,表示将项目放在我们现在所在的文件夹中。如果没有这个选项,默认情况下将创建一个额外的子文件夹,文件夹名称与提供的项目名称相同。然而,这样做将导致没有解决方案文件,现在也没有通过 Visual Studio IDE 运行项目的方法。由于本章的目标是演示 IDE 的使用,这就是我们保持前面树形结构的原因。还有其他选项可以做到这一点,这将引导您在 Visual Studio 中使用存储库根目录中的 requirements.txt 文件创建新项目以使用此配置。
我们将在下一节讨论如何做。
配置 requirements.txt 文件
在 manage.py 文件所在的同一文件夹中,Visual Studio 为我们创建了一个 requirements.txt 文件。在 第一章,承担一个庞大的项目,我们已经创建了一个空的 requirements.txt 文件,只是为了满足 Heroku 在部署到我们创建的每个环境时的需求。如果存储库根目录中没有 requirements.txt 文件,Heroku 将无法部署。Heroku 需要这个副本来识别一个项目为 Python 项目;这正是他们的测试脚本编写的方式。这就是我们现在有两个 requirements.txt 文件可以工作的原因。另一个文件位于 /becoming_a_django_entdev/ 中的一级文件夹内,这是为了启用 Visual Studio IDE 提供的所有功能和服务。这个 requirements.txt 文件是我们将写入所有必需依赖的地方。
在您本地存储库根目录中的 requirements.txt 文件中,该文件夹与您的 .git 文件夹位于同一位置,添加以下代码:
# ././requirements.txt in Root Directory
# Path to Visual Studio requirements.txt below
-r becoming_a_django_entdev/requirements.txt
前面的代码声明了 requirements.txt 文件的位置,该文件嵌套在我们现在将称之为项目根目录的 /becoming_a_django_entdev/ 中。在你的项目根目录中打开 requirements.txt 文件,该目录也是 manage.py 文件所在的同一文件夹,然后添加以下项:
# requirements.txt
django~=4.0
django-extensions
django-heroku
dj-database-url
gunicorn
python-dotenv
pydotplus
psycopg2
psycopg2-binary==2.8.6
whitenoise
secret-key-generator
确保每个项目都位于单独的一行上。你项目所需的任何包都将始终放在这个文件中。你可以更加模块化,将这些文件添加到其他导入中,称为 -r becoming_a_django_entdev/requirements.txt 以指定文件路径。
在前面的例子中,使用了 ~= 操作符,表示大于或等于指定的版本号。例如,它将提供 4.0 版本的 Django,这意味着如果存在 4.0.9 版本,它将使用该版本,但不会提供 4.1 或更高版本。它只会提供 4.0.X 范围的最高版本。== 操作符表示包含与指定确切版本号匹配的包。完全不指定版本意味着它将抓取安装、重新安装或升级时存在的该包的最新版本。你可以在 PyPI 包库中找到的任何内容都可以放在这个文件中,要求它必须安装在你的虚拟环境中。
django 包是最重要的,因为它是我们使用框架的基础。django-heroku 包是由 Heroku 创建的包,它包含了一系列 Heroku 本身依赖的其他包。当父包安装时,这些 Heroku 依赖项将自动为你安装。之前列出的其他包将在下一节中使用,以帮助我们正确配置项目,以便我们在本书的其余部分使用 Django。
现在我们已经定义了要安装到虚拟环境中的多个包,让我们使用 Visual Studio IDE 创建我们的虚拟环境。
使用 IDE
对于那些希望使用命令行的人来说,请跳转到标题为 使用命令行 的子节。
在你的 Visual Studio IDE 中,导航到 IDE 的 解决方案资源管理器 部分,右键单击 Python 环境,然后选择 添加环境。打开的窗口如下所示:

图 2.4 – Visual Studio – 添加环境
当窗口弹出时,将虚拟环境名称输入为 virtual_env 并选择您的基解释器。这个名字可以是您选择的任何名字,但为了与本书中的示例保持一致,请命名为 virtual_env。在创建此练习时选择了 Python 版本 3.9 作为基解释器。位置非常重要,因为它会询问您是否想要 requirements.txt 文件与您的 manage.py 文件位于同一文件夹中。更改此位置可能会破坏使用您的 IDE 的一些功能,例如与您的虚拟环境一起工作的操作,并导致您遇到未解决的导入问题。未解决的导入可能会允许您的项目仍然运行,但代码高亮和格式化通常会中断。现在您应该在您的解决方案资源管理器和 IDE 中的Python 环境窗口中看到您的虚拟环境,如图所示:
![图 2.5 – Visual Studio – 虚拟环境成功
![图片 2.05_B17243.jpg]
图 2.5 – Visual Studio – 虚拟环境成功
如果您以后想安装包,例如,如果有人在 2 个月后将包添加到 requirements.txt 文件中,开发者可以在解决方案资源管理器中右键单击虚拟环境,然后选择从 requirements.txt 安装。Visual Studio 将更新任何版本并安装任何缺失的包。Visual Studio 和 pip 通常不会删除未使用的包;如果您遇到冲突,可能需要手动卸载包。
运行/激活项目
到目前为止,我们所做的一切都应该允许我们在本地运行项目,因为 Visual Studio 为我们做了很多繁重的工作。如果您决定不使用 IDE,您目前无法运行您的项目,并且必须在本章的大部分内容中工作才能达到这个结果。在任何情况下,您目前都无法在 Heroku 上运行您的项目。使用 IDE 顶部的Play/Run Web Server按钮,选择您想要的浏览器(如果它提供了选择下拉菜单),然后按播放,如以下截图所示:
![图 2.6 – Visual Studio – 运行项目
![图片 2.06_B17243.jpg]
图 2.6 – Visual Studio – 运行项目
当按下“播放”按钮时,将打开一个命令窗口,您可以在其中查看打印到屏幕上的任何控制台消息,并且浏览器中选中的新标签页也将打开。地址栏中的地址将指向 http://localhost:#####/,并且您的项目现在应该正在成功运行。端口号通常是一个随机数字。Visual Studio 使用的是它在您的机器上当前未使用的端口号,并且也不是默认或保留端口号,例如 8000。这是 Visual Studio 为同时运行多个项目的人提供的内置功能,因为我经常发现自己这样做。
如果您看不到此按钮,您可以通过在 Visual Studio 工具栏区域的空白处右键单击来将其添加到工具栏。在出现的下拉菜单中,从选择列表中选择标准。如果您从列表中选择Python,您可以直接从工具栏中使用与虚拟环境一起工作的工具。
除了打开的命令行窗口外,您选择的浏览器也会在新标签页中打开,指向http://127.0.0.1:#####/,其中#####是 Visual Studio 使用的随机端口。在这里,您将看到成功安装 Django 的标志性登录页面,如下面的截图所示:

图 2.7 – Django 安装成功
要在 Visual Studio 中使用标准端口8000并指向http://localhost:8000/的播放按钮,请按照下一小节的步骤操作。
手动设置端口
我们可以在 Visual Studio 中非常容易地指定端口号,以控制每个项目使用哪个端口。在 IDE 顶部的项目选项卡中,从下拉菜单中选择成为 Django 企业开发者属性。在打开的编辑器选项卡中,点击内部的调试选项卡,并在标题为端口号的字段中指定端口号。
接下来,我们将看到如何使用命令行创建虚拟环境。
使用命令行
对于许多更喜欢终端或命令行窗口的您,一个流行的模块,可用于在 Windows、Mac 和 Linux 上创建虚拟环境,称为venv。
按照以下步骤创建您的虚拟环境:
-
打开您的终端或命令行窗口,导航到我们在第一章“承担一个巨大项目”中创建的本地 Git 仓库。
-
导航到您第一个名为
becoming_a_django_entdev的文件夹。这个文件夹应该已经存在,并且包含在您使用命令行或 IDE 在此章早期启动项目时创建的文件:PS C:\Projects\Packt\Repo> cd becoming_a_django_entdev -
在此目录中,即
manage.py文件所在的目录,运行以下命令以创建一个名为virtual_env的虚拟环境:PS C:\Projects\Packt\Repo\becoming_a_django_entdev> python -m venv virtual_env
venv模块应该与所有 Python 安装一起提供,但如果您在三个主要平台上运行此命令时遇到问题,请访问以下文档以帮助您调试问题:docs.python.org/3/library/venv.html。
-
对于 Windows,激活您的虚拟环境:
PS C:\Projects\Packt\Repo\becoming_a_django_entdev> virtual_env/Scripts/activate
Mac 和 Linux 用户应跳转到激活虚拟环境小节,了解如何在那些平台上激活虚拟环境。
-
接下来,通过运行以下命令安装项目根目录中
requirements.txt文件定义的包,其中manage.py文件位于:PS C:\Projects\Packt\Repo\becoming_a_django_entdev> pip install -r requirements.txt
在 Windows、Mac 和 Linux 上创建虚拟环境的另一种方法是使用virtualenv,如下所示:
PS C:\Projects\Packt\Repo\becoming_a_django_entdev> pip install virtualenv
PS C:\Projects\Packt\Repo\becoming_a_django_entdev> virtualenv virtual_env
现在我们已经为我们的项目创建了一个虚拟环境,让我们激活这个虚拟环境并运行项目。
激活虚拟环境
我们可以不使用 IDE,而是激活一个虚拟环境,直接从命令行运行项目。如果您已经在上一小节中激活了虚拟环境,您可以跳过这一小节。以下是一些示例,展示了如何为每个主要平台(Windows、Mac 和 Linux)激活虚拟环境:
按照以下步骤激活您的虚拟环境:
-
对于 Windows 用户,导航到项目的根目录,其中包含
manage.py文件,然后使用以下命令激活您的虚拟环境:PS C:\Projects\Packt\Repo\becoming_a_django_entdev> virtual_env/Scripts/activate -
Mac 和 Linux 用户需要运行以下命令:
PS C:\Projects\Packt\Repo\becoming_a_django_entdev> source virtual_env/bin/activate
如果成功,您现在将在终端中看到以下提示,等待在虚拟环境中执行下一个命令:
(virtual_env) PS C:\Projects\Packt\Repo\becoming_a_django_entdev>
只有在激活了虚拟环境后,您才能执行标准的 Django 管理命令。
注意
从现在起,为了节省空间、去除杂乱和防止在提供活动虚拟环境中的终端或命令行示例时产生混淆,以下示例将按照以下方式展示:
(virtual_env) PS >
现在虚拟环境已经激活,让我们运行我们的项目。
运行项目
如果您决定使用命令行而不是 Visual Studio IDE 来创建项目,您目前将无法运行项目。这是因为 Visual Studio 创建了一个本地 SQLite 数据库,对任何必要的迁移进行了处理,并自动为我们在requirements.txt文件中包含的每个包进行了迁移。作为参考,运行项目的命令如下所示。您需要完成本章PostgreSQL部分中的练习来配置数据库,然后才能成功执行以下命令。完成这些操作后,您可以回到这一节。
确保您仍然位于与manage.py文件相同的文件夹中,并且您的虚拟环境处于激活状态,然后执行以下runserver命令:
(virtual_env) PS > python manage.py runserver
如果运行成功,您将在终端中看到以下信息。每次您加载页面并且项目正在运行时,您都会在这个终端中看到所有打印到屏幕上的消息:
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
Django version 2.2.24, using settings 'becoming_a_django_entdev.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
手动设置端口号
使用终端或命令行窗口手动指定端口号很简单。然而,每次我们运行项目时都必须这样做,这与在 IDE 中设置项目配置不同。以下示例将端口号选项添加到上一节中使用的相同runserver命令,指定使用端口号8000来运行项目:
(virtual_env) PS > python manage.py runserver 8000
项目配置
所有项目都需要以某种方式配置,以便与所有涉及的包和主机一起工作。我们将使用我们刚刚创建的项目,并配置 settings.py、.env、procfile 和 .gitignore 等文件。以 settings.py 文件为例——此文件或文件将存储项目中使用的所有全局常量。当使用包时,它们通常提供一种方式,可以从主设置文件中自定义该包的行为。其他文件,如 .env 和 procfile,将用于防止在与 Heroku 作为主机工作时出现部署问题。目前,由于我们创建了项目文件,我们无法成功部署到 Heroku 环境。在尝试成功部署之前,请先完成以下配置部分。
Django settings.py 文件
在我们创建项目时自动为我们生成的 settings.py 文件中,我们需要添加特定于我们项目的设置。
按照以下步骤配置您的项目设置:
-
在
settings.py文件的顶部,在存在的前两个导入下面添加import django_heroku。还要添加import dotenv和dj_database_url。这两个包将用于建立数据库连接。您的settings.py文件顶部应类似于以下示例:# /becoming_a_django_entdev/settings.py import os import posixpath import django_heroku import dj_database_url import dotenv
注意,某些系统可能在其文件顶部显示 from pathlib import Path 而不是 import os 和 import posixpath。
在 settings.py 文件的底部,添加以下代码:
# /becoming_a_django_entdev/settings.py
...
django_heroku.settings(locals())
此声明将从 django_heroku 包导入特定的 Heroku 设置。
-
为了使您的 DNS 正确工作,您需要告诉 Django 您允许主机访问此站点。这是 Django 的内置安全功能,旨在阻止常见的
ALLOWED_HOSTS,应类似于以下示例:# /becoming_a_django_entdev/settings.py ... ALLOWED_HOSTS = [ 'your-domain.com', 'www.your-domain.com', 'dev.your-domain.com', 'staging.your-domain.com', 'becoming-an-entdev.herokuapp.com', 'mighty-sea-09431.herokuapp.com', 'pure-atoll-19670.herokuapp.com', ]
可用的选项包括通配符,如星号字符(*),这将允许任何内容。然而,这并不被视为最佳实践,并且非常不安全。如果一个域名以单个点开始,就像以下示例中的第二个条目,它也将作为通配符,允许所有对应父域的子域。如果您决定使用这些通配符选项,请谨慎使用:
# /becoming_a_django_entdev/settings.py
...
ALLOWED_HOSTS = [
'*',
'.your-domain.com',
]
-
从 Django 3.2 版本开始,我们必须在设置文件中添加一个名为
DEFAULT_AUTO_FIELD的变量。在此版本之前,这并不是必要的。此设置告诉 Django 如何处理和处理所有对象的全部主键。如果没有此设置,我们否则必须为每个我们创建的模型类添加一个名为id = models.AutoField(primary_key=True)的字段。由于这是一项艰巨的任务,我们可以通过在settings.py文件的任何位置放置以下示例来避免它:# /becoming_a_django_entdev/settings.py ... DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
目前,我们只需保留默认的 DATABASES = {...} 设置。在本章的 为 Heroku 准备 PostgreSQL 部分,我们将讨论根据我们的特定用途更改此设置。还将提供所有支持数据库类型的示例,以便在处理不遵循本书中每个示例的项目时提供快速参考。
让我们创建我们的环境文件。
创建环境文件
Heroku 将使用名为 .env 的环境文件来存储在各个环境中运行项目时使用的环境相关变量。我们使用它来告诉 Heroku 诸如每个环境使用什么数据库或是否应该开启/关闭调试等信息。Heroku 建议我们在本地使用 SQLite,但在他们的环境应用中不使用。它可以在本地配置为使用 PostgreSQL,我们将在本章末尾进行演示。即使您在本地使用 SQLite3,您仍然需要在您的机器上安装 PostgreSQL 软件套件,以便驱动程序可以与所有远程连接一起工作。在我们讨论在本地设置 PostgreSQL 之前,我们需要完成配置项目的其余部分。
本地变量
要创建您的本地变量,请按照以下步骤操作:
-
从您的项目根目录运行以下命令,其中包含您的
manage.py文件:(virtual_env) PS > echo 'DATABASE_URL=sqlite:///db.sqlite3' > .env
echo 语句用于创建一个名为 .env 的新文件,其中包含一行内容,即 DATABASE_URL=sqlite:///db.sqlite3。
Windows 用户在尝试运行上述命令时可能会遇到类似 UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte 的错误消息。这是在 Windows 机器上经常出现的问题,会导致此类错误。为了克服这个问题,请使用 Notepad++ 打开您的 .env 文件,转到 编码 下拉菜单,从选择列表中选择 UTF-8,然后保存。如果这失败了,这是可能的,只需删除文件并使用 Notepad++ 从头开始重新创建它,使用正确的编码创建文件。
-
无论此文件是如何创建的,请确保以下代码行存在于您的本地
.env文件中:# /becoming_a_django_entdev/.env DATABASE_URL = sqlite:///db.sqlite3
我们放置在此文件中的代码指向名为 db.spqlite3 的 SQLite3 数据库文件的位置。如果您没有使用 Visual Studio IDE 创建项目,此文件尚不存在,并且您的项目目前无法运行。
-
我们还可以向此文件添加其他变量,然后使用
settings.py文件中的python-dotenv包来访问这些变量。要访问这些变量,我们必须使用以下示例在settings.py文件中加载.env文件:# /becoming_a_django_entdev/settings.py ... import dotenv BASE_DIR = os.path.dirname( os.path.dirname(os.path.abspath(__file__)) ) dotenv_file = os.path.join(BASE_DIR, ".env") if os.path.isfile(dotenv_file): dotenv.load_dotenv(dotenv_file) SECRET_KEY = os.getenv('SECRET_KEY')
将高亮代码放置在settings.py文件顶部附近,位于现有的BASE_DIR变量下方。在此示例中,我们还用os.getenv('SECRET_KEY'),替换了SECRET_KEY变量的字符串值,通过os.getenv()我们可以访问.env文件中找到的任何变量。
-
为了使我们的项目在本地运行,我们需要将
settings.py文件中SECRET_KEY变量的字符串值添加到.env文件中。使用以下示例编写您的变量,不要在此文件中为任何字符串值添加引号:# /becoming_a_django_entdev/.env ... SECRET_KEY = my_randomly_generated_key
就这样,我们的本地变量现在已配置。接下来,让我们配置我们的远程变量。
远程变量
远程变量与我们在本地创建的.env变量相同,但现在具有与每个远程环境相关的值。由于我们将在我们的仓库中忽略.env文件,我们将在名为“创建.gitignore 文件”的子节中讨论这一点,因此我们需要为每个存在的 Heroku 环境手动在.env文件中创建SECRET_KEY变量。由于 Heroku 已经为我们创建了这些.env文件,我们将借此机会添加所需的变量。这些步骤也可以在任何时候在 SDLC(软件开发生命周期)中根据需要添加变量。
要添加您的远程变量,请使用 Heroku命令行界面(CLI)登录您的 Heroku 账户,并将以下代码块中提供的应用程序名称替换为您的 Heroku 应用程序名称:
(virtual_env) PS > heroku login
(virtual_env) PS > heroku config:add SECRET_KEY=my_randomly_generated_key --app becoming-an-entdev
(virtual_env) PS > heroku config:add SECRET_KEY=my_randomly_generated_key --app mighty-sea-09431
(virtual_env) PS > heroku config:add SECRET_KEY=my_randomly_generated_key --app pure-atoll-19670
在前面的代码中,为存在的三个环境中的每一个提供了一个示例。为每个环境逐个执行它们。
最好为每个环境提供一个不同的SECRET_KEY值。在第三章,“模型、关系和继承”一节中,我们在名为“生成SECRET_KEY变量”的子节中讨论了与 Django shell 一起工作后,将探讨如何以更安全的方式生成SECRET_KEY变量。将此SECRET_KEY变量添加到每个 Heroku 环境中的操作仍将如本节所述进行。
接下来,让我们创建一个Procfile,即进程文件。
创建 Procfile
存储在仓库根目录(.git文件夹所在位置)的procfile。此文件告诉 Heroku 其余项目文件的存放位置,具体来说,是procfile文件内wsgi.py或asgi.py文件的存放位置,使用标准的 Python 路径语法。wsgi.py文件通常位于任何 Django 项目的project_name文件夹中,创建时;该文件夹位于manage.py文件所在的同一文件夹中。如果您想使用异步服务器网关接口,则应指定asgi.py文件的存放位置。
假设manage.py文件和 Django 项目文件夹与仓库的根目录位于同一文件夹中,我们将在该 procfile 中包含以下路径:
# ./Procfile in Root of Repository
web: gunicorn becoming_a_django_entdev.wsgi
我们的项目文件嵌套在 /becoming_a_django_entdev/ 文件夹中一层,就像在 IDE 中操作或在本书早期使用 startproject 命令创建项目时那样。如果我们尝试使用标准的 Python 路径语法添加此目录,我们将在部署到 Heroku 时遇到问题。相反,在一行代码中,告诉 Heroku 首先更改目录,然后执行前面的命令,使用以下代码示例:
# ./Procfile in Root of Repository
web: sh -c 'cd ./becoming_a_django_entdev/ && exec gunicorn becoming_a_django_entdev.wsgi --log-file -'
请使用第二个示例。只有当你已经将你的项目结构设置为存储在仓库的根目录,并且不太可能使用 Heroku 作为你的主机时,才使用第一个示例。如果没有之前显示的 --log-file – 参数,Heroku 也无法部署。使用 log 参数,你可以从每个应用的 Heroku 仪表板中读取部署错误。
接下来,我们需要控制当 Django 与 Heroku 一起使用时如何管理静态文件。
Django 静态文件
传统上,我们不需要修改设置文件以允许 Django 在项目中使用静态文件。在我们的情况下,我们需要添加到现有的设置中,以便 Heroku 能够与它们一起工作。Heroku 使用一个名为 whitenoise 的包来处理你的静态文件,该包在 requirements.txt 文件中安装。这些文件是 .css、.js、图像或字体文件,它们位于任何 Django 应用程序的 static 文件夹中。
按照以下步骤配置你的项目以使用 whitenoise 包:
-
将以下行添加到你的
settings.py文件中找到的MIDDLEWARE设置。添加到该列表中任何现有项的下方:# /becoming_a_django_entdev/settings.py ... MIDDLEWARE = [ ..., 'whitenoise.middleware.WhiteNoiseMiddleware', ] -
Heroku 还需要我们添加一个名为
STATICFILES_STORAGE的变量。将此变量添加到你的settings.py文件中,位于你的STATIC_URL和STATIC_ROOT变量之上,如下所示:# /becoming_a_django_entdev/settings.py ... STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' STATIC_URL = '/staticfiles/' STATIC_ROOT = posixpath.join( *(BASE_DIR.split(os.path.sep) + ['staticfiles']) ) -
如果你的值尚未更改,将你的
STATIC_URL和STATIC_ROOT变量的值更改为前面代码中显示的值。
我们静态文件现在应该已经正确连接。到目前为止,我们已经在本地配置了一切,以便成功部署到我们的任何远程 Heroku 环境。
注意
在 Django 中,当项目在本地运行时,如果 DEBUG = False,即使静态文件出现在你的远程环境中,它们也无法加载。这种情况发生的原因是服务器通常配置为处理如何提供你的静态文件,而不是 Django 控制这一点,类似于 Heroku 使用 whitenoise 包来提供其静态文件的方式。当 DEBUG 设置为 False 时,whitenoise 包也用于在本地提供静态文件。
让我们接下来连接我们的媒体文件。
Django 媒体文件
与静态文件类似,媒体文件路径需要配置,以便与 whitenoise 包和 Heroku 一起使用。媒体文件被认为是用户上传到你的系统中的任何内容,例如图片、音频文件或另一个文档。这两个变量,MEDIA_URL 和 MEDIA_ROOT,在 Django 中默认定义为空字符串;我们只需设置它们的值,指向我们想要放置它们的媒体文件夹。除了这些设置之外,还可能需要额外的步骤才能与 Heroku 和媒体文件一起使用。请参考这里找到的详细 Python 指南以了解更多信息:devcenter.heroku.com/articles/s3-upload-python。
要定义你的媒体相关变量,在你的 settings.py 文件中,在 STATIC_URL 和 STATIC_ROOT 变量下方,包括以下两个变量:
# /becoming_a_django_entdev/settings.py
…
MEDIA_URL = '/media/'
MEDIA_ROOT = posixpath.join(
*(BASE_DIR.split(os.path.sep) ['media'])
)
就这些了,这就是我们需要在我们的 settings.py 文件中配置的,以连接静态和媒体文件。在 第四章 的 URLs, Views, and Templates 小节中,标题为 Function – static() 的部分,我们将在这些内容完全集成到我们的项目中之前,需要配置额外的 URL 模式。
在我们尝试部署之前,我们需要创建前面提到的 .gitignore 文件,以及完成本章的 为 Heroku 准备 PostgreSQL 部分的操作。
创建一个 .gitignore 文件
在部署之前,我们想要创建的最后一个文件是一个 .gitignore 文件,这样我们就可以只分享我们想要分享的内容,并排除像现在在 virtual_env 和 .vs 文件夹中看到的那些庞大的文件。这个文件将用于确保我们不会意外地将任何不想要的代码推送到我们的远程仓库。一旦文件被推送到仓库,它将始终被跟踪,直到它被删除;因此,我们想要确保我们首先这样做。忽略由 Visual Studio 创建的 .suo 文件也是一个好主意。这些文件包含用户特定的信息,例如断点和调试器监视器。你还会想要忽略任何不需要与其他开发者共享的 build、bin 和 log 文件。这也是我们将定义一个模式来忽略定义环境特定变量的 .env 文件的地方。当我们到达本章的 PostgreSQL 小节时,我们将很快创建开发、预发布和生产的 .env 文件,这是成功部署所必需的。
在你的仓库根目录中创建一个名为 .gitignore 的文件,其中你的 .git 文件夹位于。然后,添加这里显示的项目。它们将作为你的忽略模式:
# ./.gitignore in Root of Repository
# Keep Rule
!gitkeep.txt
# Django #
db.sqlite3
*.log
*.pyc
__pycache__
# Media – User Generated Content #
media/
# Environments #
.env
virtual_env/
# Visual Studio and Visual Studio Code #
*.suo
*.pyproj
*.pyproj.user
*.history
.vs/
obj/
# Heroku
staticfiles/
这些只是与我们的特定 Heroku/Django 和 Visual Studio 配置相关的几个示例。您的 .gitignore 文件可以包含更多的忽略模式。在这本书提供的源代码中,已经提供了许多其他忽略模式的示例,并按类别划分,如果您需要,欢迎使用。
忽略模式接受通配符,例如前面示例中使用的星号(*)。它们还接受一个排除操作符,用感叹号(!)符号表示。!gitkeep.txt 模式在需要空文件夹存在于仓库中的情况下常用,例如媒体文件夹。如果文件夹内没有任何内容,Git 会自动忽略该文件夹;如果我们把一个 gitkeep.txt 文件放在那个媒体文件夹里,我们就可以绕过忽略该文件夹内所有内容的困境。媒体文件夹用于用户生成的内容,但我们不希望跟踪放入其中的文件。实际上,一些空文件夹是必需的,以防止运行时出错,例如当上传图片到系统而媒体文件夹尚未存在时。有时,这甚至可以防止一个仓库的新克隆版本在第一次运行时完全无法正常运行,这取决于该文件夹的存在与否。
您现在可以根据需要将代码推送到开发、预发布或生产环境。只需在执行 Git 操作时注意您正在推送到或从哪个远程环境和分支,当完成这些操作后,您可以访问之前讨论过的 Heroku 仪表板第一章,承担一个巨大的项目,以查看部署是否成功,如果不成功,请查看日志以了解失败的原因。
创建 Django 应用
在 Django 中,您的 manage.py 文件所在的文件夹中的所有文件集被认为是您的项目。您的项目可以包含许多应用,其中 Django 中的 应用 被视为在项目中执行某些操作的过程,例如日志记录、琐事或记录保存,仅举几个例子。它们也可以是简单的东西,例如来自 PyPI 包的特殊表单字段或事件监听器,其中该特定包基本上被视为我们正在安装的 Django 应用。
应用是我们编写模型、视图、测试用例、表单、管理类、HTML 模板和与该应用相关的静态文件的地方。应用也是项目中大部分代码存在的地方。它们也被设计成模块化的,如果我们想的话,一个应用可以在多个项目中共享。接下来,我们将为自己创建一个应用,并将其命名为 chapter_2。每一章都将遵循相同的命名约定。我们将尝试根据本书每一章的内容来组织应用。有些章节可能涉及项目中的每个文件,本书的这一章就是这样。这是因为在本章中,我们正在处理项目的全局文件。选择您想要使用的创建应用的方法,无论是使用 IDE 还是命令行驱动的方法。
使用 IDE
要轻松创建应用,请在此处右键单击 chapter_2 中的项目名称,如图所示:

图 2.8 – Visual Studio – 添加应用
在 Visual Studio 中,您可以选择项目中的任何文件夹或子文件夹来创建 Django 应用。如果您希望以不同的方式组织项目结构,只需右键单击您想要创建该应用的文件夹,而不是之前描述的文件夹。
注意
Visual Studio 将使用虚拟环境中安装或更新的 Django 版本来安装应用。没有像之前通过 IDE 创建项目时那样的特殊版本 2.1.2 用例;要安装的应用文件版本将是 Django 4.0。此外,请注意,您可以使用 IDE 在项目根目录中的任何目录中创建应用。您不必将它们安装在本练习中使用的目录中。
让我们使用命令行创建一个 Django 应用。
使用命令行
要使用命令行创建 Django 应用,您首先需要为新应用创建文件夹结构。在这里,我们希望它完全模仿之前练习中 IDE 创建的文件夹结构,以确保每种方法都能产生相同的结果,并与本书的其他部分协同工作。从 manage.py 文件所在的同一文件夹开始,确保您的虚拟环境已激活,并运行以下创建文件夹命令。然后,执行下面的传统 startapp 命令,以创建名为 chapter_2 的应用:
(virtual_env) PS > mkdir becoming_a_django_entdev/chapter_2
(virtual_env) PS > python manage.py startapp chapter_2 becoming_a_django_entdev/chapter_2
在前面的示例中,我们首先使用 mkdir 命令创建了 chapter_2 文件夹,然后执行了 startapp 命令。我们还提供了一个参数,指定了应用将被安装到的文件夹,也就是 Visual Studio 为我们放置的同一个文件夹。再次强调,您不必将应用安装在这个目录中——如果您以不同的方式创建了文件夹结构,在整个书籍中根据需要调整即可。
激活新的 Django 应用
Django 应用创建后,除非你将其作为已安装的应用包含在 Django 中,否则它不会自动在你的项目中工作。
要激活你的应用,请按照以下步骤操作:
-
所有 Django 应用都必须包含在
INSTALLED_APPS列表中。添加你的章节应用,如下所示:# /becoming_a_django_entdev/settings.py ... INSTALLED_APPS = [ ... 'django_extensions', 'becoming_a_django_entdev.chapter_2', ] DEFAULT_AUTO_FIELD'= 'django.db.models.AutoField' -
有时候,有必要告诉 Django 在你的目录树中查找你的应用的位置。在你刚刚创建的应用的
apps.py文件中,你可以使用标准的 Python 路径语法指定你的应用的位置。修改以下示例中name =变量的值:# /becoming_a_django_entdev/chapter_2/apps.py from django.apps import AppConfig class chapter_2Config(AppConfig): name= 'becoming_a_django_entdev.chapter_2'
注意,你必须为所有未来创建的章节应用都这样做。
项目文件结构现在应该看起来像以下树,其中突出显示的项目是我们从开始配置虚拟环境以来所做的添加:
├── .git
│ ├── .gitignore
│ ├── procfile
├── requirements.txt
├── readme.md
├── becoming_a_django_entdev
│ ├── .vs
│ ├── becoming_a_django_entdev.sln
│ ├── db.sqlite3
│ ├── manage.py
│ ├── media
│ ├── obj
│ ├── requirements.txt
│ ├── virtual_env
│ ├── staticfiles
│ └── becoming_a_django_entdev
│ ├── chapter_2
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
接下来,让我们讨论 Django 支持的不同类型的数据库。
使用基本的数据库设置
一个网站本身如果没有数据库与之交互就远远不够有用;这就是为什么 Visual Studio IDE 附带了一个轻量级且便携的 SQLite3 数据库。每当执行startproject命令时,Visual Studio 都会在manage.py文件所在的同一文件夹中创建一个名为db.sqlite3的文件。如果你使用终端或命令行窗口创建了项目,那么你将不会在下面的配置示例中使用 SQLite 数据库,如果你尝试在没有此数据库的情况下运行项目,它将失败。这是 Django 直接支持的五种标准数据库类型之一。除了 Django 直接支持的五种类型之外,还可以使用其他数据库类型。我们还将提供一个如何配置其他类型数据库的示例,例如 Microsoft SQL Server 数据库。不属于五种标准类型的类型将需要使用 Django 提供的引擎之外的引擎,这意味着你需要找到一个 PyPI 包来安装或为你要支持的数据库类型编写自己的。
Django 的五种标准数据库类型如下:
-
SQLite
-
MySQL
-
MariaDB
-
Oracle
-
PostgreSQL
以下示例将帮助您配置您选择的现有数据库。最后一个示例,PostgreSQL,将是本章后续示例中使用的类型。您可以使用本书剩余章节中的任何类型。使用数据库还有更多内容,例如创建表和执行查询,这些内容将在后续章节中讨论。这些示例仅涉及为建立与数据库的连接而编写的设置,无论该连接是本地还是远程。
SQLite
SQLite 是一种基于 C 语言的 关系数据库管理系统(RDBMS)。它非常轻量级且易于携带,在数据库管理方面有时被称为首选的 磁盘存储 方法。这是在极短的时间内将 概念验证 项目启动起来的首选方法。这种数据库类型甚至可以在驱动器和存储库中共享,以便快速传输和携带项目及其数据。如果安全性对您来说和它应有的重要性一样重要,则不建议这样做。
在实际应用中使用此数据库类型以及与 Heroku(如我们用于托管应用程序的方式)结合使用时,存在许多问题。您可能会发现您可以在远程 Heroku 环境中暂时使其工作。然而,在应用程序的生命周期中,每次部署都会导致数据的完全丢失。因此,我们将不得不偏离 Django 附带的标准数据库,并依赖我们环境中更健壮的数据库系统。
一个标准的 SQLite3 数据库配置示例如下:
# /becoming_a_django_entdev/settings.py
...
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
根据需要添加额外的 default 参数。
MySQL
MySQL 是一种更健壮的 SQL RDBMS。它是开源的,就像 SQLite 一样,它与 Windows、Mac 和 Linux 系统兼容。它被设计成作为客户端-服务器模型工作,这意味着软件安装在客户端机器上以执行服务器端将监听并响应的请求操作。MySQL 已经成为有史以来最受欢迎和最常用的数据库类型之一,尤其是如果您考虑了所有使用 MySQL 分支构建的其他数据库选择。
按照以下步骤配置您的 MySQL 数据库连接:
-
一个标准的 MySQL 数据库连接示例如下:
# /becoming_a_django_entdev/settings.py ... DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'database_name', 'USER': 'database_user', 'PASSWORD': 'database_password', 'HOST': 'localhost', 'PORT': '3306', } } -
为了使这种数据库类型正常工作,您还需要安装以下包并将其包含在您的
requirements.txt文件中:# requirements.txt ... Mysqlclient
根据您的项目需要调整默认参数。
MariaDB
MariaDB 实际上是 MySQL 的一个分支;它是一个社区开发的版本,随着时间的推移发展成现在的样子。MariaDB 和 MySQL 数据库类型都提供技术支持。由于两者都是开源的,因此在线上也有大量的信息和资源免费提供。尽管 MariaDB 的使用不如 MySQL 那么广泛,但它仍然非常受欢迎。由于 MariaDB 实质上只是另一个 MySQL 安装,因此你的 settings.py 文件中 DATABASES 配置的 ENGINE 将与上一小节中标题为 MySQL 的示例相同。两者也将需要你安装相同的 mysqlclient 包,并将其包含在 requirements.txt 文件中。
Oracle
Oracle 数据库是由 Oracle 创建的多模型、对象关系型数据库管理系统。它主要用于在线事务处理(OLTP)和数据仓库。与其他网络开发数据库相比,这个数据库在使用和功能方面要复杂一些。它旨在用于企业网格计算,这是一种将一系列网络计算机组合起来作为一台更大的虚拟超级计算机的方法。Oracle 开发了一个数据库专门用于这些网络集群配置。它被认为是当今最复杂和最健壮的数据库系统之一,超出了本书的范围。本书中的大部分代码仍然可以与这种数据库类型一起使用,但可能需要进行一些修改。
按照以下步骤配置你的 Oracle 数据库连接:
-
可以使用以下示例建立典型的 Oracle 数据库连接:
# /becoming_a_django_entdev/settings.py ... DATABASES = { 'default': { 'ENGINE': 'django.db.backends.oracle', 'NAME': 'xe', 'USER': 'database_user', 'PASSWORD': 'database_password, 'HOST': 'database_host', 'PORT': 'database_port_#', } } -
Oracle 数据库类型还需要安装以下包,并将其包含在
requirements.txt文件中:# requirements.txt ... cx_Oracle
Oracle 还需要遵循额外的设置和配置步骤,并且它们在其网站上提供了指导:cx-oracle.readthedocs.io/en/latest/user_guide/installation.html。
SQL Server
这是一种 Django 支持的五种标准数据库类型之外的数据库类型,我们将提供一个示例。由微软开发的微软 SQL Server 数据库类型也是 MySQL 的一个分支,并且目前在微软的企业数据库系统中得到广泛应用。许多系统今天都依赖于这种数据库类型,它通常与 Azure 托管的应用程序结合使用。
按照以下步骤配置你的微软 SQL Server 数据库连接:
-
可以使用以下连接设置建立微软 SQL Server 数据库类型:
# /becoming_a_django_entdev/settings.py ... DATABASES = { 'default': { 'ENGINE': 'sql_server.pyodbc', 'NAME': 'database_name', 'USER': 'database_user', 'PASSWORD': 'database_password', 'HOST': 'database_host', 'PORT': '1433', 'OPTIONS': { 'driver': 'ODBC Driver 17 for SQL Server', }, } } -
此数据库类型还需要安装以下包,并将它们包含在你的
requirements.txt文件中。根据你的设置,你可能只需要以下三个包中的一个或两个;然而,添加所有三个也不会有害:# requirements.txt ... pyodbc django-pyodbc django-pyodbc-azure
接下来,我们将安装本书其余部分使用的类型,即 PostgreSQL。
PostgreSQL
PostgreSQL已成为大多数 Django 开发者首选的数据库,除非有特定原因需要使用其他选择。它被认为是一种关系型数据库管理系统(RDBMS),这是一种在相关表中存储面向对象数据的常用工具,这些表被称为对象。其主要特性包括ANSI SQL 兼容性和可扩展性,这意味着它是一种旨在让人们能够扩展的数据库类型。它将在每个主要操作系统上运行,如 Windows、Mac 和 Linux,使其具有多功能性。
标准 PostgreSQL settings.py
如果您在与 Heroku 之外的正常 Django 项目工作,请配置您的 PostgreSQL 数据库连接。
将以下设置添加到您的settings.py文件中:
# /becoming_a_django_entdev/settings.py
…
DATABASES = {
' 'defa'lt': {
' 'ENG'NE': 'django.db.backends.postgre'ql',
' 'N'ME': 'database_n'me',
' 'H'ST': 'localh'st',
' 'U'ER': 'database_u'er',
' 'PASSW'RD': 'database_passw'rd',
' 'P'RT': '5'32',
}
}
由于我们正在使用 Heroku,尽管我们仍然在每个远程 Heroku 环境中使用 PostgreSQL,但我们不能使用前面的设置。下一节将为我们提供特定于 Heroku 环境的设置和工具,以及如何在本地上使用 PostgreSQL 的其余说明。
为 Heroku 准备 PostgreSQL
本节专门用于配置每个环境,无论您是使用 IDE、命令行窗口还是终端。要安装和使用 PostgreSQL,我们需要在本地安装它,然后在每个远程环境中再次安装。
安装 PostgreSQL
本节将指导您在本地和远程机器上安装所需的 PostgreSQL 软件。
本地安装
要使用 PostgreSQL,我们需要在每台机器上安装一系列软件和驱动程序。在 Heroku 仪表板中,我们需要包括附加组件以在这些环境中安装 PostgreSQL。要在您的开发机器上安装它,您可以选择直接从出版商的网站下载您平台的安装程序,网站地址为:www.postgresql.org/download/。在安装过程中,请注意您设置的端口号、创建的密码以及是否要求您将任何内容添加到环境 PATH 变量中,请这样做!与手动配置相比,在安装期间检查/启用这些选项总是更容易。
在安装过程中,安装程序将询问您是否想要安装任何开发工具,例如数据库命令行工具或数据库管理工具。在安装您的 PostgreSQL 驱动程序时选择PgAdmin;我们将使用此工具在本章中演示几个示例。PgAdmin 管理工具用于访问和查看数据库表结构及其中的数据。PgAdmin 也与每个主要操作系统兼容,并且可以从 PostgreSQL 安装中单独下载和安装;您可以在他们的网站上找到它:www.pgadmin.org/download/。
在我们之前创建的本地存储库中找到的.env文件中,将DATABASE_URL=sqlite:///db.sqlite3替换为以下值:
# .env
DATABASE_URL=postgres://postgres:your_password@localhost:5432/local_postgresql
将your_password占位符替换为您在安装 PostgreSQL 和 PgAdmin 时输入的密码。如有需要,替换任何其他占位符。默认情况下,用户名通常是postgres,但在安装过程中可以更改为其他名称。在我们可以实际运行项目之前,我们需要使用数据库管理工具 PgAdmin 创建本地数据库。即使您之前使用 Visual Studio IDE 创建项目,现在也会出现错误。
本书代码附带了一个名为example.env的文件,您可以将其用作参考,以确保您的文件包含适合我们软件堆栈的正确设置。
远程安装 – Heroku 仪表板
在 Heroku 中,我们必须逐个将 PostgreSQL 添加到我们所有的三个远程环境中。有两种方法可以做到这一点:通过 Heroku 仪表板或 Heroku CLI。我们将演示这两种方法。导航到以下位置的 Heroku 仪表板:dashboard.heroku.com/apps。我们应该看到所有三个环境,也称为 Heroku 应用。点击每个应用,并在提供的搜索字段中导航到Heroku Postgres。接下来,它将询问您选择哪种计划;您可以使用名为Hobby-Dev的免费计划,这是本书演示中将要使用的计划。稍后,您可以决定是否升级并支付新的计划费用。对于更技术性的需求,这些计划包括增加流量支持、同时连接数、RAM 量、加密措施、增加安全性以及更多。您可以访问他们的计划页面了解更多他们提供的内容:elements.heroku.com/addons/heroku-postgresql。
如果成功,您将看到 Heroku 为我们自动创建了一个DATABASE_URL配置变量。您可以通过转到设置选项卡,然后在页面下方滚动到配置变量下找到这个变量。点击显示配置变量后,它也会出现。以下截图还显示,我们之前在本章中使用的 CLI 创建的相同SECRET_KEY变量也可以在这里找到。任何其他敏感的环境变量都可以以相同的方式创建并在这里找到。点击添加按钮可以消除使用 CLI 创建环境变量操作的需求:

图 2.9 – Heroku – DATABASE_URL
让我们通过使用 Heroku CLI 而不是 Heroku 仪表板来安装 PostgreSQL。
远程安装 – Heroku CLI
使用 Heroku CLI,我们首先需要在终端或命令行窗口中登录我们的账户。以下示例显示了如何在我们的每个远程环境中安装 PostgreSQL,从生产环境开始:
(virtual_env) PS > heroku login
(virtual_env) PS > heroku addons:create heroku-postgresql:hobby-dev
(virtual_env) PS > heroku addons:create heroku-postgresql:hobby-dev --app mighty-sea-09431
(virtual_env) PS > heroku addons:create heroku-postgresql:hobby-dev --app pure-atoll-19670
我们必须指定 Heroku 在我们创建这些环境时为我们生成的随机应用名称。如果我们需要指定我们想要使用的 PostgreSQL 版本,我们可以使用以下示例。如果不指定版本号,将使用可用的最新版本:
(virtual_env) PS > heroku addons:create heroku-postgresql:hobby-dev --version=10
而不是去您的 Heroku 仪表板查看这些变量,执行以下命令也会显示每个环境存在的变量列表。将以下示例中的每个应用名称替换为您自己的 Heroku 应用名称:
(virtual_env) PS > heroku config --app becoming_an_entdev
(virtual_env) PS > heroku config --app mighty-sea-09431
(virtual_env) PS > heroku config --app pure-atoll-19670
使用 PgAdmin 工具
PgAdmin 是我们将要使用的数据库管理工具,用于管理所有本地和远程数据库,以及创建我们需要连接的本地数据库。这个工具应该在 安装 PostgreSQL 子节中更早地安装过。如果没有安装,请重新查看该子节以获取更多信息。
创建本地数据库
在 postgres。在代码示例中引用我的本地数据库时,我给定的数据库名称是 local_postgresql。

图 2.10 – PgAdmin – 创建本地数据库
接下来,我们将连接到远程数据库。
连接到远程数据库
对于每个远程数据库,我们都需要在 PgAdmin 工具中添加一个新的服务器连接。
按照以下步骤连接到远程数据库:
- 首先从您的 Heroku 账户收集一些信息。在您的 Heroku 仪表板中,在每个 Heroku 应用下,导航到 资源 选项卡,并在 附加组件 下,点击如下截图所示的 在新标签页中打开 图标:

图 2.11 – Heroku – 在新标签页中打开数据库
- 在新打开的浏览器标签页中,导航到 设置 选项卡,并记下提供的信息。以下截图中的高亮字段是我们将在 PgAdmin 工具内部建立连接所需的重要信息。请记下它们:

图 2.12 – Heroku – 数据库信息
- 打开 PgAdmin,并从该应用的顶部导航栏中,在 对象 选项卡下,选择 创建 | 服务器。在打开的窗口中,在 常规 选项卡下,输入您选择的任何名称。这是为了仅用于本地参考来命名您的服务器连接。命名它们,以便您知道哪个环境是哪个,如下面的截图所示:

图 2.13 – PgAdmin – 创建 - 服务器
- 此任务仅在 PgAdmin 工具中创建您的服务器连接;它并不是创建数据库服务器本身。使用在图 2.12中找到的信息,在连接选项卡下填写相应的字段,如下面的截图所示:

图 2.14 – PgAdmin – 创建服务器连接
-
将标记为维护数据库的字段中的值替换为 Heroku 提供的数据库名称;对用户名也做同样的操作。
-
如果成功,您现在将在PgAdmin应用的浏览器面板中看到您的新服务器连接。浏览器面板通常位于 PgAdmin 程序的左侧。由于我们使用的是名为Hobby-Dev的免费计划,我们将在这个列表中看到成百上千的其他数据库。除了一个之外,所有其他数据库都将被灰色显示,就像以下截图所示:

图 2.15 – PgAdmin – 查看表
在图 2.12中标记为颜色的数据库就是您的数据库名称,如图 2.12所示。展开您的数据库,您应该能够访问;尝试打开任何其他数据库将导致权限被拒绝。您可以通过购买 Heroku 提供的专用数据库托管计划来避免这种共享计划,这要安全得多。
现在,将您的文件更改推送到您的 Git 开发仓库,您应该会看到您的 Heroku 应用成功部署。
接下来,我们将调整我们的环境连接设置以在本地使用 PostgreSQL。
环境连接设置
对于我们的 Heroku 配置,我们需要在settings.py文件中使用以下示例定义一个默认数据库,而不是使用标准的 PostgreSQL 连接设置。
要为 Heroku 和本地使用配置 PostgreSQL,请按照以下步骤操作:
-
将您的
DATABASES变量更改为使用以下代码:# /becoming_a_django_entdev/settings.py ... DATABASES = { 'default': dj_database_url.config( conn_max_age=600 ) }
您也可以通过调整conn_max_age的值来增加已建立数据库连接允许存在的时长。默认值是600秒,相当于 10 分钟。由于dj_database_url模块将尝试使用settings.py文件登录 Heroku。
-
在我们之前在这个文件底部放置的
django_heroku.settings(locals())行下面放置以下代码;它应该看起来像以下示例:# /becoming_a_django_entdev/settings.py ... django_heroku.settings(locals()) options = DATABASES['default'].get('OPTIONS', {}) options.pop('sslmode', None)
就这些了。让我们接下来在本地构建初始表结构。
构建初始表结构
接下来,我们需要创建与项目中的模型和任何第三方包相关的表结构。这些操作可以使用 Visual Studio IDE 或通过终端或命令行窗口执行。从以下选项中选择您的方法。
使用 IDE
在你的 IDE 中,你需要执行三个操作:创建迁移、迁移和创建超级用户,按照这个顺序。在 Visual Studio IDE 中,转到你的 解决方案资源管理器,然后右键单击你的项目名称。在弹出的菜单中,在 Python 下选择 Django 创建迁移,然后选择 Django 迁移…,如图所示。然后,选择 Django 创建超级用户 操作:


图 2.16 – Visual Studio – manage.py 命令
接下来,我们将使用命令行驱动的方法。
命令 – makemigrations 和 migrate
如果你没有使用 IDE 或者决定在 IDE 之外运行你的项目,这些命令就是为你准备的。确保你已经激活了你的虚拟环境,并且你位于与你的 manage.py 文件相同的目录中。使用终端或命令行窗口,通过以下两个示例在你的本地数据库中创建你的表结构:
(virtual_env) PS > python manage.py makemigrations
(virtual_env) PS > python manage.py migrate
由于我们还没有创建任何模型,这两个命令将只创建默认的 User Auth 和其他所有 Django 安装的标准 Django 管理模型。
命令 – createsuperuser
第一次创建数据库表时,我们需要创建一个超级用户,这样我们才能成功访问管理面板。额外的用户可以在 Django 管理面板内部创建,或者通过再次执行以下代码片段中的命令来创建。现在运行此命令以创建超级用户:
(virtual_env) PS > python manage.py createsuperuser
接下来,当被提示这样做时,输入它要求你提供的用户名、电子邮件地址和密码。记住这些信息,因为你将需要它们来访问在 第六章 中介绍的 Django 管理站点,探索 Django 管理站点。
远程数据迁移
执行 远程数据迁移意味着运行与我们为本地数据库执行的相同迁移命令,但针对每个远程数据库。
为了手动为每个我们创建的远程环境运行迁移命令,我们首先需要为每个环境激活 Bash shell。按照以下步骤进行操作:
-
登录你的 Heroku 账户,然后逐个执行以下命令,每个环境一次。注意,在启动每个 shell 之后,你应该遵循以下步骤,然后再启动下一个 shell:
(virtual_env) PS > heroku login (virtual_env) PS > heroku run bash --app becoming-an-entdev (virtual_env) PS > heroku run bash --app mighty-sea-09431 (virtual_env) PS > heroku run bash --app pure-atoll-19670 -
对于每个你为它激活了 Bash shell 的环境,一旦它加载完成,你会看到你的命令行现在以美元符号字符(
$)开头。你也会注意到,无论你之前在哪个目录中,它都会把你带到你的.git文件夹现在所在的目录。使用cd命令导航到你的manage.py文件所在的位置,然后运行以下迁移命令,就像我们为本地数据库所做的那样:$ cd becoming_a_django_entdev $ python manage.py makemigrations $ python manage.py migrate -
输入
exit并等待它从每个 shell 中退出,然后进入下一个环境。
请记住,所有这些命令所做的只是迁移您的表结构;它们实际上并没有迁移那些表中的数据。
接下来,我们将练习使用 Heroku 提供的推送/拉取操作来迁移数据和表结构,从而无需在 Bash shell 中运行这些命令。
Heroku 数据库推送/拉取操作
Heroku 内置了将数据推送到和从指定数据库的功能。这些命令将允许我们合并数据和表结构。有了这个,我们可以在本地运行makemigrations和migrate命令,用示例数据填充我们的数据库,然后将它推送到我们的其他环境,从而无需在每个远程环境中单独运行这些命令。您应该非常小心地执行这些任务,因为执行错误的操作时,完全有可能覆盖或完全丢失数据。好消息是,每当执行这些命令时,Heroku 都会自动生成数据库当前状态的数据备份,让您能够撤销任何已执行的操作。
按照以下步骤执行您的操作:
-
要直观地看到正在发生的变化,请导航到包含添加或删除表的
push或pull操作。 -
使用我们从图 2.12页面收集的信息,执行以下命令以推送您的数据库,包括一个参数来指示我们正在推送到的环境:
(virtual_env) PS > heroku login (virtual_env) PS > PGUSER=postgres PGPASSWORD=password heroku pg:push local_postgresql postgresql-tetrahedral-62413 --app mighty-sea-09431
确保您使用的是创建本地 PostgreSQL 数据库时指定的用户名和密码。将postgresql-tetrahedral-62413替换为--app参数标签的内容。local_postgresql是我为我的本地数据库使用的名称。
您现在应该能够看到Auth和 Django 管理表中有总共 10 个表,这些表是在我们首次在本地运行迁移命令时创建的。
Windows 用户注意事项
Windows 用户可能难以执行前面的命令。请确保您的 Windows 环境变量设置中包含了bin文件夹的路径。该文件夹通常位于 Windows 10 安装的C:\Program Files\PostgreSQL\##\bin,其中##是您机器上安装的 PostgreSQL 的数字版本号。这将使heroku pg:命令和psql命令在您的 CLI 中正常工作。在更改环境变量后,通常建议重启您的 CLI 或操作系统。以下命令将在您的 CLI 终端窗口打开期间设置变量。在 Windows 中将它们一起运行在一行上通常会导致错误。当这些变量设置好并且您的窗口保持打开状态时,您将能够根据需要运行您的push或pull命令:
(virtual_env) PS > heroku login
(virtual_env) PS > set PGHOST=localhost
(virtual_env) PS > set PGUSER=postgres
(virtual_env) PS > set PGPASSWORD=password
(virtual_env) PS > set PGPORT=5432
(virtual_env) PS > heroku pg:push local_postgresql postgresql-tetrahedral-62413 --app mighty-sea-09431
如果我们想要拉取数据而不是推送数据,那么这个命令与将push改为pull一样简单,如下面的示例所示:
(virtual_env) PS > heroku pg:pull local_postgresql postgresql-tetrahedral-62413 --app mighty-sea-09431
要查看有关如何使用 Heroku 命令行操作针对 PostgreSQL 的完整指南,请查看他们的知识库文章:devcenter.heroku.com/articles/heroku-postgresql。
摘要
到目前为止,我们已经做了很多工作,但我们还没有真正开始为项目构建任何 Django 应用。到目前为止所做的一切工作都可以被视为在将项目交给开发团队之前必要的初步工作。已经提供了两种创建项目的方法:一种方法使用一个工具来帮助简化生产,称为 IDE,另一种方法使用终端或命令行窗口中的命令。我们在仓库中跟踪一个解决方案文件,以便我们可以在团队内部共享它,但我们不会跟踪在运行项目时自动创建的个人设置和调试文件。即使与使用 IDE 的人共享项目配置文件,不使用 IDE 的开发者仍然可以与代码库一起工作。在那之后,我们在项目级别和数据库级别配置了 Django 以与主机提供商 Heroku 一起工作。最后,我们激活了允许开发者在本地或远程数据库中查看和编辑数据的工具。
现在,你可以将这个解决方案交给你的团队,并开始将任务委派给该团队中的每个成员。在下一章中,我们将开始创建在数据库内部构建表的模型。这些模型可以被视为在本书中第三章,“模型、关系和继承”之后构建其余章节的元素。
第三章:第三章:模型、关系和继承
模型代表数据库中的表,也称为对象。Django 提供了一种简单的方法将对象映射到项目的底层数据库(或数据库)。我们将使用这种映射系统在本书的后续章节中与其他 Django 组件一起工作,例如模板、视图或表单,仅举几例。任何依赖于从数据库内部访问数据的东西都将依赖于我们创建的模型。如果一个项目连接到外部数据库系统或项目使用 API 与数据交互,那么在这种情况下就不需要创建任何模型。
本章将涵盖以下内容:
-
编写模型类以创建数据库表
-
使用标准字段类型和第三方字段类型
-
配置字段验证器
-
通过字段关系链接表
-
与模型元类和选项一起工作
-
使用模型方法和方法装饰器
-
练习扩展模型
-
介绍使用 Django shell 作为执行查询和添加数据的工具
-
创建模型管理器以格式化和控制数据
技术要求
要使用本章中的代码,以下工具需要在你的本地机器上安装:
-
Python 版本 3.9 – 作为项目的底层编程语言
-
Django 版本 4.0 – 作为项目的后端框架
-
pip 包管理器 – 用于管理第三方 Python/Django 包
我们将继续使用在第二章“项目配置”中创建的解决方案进行工作。然而,并不需要使用 Visual Studio IDE。主要项目本身可以使用其他 IDE 或从项目根目录独立运行,使用终端或命令行窗口。这就是manage.py文件所在的位置。无论你使用什么编辑器或 IDE,都需要一个虚拟环境来与 Django 项目一起工作。有关如何创建项目和虚拟环境的说明可以在第二章“项目配置”中找到。你需要一个数据库来存储项目中的数据。在上一章的示例中选择了 PostgreSQL;然而,你可以为你的项目选择任何数据库类型来与本章中的示例一起工作。
本章中创建的所有代码都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Becoming-an-Enterprise-Django-Developer。本章中展示的大部分代码可以在/becoming_a_django_entdev/becoming_a_django_entdev/chapter_3/目录中找到。
查看以下视频以查看代码在行动:bit.ly/3zZ68RS
为本章做准备
首先,按照在第二章中讨论的步骤,在你的项目中创建一个名为chapter_3的新应用。正如该部分所讨论的,不要忘记将/becoming_a_django_entdev/becoming_a_django_entdev/chapter_3/apps.py文件中找到的你的应用类中的name =变量的值更改为指向你安装应用的位置。确保还将此应用包含在settings.py文件中的INSTALLED_APPS变量中。
编写模型类
你项目中的每个模型都代表数据库中的一个表。在这些模型中创建的字段都与该表中的列相关。Django 提供了一种称为settings.py文件的项目技术。ORM 技术是一种用于在两种不兼容的数据类型系统之间转换数据的过程。这意味着 Django 消除了直接使用结构化查询语言(SQL)进行查询的烦恼。Django ORM 在解释 SQL 时消除了各种数据库类型之间的差异,使其成为处理所有数据结构的通用工具。现在,你和你的开发者可以更多地专注于开发,而不是处理这些烦恼。Django 不要求使用 SQL 作为标准写作实践。然而,如果你想或需要这样做,Django 确实提供了一种在执行查询操作时使用基本 SQL 的方法。
接下来,假设我们正在构建一个允许用户查看提供特定汽车详细信息的页面的网站,通常称为详情页面/视图。让我们假设我们正在构建一个基本的网站,仅用于存储和查找待售汽车的详细信息。用户可能出于多种原因这样做;可能是租车、购买、租赁或出售汽车。在任何情况下,我们都需要一个表示车辆对象的表,另一个表示车辆型号的表(不要与 Django 模型混淆),以及另一个表示发动机类型的表。在现实世界的场景中,你的项目可能包含许多其他表,这些表的结构也可能在许多方面有所不同。在这个特定的练习中,我们不会为制造商创建模型,也称为车辆的型号。制造商将作为一个集合来创建,以展示某些教育目的的概念:
-
在刚刚创建的
chapter_3目录下的models.py文件中,编写三个空类,每个类对应于与我们的车辆练习相关的表格,按照以下示例进行:# /becoming_a_django_entdev/chapter_3/models.py from django.db import models class Vehicle(models.Model): pass -
为
Vehicle、VehicleModel和Engine创建一个类。我们将把与车辆模型相关的类命名为VehicleModel而不是Model,以防止我们在完成每个练习时产生混淆。小贴士
在前面的示例中,创建了三个类,通过直接在它们中写入
pass语句,目前它们什么也不做。这是一个工具,可以添加到您的工具箱中,用于编写骨架代码,并允许其他组件在您编写代码时继续运行。如果类中没有放置任何内容,Python 会给出错误;当您准备好为该类编写代码时,请删除pass语句,并用您的实际代码替换它。
分配给模型类的名称几乎可以是您想要的任何名称,但不能是 Python 的保留关键字,例如 True、False、class、pass 和 import。在您的代码中其他地方使用时,请为您的类命名任何有意义的名称。您可以使用大写或小写字母,但 Django 创建您的表时,名称将始终是小写的。这就是为什么不允许用不同的字母大小写来命名两个不同的类。例如,如果我们命名两个类 class Vehicle 和 class vehicle,当 Django 尝试进行迁移时,会告诉我们以下 RuntimeError:
RuntimeError: Conflicting 'vehicle' models in application 'chapter_3':
因此,最好采用一种使用一个或另一个字母大小写的写作风格,并在整个项目中坚持这种模式。
让我们讨论现有的各种字段类型,并看看哪些我们可以用于我们的车辆练习。
标准字段类型
开箱即用,Django 提供了众多可供选择的标准字段类型。以下表格可以在编写模型时作为速查表使用:


若要详细了解所有可选择的字段类型,您可以访问官方 Django 文档中的字段类型部分,链接如下:docs.djangoproject.com/en/4.0/ref/models/fields/.
字段参数
每个字段类都会在我们的字段上经常接受 verbose_name、blank 和 null 参数。verbose_name 将指定字段的可读名称。对于任何我们使用的 CharField 实例,我们应该指定一个 max_length 参数,以限制该字段的字符长度。如果没有设置 max_length 参数,理论上字符长度限制是无限的。然而,您受到所使用数据库的物理限制;限制将是几千或几十万个字符,而不是字面上的无限限制。null 和 blank 参数几乎被认为是同一件事,除了 null 参数表示数据库可以为任何空记录存储一个空值。blank 参数与在数据库级别对该字段进行的验证相关,以检查用户在保存或创建数据库中的该记录时是否尝试输入空值。
default参数将用于在创建或更新对象时未提供值的情况下,默认分配一个值给字段。默认值用于数据从外部源迁移到您的数据库的情况,其中数据与您在表上设置的约束不一致。例如,如果您有一个字段参数设置为null=False,并且导入了包含空值的数据,则可能会出现错误。假设您有一个带有default=True/False参数的BooleanField,然后执行相同的数据导入;那么,所有这些空值将在导入过程中自动转换为True/False。default参数几乎可以应用于任何字段类型。
choices参数允许我们传递一个包含值和该值的人类可读解释的预定义选择集。选择可以在CharField和BooleanField实例上使用,以及一些其他字段类型。它们可以被制作成下拉选择框,或者用于一组复选框或单选按钮。choices参数接受一个元组的列表,其中每个元组的第一个元素是字段的值,第二个元素是该值的人类可读字符串表示。在这个练习中,我们将把人类可解释的是/否选择转换为计算机可解释的真/假值,在BooleanField上。我们还将在此章的后面部分应用这种技术,以便在制造商/制造字段上存储整数值。
另一个可能很有用的论点是editable=False属性。这个属性会在模板中的任何表单对象中隐藏字段。用户将无法以任何方式查看或编辑该字段。unique=True参数也可能很有用。这个参数表示表中两行/记录在该特定字段上不能有相同的值。例如,如果将电子邮件字段用作模型的唯一标识符,可以防止重复的存在。在保存或创建新记录时,我们可能会得到冲突的结果,错误信息会告诉我们电子邮件地址已存在。
模型字段参数与表单字段参数不同,因为模型参数将在数据库级别应用规则到您的列上。我们将在第五章“Django 表单”中介绍您的表单字段参数,这些参数是仅适用于特定表单中字段的规则。这意味着您可以在模型字段上设置blank=True,使其在数据库级别不是必需的,但在required=True的表单字段集中是必需的,使其对该特定表单是必需的。或者,我们可以通过在数据库级别使用blank=False设置约束,使该字段在所有表单中都是必需的。在编写模型类时请记住这一点。
接下来,让我们开始编写我们车辆场景所需的各个字段。
添加标准字段类型
从前面提供的 Django 字段列表中,我们将为每个模型使用 CharField。CharField 将用于在将每个项目添加到数据库时提供名称,除了 Vehicle 模型类。对于 Vehicle 模型,我们将使用 CharField 作为 BooleanField 在 Vehicle 模型上,以存储一个值,表示该车辆是否已售出。这就是我们将创建一个 是/否 选项列表来代替 true/false 值的字段。
按照以下步骤为每个模型类创建字段。记住要删除 pass 语句:
-
在你的
/chapter_3/models.py文件中,在你的模型类上方和import语句下方创建上述 是/否 选项集,如图所示:# /becoming_a_django_entdev/chapter_3/models.py from django.db import models YESNO_CHOICES = ( (True, 'Yes'), (False, 'No') ) ... -
将以下字段添加到你的
VehicleModel类中:# /becoming_a_django_entdev/chapter_3/models.py ... class VehicleModel(models.Model): name = models.CharField( verbose_name = 'Model', max_length = 75, unique = True, blank = True, null = True, ) -
将以下字段添加到你的
Engine类中:# /becoming_a_django_entdev/chapter_3/models.py ... class Engine(models.Model): name = models.CharField( verbose_name = 'Engine', max_length = 75, blank = True, null = True, ) -
将以下字段添加到你的
Vehicle类中:# /becoming_a_django_entdev/chapter_3/models.py ... class Vehicle(models.Model): vin = models.CharField( verbose_name = 'VIN', max_length = 17, unique = True, blank = True, null = True, ) sold = models.BooleanField( verbose_name = 'Sold?', choices = YESNO_CHOICES, default = False, blank = True, null = True, )
之前显示的 VehicleModel 类上的 name 字段将使用 unique 参数,只允许在该表中不存在的名称。Engine 类上的 name 字段将不会使用 unique 参数,因此我们可以允许具有相同名称的引擎,但将它们分配给不同的车辆模型。YESNO_CHOICES 变量用作全局变量,放置在任何模型类之外,以便在需要时可以在多个字段中使用。如果变量或选项集非常独特,那么将它们放置在字段声明上方,在使用的模型类中是最佳实践。你也可以将这些变量存储在一个完全独立的文件中,只要代码保持整洁简单。
注意
上述代码是 PEP-8 风格指南的宽松风格,我们将每个参数放在单独的一行上,用逗号分隔,同时遵循基本的 Python 缩进规则。本书中的大部分代码将以这种方式编写。
-
运行 Django 的
makemigration和migrate命令,如下所示:(virtual_env) PS > python3 manage.py makemigrations Migrations for 'chapter_3': becoming_a_django_entdev\chapter_3\migrations\0001_initial.py - Create model Engine - Create model Vehicle - Create model VehicleModel (virtual_env) PS > python3 manage.py migrate Operations to perform: Apply all migrations: admin, auth, chapter_1, chapter_3, contenttypes, sessions Running migrations: Applying chapter_3.0001_initial... OK
每当模型被更改、创建或删除时,都需要执行这些 Django 迁移命令以防止运行时错误。这些命令可以从你的 IDE 或命令行或终端窗口中执行,在项目根目录下执行,其中包含你的 manage.py 文件。参考第二章,项目配置部分,标题为构建初始表结构的子部分,了解更多关于执行这些命令的不同方法。
如果这两个 Django 迁移命令执行成功,你的数据库中将会创建三个额外的表。从 PgAdmin 工具或任何其他你决定使用的数据库管理工具中查看,这些表将类似于以下截图所示:

图 3.1 – PgAdmin – 创建车辆、模型和引擎表
如果我们需要使用 Django 标准中不包含的字段,例如AddressField、MoneyField或PhoneField,我们必须安装包并配置设置,然后才能使用它们。让我们准备我们的项目,以便通过将这些示例中的MoneyField集成进来,做到这一点。
第三方字段类型
django-address包使用 Google Maps API 在用户输入单个文本字段时建议与用户输入相关的地址。这意味着该包提供了模型类以及它创建的表单字段类和其他相关工具。相关工具包括所有帮助表单工作的 JavaScript 和 CSS 库。django-image-cropping工具也非常强大:它允许用户上传图片,并让用户按照自己的意愿裁剪图片。
有一个专门用于处理货币的包叫做django-money。这个包从标准字段类型列表中取出了DecimalField,并提供了许多涉及金融行业接受方式的金钱操作。django-money包为今天存在的所有货币提供了定义,并包括它们对应的货币符号。除了执行加法、减法等算术运算外,货币还可以使用当前汇率从一种符号转换为另一种符号。这意味着这个包将与 API 通信以检索该信息。
django-phone-field包可以接受相当灵活的电话号码。电话字段允许使用国家代码,并接受特殊字符,以便可以将其掩码为所需的任何格式。django-ckeditor包是一个用于向您的页面添加富文本编辑器的工具,允许用户将 HTML 输入到您的表单字段之一。如果您使用的是 django CMS,他们还提供了一个专门用于与 django CMS 包一起使用的 django-ckeditor 包版本。
这里是一个简短的第三方字段类型列表,您可以将其添加到之前的字段类型速查表中:
-
AddressField–pypi.org/project/django-address/ -
ImageCropField–pypi.org/project/django-image-cropping/ -
MoneyField–pypi.org/project/django-money/ -
PhoneField–pypi.org/project/django-phone-field/ -
RichTextField–pypi.org/project/django-ckeditor/ -
(django CMS)
RichTextField–pypi.org/project/djangocms-text-ckeditor/
接下来,我们将向我们的Vehicle模型类中添加MoneyField。
添加第三方字段类型
由于一些字段,如 AddressField,需要直接从 Google 获取个人 Google API 密钥,因此我们不会使用该字段类型。我们只会演示使用这些第三方字段类型之一,然后继续下一个主题。
要在项目中包含 MoneyField,请按照以下步骤操作:
-
将
django-money添加到你的requirements.txt文件中,并在你的虚拟环境中安装它,或者运行以下命令手动安装此包。确保你的虚拟环境已经激活:(virtual_env) PS > pip install django-money -
在你的
settings.py文件中,将以下应用添加到你的INSTALLED_APPS列表中:# /becoming_a_django_entdev/settings.py ... INSTALLED_APPS = [ ..., 'djmoney', 'becoming_a_django_entdev.chapter_3', ]
确保它位于你的本地应用之上,并在所有 django.contrib 应用之下,如前所述,它放置在 chapter_3 应用之前。
-
使用
MoneyField,你可以在settings.py文件中指定项目中可用的不同货币。在以下示例中,我们指定美元和欧元为该项目中可用的两种货币:# /becoming_a_django_entdev/settings.py ... CURRENCIES = ('USD', 'EUR') CURRENCY_CHOICES = [ ('USD', 'USD $'), ('EUR', 'EUR €') ] -
将以下高亮的
import语句添加到你的/chapter_3/models.py文件顶部:# /becoming_a_django_entdev/chapter_3/models.py from django.db import models from djmoney.models.fields import MoneyField -
在你的
Vehicle类中,添加以下价格字段:# /becoming_a_django_entdev/chapter_3/models.py ... class Vehicle(models.Model): ... price = MoneyField( max_digits = 19, decimal_places = 2, default_currency = 'USD', null = True, )
你还需要使用前面示例中描述的字段参数(根据需要调整值)。
让我们探索我们可以应用于我们正在使用的字段类型的验证。
模型字段验证器
字段验证器是数据库级别的规则,可以在模型字段上设置。在需要使用 DecimalField 而不是使用 max_length 参数来控制字符长度的情况下,它们非常有用。我们定义一个最小或最大数值。如果不符合该标准,Django 将抛出 ValidationError。由于这是一个可调用的对象,你可以编写自己的函数来满足你的需求,或者使用 Django 内置的许多不同的可调用函数之一。例如,当指定最小或最大数值约束时,我们可以使用 MinValueValidator 和 MaxValueValidator 函数。你可以在以下位置查看 Django 提供的完整验证器函数列表:docs.djangoproject.com/en/4.0/ref/validators/。
设置字段验证器
MoneyField 提供了一些自己的字段验证器,这些验证器对项目中定义的任何类型的货币值添加了约束。货币验证器简化了使用 Django DecimalValidator 或编写自己的可调用方法的过程。
按照以下步骤在现有的 Vehicle 模型上设置验证器:
-
在你的
/chapter_3/models.py文件顶部,并在现有的import语句下方,添加以下import语句:# /becoming_a_django_entdev/chapter_3/models.py ... from djmoney.models.validators import MaxMoneyValidator, MinMoneyValidator -
不论是标准的 Django 字段验证器还是由第三方包提供的验证器,它都将作为字段参数传入,如下所示,这是在
Vehicle模型的price字段中:# /becoming_a_django_entdev/chapter_3/models.py ... class Vehicle(models.Model): ... price = MoneyField( max_digits = 19, decimal_places = 2, default_currency = 'USD', null = True, validators = [ MinMoneyValidator( {'EUR': 500, 'USD': 400} ), MaxMoneyValidator( {'EUR': 500000, 'USD': 400000} ), ])
这些函数通常自己接受一个或多个参数。在这个例子中,我们指定了最小和最大货币值的参数。前面的例子指出,所有欧元(EUR)值应在 500 到 500,000 欧元之间,美元值(USD)应在 400 到 400,000 美元之间。这些只是用于演示的粗略估计,并不代表确切的转换率。
目前,我们的三个模型类彼此独立存在。这意味着它们目前还没有以任何方式相互链接。在我们完成之前,我们需要使它们相互关联。接下来,我们将通过处理模型字段关系来链接这些表。
处理模型字段关系
Django 提供了三种关系类型用于链接表:
-
多对一关系
-
多对多关系
-
一对一关系
使用ForeignKey字段定义一个多对一关系,其他两种关系类型则使用自解释的ManyToManyField和OneToOneField定义。这些字段根据它们所代表的关系类型适当地命名。
接下来,我们将讨论处理模型字段关系的关键组件。
字段参数
三种字段类型,ForeignKey、ManyToManyField和OneToOneField,都接受其他字段类型接受的标准的default、blank和verbose_name字段参数。null参数对ManyToManyField没有影响,只会应用于ForeignKey和OneToOneField类型。这两种字段类型中的两种——ForeignKey和OneToOneField——至少需要两个位置参数,第一个是字段相关的模型类,第二个是on_delete参数。位置参数意味着它们需要按顺序排列,必要参数意味着它们必须指定。on_delete参数指定了当删除父或子对象时,数据库将如何处理相关表中的记录。
on_delete选项包括以下内容:
-
models.CASCADE– 当从该表中删除对象时,用于自动删除任何相关对象。 -
models.PROTECT– 用于防止删除任何对象。 -
models.RESTRICT– 用于在特定场景中防止删除。 -
models.SET_DEFAULT– 用于将相关对象的字段设置为默认值。 -
models.SET_NULL– 用于将相关对象的字段设置为空值。 -
models.SET()– 接受一个可调用的函数,用于设置值。 -
models.DO_NOTHING– 不会采取任何行动;使用此选项可能会导致IntegrityError,应谨慎使用。
我们将 on_delete 参数的值设置为 models.CASCADE,确保如果从数据库中删除一个 Vehicle,相关的 VehicleModel 和 Engine 对象将不会受到影响。但如果从数据库中删除一个 VehicleModel 或 Engine 对象,依赖于即将删除的对象的相关 Vehicle 也会被删除。如果我们想在删除其中任何一个的情况下保留 Vehicle,我们应该使用 models.SET_DEFAULT 值。
三个字段类型—ForeignKey、ManyToManyField 和 OneToOneField—都允许我们在执行查询时向前和向后跟踪关系,这意味着如果你查询一个父对象,你可以跟随这个查找向前以获取所有子对象。反向查找意味着你查询一个子对象,并跟随其查找向后以获取其父对象。这些向前和反向关系是通过使用 related_name 和 related_query_name 参数在字段上定义的,这将在下面演示。
ForeignKey 字段和 ManyToManyField 可以接受一个 limit_choices_to 参数,该参数对相关查询应用一个过滤器。limit_choices_to 参数将接受一个字典或 Q 对象。它还可以接受一个可调用的函数,该函数返回一个字典或 Q 对象。
注意
filter()、all() 或 order_by() 语句无法提供。要了解更多关于使用 Q 对象进行复杂查找的信息,请访问官方 Django 文档:docs.djangoproject.com/en/4.0/topics/db/queries/#complex-lookups-with-q-objects。要了解更多关于执行查询的一般信息,请跳转到本章后面的标题为 执行查询 的部分。
继续使用我们的车辆类,我们可以将这些模型关系和参数应用到我们已编写的类中。它们将帮助我们将一个假设的 Seller 与 Vehicle、Vehicle 与 VehicleModel 以及 VehicleModel 与 Engine 相关联。
字段类型 – ForeignKey
我们将使用 ForeignKey 字段来表示 Vehicle 类与 VehicleModel 和 Engine 类之间的 多对一 关系。
按照以下步骤创建你的 ForeignKey 字段:
-
在你的
/chapter_3/models.py文件中,将以下两个字段添加到现有的Vehicle模型类中,如下所示:# /becoming_a_django_entdev/chapter_3/models.py ... class Vehicle(models.Model): ... vehicle_model = models.ForeignKey( VehicleModel, on_delete = models.CASCADE, verbose_name = 'Model', related_name = 'model_vehicle', blank = True, null = True, ) engine = models.ForeignKey( Engine, on_delete = models.CASCADE, verbose_name = 'Engine', related_name = 'engine_vehicle', blank = True, null = True, ) -
在你的
/chapter_3/models.py文件中,将以下字段添加到现有的Engine模型类中,如下所示:# /becoming_a_django_entdev/chapter_3/models.py ... class Engine(models.Model): ... vehicle_model = models.ForeignKey( VehicleModel, on_delete = models.CASCADE, verbose_name = 'Model', related_name = 'model_engine', blank = True, null = True, ) -
现在,再次运行你的 Django 迁移命令,如在第二章的子节标题为 构建初始表结构 中所述,项目配置。你可以看到我们创建的两个字段,
vehicle_model和engine,现在显示在我们的数据库管理工具的列列表中:

图 3.3 – pgAdmin – ManyToManyField
我们还应该看到任何自动创建的附加表,这些表用于管理 Seller 和 Vehicle 之间的关系。该表在先前的屏幕截图中被显示为 chapter_3_seller_vehicle。
可变对象与不可变对象
可变性 是 Python 语言的一个基本概念,分为可变对象和不可变对象。如果一个对象的价值可以随时间改变,那么这个对象就被说成是 可变的。如果一个对象的价值不会改变,那么这个对象就被说成是 不可变的。在 Python 中,一个对象的可变性也由其数据类型定义。例如,可变对象使用 list、dictionary、set 或 QuerySet 来表示。不可变对象使用 bool、decimal、float、int、range、string 和 tuple 数据类型来定义。如果被搜索的对象是不可变的而不是可变的,查询将表现得更好。大多数时候,这种差异微乎其微,实际上在纳秒或毫秒级别。当你的项目上线并且数据库开始收集成千上万,甚至数百万条记录时,查询所需的时间将会在它需要几秒钟,甚至几分钟或十几分钟来完成单个查询时被注意到。
例如,我们可以使用元组对象将一组选择表示为 PositiveIntegerField,将可读的字符串表示与数值整数关联。以本章前面提到的车辆制造商为例。除非我们需要存储其他相关联的信息,或者有项目需求表明用户应该能够添加/编辑这些选择,否则我们实际上不需要一个表来存储这些信息。
通过以下步骤可以将这些值作为不可变数据类型硬编码:
-
在
/chapter_3/models.py文件中,在你的模型类上方和import语句下方添加以下设置:# /becoming_a_django_entdev/chapter_3/models.py ... MAKE_CHOICES = ( (1, 'Buick'), (2, 'Cadillac'), (3, 'Chevrolet'), ... ) -
将该集合用作你的
Vehicle类中make字段的choices参数的值,如下所示:# /becoming_a_django_entdev/chapter_3/models.py ... class Vehicle(models.Model): ... make = models.PositiveIntegerField( choices = MAKE_CHOICES, verbose_name = 'Vehicle Make/Brand', blank = True, null = True, )
接下来,让我们讨论一下 Meta 子类是什么以及它是如何被用来控制模型行为,甚至比我们之前所做的一切都要多的。
使用 Meta 子类
Meta。这不是必需的,完全可选,但它确实使得在模型中包含它时使用 Django 更加有用。元数据提供了所有在模型字段参数中未定义的“其他”信息。在这个类内部定义的设置被称为 元选项,有很多可供选择。在接下来的几节中,我们将介绍一些最常用的选项以及它们如何有所帮助。所有选项的完整分解可在以下链接中找到:docs.djangoproject.com/en/4.0/ref/models/options/。
Meta 选项 – verbose_name 和 verbose_name_plural
我们可以使用 verbose_name 和 verbose_name_plural 选项来指定在 Django 管理站点区域中使用的可读文本,或者在我们稍后编写的代码中查找时使用的文本。我们将在 第六章 中介绍 Django 管理站点,探索 Django 管理站点。
要将这些选项添加到你的模型类中,使用名为 VehicleModel 的类,将这些详细选项设置为 Vehicle Model 和 Vehicle Models,如图所示:
# /becoming_a_django_entdev/chapter_3/models.py
...
class VehicleModel(models.Model):
...
class Meta:
verbose_name = 'Vehicle Model'
verbose_name_plural = 'Vehicle Models'
现在,在你的代码和 Django 管理站点中,这些值将被用作你的对象(复数)的单数和复数表示。
Meta 选项 – 排序
当获取对象列表时,使用 ordering 选项。如果没有指定其他排序规则,则默认情况下,此设置将接受一个或多个字段作为参数进行排序。如果没有放置破折号(–)字符,则将按升序排序;如果使用了破折号,则结果将按降序显示。
要将此选项添加到你的模型类中,我们可以按 name 字段对 VehicleModel 类进行升序排序,然后再按降序排序,如下面的代码所示:
# /becoming_a_django_entdev/chapter_3/models.py
...
class VehicleModel(models.Model):
...
class Meta:
...
#ordering = ['name', 'secondary_field',]
ordering = ['-name']
上述代码块中注释掉的第一个示例显示我们可以按额外的字段排序,字段之间用逗号分隔,也可以按升序排序。上述代码块中的最后一个示例描述了按字母顺序降序排序结果,从 Z 到 A。
元选项 – 索引
indexes选项与一个称为indexes元选项的标准数据架构概念相关。
按照以下步骤将此选项添加到你的模型类中:
-
要在
VehicleModel类的名称字段上创建索引,可以写成如下所示:# /becoming_a_django_entdev/chapter_3/models.py ... from django.db.models.functions import Lower ... class VehicleModel(models.Model): ... class Meta: ... indexes = [ models.Index(fields=['name']), models.Index( fields = ['-name'], name = 'desc_name_idx' ), models.Index( Lower('name').desc(), name = 'lower_name_idx' ) ]
上述示例创建了三个独立的索引,一个用于升序排列的名称,一个用于降序排列,另一个用于仅小写字母的名称的升序排列。
-
接下来,再次运行你的 Django 迁移命令。在你的命令行或终端窗口中,应该出现以下消息:
- Create index chapter_3_v_name_055414_idx on field(s) name of model vehiclemodel - Create index desc_name_idx on field(s) -name of model vehiclemodel - Create index lower_name_idx on OrderBy(Lower(F(name)), descending=True) on model vehiclemodel
如果我们不指定name=属性,如前一个示例中的第一个索引未做的那样,Django 将使用其默认命名约定来命名它。这就是前一个示例中第一个索引消息的结果,即名称为chapter_3_v_name_055414_idx。前一个示例从django.db.models.functions库中导入了一个名为Lower的类。Lower类允许我们在前一个代码块中的最后一个索引上创建所有小写字符表示的name字段的索引。Django 提供了许多数据库函数,这些函数的完整说明可以在官方 Django 文档中找到:docs.djangoproject.com/en/4.0/ref/models/database-functions/。
每个表的索引通常在数据库管理工具中显示。例如,在 PgAdmin 中,从浏览器选项卡中导航数据树以找到chapter_3_vehiclemodel索引。这是一个非常深的导航:转到PostgreSQL 13 | 数据库 | local_postgresql | 模式 | public | 表 | chapter_3_vehiclemodel | 索引,你应该能看到你的索引,如下面的截图所示:

图 3.4 – PgAdmin – 模型索引
元选项 – db_table
有时,一个项目可能有如此多的模型,它们开始相互冲突,或者管理起来变得过于混乱。db_table选项用于指定数据库中的表名。如果此选项未设置,则 Django 默认将使用{{ app_name }}_{{ model_name }}命名约定来命名你的表。我们可以使用此选项在特定情况下指定一个独特的表名,该表名与默认命名约定不同。
例如,让我们创建一个名为engine2的新类,这次使用小写。这样,我们知道小写类是为了辅助练习,与主要类分开,主要类将以首字母大写命名。在这里,我们将添加之前在本章编写模型类部分中提到的RuntimeError。
按照以下步骤将此选项添加到你的模型类中:
-
在
/chapter_3/models.py文件中,创建engine2类并将Engine类中的名称字段复制到其中,如下所示:# /becoming_a_django_entdev/chapter_3/models.py ... class engine2(models.Model): name = models.CharField( verbose_name = 'Engine', max_length = 75, blank = True, null = True, ) -
创建
Meta子类并设置db_table选项,如图所示。不要在Engine类上使用此选项:# /becoming_a_django_entdev/chapter_3/models.py ... class engine2(models.Model): ... class Meta: db_table = 'chapter_3_practice_engine'
将db_table的值设置为'chapter_3_practice_engine',如图所示。
- 然后,再次运行你的 Django 迁移命令。在你的数据库管理工具,如 PgAdmin 中,
chapter_3表应该看起来类似于以下内容:

图 3.5 – PgAdmin – db_table 选项
我们可以查看engine2模型类。另一个你可能经常使用的有用元选项是抽象选项。这个选项主要用于扩展模型类,最好在本章标题为扩展模型的部分中稍后解释。
在扩展模型类之前,让我们先探索使用模型方法和方法装饰器。
定制模型
模型方法是写在模型类内部的定制函数,为表中的单个记录提供附加功能。它们让我们能够创建自己的业务逻辑,并按需格式化字段数据。Django 为我们提供了几个默认方法,我们也可以编写自己的定制方法。定制方法可以将字段组合起来,并从这些两个或更多字段中返回派生数据。装饰器有时与模型方法结合使用,以提供更多的功能。
一些方法可以在对象在数据库级别保存和/或删除时执行特殊操作。其他方法用于查询执行或模板内渲染对象时使用。我们将讨论 Django 提供的一些方法,并演示它们的用法。要了解使用 Django 模型方法的全部功能,请访问他们的文档,网址为docs.djangoproject.com/en/4.0/topics/db/models/#model-methods。
编写方法
编写模型方法类似于编写Meta子类,除了我们现在是在该类内部使用def关键字编写一个函数,而不是编写一个类。
这里定义了四个最有帮助且最常用的方法:
-
def save(self, *args, **kwargs)– 用于覆盖此模型在数据库级别的保存操作。你可以通过调用此方法在保存之前或之后注入自己的逻辑。 -
def delete(self, *args, **kwargs)– 这与save方法类似,不同之处在于你可以在对象在数据库级别被删除之前或之后添加自己的逻辑。 -
def get_absolute_url(self)– Django 用来制定该对象的规范 URL。这用于重新定义 Django 创建这些对象 URL 结构的默认行为。这也是在 Django 管理站点中访问此对象所使用的 URL。 -
def __str__(self)– 用来重新定义 Django 将用于创建表中单个记录的字符串表示的默认方式。
我们将使用__str__()方法来演示如何覆盖 Django 提供的方法并访问项目代码中的方法。
模型方法 – str
使用之前创建的相同的MAKE_CHOICES元组,我们将覆盖__str__()方法,为所有Vehicle对象制定一个自定义名称。我们将定义的Vehicle对象的默认字符串表示将使用以下命名约定,{{ vehicle make }} {{ vehicle model }},中间有一个空格。
要在Vehicle类中配置此方法,在你的/chapter_3/models.py文件中,在你的现有Vehicle模型类中编写__str__()方法,如下面的代码块所示:
# /becoming_a_django_entdev/chapter_3/models.py
...
class Vehicle(models.Model):
...
def __str__(self):
MAKE_CHOICES_DICT = dict(MAKE_CHOICES)
return MAKE_CHOICES_DICT[self.make] + ' ' + self.model.name
很容易看出,模型方法只是接受自身实例作为参数,对自己执行某种操作,然后返回转换后的值。前一个例子中的值是一个字符串,对于__str__()方法,它应该始终返回一个字符串,而其他方法,包括你创建的自定义方法,可以返回任何其他数据类型,例如整数、字典、QuerySet或日期/时间对象,仅举几例。
接下来,让我们讨论编写我们自己的自定义模型方法,这是 Django 没有为我们提供的。
自定义模型方法
如果我们想要显示比__str__()方法为模型类提供的更深入的名字,自定义方法将很有用。例如,让我们包括发动机类型,除了__str__()方法返回的信息。命名约定将是{{ vehicle make }} {{ vehicle model }} – {{ vehicle engine }},中间有空格和破折号。
要在Vehicle类上创建自己的模型方法,在你的/chapter_3/models.py文件中,在你的Vehicle模型内部创建一个新的方法,并将其命名为full_vehicle_name(),如下所示:
# /becoming_a_django_entdev/chapter_3/models.py
...
class Vehicle(models.Model):
...
def full_vehicle_name(self):
return self.__str__() + ' - ' + self.engine.name
前一个例子使用了__str__()方法中找到的相同逻辑。我们只是在自定义方法中使用self.__str__()表达式调用该方法,而不是在两个不同的地方编写相同的代码。
接下来,我们将在新创建的自定义方法周围应用装饰器,改变我们与这些数据交互的方式。
装饰器
装饰器是一种标准的 Python 设计模式,允许开发者在不永久更改对象行为的情况下扩展对象的功能。装饰器的概念可以应用于项目中几乎任何存在的类或方法。我们将应用这个概念到我们刚刚创建的 full_vehicle_name() 方法上,将其从可调用对象变为该模型的元属性。
装饰器 – @property
@property 装饰器允许我们编写一个方法,使其作为模型实例的常规属性,而不是作为函数。使用此装饰器,我们可以像访问表中任何其他字段一样访问 full_vehicle_name。唯一不能做的事情是像其他任何字段一样保存数据,因为该属性在技术上不是表中存储数据的独立列。
如果没有 @property 装饰器,数据访问方式将类似于以下演示:
>>> print(my_object.my_custom_method())
如果存在 @property 装饰器,print 语句的编写方式将类似于以下演示:
>>> print(my_object.my_custom_method)
要将你的方法包裹在 @property 装饰器中,在你的 /chapter_3/models.py 文件中,在 Vehicle 模型类内部,创建一个新的方法名为 fullname(),如下所示:
# /becoming_a_django_entdev/chapter_3/models.py
...
class Vehicle(models.Model):
...
@property
def fullname(self):
return self.__str__() + ' - ' + self.engine.name
上述示例将执行与 full_vehicle_name() 方法相同的功能,只是应用了 @property 装饰器。当我们稍后在该节标题为 执行查询 的部分进行查询操作时,我们将比较两种方法的差异,以查看数据是如何返回和使用的。
现在我们已经讨论了构成 Django 模型的大部分核心概念,让我们练习扩展这些模型,以保持一种不要重复自己(DRY)的编写风格。
扩展模型
User 模型,这是一个非常常见的扩展模型。
接下来,我们将练习扩展我们的实践模型 engine2,然后扩展 Django 的 User 模型,将其变为 Seller 模型。这将使 Seller 对象与 Vehicle 相关,并充当 User,提供基于权限的角色和权限组功能。
扩展基本模型类
扩展常规模型类相当简单。按照以下步骤扩展 engine2 实践类:
-
在你的
/chapter_3/models.py文件中,在名为engine2的类中,保持名称字段不变,然后添加一个新的字段名为vehicle_model,其related_name属性值如以下代码块所示:# /becoming_a_django_entdev/chapter_3/models.py ... class engine2(models.Model): name = models.CharField(...) vehicle_model = models.ForeignKey( VehicleModel, on_delete = models.CASCADE, verbose_name = 'Model', related_name = 'model_engine2', blank = True, null = True, ) -
确保你的
engine2类具有以下Meta类选项:# /becoming_a_django_entdev/chapter_3/models.py ... class engine2(models.Model): class Meta: abstract = True db_table = 'chapter_3_practice_engine' ordering = ['name',] verbose_name = 'Practice Engine' verbose_name_plural = 'Practice Engines'
我们基本上想让 engine2 类与 Engine 类完全一致,除了我们想要保持原始类不变,并编写一个名为 engine3 的新类,该类由 engine2 构造。我们还需要给 engine2 类中的 vehicle_model 字段提供一个新的唯一值作为 related_name 参数。否则,当我们运行 Django 迁移命令时,我们将遇到与 Engine 类的冲突错误。在 engine2 类中,指定与前面示例中相同的 abstract = True 选项。该选项允许我们将其用作父类。
-
现在,在你的
engine2类下面创建一个名为engine3的新类,如下面的代码块所示:# /becoming_a_django_entdev/chapter_3/models.py ... class engine3(engine2): other_name = models.CharField( verbose_name = 'Other Engine Name', max_length = 75, blank = True, null = True, )
在这里展示的 engine3 类中,我们并没有创建一个 Meta 子类,我们只给它提供一个字段。我们还用 engine2 替换了 models.Model。这就是我们传入我们想要从该类构造新类(也称为扩展或从该父类继承)的类名的地方。
现在运行你的 Django 迁移命令将导致错误,告诉我们 chapter_3_practice_engine 表已经存在。为了防止这种情况,我们可以做两件事之一。我们可以重命名 engine2 类的 Meta 类选项 db_table,或者我们可以删除数据库中的所有表并从头开始。
删除数据库表
由于目前我们没有任何实际数据需要担心,并且因为我们处于开发生命周期的早期阶段,删除我们的表是可以的。这是可以接受的,因为我们还在构建我们项目骨架代码的起点。我们还在使用本地数据库,这意味着我们不会通过执行此任务来干扰其他开发者的工作流程。你可以使用任何数据库管理工具来删除你的表。
按照以下步骤使用 PgAdmin 删除你的表:
-
在 PgAdmin 中,导航到 工具 | 查询工具。
-
在打开的选项卡中,输入以下两个命令:
# In the Query Tool of the PgAdmin App DROP SCHEMA public CASCADE; CREATE SCHEMA public; -
通过点击 执行/刷新 按钮或按键盘上的 F5 来执行这些命令。
-
你还需要删除任何迁移文件夹中找到的所有迁移文件,例如
/chapter_3/migrations/和/chapter_3/migrations/__pycache__/文件夹。注意
每次删除表时,数据都会丢失。下次执行 Django 迁移命令时,应该执行
createsuperuser命令或加载数据固定。 -
再次执行你的 Django 迁移命令。以下截图显示,之前在
engine2类中存在的所有字段和Meta类选项现在都存在于engine3类中,尽管我们没有为engine3类编写它们:

图 3.6 – PgAdmin – 扩展引擎
我们可以看到这是因为 Meta 类的 db_table = 'chapter_3_practice_engine' 选项放置在 engine2 类中,这就是 engine3 表的名称。没有为 engine2 类创建表,因为它被配置为抽象类。我们还看到来自 engine2 类的两个字段,name 和 vehicle_model,也应用于 engine3 类。
接下来,让我们扩展内置的 Django User 类。
扩展 Django 用户模型
扩展 Django 的 User 模型将转换 Seller 模型,使其在系统中充当 User 角色。这意味着你可以创建一个用户资料,它将包含 Django 标准中不存在的字段;它将包含我们创建的字段。这是通过使用 AbstractUser 或 AbstractBaseUser 类作为父类来构造 Seller 类实现的。
按照以下步骤扩展你的 User 类:
-
在你的
/chapter_3/models.py文件中,在Seller类中,将models.Model替换为AbstractUser父类,并包含此处所示的import语句:# /becoming_a_django_entdev/chapter_3/models.py ... from django.contrib.auth.models import AbstractUser ... class Seller(AbstractUser): ...
AbstractUser 类将允许我们保留 User 模型中存在的所有原始字段。如果我们想从头开始创建一个全新的 User 模型,请使用 AbstractBaseUser 父类。
-
我们还需要调整
settings.py文件中AUTH_USER_MODEL变量的值,如下所示:# /becoming_a_django_entdev/settings.py ... AUTH_USER_MODEL = 'chapter_3.Seller'
使用 app_name.model_name 命名约定,注意模型类字母的大小写。如果不调整此值,我们将为该项目获得一个 User 模型;相反,将使用 Seller 模型。
- 如果我们现在尝试运行 Django 迁移命令,Django 将会要求我们为用户名和密码字段分配一个默认值。由于用户名字段需要唯一,我们无法轻易地为这个对象设置默认值,因为这会导致重复的用户名。这种情况发生的原因是我们破坏了数据库中之前的
auth_user表,并为User创建了一个全新的关系集。按照之前小节中标题为 删除数据库表 的步骤,继续删除你的表。现在运行 Django 迁移命令。以下截图显示chapter_3_seller表现在有许多我们没有编写的其他字段:
![Figure 3.7 – PgAdmin – 用户模型扩展]

图 3.7 – PgAdmin – 用户模型扩展
现在我们已经涵盖了编写和扩展模型的基础知识,让我们使用 Django shell 来执行查询。我们可以使用 Django shell 来查看查询结果,而无需先学习渲染模板,这正是所有 第四章,URLs、视图和模板 将要涵盖的内容。
使用 Django shell
Django shell 是任何工具箱中一个强大的工具。它将激活 Python 交互式解释器,并使用 Django 数据库抽象 API 让我们能够直接连接到项目中配置的数据库(们)。有了这个,我们就可以从终端或命令行窗口直接编写 Python 代码和执行查询。
要激活 Django shell,请按照以下步骤操作:
-
打开您的终端或命令行窗口,导航到项目的根目录。确保您的虚拟环境已经激活,然后执行以下命令:
(virtual_env) PS > python3 manage.py shell -
您应该会看到它打印出有关启动的
InteractiveConsole的以下信息:Python 3.7.8 (tags/v3.7.8:4b47a5b6ba, Jun 28 2020, 08:53:46) [MSC v.1916 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> -
您的终端现在将显示三个右尖括号,您可以在其中逐行编写和执行 Python 代码。从理论上讲,您可以通过这种方式输入整个脚本,但它不会被保存到任何地方,并且当窗口关闭或
InteractiveConsole终止时,您的代码将丢失。
现在这个 shell 已经激活,让我们添加一些示例数据并执行几个查询来观察它的行为。
运行基本的 Python 脚本
在本章前面的部分,在标题为可变对象与不可变对象的小节中提到,Python 字符串是存在的不可变数据类型之一。不可变字符串是指在创建后不能重新分配该字符串特定索引处的字符。这意味着允许的是将整个值重新分配给该字符串,而不允许的是更改特定索引处的字符值。这是 Python 的基本原则,对于 Python 新手来说可能会感到困惑。在下一个例子中,我们将演示如何使用 Django shell,同时演示是什么使得字符串不可变:
-
通过运行以下 Django shell 命令来启动 Python 交互式解释器:
(virtual_env) PS > python3 manage.py shell -
分配一个新的变量
myvar,并给它一个初始值my_string,如下所示:>>> myvar = 'my_string' >>> myvar[2] = '' Traceback (most recent call last): File "<console>", line 1, in <module> TypeError: 'str' object does not support item assignment >>>
通过执行前面代码块中显示的第二个语句,我们尝试从字符串索引2处删除下划线,我们收到了一个错误,指出TypeError: 'str' object does not support item assignment。
-
如果我们只是重新分配
myvar变量的值,如下面的代码块所示,我们将能够以这种方式删除下划线:>>> myvar = 'my_string' >>> print(myvar) my_string >>> myvar = 'mystring' >>> print(myvar) mystring >>>
在前面的例子中,第一个print语句返回my_string,然后,在我们更改myvar的值之后,下一个print语句返回mystring。
-
我们可以使用字符串索引来查找字符并将它们组合,但我们不能重新分配索引处的字符。以下示例将通过查找指定索引范围内的字符来删除下划线:
>>> myvar = 'my_string' >>> print(myvar[0:2] + myvar[3:9]) mystring >>> -
输入
exit()退出交互式解释器,并返回使用manage.py命令:>>> exit()
现在我们已经知道如何在交互式解释器中执行基本的 Python 脚本,让我们使用这个工具生成一个自定义的SECRET_KEY并设置项目的.env文件。
生成一个 SECRET_KEY 变量
Django 中的SECRET_KEY变量用作哈希来保护一些内容,例如您的会话、cookie 存储、密码令牌化以及所有其他用于保护您网站的加密签名方法。您不必使用在线工具生成此密钥,因为从该源传输或接收可能会被破坏,您可以使用 Django shell 生成自己的密钥。我们正在生成一个随机字符串。这个操作没有特别之处;技术上您也可以使用键盘上输入的任何字母和数字组合。虽然 Django 在创建新的 Django 项目时已经为我们生成了一个唯一的密钥,但这是一个有用的步骤,允许我们在每个 Heroku 环境中使用不同的密钥。这样,我们就不会共享相同的SECRET_KEY。
要生成自己的SECRET_KEY,请按照以下步骤操作:
-
在您的终端或命令行窗口中激活 Django shell,然后导入以下代码块中所示的方法:
(virtual_env) PS > python3 manage.py shell >>> from secret_key_generator import secret_key_generator
这里所示的方法来自名为secret_key_generator的包,我们在第二章,项目配置中安装了它。
-
接下来,执行以下
print语句:>>> print(secret_key_generator.generate()) your_randomly_generated_key_printed_here -
取出屏幕上打印的密钥,并使用它来设置或重置您的环境变量。要重置变量,只需遵循第二章,项目配置中标题为远程变量的子节中讨论的相同步骤,它将使用新值更新您的值。
上述 shell 命令还为我们创建了一个名为.secret.txt的文本文件,位于项目的根目录中,您的manage.py文件也位于此处。您可以删除.sectret.txt文件,因为它不是必需的。
现在,让我们使用 Django shell 向我们的表中添加数据,这样我们就可以在之后使用 Django shell 进行查询。
保存数据
使用 Django shell 将对象保存到数据库中很容易。在我们激活 Django shell 之后,我们需要将我们想要与之一起工作的模型导入内存中,就像在.py文件的顶部导入某个东西一样。
按照以下步骤使用 Django shell 创建和保存数据:
-
使用
InteractiveConsole窗口通过执行以下命令导入您的车辆类对象:(virtual_env) PS > python3 manage.py shell >>> from becoming_a_django_entdev.chapter_3.models import Engine, Seller, Vehicle, VehicleModel
这些对象将可供您使用,直到该窗口关闭或执行exit()命令。
-
当这些对象被加载时,只需以下代码块中的两行代码就可以创建一个新的对象并将其保存:
>>> vehicle_model = VehicleModel(name = 'Enclave Avenir', make = 1) >>> vehicle_model.save()
前面的行将创建并保存一个名为Enclave Avenir的VehicleModel对象到chapter_3_vehiclemodel表中。在之前创建vehicle_model对象时,我们为该类存在的所有字段提供了值。make字段的值使用我们之前创建的元组的数值,称为MAKE_CHOICES。
-
然而,如果我们尝试使用
vehicle_model字段的数值来创建一个Engine对象,那么我们会收到一个ValueError,如下所示:>>> engine = Engine(name = '3.6L DI DOHC 6cyl', vehicle_model = 1) Traceback (most recent call last): ... ValueError: Cannot assign "1": "Engine.model" must be a "VehicleModel" instance. -
为了成功创建一个
Engine对象,我们首先必须创建一个VehicleModel对象,就像我们在第 2 步中为vehicle_model临时对象所做的那样。然后,使用该变量将model字段的值设置为,而不是使用数值整数,如下所示:>>> vehicle_model = VehicleModel(name = 'Enclave Avenir', make = 1) >>> vehicle_model.save() >>> engine = Engine(name = '3.6L DI DOHC 6cyl', vehicle_model = vehicle_model) >>> engine.save()
前一个示例的第 4 步可能会因为我们在VehicleModel类的name字段上添加了unique = True参数而导致错误。这也是因为我们刚刚在第 2 步中使用相同的名称创建了一个对象。你可以通过提供一个唯一的名称或忽略它并继续前进来解决这个问题。这个错误是为了学习目的而故意制造的。你收到的错误应该看起来像以下代码块中显示的错误,表明你有重复条目:
Traceback (most recent call last):
File "C:\Projects\Packt_Publishing\Repo\becoming_a_django_entdev\virtual_env\lib\site-packages\django\db\backends\utils.py", line 84, in _execute
return self.cursor.execute(sql, params)
psycopg2.errors.UniqueViolation: duplicate key value violates unique constraint "chapter_3_vehiclemodel_name_a94a4619_uniq"
DETAIL: Key (name)=(Enclave Avenir) already exists.
...
The above exception was the direct cause of the following exception:
django.db.utils.IntegrityError: duplicate key value violates unique constraint "chapter_3_vehiclemodel_name_a94a4619_uniq"
DETAIL: Key (name)=(Enclave Avenir) already exists.
为了解决这个问题,我们需要使用update_or_create()方法而不是save()方法。
如果我们尝试创建并保存一个vehicle_model字段尚未保存到数据库中的Engine对象,我们将收到一个ValueError,告知我们存在未保存的相关对象。如果你希望看到这个,请使用以下值创建vehicle_model对象。然后,使用该对象将engine对象上的vehicle_model字段的值设置为该对象,并尝试保存:
>>> vehicle_model = VehicleModel(name = 'Blazer LT', make = 3)
>>> engine = Engine(name = '4 Cylinders 4 2.0L DI Turbo DOHC 122 CID', vehicle_model = vehicle_model)
>>> engine.save()
Traceback (most recent call last):
...
ValueError: save() prohibited to prevent data loss due to unsaved related object 'model'.
一旦尝试保存该engine,前一个代码块中显示的错误就会打印到你的屏幕上,这就是为什么我们需要在创建依赖于它们的对象之前保存每个相关对象的原因。
接下来,让我们讨论使用update_or_create()方法。
模型方法 – update_or_create()
我们使用update_or_create()方法而不是save()方法来创建或修改现有对象。
按照以下步骤使用此方法:
-
确保 Django shell 已激活,然后执行以下命令:
(virtual_env) PS > python3 manage.py shell >>> vehicle_model, created = VehicleModel.objects.update_or_create(name = 'Enclave Avenir', make = 1, defaults={'name': 'Enclave Avenir', 'make': 1},)
前面的示例应该已经成功。如果成功了,那么你没有收到任何错误,你会看到三个右尖括号等待你的下一个输入命令。
-
使用数据库管理工具,如 PgAdmin,检查你的表中是否有一个名为
Enclave Avenir的VehicleModel记录。前一个示例中的defaults参数是一个可选参数,它定义了在创建新记录时你想设置的值。如果没有它,系统将默认使用你的模型字段上设置的值。 -
在这一步中,我们将向数据库中添加一个
Vehicle对象。这需要使用Money类来创建一个Money对象。要使用Money类,请执行以下import语句:>>> from djmoney.money import Money -
现在,执行以下三个
update_or_create()命令:>>> vehicle_model, model_created = VehicleModel.objects.update_or_create(name = 'Blazer LT', make = 3,) >>> engine, engine_created = Engine.objects.update_or_create(name = '3.9L DI DOHC 6cyl', vehicle_model = vehicle_model,) >>> vehicle, vehicle_created = Vehicle.objects.update_or_create(vin = 'aa123456789012345', sold = True, price = Money(10000, 'USD'), make = 3, vehicle_model = vehicle_model, engine = engine,)
本小节中的命令应该都成功执行,没有错误。
注意
如果之前已经通过导入chapter_3数据固定文件中找到的数据创建了一个具有相同 VIN 号的车辆,那么你只需将前一个示例中的vin值更改为一个新的唯一vin值即可。这样,当你使用像 PgAdmin 这样的数据库管理工具查看时,你可以看到一条新记录被添加到你的表中。
让我们接下来讨论加载chapter_3数据固定文件。
加载chapter_3数据固定文件
我们不会提供如何创建本章和本书其余部分所需的所有数据的步骤,而是将从数据固定文件中添加数据。添加数据可以比在 Django shell 中执行的方式简单得多。我们将在第十章 数据库管理中更深入地讨论这个概念,并稍后创建我们自己的固定文件。现在,请确保将/becoming_a_django_entdev/becoming_a_django_entdev/chapter_3/fixtures/文件夹以及该文件夹中本书代码中找到的所有文件复制到你的/chapter_3/应用文件夹中。chapter_3固定文件将为我们提供足够的数据,以便我们处理本章剩余的示例。
要加载数据固定文件,请确保你已经退出了 Django shell,并且你的虚拟环境是激活的,然后执行以下命令:
(virtual_env) PS > python3 manage.py loaddata chapter_3
如果你导入此固定文件存在问题,请确保你的表结构与本书chapter_3应用中提供的模型结构相匹配。
另一种选择是遵循本章保存数据小节中的步骤,逐个添加你的样本数据。使用这些示例创建并保存你想要的任何数量的对象。如果你要创建一个Vehicle对象,它将以创建Engine对象相同的方式进行,但现在你需要定义两个相关对象的值,而不是一个,以便成功保存。我们只需要在下一章的练习中玩转几个对象。
执行查询
使用 Django shell 执行查询将让我们了解查询是如何工作的。在以下小节中,我们将讨论一些常用的方法。
模型方法 – all()
all()方法返回该模型对象表中找到的所有记录。此方法将返回以下格式的 QuerySet,表示它找到的所有条目:
(virtual_env) PS > python3 manage.py shell
>>> from becoming_a_django_entdev.chapter_3.models import Engine, Seller, Vehicle, VehicleModel
>>> VehicleModel.objects.all()
<QuerySet [<VehicleModel: Blazer LT>, <VehicleModel: Enclave Avenir>, <VehicleModel: Envision Avenir>]>
chapter_3 数据固定只提供了三个 VehicleModel,这就是为什么只返回了三个对象的集合。您的结果可能会有所不同。我们创建 __str__() 方法的原因之一,就像在本章的 模型方法 – str 小节中做的那样,是为了使它能够在代码使用中以一种逻辑方式表示,其中打印出来的对象名称是我们能理解的名称,而不是一些没有意义的名称。如果没有在 VehicleModel 类中定义 __str__() 方法,查询集将按以下示例所示返回给我们:
<QuerySet [<VehicleModel: VehicleModel object (3)>, <VehicleModel: VehicleModel object (2)>, <VehicleModel: VehicleModel object (1)>]>
仅通过查看此代码块中打印的集合,我们无法区分哪些对象是哪些,以及它们的顺序。
模型方法 – get()
get() 方法用于定位特定的数据库记录。
按照以下步骤查看此方法的效果:
-
在执行查询时使用
get()来定位车辆的vin值,就像这里所做的那样:>>> vehicle = Vehicle.objects.get(vin = 'aa123456789012345') >>> print(vehicle) Chevrolet Blazer LT -
使用返回给我们的单个对象,再次使用我们之前创建的
full_vehicle_name()方法运行print语句,以查看生成的结果差异:>>> print(vehicle.full_vehicle_name()) Chevrolet Blazer LT - 3.6L DI DOHC 6cyl -
接下来,使用其他
fullname方法与@property装饰器返回完全相同的结果:>>> print(vehicle.fullname) Chevrolet Blazer LT - 3.6L DI DOHC 6cyl
Django 正在使用我们在 __str__ 方法中定义的格式来生成字符串,该字符串在 步骤 1 中被打印到屏幕上。我们已经知道 vin 字段被设置为 unique = True,这意味着数据库中永远不会有两个具有相同 vin 值的对象,因此我们知道在所有前面的步骤中使用 get() 方法是安全的。如果有多个具有相同值的项并且使用了 get() 方法,那么您将需要使用 filter() 方法。
模型方法 – filter()
filter() 方法用于在可能具有相同字段值的数据库中查找记录。此方法将返回一个结果集合而不是单个结果。集合将以 QuerySet 的形式返回给我们。例如,我们可以过滤 VehicleModel 表,我们知道它包含三行。
以下示例将根据 Buick 的值过滤 make 字段,以返回仅包含两个对象的集合而不是三个:
>>> print(VehicleModel.objects.filter(make = 1))
<QuerySet [<VehicleModel: Enclave Avenir>, <VehicleModel: Envision Avenir>]>
查询可以比使用简单的 all()、get() 或 filter() 方法复杂得多。Q 对象也提供了更复杂的查询。有关如何在 Django 中使用 Q 对象的完整说明,请访问以下文档:https://docs.djangoproject.com/en/4.0/ref/models/querysets/#q-objects。
我们甚至可以使用算术函数来获取对象的摘要,这把我们带到了下一个子节,讨论聚合。
聚合
Django 提供了一个简单的方式来生成对象集合的摘要,这被称为聚合。这意味着我们可以执行查询并使用 Django 提供的许多聚合函数中的任何一个。这可以用来生成所有车辆的平均价格,生成仅售出车辆的平均价格,或者为特定卖家生成车辆的总数。虽然关于聚合和注解的信息有很多,但我们将讨论一些基本用法。有关在 Django 中生成聚合的完整指南,请参阅此处:docs.djangoproject.com/en/4.0/ref/models/querysets/#aggregation-functions。
模型方法 – aggregate()
聚合用于生成查询集中每个对象的摘要。为了获取表中每个存在的车辆的平均价格,我们可以使用Avg聚合函数。我们传递给Avg函数的参数是我们想要执行此操作的字段。
按照以下步骤练习使用聚合:
-
按照以下步骤导入您的
Vehicle模型和Avg类对象,如下所示:(virtual_env) PS > python3 manage.py shell >>> from becoming_a_django_entdev.chapter_3.models import Vehicle >>> from django.db.models import Avg -
使用
all()方法和aggregate()方法执行查询查找,如下所示:>>> vehicles = Vehicle.objects.all().aggregate(Avg('price')) -
打印您的
vehicles对象:>>> print(vehicles) {'price__avg': Decimal('16335.428571428571')}
摘要作为字典对象返回。
-
您可以通过执行以下
print语句来获取price__avg键的值:>>> print(vehicles['price__avg']) 16335.428571428571
平均值的结果当然还没有实际格式化为任何特定的货币类型。
-
我们可以通过应用之前在数据库中创建并保存我们的第一个车辆时使用的相同的
Money()转换来将其格式化为美元,执行以下命令:>>> from djmoney.money import Money >>> print(Money(vehicles['price__avg'], 'USD')) $16,335.43
在之前步骤 2中我们写了Vehicle.objects.all().aggregate(),这里的all()方法是多余的。aggregate()方法基本上与all()方法做同样的事情,这意味着我们可以将我们的语句写成如下,并产生相同的结果:
>>> vehicles = Vehicle.objects.aggregate(Avg('price'))
我们也可以用任何标准查询方法替换all()方法,如以下示例所示:
>>> vehicles = Vehicle.objects.filter(sold=False).aggregate(Avg('price'))
>>> print(Money(vehicles['price__avg'], 'USD'))
$18,419.60
接下来,让我们讨论注解。
模型方法 – annotate()
当我们在查询集中有与其它对象相关联的对象,并且我们想要生成该查询集中每个相关对象的摘要时,我们会使用注解。
按照以下步骤练习使用注解:
-
执行以下命令以提供查询所有存在于表中的卖家,然后生成仅找到的已售车辆的数量:
(virtual_env) PS > python3 manage.py shell >>> from becoming_a_django_entdev.chapter_3.models import Seller, Vehicle >>> from django.db.models import Avg, Count >>> sellers = Seller.objects.filter(vehicles__sold=True).annotate(Count('vehicles')) >>> print(sellers[0].vehicles__count) 2 -
将前面的
filter语句修改为仅计算未售出的车辆,如下所示:>>> sellers = Seller.objects.filter(vehicles__sold=False).annotate(Count('vehicles')) >>> print(sellers[0].vehicles__count) 5
我们需要在sellers[0]处指定索引,因为filter()方法总是会返回一个对象集合,即使查询只产生一个对象。
-
将
sellers集合打印到屏幕上,如下所示:>>> print(sellers) <QuerySet [<Seller: admin>]>
我们可以看到,目前数据库中只有一个 Seller。我们得到了数字 2 和 5 作为结果,总共是七个与该卖家相关的车辆。
接下来,我们将讨论模型管理器以及它们如何用于执行高级查询。
编写模型管理器
现在我们知道,当我们想要应用与表中单个对象相关的逻辑时,我们将考虑编写模型方法。一个高级 Django 概念可以让我们添加与整个对象表相关的逻辑。这可以通过使用 objects 管理器来实现,就像我们编写查询语句 MyModel.objects.all() 一样。由于 objects 管理器已经为我们创建,实际上我们根本不需要创建模型管理器。然而,自定义模型管理器可以在项目中使用,以提供整个表使用的额外方法。我们将讨论这个概念的简单用法,即向表中添加过滤器。要了解更多关于模型管理器如何深入使用的知识,请访问官方 Django 文档,网址为:docs.djangoproject.com/en/4.0/topics/db/managers/。
按照以下步骤应用过滤器,根据制造商/品牌区分车辆对象。在这里,我们将编写一个用于 Buick 车辆的管理器,另一个用于 Chevy 车辆的管理器。在一个大型项目中,将你的管理器放在一个单独的 managers.py 文件中并在使用之前将其导入到 models.py 中也是一个明智的选择。现在,让我们先将它们全部添加到 models.py 文件中:
-
在
/chapter_3/models.py文件中,在你的模型类上方和任何现有的import语句下方添加以下两个模型管理器类,如下所示:# /becoming_a_django_entdev/chapter_3/models.py ... class BuickVehicleManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(make=1) class ChevyVehicleManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(make=3) ... -
在
/chapter_3/models.py文件中,在你的Vehicle类中模型字段下方和Meta子类上方添加以下三个模型管理器语句,如下所示:# /becoming_a_django_entdev/chapter_3/models.py ... class Vehicle(models.Model): # Place Model Fields Here # The Default Model Manager objects = models.Manager() # The Buick Specific Manager buick_objects = BuickVehicleManager() # The Chevy Specific Manager chevy_objects = ChevyVehicleManager() # Place Meta Class and Model Methods Here -
接下来,打开你的终端或命令行窗口,激活你的虚拟环境和 Django shell。然后,将
Vehicle模型导入到InteractiveConsole中,如下所示:(virtual_env) PS > python3 manage.py shell >>> from becoming_a_django_entdev.chapter_3.models import Vehicle -
执行以下
objects管理器count()方法:>>> Vehicle.objects.all().count() 7 -
执行以下
buick_objects管理器count()方法:>>> Vehicle.buick_objects.all().count() 2 -
执行以下
chevy_objects管理器count()方法:>>> Vehicle.chevy_objects.all().count() 5
我们得到的是与每个我们创建的管理器相关的车辆,首先是 objects 管理器,然后是 buick_objects 和 chevy_objects。这计算了过滤后的对象数量,而不是提供该表中所有对象的总量。尽管我们仍在使用 all() 方法,但我们只得到与该过滤器相关的所有对象。我们还应用了 count() 方法来打印返回的查询集的数字计数,而不是像之前查询示例中那样打印每个对象的名称。
摘要
在本章中,我们了解到模型是我们构建的其他所有访问数据库数据的构建块。它们提供了容器,其中所有项目的数据都将作为此应用程序的数据存储设备存在。我们现在有一个工具箱,其中包含与表格结构相关的工具,例如存在的列或我们应用到的规则/约束。其他工具帮助我们将这些表格链接在一起,建立这些表格之间的关系。我们还知道如何将我们拥有的数据进行转换,以提供那些表格中未保存的其他数据,而是从中派生出来的。一些概念通过在后台执行工作、索引数据和减少查找信息所需的时间来增加性能。查询对象也是一个复杂的话题,有很多关于它的材料;使用本章中的概念来帮助您研究更高级的数据查询方式,以帮助处理复杂的现实世界场景。稍后,在第十章 数据库管理中,我们将讨论其他有助于在执行数据库查询时提高数据库性能的技巧。
接下来,让我们将本章创建的模型渲染到实际的网页上,最终在浏览器中查看。这些将是我们在下一章中创建的 URL、视图和模板。
第二部分 – Django 组件
在本部分,您将了解 Django 框架的主要组件以及如何在项目中使用它们。您将学习 Django 模板、电子邮件模板、PDF 模板、URL 模式、视图、表单以及 Django 管理站点。这些是 Django 框架的基本组件,对于几乎任何项目都是必要的。
本部分包括以下章节:
-
第四章, URL、视图和模板
-
第五章, Django 表单
-
第六章, 探索 Django 管理站点
-
第七章, 处理消息、电子邮件通知和 PDF 报告
第四章:第四章:URL、视图和模板
在本章中,我们将构建将路由到不同视图的URL模式,处理发送到服务器的请求。视图的一个工作是将处理过的信息以上下文的形式发送到将用于渲染静态或动态更改内容的模板。到本章结束时,我们将为用户创建几个 URL 模式以访问和查看数据。一些示例将故意触发错误或未找到异常,以帮助展示本章提供的概念。
Django 基于所谓的模型-模板-视图(MTV)架构设计模式,这与今天用于各种流行基于 Web 的软件系统的知名模型-视图-控制器(MVC)设计模式类似。在这两种架构设计模式中,视图是人们开始学习 Django 并来自 MVC 背景的人有时会感到困惑的部分。在这两种模式中,模型是相同的,并且两者都对应于数据库中的表。在 Django 中,视图最好与 MVC 设计模式中的控制器相比较,而 Django 的 MTV 模式中的模板最好与 MVC 设计模式中的视图相比较。
我们将以此章节开始,讨论 URL 模式,这些模式允许我们告诉 Django 我们希望在网站上可用的路径,在我们项目内部。一个 URL 的.com、.org或.edu部分。www.example.com/my-url-pattern/中的路径将是/my-url-pattern/。我们可以告诉 Django 将不同的 URL 模式映射到不同的视图,并且我们可以将不同的 URL 模式指向相同的视图。视图是处理请求并返回响应的部分。通常,响应以 HTML 模板的形式返回,但响应也可以是 JSON、XML 或其他数据类型。模板接受视图和/或上下文处理器提供的上下文,然后使用该上下文数据在客户端浏览器中渲染动态 HTML。上下文实际上是一个动态变量的字典,这些变量会随着你的应用程序中条件和状态的改变而改变。存在于数据库中的数据也通过相同的上下文提供给模板。视图执行查询和/或与缓存系统和 API 通信,以从数据存储设备中获取数据,用于渲染模板。
在本章中,我们将涵盖以下内容:
-
配置 URL 模式
-
映射 URL 模式
-
解析 URL
-
解析绝对 URL
-
使用复杂视图
-
使用模板
技术要求
要在此章节中与代码一起工作,以下工具需要安装在你的本地机器上:
-
Python 版本 3.9 - 作为项目的底层编程语言使用
-
Django 版本 4.0 - 作为项目的后端框架使用
-
pip 包管理器 - 用于管理第三方 Python/Django 包
我们将继续使用第二章中“项目配置”中创建的解决方案。然而,没有必要使用 Visual Studio IDE。主要项目本身可以使用其他 IDE 运行,或者从项目根目录中独立使用终端或命令行窗口运行。这就是manage.py文件所在的位置。无论你使用什么编辑器或 IDE,都需要一个虚拟环境来与 Django 项目一起工作。有关如何创建项目和虚拟环境的说明可以在第二章中“项目配置”中找到。你需要一个数据库来存储项目中的数据。在上一章的示例中选择了 PostgreSQL;然而,你可以为你的项目选择任何数据库类型来与本章中的示例一起工作。
我们还将使用第三章中“模型、关系和继承”小节标题为“加载 Chapter_3 数据固定文件”中提供的 Django 固定数据。确保chapter_3固定文件已加载到你的数据库中。如果这已经完成,则可以跳过下一个命令。如果你已经创建了第三章中“模型、关系和继承”中提到的表,并且尚未加载该固定文件,那么在激活你的虚拟环境后,运行以下命令:
(virtual_env) PS > python manage.py loaddata chapter_3
本章创建的所有代码都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Becoming-an-Enterprise-Django-Developer。本章使用的代码的大部分可以在/becoming_a_django_entdev/becoming_a_django_entdev/chapter_4/目录中找到。
查看以下视频以查看代码在实际应用中的效果:bit.ly/3A6AxNU。
准备本章内容
首先,按照第二章中“项目配置”小节标题为“创建 Django 应用”的步骤,在你的项目中创建一个名为chapter_4的新应用。正如该章节所述,不要忘记将你的应用类中的name =变量的值改为指向你安装应用路径。确保也将此应用包含在settings.py文件中的INSTALLED_APPS变量中。
配置 URL 模式
Django 通过它所称为的 urls.py 文件来控制和处理 URL 模式,该文件作为 ROOT_URLCONF 变量指定,位于 settings.py 文件中。当创建项目时,Visual Studio 自动为我们创建了 ROOT_URLCONF 变量,并且在执行 Django startproject 命令时也应该如此。
如果你的项目没有创建此变量,请将以下设置添加到你的 settings.py 文件中:
# /becoming_a_django_entdev/settings.py
...
ROOT_URLCONF = 'becoming_a_django_entdev.urls'
在 ROOT_URLCONF 变量中定义的 urls.py 文件是 Django 认为可以通过使用 import() 函数导入来相互链接的 url.py 文件。Django 在这些 urls.py 文件中只寻找一件事情,那就是一个名为 urlpatterns 的单个变量,它包含了一组为项目或可重用应用定义的 URL 模式。此文件可以包含许多方法、类和其他实用工具,帮助你制定这些模式。
基本路径函数
Django 提供了各种路径函数来构建 URL 模式。这些函数创建并返回将被包含在任何 urlpatterns 变量中的元素。path() 和 re_path() 函数可以接受最多四个位置参数,顺序如下:route、view、kwargs 和 name。其中前两个参数是必需的,并且必须定义。第一个参数 route 期望一个字符串;这可以是一个简单的字符串,也可以是一个相当复杂的字符串,当结合路径转换器和使用正则表达式时。如果你为这个参数使用某种方法来执行逻辑,它只需要返回一个字符串。route 参数是 Django 监听并映射到第二个参数 view 的路径。view 参数用于告诉 Django 如何处理 URL 模式的 GET 请求。view 可以执行任何类型的逻辑。第三个参数是 view。最后一个参数 name 是在与其他函数(如反向查找)使用时映射 URL 模式的一种方式。
在我们深入更复杂的 URL 模式和路径转换器之前,让我们先看看一些使用基本函数的例子。
函数 – static()
Django 提供了 static() 函数,用于在本地运行项目且开启调试模式时提供静态文件服务。这些文件包括图像、CSS 和 JavaScript 文件,它们被放置在 Django 应用程序的 static 文件夹中。此函数将启用对这些静态文件夹的访问,允许你在不运行 python manage.py collectstatic 命令的情况下,运行项目并添加、删除和编辑这些文件。当然,在浏览器中,除非你安装了其他工具/插件来更新使用中的文件变化时刷新页面,否则你仍然需要手动刷新。
静态文件
要在本地环境中激活静态文件,在你的主 urls.py 文件中,添加以下 import 语句并将以下函数追加到 urlpatterns 变量中:
# /becoming_a_django_entdev/urls.py
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [...] + static(
settings.STATIC_URL,
document_root = settings.STATIC_ROOT
)
在前面的示例中,我们导入了 settings.py 文件以获取 STATIC_URL 和 STATIC_ROOT 变量的值。由于我们安装了 pip 的 whiteNoise 包,为了与 Heroku 作为我们的主机一起工作,我们实际上不需要编写前面显示的 static() 函数。这意味着如果我们想的话,我们可以跳过编写前面的代码,但这也不会造成伤害,并且将允许你的项目在其他主机上工作。
这也可以使用一个条件语句来编写,该语句检查 DEBUG 是否已启用。
替代方案可以写成如下:
# /becoming_a_django_entdev/urls.py
...
urlpatterns = [...]
if settings.DEBUG:
urlpatterns += static(
settings.STATIC_URL,
document_root = settings.STATIC_ROOT
)
请只使用本小节中描述的示例之一,而不是同时使用两个。你可以注释掉未使用的那个。
让我们接下来配置媒体文件。
媒体文件
即使有 whitenoise 包,我们仍然需要使用 static() 函数来提供媒体文件。FileField、ImageField 或将文件上传到媒体存储设备的其他几种方法。这些文件也被称为 用户生成内容(UGC),它们可以是任何东西,如图像到 PDF 文档、Excel 文档、Word 文档、音频文件,甚至是电影文件。上传的文件被放置在我们创建和配置的媒体文件夹中,如 第二章**, 项目配置。
要在本地运行项目时访问这些图像,请按照以下步骤操作:
-
在你的主
urls.py文件中,插入以下突出显示的代码:# /becoming_a_django_entdev/urls.py ... from django.conf import settings from django.conf.urls.static import static urlpatterns = [...] + static( settings.STATIC_URL, document_root = settings.STATIC_ROOT ) + static( settings.MEDIA_URL, document_root = settings.MEDIA_ROOT ) -
这也可以添加到前一小节中显示的调试条件语句,标题为 静态文件。
-
如果你下载了这本书附带代码,一个示例图像已经被包含在名为
/media的目录中,并用于测试访问媒体文件是否实际可行。如果你的项目中此时没有创建/media文件夹,请继续在你的 IDE 或文件浏览器中创建它,或者通过运行以下命令:(virtual_env) PS > mkdir media -
将
/becoming_a_django_entdev/media/media.jpg文件复制到你的项目中同一目录下。 -
运行你的项目,并尝试导航到 URL
http://localhost:8000/media/media.jpg(不使用前面的代码),你应该得到一个 404 响应。然后尝试使用前面的代码,你应该看到这里显示的媒体图像:
![图 4.1 – 媒体示例图像
![img/Figure_4.01_B17243.jpg]
图 4.1 – 媒体示例图像
让我们进一步探讨这些函数,并构建我们的第一个路径。
函数 – path()
一个 path() 函数接受 route、view、kwargs 和 name 属性,并返回一个要包含在 urlpatterns 列表中的单个元素。path() 函数可以被视为处理静态路径以及使用路径转换器处理动态路径。如果你想使用正则表达式来注册动态路径转换器,你应该使用 re_path() 函数。
按照以下步骤在你的项目中使用 path() 函数:
- 首先,运行你的项目并导航到项目的基 URL
http://localhost:8000/。你可能想知道为什么我们会看到如这里所示的“页面未找到(404)”消息:
![图 4.2 – 启用调试时的 404]
![Figure 4.02_B17243.jpg]
图 4.2 – 启用调试时的 404
当我们激活了 static 和 media URL 模式时,导致了这个错误消息的发生。这就是为什么我们没有看到我们习惯看到的著名的 Django 成功火箭船。这没有什么好担心的;这仅仅意味着我们还没有创建一个 URL 模式来处理主页。这个错误消息可以被视为创建主页的提醒,我们将在下一步完成。
使用 path() 函数,我们将定义一个单一的静态 URL 模式,它将监听主页 URL。在我们这样做之前,让我们创建它将提供服务的 HTML 文件。当我们使用 Visual Studio 创建 chapter_4 应用程序时,一个名为 index.html 的文件被自动创建在 /becoming_a_django_entdev/chapter_4/templates/chapter_4/ 目录中。
- 如果你没有在
/chapter_4/templates/chapter_4/index.html目录中找到该文件,现在就创建这个文件,或者复制本书提供的那个文件。有时这个文件不会自动为我们创建。
index.html 文件将被用作自定义主页,我们目前将只关注 URL 模式;到本章结束时,我们将更深入地探讨模板。
相比于编写其他复杂的 URL 模式,编写监听主页的 URL 模式要简单得多。Django 会尝试通过从 urlpatterns 列表中的第一个到最后一个的顺序来匹配 URL 与模式。通常最好将静态 URL 模式放在顶部,然后在其下方放置动态模式。如果静态模式与动态模式相似,静态 URL 模式将首先匹配,这可能是你想要的。
-
在你的主
/becoming_a_django_entdev/urls.py文件中,添加以下代码:# /becoming_a_django_entdev/urls.py ... from django.urls import path from django.views.generic import TemplateView urlpatterns = [ path( '', TemplateView.as_view( template_name = 'chapter_4/index.html' ) ), ]
之前的 path() 函数用于监听一个定义为空('')的路由/路径,然后我们使用 django.views.generic 库提供的内置 TemplateView 类,以模板形式提供主页。由于这是一个静态页面和一个静态 URL,意味着页面上的内容不会改变,URL 本身也不会改变,所以我们不需要编写一个视图类来处理页面上下文的变化。相反,我们可以通过使用 TemplateView 类来跳过创建视图。使用 TemplateView 类,我们仍然可以传入关键字参数并定义 name 参数。如果我们想传入 kwargs,那将是通过以下步骤完成的。
-
为你的主页添加
kwargs:# /becoming_a_django_entdev/urls.py ... urlpatterns = [ path( '', TemplateView.as_view( template_name = 'chapter_4/index.html' ), kwargs = { 'sub_title': 'I am the sub title.' } ), ] -
在本书提供的
/chapter_4/index.html文件中,一个条件将检查sub_title是否有值,然后在模板中显示该值。将此条件复制到你的文件中,如下所示:# /becoming_a_django_entdev/chapter_4/templates/chapter_4/index.html {% load static %} <html> <head><title></title></head> <body style="text-align:center"> <p>Home Page</p> <img src="img/{% static 'chapter_4/home_page.jpg' %}" role="img" alt="Home Page Image" width="400" style="margin: 0 auto" /> {% if sub_title %} <p>{{ sub_title }}</p> {% endif %} </body> </html>
我们将在本章结束前解释更多关于构建模板的内容,在 与模板一起工作 部分。
我们将项目配置为处理静态文件的原因之一是在 第二章,项目配置 以及本章的 静态文件 小节中,以便在模板中访问这些文件,就像之前示例中展示的那样。{% load static %} 标签语句允许我们开始使用 static 模板标签,例如 {% static 'chapter_4/home_page.jpg' %}。{% static %} 标签返回一个有效的 URL,指向 http://localhost:8000/chapter_4/home_page.jpg 上的图像文件。
-
使用 IDE、文件浏览器或以下命令在你的项目中创建
/static/chapter_4/文件夹:(virtual_env) PS > mkdir becoming_a_django_entdev/chapter_4/static/chapter_4 -
将本书代码提供的
/chapter_4/home_page.jpg文件复制到你的项目中。
Django 会自动搜索项目中每个应用中找到的 static 文件夹。通常的做法是在项目的任何应用中的静态文件夹中包含相同的路径和文件名来覆盖已安装在你的虚拟环境中的包的静态文件,如图像、CSS 和 JavaScript 文件。同样的原则也适用于处理模板文件。
在 步骤 5 中,高亮的 {{ sub_title }} 变量标签是传递到 步骤 4 中的 URL 模式中的关键字参数。也可以使用自定义函数/可调用对象来代替这里硬编码的值。可以使用括号语法 {{ }} 在模板中召回任何上下文变量。字典、列表、集合和查询集等对象都可以使用点来访问每个键和子键,例如 {{ context_variable.key.subkey }}。
- 现在,运行你的项目,你应该不再看到 404 调试信息。相反,你应该看到以下截图:

图 4.3 – 项目主页
接下来,让我们使用 include() 函数来导入其他应用/包的 URL 模式。
函数 – include()
使用 include() 函数导入包含自己的 urlpatterns 变量的额外 urls.py 文件。这就是我们如何编写可重用应用的 URL 模式,并在网站的 ROOT_URLCONF 中包含它们以供项目使用。
让我们按照以下步骤来更好地组织我们章节特定的 URL 模式:
-
如果这个文件还没有为你创建,请继续在
/becoming_a_django_entdev/chapter_4/文件夹内创建一个urls.py文件,然后向该文件添加以下 URL 模式:# /becoming_a_django_entdev/chapter_4/urls.py from django.urls import path from django.views.generic import TemplateView urlpatterns = [ path( 'chapter-4/', TemplateView.as_view( template_name='chapter_4/chapter_4.html' ) ), ] -
在我们一直在使用的
/becoming_a_django_entdev/urls.py主文件中,注释掉本章中创建的先前模式,并添加以下path语句:# /becoming_a_django_entdev/urls.py ... from django.urls import include, path urlpatterns = [ path( '', include( 'becoming_a_django_entdev.chapter_4.urls' ) ), ] -
将本书代码提供的
chapter_4.html模板文件复制到你的项目中,位于/becoming_a_django_entdev/chapter_4/templates/chapter_4/目录下。 -
导航到 URL
http://localhost:8000/chapter-4/,你应该看到一个只写着这是第四章的空白页面,如下面的截图所示:
![Figure 4.4 – URL pattern – include()
![img/Figure_4.04_B17243.jpg]
图 4.4 – URL 模式 – include()
现在我们已经使include()示例工作,我们将把所有新的 URL 模式放入/chapter_4/urls.py文件中,并以类似的方式组织所有未来的章节。
现在,让我们练习重定向 URL。
重定向模式
与我们之前使用TemplateView类不同,我们可以编写 URL 模式来处理项目内的重定向,而无需直接在 Web 服务器中配置。这很方便,因为在传统的 Web 开发中,重定向由 Web 服务器处理,在项目中管理比在 Web 服务器中管理要容易得多。重定向可以使用 Django 提供的RedirectView类来处理。
我们将在http://localhost:8000/my_path/my_unwanted_url/路径上指定一个重定向规则,将其重定向到http://localhost:8000/my_wanted_url/。按照以下步骤配置你的重定向:
-
将以下模式添加到现有的
/chapter_4/urls.py文件中:# /becoming_a_django_entdev/chapter_4/urls.py ... from django.urls import include, path from django.views.generic import ( TemplateView, RedirectView ) urlpatterns = [ ..., path( 'my_path/my_unwanted_url/', RedirectView.as_view( url = 'http://localhost:8000/my_wanted_url/' ) ), ] -
运行你的项目并导航到 URL
http://localhost:8000/my_path/my_unwanted_url/。现在你应该被重定向到,并且在浏览器的地址栏中看到http://localhost:8000/my_wanted_url/。在页面主体中,你应该看到一个 404 响应,因为我们还没有为my_wanted_url路径定义 URL 模式。在执行此步骤时,这是预期的。 -
可以包含额外的参数,例如通过以下示例中的路径来指定我们希望这是一个永久或临时重定向:
# /becoming_a_django_entdev/chapter_4/urls.py ... urlpatterns = [ ..., path( 'my_path/my_unwanted_url/', RedirectView.as_view( url = 'http://localhost:8000/my_wanted_url/', permanent = True ) ), ]
Django 还允许我们定义pattern_name和query_string作为RedirectView类的额外参数。
注意
前面的路径有一个硬编码的值http://localhost:8000/,这在不是本地机器的远程环境中可能会成为一个问题。为了克服这个问题,你需要采用本章后面讨论的创建上下文处理器小节中提到的全局上下文变量概念。
接下来,让我们讨论使用路径转换器来监听动态路径路由。
使用路径转换器
str、int、slug、uuid 和 path。这些都是预格式化的转换器,允许多种选择,并允许在模式内使用字符串和整数。例如,path 路径转换器在以下代码中用于搜索 URL 可以拥有的任何字符、数字和某些符号。
要练习使用路径转换器,请按照以下步骤操作:
-
将以下 URL 模式添加到你的
/chapter_4/urls.py文件中:# /becoming_a_django_entdev/chapter_4/urls.py ... from django.urls import include, path from django.views.generic import ..., TemplateView urlpatterns = [ ..., path( 'my_path/<path:my_pattern>/', TemplateView.as_view( template_name = 'chapter_4/index.html' ) ), ] -
现在,导航到 URL
http://localhost:8000/my_path/testing/,你应该看到之前看到的主页。我们看到相同的页面,因为我们指向的是相同的index.html文件,只是为了确认它正在工作。此外,如果我们导航到 URLhttp://localhost:8000/my_path/2022/,我们也会看到相同的主页。这是预期的。我们不会看到这个页面上的sub_title关键字参数的值,因为我们没有将此参数传递给这个 URL 模式。在该模板中找到的{% if sub_title %}条件语句用于防止在没有sub_title的情况下发生破坏。 -
将 步骤 1 中显示的现有
my_path路径转换器从路径转换为int,并将my_path更改为my_year_path,如下面的代码所示,允许 URLhttp://localhost:8000/my_year_path/2022/正常工作:# /becoming_a_django_entdev/chapter_4/urls.py ... from django.urls import include, path from django.views.generic import ..., TemplateView urlpatterns = [ ..., path( 'my_year_path/<int:my_year>/', TemplateView.as_view( template_name = 'chapter_4/index.html' ) ) ] -
接下来,再次运行你的项目。使用
int路径转换器时,当我们尝试导航到http://localhost:8000/my_year_path/testing/这个 URL 时,它将不再工作。相反,我们应该看到之前看到的相同的 404 调试信息。现在它只允许任何长度的数字值。这意味着当我们访问 URLhttp://localhost:8000/my_year_path/2/或任何数字值时,我们应该看到主页图片。
当我们编写 int:my_year 时,此参数中的 my_year 可以命名为我们想要的任何名称。同样,path:my_pattern 参数中的 my_pattern 以及任何其他转换类型中的 my_pattern 也适用。第二个参数是用于在视图类或方法中访问该关键字参数的内容。
让我们接下来编写一个自定义路径转换器。
自定义路径转换器
自定义路径转换器是我们编写一个使用正则表达式来定义 Django 监听的路径的类的方式。转换器类以返回在视图中预期使用的数据类型的方式进行结构化,例如在上一小节示例中使用的 int 数据类型。此类还返回数据类型的另一个字符串表示形式,该表示形式是预期用于 URL 的。例如,如果我们不希望 http://localhost:8000/my_year_path/2/ 是一个有效的 URL,并且我们只想允许四位数,可以使用自定义路径转换器来完成此操作。
按照以下步骤创建你的自定义路径转换器:
-
在你的
/chapter_4/应用程序目录中,创建一个名为converters.py的新文件。 -
在文件内部,添加以下类,包含提供的两个方法:
# /becoming_a_django_entdev/chapter_4/converters.py class YearConverter: regex = '[0-9]{4}' def to_python(self, value): return int(value) def to_url(self, value): return '%04d' % value -
在你的
/chapter_4/urls.py文件中,添加以下代码,该代码注册了新创建的转换器类,以便在以下代码块中突出显示的<year:year>处使用:# /becoming_a_django_entdev/chapter_4/urls.py ... from django.urls import path, register_converter from .converters import YearConverter register_converter(YearConverter, 'year') urlpatterns = [ ..., path( 'my_year_path/<year:year>/', TemplateView.as_view( template_name = 'chapter_4/index.html' ) ), ] -
现在,运行你的项目并导航到 URL
http://localhost:8000/my_year_path/2/;你应该看到一个 404 调试信息。这是因为前面的模式现在只会接受四位数的整数,包括0001和1111,这是预期的。
我们可以通过编写基于方法或类的视图来进一步深入了解,在该视图中比较年份是否大于,比如说,1900 年,如果不是,则返回 404 响应。我们将在本章的处理条件响应部分中很快讨论如何做。
接下来,让我们练习使用正则表达式路径。
函数 – re_path()
re_path()函数,也称为正则表达式路径函数,类似于path()函数,但允许我们传递一个格式化的正则表达式字符串作为路由参数,而无需创建自定义路径转换器。
例如,我们可以像之前一样写出相同的年示例,而不使用转换器类。在你的/chapter_4/urls.py文件中,添加以下路径,并注释掉之前的my_year_path:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls
import path, re_path
...
urlpatterns = [
...,
re_path(
'my_year_path/(?P<year>[0-9]{4})/$',
TemplateView.as_view(
template_name = 'chapter_4/index.html'
)
),
]
使用re_path()函数和编写自己的转换器类之间实际上是有区别的。区别在于我们在视图类或方法中使用该值时识别到的模式值的类型。使用re_path()函数时,该值在视图中的数据类型始终是字符串,而使用转换器类时,该值的数据类型始终是该类def to_python()方法定义的数据类型,这意味着如果你需要,你可以将数据类型转换为任何你想要的类型。
在我们说明使用转换器类和使用re_path()函数之间的数据类型差异之前,让我们将一个 URL 模式映射到一个简单视图。
映射 URL 模式
编写自定义视图是我们执行渲染包含我们想要的所有内容的页面所需的所有任务和服务的一种方式。在视图中,我们可以根据业务逻辑规则进行验证,以确定如何处理请求。
在这个练习中,我们将使用本章前面写下的年模式,只允许年份大于 1900。任何小于这个年份的,我们将告诉 Django 返回 404 响应。
使用简单视图
简单视图也称为基于方法的视图,是 Python 中的一个可调用函数。
按照以下步骤将映射到你的项目中的简单视图:
-
在你的
/chapter_4/urls.py文件中,恢复使用本章使用路径转换器小节中编写的相同的转换器类。参考我们将在不同文件中编写的视图practice_view(),如下所示:# /becoming_a_django_entdev/chapter_4/urls.py ... from django.urls import ..., register_converter from .converters import YearConverter from .views import practice_view register_converter(YearConverter, 'year') urlpatterns = [ ..., path( 'my_year_path/<year:year>/', practice_view ), ]
我们与之前不同的地方是,我们将 TemplateView 类替换为名为 practice_view 的自定义简单视图类。
-
在你的
/becoming_a_django_entdev/chapter_4/目录下的views.py文件中创建一个名为practice_view()的视图方法。然后,添加以下代码:# /becoming_a_django_entdev/chapter_4/views.py from django.template.response import ( TemplateResponse ) def practice_view(request, year): return TemplateResponse( request, 'chapter_4/my_practice_page.html', { 'year': year } ) -
将本书代码中提供的模板文件复制到你的项目中,位于
/becoming_a_django_entdev/chapter_4/templates/chapter_4/my_practice_page.html。 -
导航到 URL
http://localhost:8000/my_year_path/1066/,你应该能看到以下截图所示的练习页面:

图 4.5 – 超出范围的年份返回有效响应
我们已经接近完成了。这里看到的成功消息是预期的。我们实际上想要返回一个 404 响应而不是有效的路径,以便符合之前讨论的业务逻辑,只允许年份大于或等于 1900。因此,我们需要使用关键字参数和条件语句在处理请求时执行自定义验证,我们将在下一步中这样做。
在视图中使用 kwargs
要在视图方法内部访问关键字参数,我们需要将其作为该方法的定位参数传递。在示例 def practice_view(request, year): 中,year 将是定位关键字参数。由于我们在 urls.py 文件中定义了一个名为 year 的路径转换器,因此当访问具有相同名称的视图时,我们必须将 year 作为定位参数包含在内。如果没有这个参数,Django 在运行时会给我们一个错误。
按照以下步骤配置你的 view 方法:
-
在你的
/chapter_4/urls.py文件中,使用以下 URL 模式,以及我们之前创建的相同的路径转换器类。注释掉其他my_year_path模式:# /becoming_a_django_entdev/chapter_4/urls.py ... from django.urls import ..., register_converter from .converters import YearConverter from .views import ..., practice_year_view register_converter(YearConverter, 'year') urlpatterns = [ ..., path( 'my_year_path/<year:year>/', practice_year_view ), ] -
在你的
/chapter_4/views.py文件中,编写这里提供的新方法:# /becoming_a_django_entdev/chapter_4/views.py from django.template.response import ( TemplateResponse ) ... def practice_year_view(request, year): print(type(year)) print(year) return TemplateResponse( request, 'chapter_4/my_year.html', {'year': year} ) -
复制本书代码中提供的模板文件,位于
/becoming_a_django_entdev/chapter_4/templates/chapter_4/my_year.html。 -
现在,导航到 URL
http://localhost:8000/my_year_path/2022/,你应该看到一个成功的响应。当我们查看终端或命令行窗口时,我们会看到它告诉我们year的值是2022,并且它是整数类型,<class'int'>,如图所示:

图 4.6 – 转换类 – 整数数据类型
-
将你的
/chapter_4/urls.py文件中的 URL 模式改回使用re_path()函数,而不是之前使用的自定义YearConverter示例,如下所示:# /becoming_a_django_entdev/chapter_4/urls.py ... from .views import ..., practice_year_view #register_converter(YearConverter, 'year') urlpatterns = [ ..., re_path( 'my_year_path/(?P<year>[0-9]{4})/$', practice_year_view ), ]
你可以注释掉之前使用的 register_converter。
- 再次访问 URL
http://localhost:8000/my_year_path/2022/。你应该能在你的终端或命令行窗口中看到输出如何从<class'int'>变为<class 'str'>,如图所示:

图 4.7 – 转换类 – 字符串数据类型
现在,我们实际上可以看到使用 re_path() 编写模式和使用创建自定义转换器类的替代方法的区别。使用 re_path() 函数,我们现在必须在视图中采取额外的步骤将关键字参数转换为整数,然后我们才能检查 year 值是否大于某个年份。如果我们不进行这种转换,我们最终会收到一个错误,告诉我们 '>=' 不支持在 'str' 和 'int' 实例之间。如果使用相同的正则表达式模式重复使用,这意味着将字符串转换为整数多次,每次为使用该模式的每个视图。这就是所谓的 写两次一切 (WET) 设计原则,通常是不受欢迎的。编写转换器类将解决这个问题,并允许你根据 不要重复自己 (DRY) 设计原则只编写一次。
让我们接下来处理条件响应。
处理条件响应
与我们在之前的练习中返回有效的 TemplateResponse() 不同,我们最终将检查 year kwarg 的值是否大于或等于 1900。如果 year 值小于 1900,我们将引发一个 Http404() 响应。使用我们之前编写的自定义路径转换器 YearConverter 类的 URL 模式,我们将以整数而不是字符串作为关键字参数 year 的数据类型,这样我们就可以使用该值执行数学运算。
按照以下步骤配置你的条件语句:
-
在你的
/chapter_4/urls.py文件中,添加以下代码,确保注释掉或删除其他my_year_path模式:# /becoming_a_django_entdev/chapter_4/urls.py ... from django.urls import ..., register_converter from .converters import YearConverter from .views import ..., practice_year_view register_converter(YearConverter, 'year') urlpatterns = [ ..., path( 'my_year_path/<year:year>/', practice_year_view ), ] -
在你的
/chapter_4/views.py文件中,修改practice_year_view()以使其看起来像以下突出显示的代码:# /becoming_a_django_entdev/chapter_4/views.py from django.http import Http404 from django.template.response import ( TemplateResponse ) def practice_year_view(request, year): if year >= 1900: return TemplateResponse( request, 'chapter_4/my_year.html', {'year': year} ) else: raise Http404(f'Year Not Found: {year}') -
现在,访问 URL
http://localhost:8000/my_year_path/1066/,你应该会看到以下 404 错误消息,这是故意的:


图 4.8 – 超出范围的年份返回无效响应
- 接下来,访问一个年份大于
1900的路径,例如http://localhost:8000/my_year_path/2022/,你应该会看到一个成功的响应,就像这里所示的年份页面:


图 4.9 – 在范围内的年份返回有效响应
让我们接下来将模型链接到我们的视图和模板。
将模型链接到视图和模板
使用我们在 第三章 中创建的相同模型,模型、关系和继承,我们可以在模板中提供有关这些对象的信息。我们将编写一个 URL 模式,它将指向一个新的简单视图方法并显示有关车辆的信息。
按照以下步骤在模板中显示模型信息:
-
在你的
/chapter_4/urls.py文件中,包含以下 URL 模式:# /becoming_a_django_entdev/chapter_4/urls.py ... from .views import ..., vehicle_view urlpatterns = [ ..., path( 'vehicle/<int:id>/', vehicle_view, name = 'vehicle-detail' ), ]
我们的新视图将监听传递给我们的路径转换器的键,也称为 ID,作为该路径转换器的关键字参数。ID 用于在数据库中查找该对象,如果没有找到,则将提供 404 响应。我们不必写 <int:id>,可以针对路径转换器监听字符串,例如 VIN,使用 <str:vin>。然后,在执行数据库查询的视图中,搜索与 VIN 匹配的记录而不是车辆的 ID。欢迎你练习这两种选项。
-
在你的
/chapter_4/views.py文件中,添加以下import语句和view方法:# /becoming_a_django_entdev/chapter_4/views.py ... from django.http import Http404 from ..chapter_3.models import Vehicle def vehicle_view(request, id): try: vehicle = Vehicle.objects.get(id=id) except Vehicle.DoesNotExist: raise Http404(f'Vehicle ID Not Found: {id}') return TemplateResponse( request, 'chapter_4/my_vehicle.html', {'vehicle': vehicle} )
前面的 import 语句使用了两个点(..),这是 Python 路径语法,用于向上导航一个目录级别并进入同级的 chapter_3 文件夹,以便访问在 chapter_3 应用中编写的模型。当你在一个项目中处理许多不同的应用时,这是一种常见的做法。之前显示的 try/except 块检查请求的对象是否存在,如果存在,则引发一个 404 响应。
- 复制与本书代码一起提供的模板文件,位于
/becoming_a_django_entdev/chapter_4/templates/chapter_4/my_vehicle.html。
我们可以通过使用传递给 TemplateResponse 的上下文变量名称,从模板内部访问模型对象的任何字段。例如,当在模板文件中使用时,vehicle 上下文变量将被写成 {{ vehicle.vin }}。这已经在你刚刚复制到项目中的模板文件中完成了。
- 运行你的项目并导航到
http://localhost:8000/vehicle/4/。你应该在这个页面上看到车辆详细信息,如下面的截图所示:

图 4.10 – 车辆 ID = 4
如果你更改 URL 中的 ID,车辆将改变。如果你激活了 VIN 作为路径转换器,那么你将导航到 http://localhost:8000/vehicle/aa456789012345678/ 以查看相同的结果,使用 chapter_3 数据固定提供的数据。
现在我们有了可以工作的视图,我们可以练习在只提供路径转换器的 kwarg 和 name 属性值时获取反向 URL。
解析 URL
解析 URL 是将相对路径或对象转换为与唯一字段(如主键)相关的 URL 的过程。Django 的 URL 模式反向解析是一种使用我们提供的参数值生成 URL 结构的方法,而不是在位置中硬编码 URL 路径,这可能会随时间而损坏。我们可以在整个项目中使用模板标签和语句来使用 URL 模式的 name 参数。这被鼓励作为最佳实践,并遵循 DRY 设计原则,这使项目在演变过程中更不容易损坏。
让我们讨论如何使用 name 属性来获取反向解析模式。
命名 URL 模式
使用与我们在本章前面创建的相同的自定义 YearConverter 类和相同的 my_year_path URL 模式,执行以下操作以配置您的 URL 模式。
在您的 /chapter_4/urls.py 文件中,您应该有如下代码块中显示的路径,使用突出显示的 name 属性:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls
import ..., register_converter
from .converters
import YearConverter
from .views
import ..., practice_year_view
register_converter(YearConverter, 'year')
urlpatterns = [
...,
path(
'my_year_path/<year:year>/',
practice_year_view,
name = 'year_url'
),
]
现在,我们可以使用 reverse() 函数,我们将在下一步中这样做。
使用 reverse() 函数
reverse() 函数为我们提供了对象的相对 URL,提供了 name 属性值。在我们的视图中,我们将编写几个 print 语句来告诉我们当提供不同的输入参数时对象的相对路径。
按照以下步骤配置您的 view 方法:
-
在您的
/chapter_4/views.py文件中,在现有的import语句下方添加以下import语句:# /becoming_a_django_entdev/chapter_4/views.py ... from django.urls import reverse -
在您的
/chapter_4/views.py文件中,并在相同的practice_year_view()方法中,继续包含以下print语句。确保这些语句放置在执行return/raise调用的条件语句之前:# /becoming_a_django_entdev/chapter_4/views.py ... def practice_year_view(request, year): ... print(reverse('year_url', args=(2023,))) print(reverse('year_url', args=(2024,))) ...( Repeat as desired )... -
运行您的项目并使用此模式导航到任何 URL,例如
http://localhost:8000/my_year_path/2022/。在您的终端或命令行窗口中打印的内容将是每个 URL 的格式化相对路径,如下面的屏幕截图所示:

图 4.11 – 命名 URL – 视图使用
reverse() 方法是我们如何通过传递给该函数的参数来查找 URL。reverse() 方法可以在项目的任何地方导入和使用,而不仅仅是视图类或方法内部。此方法接受两个位置参数,第一个是 URL 模式的名称,例如前一个示例中突出显示的 year_url,这是必需的。第二个位置参数是传递给 reverse() 方法的关键字参数,有时也是必需的。如果为 URL 模式定义了多个路径转换器,它们将按照为该模式创建的顺序包含在 reverse() 方法中,并用逗号分隔。请记住,与每个路径转换器相关的关键字参数的位置很重要,并且遵循为该 URL 模式创建关键字参数的顺序。
使用 {% url %} 模板标签
{% url arg1 arg2 %} 模板标签的工作方式与 reverse() 方法类似,但它是直接在模板中使用的。这个标签也接受两个位置参数,就像 reverse() 方法一样。第一个参数监听 URL 模式的名称,第二个是参数列表。这些参数在使用此模板标签时用空格分隔。额外的参数按照为该 URL 模式创建路径转换器的顺序提供。当使用 {% url %} 标签时,可以包含使用和未使用关键字语法的参数。例如,以下两个标签及其使用方式都是有效的:
# Dummy Code
{% url 'year_url' 2023 5 25 %}
{% url 'year_url' year=2023 month=5 day=25 %}
上述代码块中的第二个示例将在我们实际上为 URL 模式创建了三个路径转换器(year、month 和 day)时使用。
如果我们创建了三个上下文变量 year、month 和 day 以在模板中使用,它们也可以用上下文变量替换,如下面的代码块所示:
# Dummy Code
{% url 'year_url' year month day %}
之前显示的代码仅用于说明目的,如果你在没有构建相关的 URL 模式和视图的情况下尝试使用它,它将会出错。
按照以下步骤配置你的项目以进行此练习:
-
在你现有的
/chapter_4/my_year.html文件中,取消注释以下提供的超链接,这些链接是在你将此文件复制到项目时随书代码一起提供的,或者手动添加,如所示。它们使用 Django 的{% url %}模板标签进行格式化:# /becoming_a_django_entdev/chapter_4/templates/chapter_4/my_year.html ... <html> ... <body style="text-align:center"> ... <br /> <br /> <a href="{% url 'year_url' 2023 %}">2023</a> <a href="{% url 'year_url' 2024 %}">2024</a> ...( Repeat as desired )... </body> </html> -
运行你的项目并导航到相同的 URL,
http://localhost:8000/my_year_path/2022/,你现在应该能看到以下截图所示的内容,其中超链接已渲染到页面中:

图 4.12 – 命名 URL – 模板使用
每个渲染的超链接都指向 href="/my_year_path/####/" 中的相关相对路径。我们可以继续修改这两个示例,将绝对 URL 而不是相对 URL 格式化。这意味着我们将包括 URL 的 www.example.com 部分。我们将在本章稍后的标题为 解析绝对 URL 的部分中讨论这一点。接下来,让我们处理尾部斜杠。
处理尾部斜杠
在 Django 中,我们可以结合使用 re_path() 函数和自定义的 YearConverter 类来编写一个接受带有和没有尾部斜杠 / 的路径的 URL 模式。这意味着我们可以编写一个 URL 来监听 www.example.com/my_path/,同时也会允许 www.example.com/my_path 渲染成功,本质上是将两个路径合并为一个语句。
要处理你的尾部斜杠,在你的 /chapter_4/urls.py 文件中,添加以下路径并取消注释所有其他 my_year_path 示例:
# /becoming_a_django_entdev/chapter_4/urls.py
...
from django.urls import (
...,
re_path,
register_converter
)
from .converters
import YearConverter
from .views
import ..., practice_view
register_converter(YearConverter, 'year')
urlpatterns = [
...,
re_path(
r'^my_year_path/(?P<year>[0-9]+)/?$',
practice_view
),
]
route在re_path()函数中定义为r'^my_year_path/(?P<year>[0-9]+)/?$',它以这种方式构建路径,以便监听可选的前斜杠。year也仅使用标签名编写。如果我们像在之前的练习中那样使用<year:year>来编写这个语句,那么我们将在终端或命令行窗口收到以下错误信息:
django.core.exceptions.ImproperlyConfigured: "^my_year_path/(?P<year:year>[0-9]+)/?$" is not a valid regular expression: bad character in group name 'year:year' at position 18
由于我们通过正则表达式操作监听尾随斜杠,因此无需修改settings.py文件中的值,如APPEND_SLASH。为了实际使用APPEND_SLASH变量,Django 需要安装common中间件。您可以在以下链接中了解更多关于使用此方法而不是正则表达式方法的信息:https://docs.djangoproject.com/en/4.0/ref/settings/#append-slash。使用之前显示的正则表达式基本结构,我们不需要担心中间件。
既然我们已经解决了相对 URL,接下来让我们解决绝对 URL。
解决绝对 URL
一个绝对 URL 包括 URL 的方案、主机和端口号,如下所示,scheme://host:port/path?query。这是一个绝对 URL 的例子:www.example.com:8000/my_path?query=my_query_value。
接下来,我们将介绍使用自定义上下文处理器的实践,同时解决一个绝对 URL。
创建上下文处理器
上下文处理器在许多方面都很有用:它们提供了在项目中的所有模板和视图中共享的全局上下文。或者,在视图中创建的上下文只能由使用该视图的模板使用,而不能由其他模板使用。在下一个示例中,我们将创建并激活一个自定义的全局上下文处理器,我们将添加站点的基 URL。我们将上下文变量命名为base_url,指的是整个项目站点中找到的 URL 的scheme://host:port。
按照以下步骤创建您的上下文处理器:
-
在您的
settings.py文件所在的同一文件夹中,创建一个名为context_processors.py的新文件。 -
在此文件中,放置以下提供的代码,这将根据我们在其上运行项目的环境构建站点的
http://localhost:8000部分:# /becoming_a_django_entdev/context_processors.py def global_context(request): return { 'base_url': request.build_absolute_uri( '/' )[:-1].strip('/'), }
上下文作为键值对的字典返回,我们可以打包我们想要的任意多的键。
-
为了在运行时注册此上下文处理器,我们需要将其添加到
settings.py文件中的TEMPLATES变量下。包括您的global_context()方法的路径,如下所示:# /becoming_a_django_entdev/settings.py TEMPLATES = [ { ... 'OPTIONS': { 'context_processors': [ ..., 'becoming_a_django_entdev.context_processors.global_context', ], }, },]
将您的自定义上下文处理器放置在前面列表中的任何现有context_processors下面。
上下文处理器也可以在项目中的各个应用程序中分解。将你创建的每个附加上下文处理器包含在前面列表中,并按所需顺序排列。此外,本书的代码中还包含了一些额外的全局上下文处理器变量,以供额外练习。
让我们接下来在模板中使用我们新创建的base_url上下文。
在模板中使用上下文处理器数据
使用{% url %}模板标签,我们可以修改超链接以使用我们在上一个示例中提供的上下文,该上下文称为global_context()。
按照以下步骤配置你的模板:
-
在你的
/chapter_4/urls.py文件中,添加以下路径并注释掉所有其他my_year_path示例:# /becoming_a_django_entdev/chapter_4/urls.py ... from django.urls import ..., register_converter from .converters import YearConverter from .views import ..., practice_year_view register_converter(YearConverter, 'year') urlpatterns = [ ..., path( 'my_year_path/<year:year>/', practice_year_view, name = 'year_url' ), ] -
在你的
my_year.html文件中,编写/取消注释以下超链接示例:# /becoming_a_django_entdev/chapter_4/templates/chapter_4/my_year.html ... <html> ... <body style="text-align:center"> ... <br /> <br /> <a href="{{ base_url }}{% url 'year_url' 2023 %}">2023</a> <a href="{{ base_url }}{% url 'year_url' 2024 %}">2024</a> ...( Repeat as desired )... </body> </html> -
再次导航到
http://localhost:8000/my_year_path/2022/。现在每个超链接的href属性将看起来像href="http://localhost:8000/my_year_path/####/",而不是之前渲染的内容,即href="/my_year_path/####/"。
当我们添加了{{ base_url }}模板变量时,我们引用了提供的上下文字典的键。
从请求对象中
在这个练习中,我们将使用request对象解析绝对 URL。按照以下步骤在你的项目中执行:
-
在你的
/chapter_4/views.py文件中,在你的现有practice_year_view()方法中,包含以下print语句。这些语句将使用request对象中提供的build_absolute_uri()方法,作为 Django 框架的一部分。这将返回给我们反向查找的绝对 URL:# /becoming_a_django_entdev/chapter_4/views.py ... from django.urls import reverse def practice_year_view(request, year): ... print( request.build_absolute_uri( reverse('year_url', args=(2023,)) ) ) print( request.build_absolute_uri( reverse('year_url', args=(2024,)) ) ) ...( Repeat as desired )...
前面的print语句还利用了在django.urls库中找到的reverse()方法。
- 运行你的项目并导航到
http://localhost:8000/my_year_path/2022/。你应该在终端或命令行窗口中看到以下路径被打印出来:

图 4.13 – 命名 URL – 视图使用 – 绝对 URL
注意
当前页面的相对路径可以通过使用request.path从request对象中检索。在这个页面上,它将返回/my_year_path/2022/。使用print(request.build_absolute_uri())而不使用reverse()查找函数将返回该特定请求的绝对路径。
让我们练习在模型类内部查找绝对 URL。
在模型类内部
我们将扩展相同的vehicle_view()方法来进行下一个示例,从现有对象中获取格式化的 URL。我们将在这个与我们在第三章,“模型、关系和继承”中工作的相同的/chapter_3/models.py文件中工作。
按照以下步骤配置你的模型类:
-
在你的
/chapter_3/models.py文件中,向现有的Vehicle模型类添加以下两个方法(get_url()和get_absolute_url()):# /becoming_a_django_entdev/chapter_3/models.py class Vehicle(models.Model): ... def get_url(self): from django.urls import reverse return reverse( 'vehicle-detail', kwargs = {'id': self.pk} ) def get_absolute_url(self, request): from django.urls import reverse base_url = request.build_absolute_uri( '/' )[:-1].strip('/') return base_url + reverse( 'vehicle-detail', kwargs = {'id': self.pk} )

-
在你的
/chapter_4/views.py文件中,在现有的vehicle_view()方法中,在except捕获之后,作为else捕获的一部分添加以下print语句:# /becoming_a_django_entdev/chapter_4/views.py ... from django.http import Http404 from ..chapter_3.models import Vehicle ... def vehicle_view(request, id): try: vehicle = Vehicle.objects.get(id=id) except Vehicle.DoesNotExist: raise Http404(f'Vehicle ID Not Found: {id}') else: print(vehicle.get_url()) print(vehicle.get_absolute_url(request)) ...
else捕获意味着它所搜索的Vehicle对象没有错误地找到。请记住,在vehicle_view()方法的末尾留下我们之前写的相同的return语句。
- 运行你的项目,导航到
http://localhost:8000/vehicle/4/。在你的终端或命令行窗口中,你应该看到在vehicle_view()方法中查找的对象的两个不同的相对和绝对路径,如下所示:
图 4.14 – 模型 URLs
![Figure 4.14 – Model URLs
按照以下步骤配置你的基于类的视图:
我们一直在练习简单的视图,也称为基于方法视图。许多项目需要视图提供更多功能和可用性,这可以通过基于类的视图来实现,我们将在下一节中创建它。
复杂视图的工作
视图方法适用于许多不同的情况。对于更健壮和大规模的项目,我们可以应用一些技巧来使这些视图在复杂的使用案例中更具适应性。在编写可适应和可重用的应用程序时,我们使用基于类的视图。
基于类的视图
使用基于类的视图,我们可以编写易于重用和扩展的代码。就像我们在第三章“模型、关系和继承”中扩展模型一样,我们可以以完全相同的方式扩展视图类,而基于函数的视图方法则无法提供这种能力。本书源代码中提供了两个模板,用于下一项练习。这两个文件与my_vehicle.html文件完全相同,只是每个文件中<h1>标签的标题分别更改为VehicleView Class 1和VehicleView Class 2,这样当我们运行以下示例时,我们可以看到它们之间的差异。
这些方法导入本节之前介绍的reverse()函数,以获取对象的 URL。import语句被添加到方法本身,而不是在本文档的顶部,以便在使用这些模型类方法时更好地处理性能。第一个方法get_url()用于返回对象的相对 URL 路径,而另一个方法get_absolute_url()旨在返回对象的绝对路径。
-
将名为
my_vehicle_class_1.html和my_vehicle_class_2.html的文件复制到本书提供的代码中的/becoming_a_django_entdev/chapter_4/templates/chapter_4/目录,并将它们放入你的项目中的同一目录。 -
在你的
/chapter_4/urls.py文件中,添加以下import语句和 URL 模式:# /becoming_a_django_entdev/chapter_4/urls.py ... from .views import ..., VehicleView urlpatterns = [ ..., path( 'vehicle/<int:id>/', VehicleView.as_view(), name = 'vehicle-detail' ), ]
不要忘记注释掉之前实验这个模式之前写的旧的/vehicle/ URL 模式。
-
在你的
/chapter_4/views.py文件中,创建一个基于类的视图VehicleView并添加import语句,如下所示:# /becoming_a_django_entdev/chapter_4/views.py ... from django.http import Http404 from django.template.response import ( TemplateResponse ) from django.views.generic import View from ..chapter_3.models import Vehicle ... class VehicleView(View): template_name = 'chapter_4/my_vehicle_class_1.html' -
将以下
get()方法添加到你的VehicleView类中:# /becoming_a_django_entdev/chapter_4/views.py ... class VehicleView(View): ... def get(self, request, id, *args, **kwargs): try: vehicle = Vehicle.objects.get(id=id) except Vehicle.DoesNotExist: raise Http404( f'Vehicle ID Not Found: {id}' ) return TemplateResponse( request, self.template_name, {'vehicle': vehicle} ) -
将以下
post()方法及其导入添加到VehicleView类中:# /becoming_a_django_entdev/chapter_4/views.py ... from django.http import ..., HttpResponseRedirect ... class VehicleView(View): ... def post(self, request, *args, **kwargs): return HttpResponseRedirect( '/success/' ) -
运行你的项目并导航到
http://localhost:8000/vehicle/4/。现在你应该看到主标题显示为 VehicleView Class 1。 -
接下来,修改 URL 模式以覆盖
template_name,使用以下示例:# /becoming_a_django_entdev/chapter_4/urls.py ... from .views import ..., VehicleView urlpatterns = [ ..., path( 'vehicle/<int:id>/', VehicleView.as_view( template_name = 'chapter_4/my_vehicle_class_2.html' ), name = 'vehicle-detail' ), ] -
现在,重新运行你的项目并导航到
http://localhost:8000/vehicle/4/的 URL。这次你应该在页面上看到标题显示为 VehicleView Class 2。
在 步骤 4 中描述的 def get() 子方法是将方法视图中的所有代码移动到那里的地方。它也是唯一必需的方法。其他可选方法,如 def post(),在处理表单对象、执行回发响应时使用。它还可以用于将用户重定向到成功页面,这在 步骤 5 的代码中有所说明,但按照我们现在使用这个类的方式,你永远不会触发 Django 的这个重定向,这是可以预料的。我们将在 第五章 Django 表单 中更深入地讨论这个问题。当我们处理 URL 的位置关键字参数时,它们被传递到视图类中,其中 id 属性是在前面的 get() 方法中编写的。如果你有多个关键字参数,它们将按照在 URL 模式中的顺序添加到 id 之后。
我们执行了 步骤 7 和 步骤 8,只是为了检查这个功能是否正常工作,并看看我们如何仍然可以像在本章早期那样覆盖默认设置。接下来,让我们扩展基于类的视图,也称为继承。
扩展基于类的视图
扩展基于类的视图,也称为继承,与我们在 第三章 模型、关系和继承 中扩展模型类的方式完全相同。通过将第一个类扩展到第二个类,我们可以显示相同的标题在页面上,从而消除了在 URL 模式本身中定义 template_name 的需要,以及其他许多好处。
按照以下步骤扩展你的类:
-
在你的
/chapter_4/urls.py文件中,取消注释之前的 URL 模式,并使用提供的代码编写一个新的模式,现在我们使用VehicleView2作为视图类:# /becoming_a_django_entdev/chapter_4/urls.py ... from .views import ..., VehicleView2 urlpatterns = [ ..., path( 'vehicle/<int:id>/', VehicleView2.as_view(), name = 'vehicle-detail' ), ] -
接下来,在你的
/chapter_4/views.py文件中,添加以下由VehicleView类构造的VehicleView2类:# /becoming_a_django_entdev/chapter_4/views.py ... class VehicleView2(VehicleView): template_name = 'chapter_4/my_vehicle_class_2.html' -
运行你的项目并导航到 URL
http://localhost:8000/vehicle/4/。你应该在页面上看到相同的标题,VehicleView Class 2。
上述示例只是对现有的 VehicleView 类的一个非常简单的扩展,展示了如何扩展视图类。在这个练习中,我们唯一更改/覆盖的是 template_name 变量,以展示这个概念。
接下来,让我们学习异步视图的用途。
异步视图
Django 还提供了对异步视图的支持,这是 Django 3.1 中首次引入的功能。异步视图是可以由单个处理线程处理并在同一时间运行的视图。这些用于构建更好的多线程应用程序。传统的 Django 项目默认使用Web 服务器网关接口(WSGI)。要实际使用基于函数和类的异步视图,我们需要配置项目和服务器以使用异步服务器网关接口(ASGI)而不是 WSGI。由于这需要配置服务器和可能的主机提供商进行相当多的工作,我们将跳过提供本节示例,但如果这是您项目中的需求,您可以从这里开始:docs.djangoproject.com/en/4.0/topics/async/。
到目前为止,我们一直在使用本书提供的预构建模板和代码来演示核心编程概念。接下来,让我们探索如何自己构建这些模板。
与模板一起工作
Django 模板语言为我们提供了一套模板标签和模板过滤器,用于在模板中直接执行简单操作。这使得执行简单的逻辑操作变得容易,例如 Python 操作。标签和过滤器实际上是两种相似的东西。Django 模板语言可以与 Shopify 的 Liquid 语法紧密比较,类似于 ASP.NET 框架中使用的 Razor 语法,但 Django 模板语言使用起来和阅读起来要简单一些。Django 还允许我们在项目中创建自定义标签和过滤器。自定义过滤器最常用于转换单个上下文变量。自定义标签提供了更强大和复杂的用例。要详细了解所有存在的模板标签和模板过滤器,请阅读官方 Django 文档,链接如下:docs.djangoproject.com/en/4.0/ref/templates/builtins/。
接下来,我们将简要介绍最常用的模板标签和过滤器功能。
模板标签
我们可以通过将模板分解成更小的组件来使其结构更像是应用程序。这些组件可以在其他模板中互换使用。例如,我们可以编写一个包含页面<head>和<body>元素的基模板,然后拆分出子模板,这些子模板结构化每个模板的正文内容。我们可以在文档的<head>和<body>中创建区域,以便将动态文本和 HTML 传递到这些区域中,例如<title>标签。
对于下一个示例,让我们使用 {% block %}、{% extend %} 和 {% include %} 模板标签来创建两个模板文件,演示如何将模板分解成可管理的部分。
按照以下步骤配置你的模板标签:
-
在你的
/chapter_4/urls.py文件中,注释掉其他路径,并包含以下路径:# /becoming_a_django_entdev/chapter_4/urls.py ... from .views import ..., TestPageView urlpatterns = [ ..., path( 'test_page_1/', TestPageView.as_view(), name = 'test-page' ), ] -
在你的
/chapter_4/views.py文件中,创建以下基于类的视图TestPageView,使用这里提供的代码:# /becoming_a_django_entdev/chapter_4/views.py ... from django.template.response import ( TemplateResponse ) from django.views.generic import View ... class TestPageView(View): template_name = 'chapter_4/pages/test_page_1.html' def get(self, request, *args, **kwargs): return TemplateResponse( request, self.template_name, { 'title': 'My Test Page 1', 'page_id': 'test-id-1', 'page_class': 'test-page-1', 'h1_tag': 'This is Test Page 1' } )
在 TestPageView 类中,我们定义了一个默认的 template_name 为 'chapter_4/pages/test_page_1.html'。在 get() 方法中,我们传递硬编码的上下文变量用于此演示。在实际场景中,这些信息将在执行生成这些值的逻辑后生成。
-
创建
/becoming_a_django_entdev/chapter_4/templates/chapter_4/pages/test_page_1.html文件,并添加以下代码:# /becoming_a_django_entdev/chapter_4/templates/chapter_4/pages/test_page_1.html {% extends 'chapter_4/base/base_template_1.html' %} {% load static %} {% block page_title %}{{ title }}{% endblock %} {% block head_stylesheets %}{% endblock %} {% block js_scripts %}{% endblock %} {% block page_id %}{{ page_id }}{% endblock %} {% block page_class %}{{ block.super }} {{ page_class }}{% endblock %} {% block body_content %} {% if h1_tag %} <h1>{{ h1_tag }}</h1> {% else %} <h1>Title Not Found</h1> {% endif %} {% endblock %}
此模板以 {% extends %} 模板标签开始,表示我们实际上想从 /chapter_4/base/base_template_1.html 文件开始,尽管我们在视图类中指定了 test_page_1.html 文件。然后,在这个文件中找到的每个 {% block %} 标签,我们都会覆盖或添加到我们在扩展的 base_template_1.html 文件中找到的相同 {% block %}。我们将视图定义中定义的 {{ title }} 的值传递到 /chapter_4/pages/test_page_1.html 文件的 {% block page_title %} 标签中。可以使用 {{ block.super }} 标签来保留 base_template_1.html 文件中相同块的内容。如果没有这个标签,父块中的所有代码都将被覆盖。可以在任何块中编写 HTML;在随后的 步骤 5 中显示的 {% block body_content %} 块是页面内容的主要部分。
-
创建
/chapter_4/base/base_template_1.html文件,并添加以下代码:# /becoming_a_django_entdev/chapter_4/templates/chapter_4/base/base_template_1.html {% load static %} <!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8" /> <title>{% block page_title %}My Page Title{% endblock %}</title> <link rel="stylesheet" href="{{ base_url }}{% static 'chapter_8/css/site.css' %}"> {% block head_stylesheets %}{% endblock %} <script defer type="text/javascript" src="img/site-js.js' %}"></script> </head> </html> -
在你刚刚创建的同一
/chapter_4/base/base_template_1.html文件中,插入以下提供的主体代码,位于现有的</head>标签下方:# /becoming_a_django_entdev/chapter_4/templates/chapter_4/base/base_template_1.html ... </head> <body id="{% block page_id %}{% endblock %}" class="{% block page_class %}base-template-class{% endblock %}" style="text-align: center;"> {% block header %} {% include 'chapter_4/headers/header_1.html' %} {% endblock %} {% block site_container %} <div class="site-container"> <div class="body-content"> {% block body_content %} {% endblock %} </div> {% block footer %} {% include 'chapter_4/footers/footer_1.html' with message='Footer of Document' %} {% endblock %} </div> {% endblock %} {% block js_scripts %}{% endblock %} </body> </html> -
将本书代码提供的
/chapter_4/headers/header_1.html和/chapter_4/footers/footer_1.html文件复制到你的项目中的同一目录。 -
将本书代码提供的
/chapter_4/static/chapter_4/css/site.css和/chapter_4/static/chapter_4/js/site-js.js文件复制到你的项目同一目录。 -
运行你的项目,并导航到
http://localhost:8000/test_page_1/。你应该在你的浏览器窗口中看到以下信息:

图 4.15 – 扩展模板
在前面的步骤中,页面的主要 HTML 结构被分解为头部、主体内容和页脚格式。之前使用的{% include %}标签展示了如何与这些文件进行不同的操作。给任何{% include %}标签添加with属性,我们就可以从父模板传递上下文到该文件。这就是对前面的页脚文件所做的那样。这意味着我们可以在不使用上下文处理器或重复编写代码的情况下使上下文可用。前面的 HTML 结构是以一种方式组织的,允许我们在需要或想要修改{% block site_container %}标签内的所有内容时变得复杂。为了做到这一点,我们会在扩展此模板文件的文件中再次编写{% block site_container %}块,并在那里编写修改后的代码。这正是步骤 3为我们所做的事情,使用了{% block body_content %}标签。
让我们接下来处理模板过滤器。
模板过滤器
模板过滤器是一种转换上下文变量值的方法。它们可以执行诸如使用{{ context_variable|upper }}或{{ context_variable|lower }}过滤器将字符串转换为大写或小写,或者使用{{ my_list|length }}过滤器查找列表中的项目数量,甚至使用{{ my_time|time:" n/j/Y" }}过滤器格式化时间等操作。当使用time过滤器时,没有必要指定该过滤器的:" n/j/Y"参数。即使没有这些指定,Django 也会默认使用你在settings.py文件中指定的TIME_FORMAT变量。要了解所有可用的过滤器,请访问以下官方 Django 文档:https://docs.djangoproject.com/en/4.0/ref/templates/builtins/#built-in-filter-reference。
让我们来看看自定义标签和过滤器。
自定义标签和过滤器
在前面的图 4.10中,我们看到了车辆的制造商值以数字3的形式显示。这是一个完美的例子,说明了我们可以编写一个自定义过滤器,该过滤器接受一个数值并返回该值的字符串表示。
按照以下步骤创建你的自定义过滤器:
-
在
/becoming_a_django_entdev/chapter_4/目录中创建一个名为templatetags的新文件夹。 -
在这个文件夹中创建一个名为
chapter_4.py的新文件,并在该文件中放置以下代码:# /becoming_a_django_entdev/chapter_4/templatetags/chapter_4.py from django.template import Library register = Library() @register.filter(name = 'vehicle_make') def vehicle_make(value): from ...chapter_3.models import MAKE_CHOICES for i, choice in enumerate(MAKE_CHOICES): if i == value: try: return choice[1] except ValueError: pass return ''
在这里,我们编写了一个非常简单的名为vehicle_make()的方法,它接受数字值3,并在模板中使用时返回字符串表示的Chevrolet。在这个方法中,我们使用 Python 路径语法导入我们在第三章中创建的MAKE_CHOICES变量,该变量位于模型、关系和继承部分的可变对象与不可变对象小节中。
-
确保你已经取消注释了之前的 URL 模式,并使用以下代码块中显示的模式:
# /becoming_a_django_entdev/chapter_4/urls.py ... from .views import ..., vehicle_view urlpatterns = [ ..., path( 'vehicle/<int:id>/', vehicle_view, name = 'vehicle-detail' ), ] -
在你现有的
/chapter_4/my_vehicle.html文件中,将{{ vehicle.make }}更改为以下代码块中突出显示的语句,并将chapter_4模板标签库添加到你的{% load %}标签中:# /becoming_a_django_entdev/chapter_4/templates/chapter_4/my_vehicle.html {% load static chapter_4 %} ... {% if vehicle %} ... <p>{{ vehicle.make }}</p> <p>{{ vehicle.make|vehicle_make }}</p> ... {% endif %} ...
为了使用我们注册的模板过滤器,我们使用{% load chapter_4 %}标签将其导入 HTML 文件,其中我们加载的模板标签集的名称是我们创建在任何templatetags文件夹中的应用程序中的 Python 文件名。
- 现在,确保你的项目正在运行,你可以导航到
http://localhost:8000/vehicle/4/这个 URL,以查看我们的车辆现在显示为雪佛兰。
通过将@register.filter(name = 'my_filter')更改为@register.tag(name = 'my_tag'),我们可以创建自定义模板标签而不是自定义过滤器。在这种情况下,标签可以在模板中使用,类似于{% my_tag %}。要了解更多关于编写自己的模板标签的复杂性以及它们如何在项目中有用的信息,请访问关于该主题的官方文档,链接如下:https://docs.djangoproject.com/en/4.0/howto/custom-template-tags/#writing-custom-template-tags。
接下来,让我们添加一些自定义错误页面。
错误页面模板
Django 提供了一个非常简单的方法来创建自定义错误页面模板,用于处理 400、403、404 和 500 等错误。其他错误,如templates目录中的400.html、403.html、404.html和500.html,只要它们没有被放置在子文件夹中。这四个模板文件与本书的代码一起提供,并遵循本章标题为模板标签的子节中描述的设计模式。为了查看自定义调试模板,我们必须在settings.py文件中关闭DEBUG。
按照以下步骤配置你的错误页面:
-
将本书代码中提供的
/becoming_a_django_entdev/chapter_4/templates/目录中的四个错误页面模板文件复制到你的项目相同目录中。这四个文件是400.html、403.html、404.html和500.html,同时也要复制同一目录中的base_error.html文件。 -
在你的
settings.py文件中,将以下值更改为False:# /becoming_a_django_entdev/settings.py ... DEBUG = False -
确保你的虚拟环境已激活,并运行此处显示的
collectstatic命令,以便访问到目前为止创建的静态文件:(virtual_env) PS > python manage.py collectstatic -
当
DEBUG关闭时,每次静态文件更改,我们必须运行collectstatic命令,以便在浏览器中看到更改的反映。 -
现在,运行你的项目并导航到网站上任何不存在的 URL,例如我们尚未创建 URL 模式的 URL,如
http://localhost:8000/asdfasdf。你应该在你的浏览器窗口中看到以下消息,而不是我们习惯看到的调试错误消息,如图 4.2所示:

图 4.16 – 自定义错误页面
摘要
到目前为止,我们可能已经构建了一个看似完整的项目,但实际上,一个应用程序将包含比本章所涵盖的内容多得多的东西。我们所拥有的,是一种将 URL 路径路由到视图并渲染每个模板中不同上下文的方法。我们学习了如何在视图中查询数据库以获取我们在模板中想要渲染的数据。我们甚至涵盖了处理和生成错误页面的不同方式,或者简单地将 URL 重定向到另一个路径。我们还使用了基于类的视图来编写可重用的类结构,从而使项目在长期中更能适应变化。
在下一章中,我们将讨论如何结合本章所学的基于函数和基于类的视图以及模板,来使用表单对象。
第五章:第五章:Django 表单
在编程中,表单是一个包含输入字段、下拉框、单选按钮、复选框和提交按钮的对象。表单的职责是从用户那里捕获信息;之后所做的一切都可以是任何事情,包括将信息存储在数据库中、发送电子邮件或使用该数据生成报告文档。在本章中,我们将尽可能多地讨论在 Django 中如何使用表单。表单对象是一个非常复杂的话题,我们在这章中只有足够的空间来涵盖基本内容和一些高级主题。本章中的一些主题可以与其他在第八章中涵盖的主题结合起来,即使用 Django REST 框架工作,以在 SPA-like 页面上创建表单对象。
在我们的第一个表单中,我们将创建一个名为ContactForm的类,并以三种不同的方式构建一个电子邮件字段。稍后,当我们在浏览器中渲染该表单时,我们将观察其行为如何通过这三种不同的机制发生变化。我们想观察每个电子邮件字段上的验证是如何进行的,以及它们之间的行为差异。这将使我们更好地理解哪种机制是我们希望实现的目标行为所需的。我们甚至会探讨编写自定义字段类,并了解它们如何从长远来看对我们有益。
根据您项目的需求,您表单的验证可能需要大量的自定义 JavaScript 才能实现您的目标。本书的重点不是 JavaScript,而是 Django 的概念。然而,在本章结束时,我们将提供一个示例,演示如何在处理表单对象上的动态内联表单集时将 JavaScript 融合到您的项目中。您可以根据项目的需求扩展此示例,构建自己的自定义 JavaScript 函数。
在本章中,我们将涵盖以下内容:
-
表单类型
-
使用表单字段
-
清理表单
-
创建自定义表单字段
-
与表单视图一起工作
-
在模板中渲染表单
-
将模型链接到表单
-
添加内联表单集
技术要求
要在此章节中与代码一起工作,您需要在本地机器上安装以下工具:
-
Python 版本 3.9 – 作为项目的底层编程语言使用
-
Django 版本 4.0 – 作为项目的后端框架使用
-
pip 包管理器 – 用于管理第三方 Python/Django 包
我们将继续使用在第二章,“项目配置”中创建的解决方案。然而,并不需要使用 Visual Studio IDE。主要项目本身可以使用其他 IDE 运行,或者从项目根目录中独立使用终端或命令行窗口运行。这就是manage.py文件所在的位置。无论你使用什么编辑器或 IDE,都需要一个虚拟环境来与 Django 项目一起工作。如何创建项目和虚拟环境的说明可以在第二章,“项目配置”中找到。你需要一个数据库来存储项目中的数据。在上一章的示例中选择了 PostgreSQL;然而,你可以为你的项目选择任何数据库类型来与本章的示例一起工作。
我们还将使用以 Django fixture 形式提供的数据,这些数据在第三章,“模型、关系和继承”中提供,在标题为加载 Chapter_3 数据 fixture的小节中。确保将chapter_3 fixture 加载到你的数据库中;如果这已经完成,那么你可以跳过下一个命令。如果你已经创建了第三章,“模型、关系和继承”中提到的表,并且尚未加载该 fixture,那么在激活你的虚拟环境后,运行以下命令:
(virtual_env) PS > python manage.py loaddata chapter_3
本章创建的所有代码都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Becoming-an-Enterprise-Django-Developer。本章中展示的大部分代码可以在/becoming_a_django_entdev/becoming_a_django_entdev/chapter_5/目录中找到。
查看以下视频以查看代码的实际应用:bit.ly/3xQQ2H3。
为本章做准备
首先,在你的项目中创建一个名为chapter_5的新应用,按照第二章,“项目配置”中讨论的步骤进行。正如该部分所讨论的,不要忘记将/becoming_a_django_entdev/becoming_a_django_entdev/chapter_5/apps.py文件中你的应用类的name =变量的值更改为指向你安装应用的位置。确保还将此应用包含在settings.py文件中的INSTALLED_APPS变量中。在第四章,“URLs、视图和模板”的末尾,我们将DEBUG = False作为练习的一部分进行设置。确保将此设置回DEBUG = True以继续本书的剩余部分。
在网站的主要urls.py文件中,添加以下路径,该路径指向我们将要创建的本章的 URL 模式:
# /becoming_a_django_entdev/urls.py
...
urlpatterns = [
path(
'',
include(
'becoming_a_django_entdev.chapter_5.urls'
)
),
]
现在我们已经为这一章创建了应用程序,让我们开始使用 Django 管理站点来管理在 第三章 中创建的模型,模型、关系和继承。
表单类型
Django 被设计用来简化处理表单时涉及的大量工作。它是通过提供将你的表单对象渲染为 HTML 和处理表单提交上的数据的方式来做到这一点的。有很多人使用和操作表单对象的不同方法,但它们都始于一个表单类。Django 为我们提供了两个不同的类来使用,ModelForm 和 Form。这两个之间的区别是,一个直接链接到数据库中的表,而另一个则不。链接到数据库的 ModelForm 类将自动创建字段并根据在该模型类中设置的字段约束进行字段验证,从数据库级别。
表单类也使用一个 Meta 子类,正如在 第三章 中使用在模型类上一样,模型、关系和继承。Django 还提供了其他表单类,例如 BaseForm 和 BaseModelForm,用于编写抽象基表单类,但这些表单类超出了本书的范围。其他类与内联表单集相关,这基本上是在表单内的表单。在本章结束时,当表单被渲染时,我们将在页面上插入一个内联表单集,并在用户点击按钮时使用 JavaScript 添加更多。
让我们先讨论在创建表单类时导入和使用 Form 和 ModelForm 类。
表单类 – 表单
Form 类用于创建不链接到数据库的字段。这在表单发送电子邮件或生成 PDF 报告等情况下使用,仅举几个例子。
按照以下步骤创建你的 Form 类:
-
在你的
/becoming_a_django_entdev/chapter_5/目录中创建一个名为forms.py的文件。 -
在此文件中,包含以下代码:
# /becoming_a_django_entdev/chapter_5/forms.py from django.forms import Form class ContactForm(Form): pass
我们将在稍后讨论如何处理字段,但接下来让我们讨论如何导入 ModelForm 类。
表单类 – ModelForm
当我们想要直接在数据库中创建或修改数据时使用 ModelForm 类。每个字段都链接到它所代表的表的列。可以创建并使用不链接到你的数据库的额外字段。例如,你可以发送一个包含添加的字段数据的电子邮件。这个字段也可以是一个注释、时间戳或另一种类型的隐藏数据字段。
要创建你的 ModelForm 类,在你的现有 /chapter_5/forms.py 文件中,包含以下类:
# /becoming_a_django_entdev/chapter_5/forms.py
from django.forms
import Form, ModelForm
class VehicleForm(ModelForm):
pass
在本章的后面部分,我们将把这个类链接到在 第三章 中创建的 Vehicle 模型,模型、关系和继承。
接下来,让我们移除这些 pass 语句并开始处理字段参数。
使用表单字段
与在 第三章 “模型、关系和继承” 中介绍的常规模型字段类类似,Django 也提供了一些可供使用的表单字段类。区别在于模型字段类与数据库的列一起工作,而表单字段类仅用于模板中 HTML <form></form> 对象内的输入字段。
以下表格可以用作速查表,以参考在编写 Form 和/或 ModelForm 类时可用哪些字段:

表单字段也接受各种不同的字段参数,这些参数可以自定义每个字段的行为。在下一节中,我们将使用前面列表中的一些字段类型在我们的表单类中编写字段,并讨论可以使用的不同参数。
要了解每个这些字段类型的完整说明,请访问官方 Django 文档中关于字段类和参数的说明,可在以下位置找到:docs.djangoproject.com/en/4.0/ref/forms/fields/。
常见字段参数
我们将在本练习中开始向表单类添加字段,并介绍字段参数。字段参数是我们设置字段属性的一种方式。
要创建你的字段,在你的 /chapter_5/forms.py 文件中,添加以下代码块中突出显示的 import 语句,并在相同的 ContactForm 类中添加一个名为 full_name 的字段:
# /becoming_a_django_entdev/chapter_5/forms.py
from django
import forms
from django.forms
import Form, ModelForm
class ContactForm(Form):
full_name = forms.CharField(
label = 'Full Name',
help_text = 'Enter your full name, first and last name please',
min_length = 2,
max_length = 300,
required = True,
error_messages = {
'required': 'Please provide us with a name to address you as',
'min_length': 'Please lengthen your name, min 2 characters',
'max_length': 'Please shorten your name, max 300 characters'
}
)
在前面的例子中,我们使用 forms.CharField 字段类定义了一个 HTML <input type="text"> 对象。CharField 对象的默认小部件是一个 type="text" 输入字段。label 参数让我们可以定义将作为此字段 <label for="my_field_id">My Form Field Label</label> 的文本。
help_text 参数将在 文档对象模型 (DOM) 中的输入字段之后渲染一个 <span class="helptext">{{ your_help_text_message</span> 元素。
文档对象模型
DOM 是所有浏览器中找到的一个接口,它以节点树的形式呈现 HTML。这些节点代表树中的对象,其中 <span> 或 <input> 节点组成一个单一的对象。
min_length 和 max_length 参数在大多数字段类型中都会使用;它们分别定义了字段中允许的最小和最大字符数。required 参数将定义字段是否必须包含一个值才能有效。这些将作为 <input type="text" maxlength="300" minlength="2" required="" /> 对象的属性来渲染。
接下来,让我们更详细地讨论一下表单验证。在接下来的两个小节中,我们将介绍 widget 和 validator 参数。要了解所有可用且未涵盖的字段参数的完整说明,请访问docs.djangoproject.com/en/4.0/ref/forms/fields/#core-field-arguments。
字段小部件
字段的widget参数允许我们定义要使用哪种类型的字段,例如日期、电子邮件、密码或文本类型的输入对象。这也可以是复选框、单选按钮、下拉选择或文本区域,仅举几个例子。除非我们想要更改默认小部件或覆盖其初始属性,否则我们不需要指定widget参数。
按照下一步操作来覆盖full_name字段,以渲染带有id、class和placeholder属性的输入。我们希望得到的渲染输出应类似于以下示例代码:
# Demo Code
<input type="text" name="full_name" id="full-name" class="form-input-class" placeholder="Your Name, Written By...">
在你的/chapter_5/forms.py文件中,按照以下所示编辑你的full_name字段:
# /becoming_a_django_entdev/chapter_5/forms.py
from django
import forms
from django.forms
import Form, ModelForm
class ContactForm(Form):
full_name = forms.CharField(
...,
widget = forms.TextInput(
attrs = {
'id': 'full-name',
'class': 'form-input-class',
'placeholder': 'Your Name, Written By...'
}
),
),
如果我们将字段的默认小部件从forms.TextInput更改为其他,例如forms.EmailInput,那么它将渲染为<input type="email">。将forms.TextInput更改为forms.DateInput将渲染为<input type="date">。使用forms.TextArea将渲染为<textarea></textarea>对象。当然,这些只是许多不同选项中的一些。要了解所有可用的小部件及其如何帮助您构建字段,请访问docs.djangoproject.com/en/4.0/ref/forms/widgets/。
接下来让我们讨论使用字段验证器。
字段验证器
当手动定义小部件时,我们有时必须编写特定的验证规则。例如,让我们以forms.EmailInput类为例;这将需要添加验证规则,以确定用户提供的字符串值是否实际上在 example@example.com 格式中,而不是像IAmAString这样的随机字符串。
按照以下步骤创建和验证电子邮件字段:
-
在你的
/chapter_5/forms.py文件中,在现有的ContactForm中,添加以下所示的email_1字段:# /becoming_a_django_entdev/chapter_5/forms.py ... from django import forms from django.forms import Form, ModelForm from django.core.validators import EmailValidator class ContactForm(Form): ... email_1 = forms.CharField( label = 'email_1 Field', min_length = 5, max_length = 254, required = False, help_text = 'Email address in example@example.com format.', validators = [ EmailValidator( 'Please enter a valid email address' ), ], error_messages = { 'min_length': 'Please lengthen your name, min 5 characters', 'max_length': 'Please shorten your name, max 254 characters' } )
虽然可以使用验证器参数以这种方式操作字段,但 Django 试图为开发者提供选项,以最小化或减少他们需要编写的代码量。例如,我们不必编写前面的示例来在CharField上强制执行电子邮件格式,我们可以直接使用EmailField类,该类已经为我们强制执行了此规则。EmailField类包括处理电子邮件字段的全部逻辑和验证。
-
为了练习使用
EmailField类,我们将创建一个额外的字段来比较和对比两种代码方法。在你的/chapter_5/forms.py文件中,在相同的ContactForm类中,添加以下所示的email_2字段:# /becoming_a_django_entdev/chapter_5/forms.py from django import forms from django.forms import Form, ModelForm ... class ContactForm(Form): ... email_2 = forms.EmailField( label = 'email_2 Field', min_length = 5, max_length = 254, required = True, help_text = 'Email address in example@example.com format for contacting you should we have questions about your message.', error_messages = { 'required': 'Please provide us an email address should we need to reach you', 'email': 'Please enter a valid email address', 'min_length': 'Please lengthen your name, min 5 characters', 'max_length': 'Please shorten your name, max 254 characters' } )
在步骤 1和步骤 2中找到的代码之间的区别是,当使用EmailField类来产生与CharField类相同的行为时,我们不需要定义小部件或验证器参数。错误信息现在位于使用email键的error_messages参数中,如下所示。
要完整了解所有可用的验证器类和方法,请访问 docs.djangoproject.com/en/4.0/ref/validators/。接下来,让我们练习清理表单,这仅仅是执行验证的另一种方式。
清理表单
我们还可以以其他方式对表单字段进行验证。在表单类中,我们可以编写方法来验证每个字段,格式如下:def clean_{{ form_field_name }}()。在这样做的时候,只能访问我们正在清理的字段值。如果我们想访问该表单中找到的其他字段值,我们必须编写一个单独的 def clean() 方法,这将允许我们比较两个字段。例如,我们可以使用 def clean() 方法,仅在另一个字段的值不为空时要求字段。
下面的两个小节将分解这两个概念。
方法 – clean_{{ your_field_name }}()
要清理单个表单字段,请按照以下步骤操作:
-
在你的
/chapter_5/forms.py文件中,在相同的ContactForm类中,添加一个名为email_3的新字段,如下所示:# /becoming_a_django_entdev/chapter_5/forms.py from django import forms from django.forms import Form, ModelForm ... class ContactForm(Form): email_3 = forms.CharField( label = 'Email Using CharField and Using Clean Method', required = False, help_text = 'Email address in example@example.com format for contacting you should we have questions about your message.', ) -
在相同的
ContactForm类中,添加以下clean_email_3方法:# /becoming_a_django_entdev/chapter_5/forms.py ... from django.core.exceptions import ValidationError from django.core.validators import ( EmailValidator, validate_email ) class ContactForm(Form): ... def clean_email_3(self): email = self.cleaned_data['email_3'] if email != '': try: validate_email(email) except ValidationError: self.add_error( 'email_3', f'The following is not a valid email address: {email}' ) else: self.add_error( 'email_3', 'This field is required' ) return email
在前面的例子中,我们是从 django.core.validators 库中导入 validate_email() 方法,以确定字符串是否为电子邮件格式。首先,我们使用一个简单的条件语句来检查字段是否有值;如果没有,我们发出一个错误信息,指出 "This field is required"。即使 email_3 字段已将 required 参数设置为 False,我们也会执行验证检查。这仅仅说明了我们可以以另一种方式完成相同的事情。如果存在值,我们将在 validate_email() 方法周围包裹一个 Try/Except 语句,如果验证失败,我们将添加 "The following is not a valid email address: {{ field_value }}" 错误信息。
Form 和 ModelForm 类中提供的 self.add_error() 方法接受两个参数:第一个参数是字段的名称,第二个是你的自定义错误信息。我们不是使用 self.add_error('email_3', 'This field is required') 来向表单添加错误信息,而是可以使用 raise ValidationError('This field is required') 类代替。但是,有一个问题:使用这个类将会从 cleaned_data 值列表中移除该字段。如果你只使用 clean_email_3() 方法本身,那么这将有效。如果你想在 def clean() 方法中访问相同的清理数据,你需要在 def clean_email_3() 中返回该值,如之前 步骤 2 的最后一行所示。Django 会在执行 clean() 方法之前,为每个字段触发单独的清理方法,并将其保存为清理方法栈中的最后一个方法。如果你的字段值没有在特定字段的清理方法中返回,那么在需要时我们将无法访问它。
接下来,让我们使用clean()方法。
方法 – clean()
clean()方法用于在表单提交时访问表单中的所有字段数据。在这个方法中,你可以在允许成功的表单提交之前比较许多字段的值。接下来的示例将允许我们比较两个字段,并引发一个或多个不同的字段验证消息。
按照以下步骤配置你的clean()方法:
-
在你的
/chapter_5/forms.py文件中,向ContactForm类添加另一个名为conditional_required的字段,如下所示:# /becoming_a_django_entdev/chapter_5/forms.py from django import forms from django.forms import Form, ModelForm ... class ContactForm(Form): conditional_required = forms.CharField( label = 'Required only if field labeled "email_3" has a value', help_text = 'This field is only required if the field labeled "email_3 Field" has a value', required = False, ) -
在相同的
ContactForm类中,添加以下clean()方法:# /becoming_a_django_entdev/chapter_5/forms.py from django import forms from django.forms import Form, ModelForm ... class ContactForm(Form): ... def clean(self): email = self.cleaned_data['email_3'] text_field = self.cleaned_data[ 'conditional_required' ] if email and not text_field: self.add_error( 'conditional_required', 'If there is a value in the field labeled "email_3" then this field is required' )
在这个clean()方法中,我们将email_3字段的值赋给名为email的变量。然后,我们将conditional_required字段的值赋给名为text_field的变量。使用一个简单的条件语句,我们检查email是否有值,如果有,检查text_field是否有值。如果满足此条件,我们将在conditional_required字段中添加所需的错误。由于我们将conditional_required字段设置为使用required = False参数,如果email_3字段中没有值,则该字段不是必需的。
让我们继续创建我们自己的自定义表单字段。
创建自定义表单字段
有时,项目的需求超过了我们提供的选项。如果默认情况下没有可用的字段类,我们有两个选择:创建自己的或使用第三方包,其中其他人已经为我们编写了一个字段类。
继续使用相同的ContactForm类,我们将通过构建一个MultipleEmailField来展示验证机制之间的差异。这将是一个单字段,接受一串用逗号分隔的电子邮件地址。然后,将独立检查每个电子邮件项,看它是否在有效的电子邮件字符串格式中。我们将使用之前使用的相同的validate_email()函数来强制执行此约束。
字段类 – Field
Django 提供了一个名为Field的类,位于django.forms.fields库中,用于构建自定义字段类。这个类中发现的任何选项和方法都可以根据需要重写。例如,重写def __init__()方法将提供一种添加、更改或删除字段参数的方法,完全改变你以后与这些字段工作的方式。对于这个练习,我们实际上不会重写__init__()方法;相反,我们将使用to_python()和validate()方法。这些是我们执行MultipleEmailField上所需验证的唯一两个方法。
按照以下步骤编写你的Field类:
-
在你的
/becoming_a_django_entdev/chapter_5/文件夹中创建一个名为fields.py的新文件。 -
在那个文件中,添加以下
MultipleEmailField类和import语句:# /becoming_a_django_entdev/chapter_5/fields.py from django.core.exceptions import ValidationError from django.core.validators import validate_email from django.forms.fields import Field from django.forms.widgets import TextInput class MultipleEmailField(Field): widget = TextInput default_validators = [] default_error_messages = { 'required': 'Default Required Error Message', 'email': 'Please enter a valid email address or addresses separated by a comma with NO spaces' } -
如果你想要使用 Django 提供的一些验证器,请使用前面代码中显示的
default_validators选项。这是你定义要使用哪个验证器的地方。我们使用在MultipleEmailField类的validate方法中找到的自己的逻辑,并且不会为我们想要实现的目标使用默认验证器。欢迎你使用 Django 在django.core.validators库中为你的字段类提供的任何验证器。 -
default_error_messages选项用于定义字段类的默认消息。在前面显示的default_error_messages选项中,我们指定了两个键:required和email。这两个键将作为在必填字段提交时没有值存在以及当值不符合电子邮件字符串格式时使用的默认消息。在default_error_messages选项中指定默认错误消息后,我们不再需要使用字段的error_messages = {}参数。如果我们想逐字段使用error_messages参数,仍然是有可能的。 -
在相同的
MultipleEmailField类中,添加以下to_python()方法:# /becoming_a_django_entdev/chapter_5/fields.py ... class MultipleEmailField(Field): ... def to_python(self, value): if not value: return [] value = value.replace(' ', '') return value.split(',')
to_python() 方法用于将值转换为 Python 对象。这个特别的方法是编写来将字符串转换为电子邮件列表,不包括逗号。
-
在相同的
MultipleEmailField类中,添加以下validate()方法:# /becoming_a_django_entdev/chapter_5/fields.py ... class MultipleEmailField(Field): ... def validate(self, value): super().validate(value) for email in value: try: validate_email(email) except ValidationError: raise ValidationError( self.error_messages['email'], code = 'email' )
validate() 方法检查电子邮件列表中的每个项目,以确保其格式正确。我们还在 Field 类中覆盖了提供的选项,例如在 步骤 2 中显示的 widget 选项。默认小部件是 TextInput。由于这已经是我们需要的内容,我们实际上不需要包括它;在先前的示例中提供它是为了说明目的。当你编写自己的自定义字段时,你可以用 django.forms.widgets 库中找到的任何 Django 小部件替换 TextInput。如果你想将字段再进一步定制,甚至可以编写自己的自定义小部件类,但这超出了本书的范围。
让我们接下来使用我们的自定义字段类。
使用自定义字段
要使用我们在前一小节中创建的 MultipleEmailField,在你的 /chapter_5/forms.py 文件中,添加以下 import 语句并将 multiple_emails 字段添加到 ContactForm 中,如下所示:
# /becoming_a_django_entdev/chapter_5/forms.py
...
from django.forms
import Form, ModelForm
from .fields
import MultipleEmailField
class ContactForm(Form):
...
multiple_emails = MultipleEmailField(
label = 'Multiple Email Field',
help_text = 'Please enter one or more email addresses, each separated by a comma and no spaces',
required = True,
)
...
在先前的示例中,我们不需要包含任何验证消息,因为我们已经在 MultipleEmailField 类中定义了想要的那些消息。
可用的参数如下所示:

这些是 Django 库中Field类的__init__()方法中包含的参数。如果我们需要使用类似于min_length和max_length这样的参数,就像我们在本章标题为“字段类 – 字段”的子节step 2中为full_name字段所做的那样,我们应该使用CharField类而不是Field类来构造MultipleEmailField类,如图所示:
# Dummy Code
from django.forms.fields
import Field, CharField
class MultipleEmailField(CharField):
我们之所以想使用CharField而不是Field类,是因为它扩展了Field类并添加了包括min_length和max_length参数的逻辑。使用这个概念,你可以扩展任何其他字段类,在编写自己的自定义类时,可以提供该类独特的任何参数或行为。
接下来,让我们使用我们的联系表单并使用视图类来提供我们的联系页面。
使用表单视图
表单视图就像任何其他视图类一样,只不过表单视图类是为处理和处理表单对象和表单提交而设计的。
Django 提供了四个主要的表单视图类,如下所示:
-
FormView -
CreateView -
UpdateView -
DeleteView
这些都可以在django.views.generic.edit库中找到。
如果我们要创建一个与之前创建的ContactForm类一起工作的视图,该类不与任何模型相关联,我们将使用简单的FormView类。其他三个类可以与与模型相关的表单一起使用。它们各自有不同的用途:在数据库中创建、更新或删除记录。例如,CreateView将渲染一个包含空白或默认值的表单,这些值旨在创建一个尚不存在的记录。UpdateView使用一个查找现有记录的表单,显示该记录的现有值,并允许进行更改。DeleteView将向用户显示一个提示或确认页面,询问用户他们是否真的想继续执行此任务,然后删除该记录。
让我们使用FormView类开始构建一个显示ContactForm类对象的页面。我们将在本章后面使用CreateView和UpdateView。有关如何使用所有这些表单视图类的完整说明,请访问docs.djangoproject.com/en/4.0/ref/class-based-views/generic-editing/。
视图类 – FormView
让我们首先使用 Django 的FormView类构建一个名为FormClassView的类。这个类将包含三个选项,第一个选项是template_name,它用于定义我们正在使用的 HTML 模板的路径。第二个选项是form_class选项,它用于定义这个视图将要处理的表单类的名称,即ContactForm类。第三个选项是success_url,它指定了一个相对 URL 路径,当表单成功提交时,将重定向用户到该路径。
按照以下步骤配置你的FormClassView类:
-
确保你的
/becoming_a_django_entdev/chapter_5/文件夹中有一个名为views.py的文件。这通常是在创建新的 Django 应用时自动为你创建的。 -
在同一文件中,添加以下代码:
# /becoming_a_django_entdev/chapter_5/views.py from django.views.generic.edit import FormView from .forms import ContactForm class FormClassView(FormView): template_name = 'chapter_5/form-class.html' form_class = ContactForm success_url = '/chapter-5/contact-form-success/'
这些选项中的任何一个都可以使用可调用函数来获取这些值,例如使用reverse()函数来指定success_url。这里展示了如何做到这一点的一个例子,但这不是实际练习的一部分:
# Dummy Code
from django.urls
import reverse
from django.views.generic.edit
import FormView
class FormClassView(FormView):
...
def get_success_url(self, **kwargs):
return reverse('pattern_name', args=(value,))
我们实际上不需要这里显示的可调用函数来制定成功 URL。我们需要的只是'/chapter_5/contact-form-success/'字符串表示形式。
-
接下来,配置
http://localhost:8000/chapter-5/form-class/的 URL 模式。如果此文件没有为你自动创建,请创建/chapter_5/urls.py文件,并添加以下表单页面模式和import语句:# /becoming_a_django_entdev/chapter_5/urls.py from django.urls import re_path from django.views.generic import ( TemplateView ) from .views import FormClassView urlpatterns = [ re_path( r'^chapter-5/form-class/?$', FormClassView.as_view() ), ] -
在同一文件中,添加以下成功模式:
# /becoming_a_django_entdev/chapter_5/urls.py ... urlpatterns = [ ..., re_path( r'^chapter-5/contact-form-success/?$', TemplateView.as_view( template_name = 'chapter_5/contact-success.html' ), kwargs = { 'title': 'FormClassView Success Page', 'page_id': 'form-class-success', 'page_class': 'form-class-success-page', 'h1_tag': 'This is the FormClassView Success Page Using ContactForm', } ), ]
在这一步中,我们添加了一个第二个模式,作为http://localhost:8000/chapter-5/contact-form-success/的成功页面。这个成功页面将用于本章的所有练习。
现在我们有一个视图类可以与之一起工作,并定义了一些基本选项,让我们来探索一下使用不同请求方法需要什么。
HTTP 请求方法
在 Django 中与FormView类一起工作,有两种 HTTP 请求方法:GET方法和POST方法。GET方法旨在将带有空白或默认值的表单渲染到页面上,并等待用户填写表单并提交。一旦表单被提交,POST方法将被执行。
GET
get()方法就像视图类中的任何其他 GET 方法一样。它是页面首次加载时的首选方法。
按照以下步骤配置你的FormClassView类的get()方法:
-
在
/chapter_5/views.py文件中,使用以下代码向现有的FormClassView类添加get()方法:# /becoming_a_django_entdev/chapter_5/views.py ... from django.views.generic.edit import FormView from django.template.response import ( TemplateResponse ) class FormClassView(FormView): ... def get(self, request, *args, **kwargs): return TemplateResponse( request, self.template_name, { 'title': 'FormClassView Page', 'page_id': 'form-class-id', 'page_class': 'form-class-page', 'h1_tag': 'This is the FormClassView Page Using ContactForm', 'form': self.form_class, } )
在前面的get()方法中,我们正在以TemplateResponse类的形式返回一个 HTTP 响应,使用self.template_name的值作为模板位置的路径。我们向该模板提供了特定于此页面的上下文,例如title、page_id、page_class、h1_tag和form变量,如前面的代码块所示。self.form_class的值用于将表单对象传递到模板。可以在页面首次加载时在表单字段上定义初始值,这是表单初始化的时候。
-
将以下
initial列表添加到FormClassView类的现有get()方法中,并将其传递到你的返回上下文中,如下面的代码块所示:# /becoming_a_django_entdev/chapter_5/views.py ... class FormClassView(FormView): def get(self, request, *args, **kwargs): initial = { 'full_name': 'FirstName LastName', 'email_1': 'example1@example.com', # Add A Value For Every Field... } return TemplateResponse( request, self.template_name, { ... 'form': self.form_class(initial), } )
在这个get()方法中,我们添加了initial变量作为列表,然后将该列表传递给self.form_class(initial)对象,这使我们能够在字段上设置初始值,定义字段名称为之前显示的键及其相应的值。
POST
当表单被提交时,使用 post() 方法来渲染相同的页面。在这个方法中,我们可以确定表单是否有效,如果是的话,我们希望将其重定向到成功 URL。如果表单无效,页面将重新加载,显示用户输入到字段中的值,并显示可能存在的任何错误消息。我们还可以使用 post() 方法修改或添加页面上下文。
在你的 /chapter_5/views.py 文件中,在相同的 FormClassView 类中,添加以下 post() 方法:
# /becoming_a_django_entdev/chapter_5/views.py
...
from django.views.generic.edit
import FormView
from django.http
import HttpResponseRedirect
from django.template.response
import (
TemplateResponse
)
class FormClassView(FormView):
...
def post(self, request, *args, **kwargs):
form = self.form_class(request.POST)
if form.is_valid():
return HttpResponseRedirect(
self.success_url
)
else:
return TemplateResponse(
request,
self.template_name,
{
'title': 'FormClassView Page - Please Correct The Errors Below',
'page_id': 'form-class-id',
'page_class': 'form-class-page errors-found',
'h1_tag': 'This is the FormClassView Page Using ContactForm<br /><small class="error-msg">Errors Found</small>',
'form': form,
}
)
在 TemplateResponse 返回语句中的高亮文本表示从 get() 方法到 post() 方法已更改的上下文。为了保留用户已输入到表单中的数据,必须将 request.POST 传递到表单类中,如前一步中高亮的 self.form_class(request.POST)。如果我们没有将 request.POST 传递到 self.form_class(),那么在首次访问此页面时,我们会渲染一个空白表单,就像我们使用 get() 方法一样。
现在我们已经编写了视图类,接下来我们可以着手编写将渲染我们的表单的模板。
在模板中渲染表单
Django 提供了五种主要方式来轻松快速地将表单对象渲染到页面上。前三种是使用段落、表格或列表结构来渲染表单。其他两种包括基于 django.forms.templates.django.forms 库中的 default.html 模板的传统渲染表单方式,以及渲染你自己的模板的方式。Django 4.0 的新特性是所有表单类上的 template_name 选项。此选项允许你指向一个模板文件,在那里你可以构建自己的 HTML 格式。
按照以下步骤渲染你的表单对象:
-
将在 第四章,URLs、视图和模板 中创建的
base_template_1.html文件复制到你的/becoming_a_django_entdev/chapter_5/templates/chapter_5/base/文件夹中。同时将所有作为{% include %}语句添加的相关部分模板文件也复制到该文件中。 -
那个
base_template_1.html文件将被重新用作本章练习的基础模板。调整任何路径以指向新的chapter_5文件夹,例如任何 CSS 和 JavaScript 文件路径。 -
将所有相关的 CSS、JavaScript 和 HTML 文件也复制到你的
chapter_5应用中。这些文件不是完成此练习所必需的,但可以防止控制台日志中出现 404 错误。 -
在你的
/becoming_a_django_entdev/chapter_5/templates/chapter_5/文件夹中创建一个名为form-class.html的新文件,并包含以下代码块中可以看到的标签:# /becoming_a_django_entdev/chapter_5/templates/chapter_5/form-class.html {% extends 'chapter_5/base/base_template_1.html' %} {% load static %} {% block page_title %}{{ title }}{% endblock %} {% block head_stylesheets %}{% endblock %} {% block js_scripts %}{% endblock %} {% block page_id %}{{ page_id }}{% endblock %} {% block page_class %}{{ block.super }} {{ page_class }}{% endblock %} -
在你的
/chapter_5/form-class.html文件中,在body_content块内添加以下代码,以尽可能简单的方式渲染表单:# /becoming_a_django_entdev/chapter_5/templates/chapter_5/form-class.html ... {% block body_content %} ... <form method="post"> {% csrf_token %} {{ form }} <input type="submit" value="Send Message"> </form> {% endblock %}
在这里,我们至少需要写出 <form> 元素并定义一个 method="post" 属性,告诉浏览器如何处理表单提交。{{ form }} 标签使用 django.forms.templates.django.forms.default.html 库模板渲染此表单存在的任何字段。{% csrf_token %} 是一个跨站请求伪造令牌,用于安全措施,并且所有 Django 表单都需要它。<input type="submit"> 指定了用于触发表单提交动作的按钮。
让我们深入了解剩余的四种机制及其使用方法。
渲染表单 – as_p
此选项将每个字段包裹在段落 <p></p> 元素中。标签将堆叠在输入字段上方,帮助文本位于其下方。如果存在错误,每个字段将渲染自己的列表对象,位于段落对象上方,列出与该字段相关的所有错误。
要以 as_p 形式渲染你的表单,在你的 /chapter_5/form-class.html 文件中,将 {{ form }} 标签更改为 {{ form.as_p }}。
这应该会渲染每个字段,看起来像这里展示的示例代码:
# Dummy code rendered, for first_name field
<ul class="errorlist">
<li>Please provide us with a name to address you as</li>
</ul>
<p>
<label for="full-name">Full Name:</label>
<input type="text" name="full_name" id="full-name" class="form-input-class field-error" placeholder="Your Name, Written By..." maxlength="300" minlength="2" required="">
<span class="helptext">Enter your full name, first and last name please</span>
</p>
接下来,让我们以表格形式渲染表单。
渲染表单 – as_table
此选项将每个字段包裹在 <tr></tr> 元素中。此字段的标签被包裹在 <th></th> 元素中,而字段本身被包裹在 <td></td> 元素中。标签将堆叠在左侧,输入对象和帮助文本以及错误消息显示在右侧,如下所示:

图 5.1 – 渲染表单 – 以表格形式
要使用此选项,我们仍然需要将表单标签包裹在 <table></table> 元素中,因为只有表格的内部内容会被渲染。
要以表格形式渲染你的表单,在你的 /chapter_5/form-class.html 文件中,将你的 {{ form }} 标签更改为 {{ form.as_table }} 并将其包裹在 <table> 标签中,如下所示:
# /becoming_a_django_entdev/chapter_5/templates/chapter_5/form-class.html
...
<table>
{{ form.as_table }}
</table>
...
接下来,让我们以列表形式渲染表单。
渲染表单 – as_ul
此选项将渲染你的表单为一个列表,每个字段都被 <li></li> 元素包裹。在该元素内部,标签首先出现,然后是输入字段,最后是帮助文本。如果发生错误,它将作为自己的列表项注入到该字段上方,如下所示:

图 5.2 – 渲染表单 – As a list
我们还必须将表单包裹在一个带有 <ul></ul> 列表元素的元素中。要在你的 /chapter_5/form-class.html 文件中以列表形式渲染表单,将 {{ form }} 标签更改为 {{ form.as_ul }} 并将其包裹在 <ul> 标签中,如下所示:
# /becoming_a_django_entdev/chapter_5/templates/chapter_5/form-class.html
...
<ul>
{{ form.as_ul }}
</ul>
...
接下来,让我们使用 Django 4.0 中引入的新方法。
渲染表单 – 使用 template_name
Django 4.0 的新特性是 template_name 功能。此功能用于在自定义模板中渲染表单。它为开发者提供了在字段渲染时结构化自己的 HTML 的能力。开发者可以创建许多不同的模板样式,并根据需要使用它们。字段通过自定义模板内的 {{ fields }} 标签访问。
按照以下步骤配置您的自定义表单模板:
-
在您的
/chapter_5/forms.py文件中,将template_name选项添加到现有的ContactForm类中,如高亮所示:/chapter_5/form-class.html file, you render your form using only the basic {{ form }} tag and not any of the other preconfigured form rendering methods. -
接下来,在您的
/chapter_5/templates/chapter_5/forms/文件夹中创建custom-form.html文件。我们不会向此文件添加{% extends %}标签;相反,我们将它视为使用{% include %}标签,其中它只是一个片段,而不是一个完整的 HTML 页面。 -
在您的
/chapter_5/custom-forms.html文件中,添加以下代码:# /becoming_a_django_entdev/chapter_5/templates/chapter_5/forms/custom-forms.html {% load static %} {% for field, errors in fields %} <div class="field-box{% if errors %} error{% endif %}"> <label for="{{ field.id_for_label }}"> {% if field.field.required %}<span class="required">*</span>{% endif %}{{ field.label|safe }} </label> <div class="form-group"> {{ field }} {{ errors|safe }} {% if field.help_text and field.help_text != '' %} <span class="help-text"> {{ field.help_text|safe }} </span> {% endif %} </div> </div> {% endfor %}
在这里,我们通过 {% for field, errors in fields %} 标签循环遍历所有字段,如前一个代码块所示。我们使用 <div>、<label> 和 <span> 元素添加了自己的 HTML 结构。字段本身是通过 {{ field }} 标签渲染的。其他信息,如帮助文本,与 safe 过滤器结合使用在 {{ field.help_text|safe }} 中。safe 过滤器用于确保字符串中的任何 HTML 都被渲染为 HTML 对象,而不是打印为该对象的字符串表示。
让我们接下来演示所有这些表单渲染的实际操作。
渲染演示
到目前为止,我们应该有一个可以工作的表单。在您的浏览器中,访问 URL http://localhost:8000/chapter-5/form-class/,您应该看到表单被渲染到页面上。通过使用 ContactForm 类提供的所有示例,我们应该在这个页面上看到六个字段。在这里,我们可以看到电子邮件字段如何在与表单交互时表现出不同的行为。例如,如果我们将 email_1 字段的 required 参数设置为 False,我们可以不在此字段中输入任何内容就提交表单,并且它会成功。在名为 email_2 的字段中,我们指定了 required 参数为 True。这给该输入字段添加了 required 属性,防止用户提交该表单。这意味着用户将永远不会看到我们在 Django 代码中提供的错误消息。此路由需要使用 JavaScript,例如 jQuery Validate 库,来处理错误状态并为我们显示错误消息。不采取任何行动会导致浏览器为我们处理错误状态,在 Chrome 中,它看起来如下截图所示:

图 5.3 – ContactForm email_2 字段
然而,在名为 email_3 的字段上,我们将必需参数设置为等于 False,并在清理方法中执行验证,检查该字段是否有值。这使得我们可以提交表单并看到在回发时提供的错误消息,如下所示:

图 5.4 – ContactForm email_3 字段
接下来,让我们将 Django 表单进一步向前推进,并开始在一个表单类中处理模型。我们在标题为 表单类 – ModelForm 的部分创建了一个占位符类,称为 VehicleForm。
将模型链接到表单
不需要任何特殊字段渲染即可将模型链接到表单相当简单。
在您的 /chapter_5/forms.py 文件中,向现有的 VehicleForm 类添加以下代码(请记住删除之前添加到该类中的 pass 语句):
# /becoming_a_django_entdev/chapter_5/forms.py
...
from django.forms
import Form, ModelForm
from ..chapter_3.models
import Vehicle
class VehicleForm(ModelForm):
class Meta:
model = Vehicle
fields = [
'vin',
'sold',
'price',
'make',
'vehicle_model',
'engine',
]
在前面的例子中,我们不需要为这个表单创建字段。Django 将自动使用与我们在 第三章 中为 Vehicle 模型编写的模型字段类型关联的表单字段类型,该章名为 模型、关系和继承。如果需要修改任何字段行为,我们将以与 ContactForm 相同的方式编写表单字段,然后根据需要对其进行定制。这里使用的 Meta 子类定义了我们使用的模型类,而 fields 选项指定了我们想要包含哪些模型字段以及它们的包含顺序。
注意
使用 fields = '__all__' 将会包含该模型中存在的所有字段,并且按照模型中定义的顺序排列。
让我们接下来处理 CreateView 类。
视图类 – CreateView
使用我们现在已经连接到 Vehicle 模型的 VehicleForm 类,让我们创建一个视图,该视图将使用 CreateView 类渲染一个没有或默认字段值的表单。当表单成功提交时,它将允许我们在数据库中创建一个新的车辆记录。
按照以下步骤配置您的 CreateView 类:
-
在您的
/chapter_5/views.py文件中,添加以下import语句并创建ModelFormClassCreateView类,如下所示:# /becoming_a_django_entdev/chapter_5/views.py ... from django.http import HttpResponseRedirect from django.views.generic.edit import ( ..., CreateView ) from django.template.response import ( TemplateResponse ) from .forms import ContactForm, VehicleForm class ModelFormClassCreateView(CreateView): template_name = 'chapter_5/model-form-class.html' form_class = VehicleForm success_url = '/chapter-5/vehicle-form-success/' -
在相同的
ModelFormClassCreateView类中,添加以下get()方法:# /becoming_a_django_entdev/chapter_5/views.py ... class ModelFormClassCreateView(CreateView): ... def get(self, request, *args, **kwargs): return TemplateResponse( request, self.template_name, { 'title': 'ModelFormClassCreateView Page', 'page_id': 'model-form-class-id', 'page_class': 'model-form-class-page', 'h1_tag': 'This is the ModelFormClassCreateView Class Page Using VehicleForm', 'form': self.form_class(), } ) -
在相同的
ModelFormClassCreateView类中,添加以下post()方法:# /becoming_a_django_entdev/chapter_5/views.py ... class ModelFormClassCreateView(CreateView): ... def post(self, request, *args, **kwargs): form = self.form_class(request.POST) if form.is_valid(): vehicle = form.instance vehicle.save() return HttpResponseRedirect( self.success_url ) else: return TemplateResponse( request, self.template_name, { 'title': 'ModelFormClassCreateView Page - Please Correct The Errors Below', 'page_id': 'model-form-class-id', 'page_class': 'model-form-class-page errors-found', 'h1_tag': 'This is the ModelFormClassCreateView Page Using VehicleForm<br /><small class="error-msg">Errors Found</small>', 'form': form, } )
在这个例子中,我们给了这个类之前使用的两个相同的方法——get()和post()。这两个方法与使用FormView类构造的类中的用法相同。在get()方法中,我们通过self.form_class()将一个空表单作为上下文传递给模板。在post()方法中,我们再次将request传递给表单以获取用户提交的数据,使用form = self.form_class(request.POST)。在那个post()方法中,使用if form.is_valid():进行验证,它将重定向到成功页面或刷新,显示带有正确错误信息的表单。如果表单验证成功,在执行重定向之前,我们使用vehicle.save()保存表单,就像我们在第三章“模型、关系和继承”中使用 Django shell 添加数据时做的那样。
-
使用
ModelFormClassCreateView类创建一个 URL 模式,将以下路径添加到你的/chapter_5/urls.py文件中,并包含以下import语句:# /becoming_a_django_entdev/chapter_5/urls.py from django.urls import re_path from django.views.generic import ( TemplateView ) from .views import ( FormClassView, ModelFormClassCreateView ) urlpatterns = [ ..., re_path( r'^chapter-5/model-form-class/?$', ModelFormClassCreateView.as_view() ), ] -
在相同的
/chapter_5/urls.py文件中,添加以下成功 URL 模式:# /becoming_a_django_entdev/chapter_5/urls.py ... urlpatterns = [ ..., re_path( r'^chapter-5/vehicle-form-success/?$', TemplateView.as_view( template_name = 'chapter_5/vehicle-success.html' ), kwargs = { 'title': 'ModelFormClass Success Page', 'page_id': 'model-form-class-success', 'page_class': 'model-form-class-success-page', 'h1_tag': 'This is the ModelFormClass Success Page Using VehicleForm', } ), ]
我们在http://localhost:8000/chapter-5/vehicle-form-success/添加了车辆表单的成功 URL 模式。
-
接下来,以创建
/chapter_5/form-class.html文件相同的方式构建你的/chapter_5/model-form-class.html文件。 -
现在,访问 URL
http://localhost:8000/chapter-5/model-form-class/,如果你使用标准的{{ form }}标签渲染表单,你应该会看到页面看起来如图下所示:

图 5.5 – 使用 ModelFormClassCreateView 的 VehicleForm
当然,这个例子是最简单的形式。如果你想要使用其他格式或你自己的模板,你将遵循本章“在模板中渲染表单”部分下的步骤。
视图类 – UpdateView
在这个例子中,我们需要使用路径转换器创建一个 URL 模式,以捕获正在查找的车辆记录的 ID。
按照以下步骤配置你的UpdateView类:
-
在你的
/chapter_5/urls.py文件中,添加以下路径:# /becoming_a_django_entdev/chapter_5/urls.py from django.urls import re_path from .views import ( ..., ModelFormClassUpdateView ) ... urlpatterns = [ ..., re_path( 'chapter-5/model-form-class/(?P<id>[0-9])/?$', ModelFormClassUpdateView.as_view(), name = 'vehicle_detail' ), ]
这个模式将允许我们通过 ID 访问数据库中的Vehicle记录,也称为主键。我们在 URL 本身中指定我们想要查找的Vehicle的 ID,例如http://localhost:8000/chapter-5/model-form-class/2/。
-
在你的
/chapter_5/views.py文件中,添加显示的ModelFormClassUpdateView类和import语句:# /becoming_a_django_entdev/chapter_5/views.py ... from django.http import HttpResponseRedirect from django.template.response import ( TemplateResponse ) from django.views.generic.edit import ( ..., UpdateView ) from .forms import VehicleForm from ..chapter_3.models import Vehicle class ModelFormClassUpdateView(UpdateView): template_name = 'chapter_5/model-form-class.html' form_class = VehicleForm success_url = '/chapter-5/vehicle-form-success/' -
在相同的
ModelFormClassUpdateView类中,添加显示的get()和post()方法:# /becoming_a_django_entdev/chapter_5/views.py from django.template.response import ( TemplateResponse ) from django.views.generic.edit import ( ..., UpdateView ) from ..chapter_3.models import Vehicle ... class ModelFormClassUpdateView(UpdateView): ... def get(self, request, id, *args, **kwargs): try: vehicle = Vehicle.objects.get(pk=id) except Vehicle.DoesNotExist: form = self.form_class() else: form = self.form_class(instance=vehicle) return TemplateResponse( request, self.template_name, { 'title': 'ModelFormClassUpdateView Page', 'page_id': 'model-form-class-id', 'page_class': 'model-form-class-page', 'h1_tag': 'This is the ModelFormClassUpdateView Class Page Using VehicleForm', 'form': form, } ) def post(self, request, id, *args, **kwargs): # Use the same code as we did for the ModelFormClassCreateView class -
现在,导航到 URL
http://localhost:8000/chapter-5/model-form-class/2/,你应该会看到预先加载了数据库中该车辆的值的表单,如图下所示:

图 5.6 – 使用 ModelFormClassUpdateView 的 VehicleForm
我们再次使用相同的两个 get() 和 post() 方法。在这里编写 get() 方法的一个细微差别是我们正在执行 Vehicle 查询。我们使用 try/except 语句来确定对象是否存在于数据库中,使用 vehicle = Vehicle.objects.get(pk=id)。如果不存在,我们使用 form = self.form_class() 创建一个空白表单对象。如果找到 Vehicle 对象,则将此实例传递给我们要初始化的表单,使用 form = self.form_class(instance=vehicle)。post() 方法的编写与为 ModelFormClassCreateView 编写的方法相同,只是我们更新了上下文字符串变量以反映此类名称。
注意
当与具有 unique = True 属性的字段一起工作并使用 UpdateView 类保存该对象时,你可能会收到一个回发错误消息,告诉你该对象已存在。为了解决这个问题,尝试从你的模型中移除 unique 属性并实现你自己的 clean() 方法来强制执行唯一性。还有其他几种方法可以在保持 unique 属性的同时解决这个问题;所有这些方法都相当难以实现,并且超出了本章的范围。请尝试自己构建一个不包含 unique 字段的表单来更新 Engine 类。
让我们添加内联表单集。
添加内联表单集
内联表单集是在表单内的表单。这是一种提供动态字段的方式,例如,用于额外的人员、评论或对象。它们通常与前端上的 JavaScript 代码结合使用,以根据用户的需求创建或删除字段集。在下一个练习中,我们将扩展 ModelFormClassCreateView 类以添加我们的内联表单集。此表单集将捕获潜在买家的信息,以捕获该潜在买家的姓名和姓氏。我们将为用户创建一个 添加另一个 按钮,以便他们可以添加他们想要的任何数量的潜在买家。JavaScript 用于控制创建和/或删除新的 DOM 对象。它还将在此过程中更新 Django 管理表单数据。你可以在此基础上添加字段和控件来使你的表单更健壮,并允许用户操作内联表单集。
按照以下章节中的步骤开始使用内联表单集。
Formset 函数 – formset_factory
表单集工厂 是我们用来注册内联表单集的控制器。
按照以下步骤创建你的表单集工厂:
-
在
/chapter_5/forms.py文件中,添加以下ProspectiveBuyerForm类,它将作为内联表单,捕获潜在买家的姓名和姓氏:# /becoming_a_django_entdev/chapter_5/forms.py from django import forms from django.forms import Form, ModelForm ... class ProspectiveBuyerForm(Form): first_name = forms.CharField( label = 'First Name', help_text = 'Enter your first name only', required = True, error_messages = { 'required': 'Please provide us with a first name', } ) last_name = forms.CharField( label = 'Last Name', help_text = 'Enter your last name only', required = True, error_messages = { 'required': 'Please provide us with a last name', } )
在前面的代码中,我们在 ProspectiveBuyerForm 类中与在之前的 ContactForm 中所做的不同之处在于没有进行任何不同的操作。相同的概念和验证措施适用于内联表单集中的字段。根据您的字段需要调整逻辑。
-
在相同的
/chapter_5/forms.py文件中,使用以下示例将表单注册为formset_factory。确保在这个文件中将ProspectiveBuyerFormSet类放在ProspectiveBuyerForm类下方:# /becoming_a_django_entdev/chapter_5/forms.py ... from django.forms import ( ..., formset_factory ) ... ProspectiveBuyerFormSet = formset_factory( ProspectiveBuyerForm, extra = 1 )
在前面的示例中,我们在一个名为 ProspectiveBuyerFormset 的表单集工厂中注册了 ProspectiveBuyerForm 类,我们将在视图类中使用它。extra = 1 参数用于在首次初始化 formset_factory 时只包含该表单集的一个实例。这里有许多其他选项,它们都在这里进行了详细解释:docs.djangoproject.com/en/4.0/topics/forms/formsets/.
注意
在这个示例中,我们使用标准的 formset_factory 为一个没有与模型链接的字段表单。与模型链接的表单集将使用 modelformset_factory() 方法将表单字段链接到数据库中的模型。当使用该方法时,数据在视图类中的保存方式与保存 VehicleForm 数据的方式相同。
让我们在视图类中使用这个内联表单集。
在视图类中使用内联表单集
按照以下步骤在视图类中使用您新创建的内联表单集:
-
在您的
/chapter_5/views.py文件中,在现有的ModelFormClassCreateView类中,对现有的get()方法进行一些小的调整,如下所示:# /becoming_a_django_entdev/chapter_5/views.py ... from django.http import HttpResponseRedirect from django.template.response import ( TemplateResponse ) from django.views.generic.edit import ( ..., CreateView ) from .forms import ..., ProspectiveBuyerFormSet from ..chapter_3.models import Vehicle class ModelFormClassCreateView(CreateView): ... def get(self, request, *args, **kwargs): buyer_formset = ProspectiveBuyerFormSet() return TemplateResponse( request, self.template_name, { ... 'form': self.form_class(), 'buyer_formset': buyer_formset, } ) -
在相同的
ModelFormClassCreateView类中,对现有的post()方法进行一些小的调整,如下所示:# /becoming_a_django_entdev/chapter_5/views.py ... class ModelFormClassCreateView(CreateView): ... def post(self, request, *args, **kwargs): form = self.form_class(request.POST) buyer_formset = ProspectiveBuyerFormSet( request.POST ) if form.is_valid(): ... else: return TemplateResponse( request, self.template_name, { ... 'form': form, 'buyer_formset': buyer_formset, } )
在前面的步骤中,我们将内联 ProspectiveBuyerFormset 表单集作为名为 buyer_formset 的额外上下文变量传递到模板中。这个表单和表单集应该始终被视为完全独立的对象。如果它们使用 ForeignKey、ManyToMany 或 OneToOne 模型关系,表单和表单集也可以相关联。
让我们将这些内联表单集渲染到模板中。
在模板中渲染内联表单集
要将您新创建的内联表单集渲染到模板文件中,在您的 /chapter_5/model-form-class.html 文件中,包括所有存在的节点、类名和 ID,如下所示:
# /becoming_a_django_entdev/chapter_5/templates/chapter_5/model-form-class.html
...
{% extends 'chapter_5/base/base_template_1.html' %}
{% load static %}
...
{% block body_content %}
...
<form method="post" id="form">
{% csrf_token %}
{{ form }}
{% if buyer_formset %}
<h3>Prospective Buyers</h3>
{{ buyer_formset.non_form_errors }}
{{ buyer_formset.management_form }}
{% for form in buyer_formset %}
<div class="formset-container {{ buyer_formset.prefix }}">
<div class="first-name">
{{ form.first_name.label }}: {{ form.first_name }}
</div>
<div class="last-name">
{{ form.last_name.label }}: {{ form.last_name }}
</div>
</div>
{% endfor %}
{% endif %}
<button id="add-formset" type="button">Add Another Prospective Buyer</button>
<input type="submit" value="Save Vehicle">
</form>
{% endblock %}
我们即将编写的 JavaScript 将依赖于这个结构,并且当你的结构发生变化时,请确保也更改你的 JavaScript。在前面代码中,表单的重要部分包括表单本身的 ID 属性,称为id="form"。我们将使用它来在即将编写的 JavaScript 中作为整体定位表单。使用条件检查在对其进行任何操作之前buyer_formset变量是否存在。例如,如果你想提供一个没有任何表单集的页面实例,那么这个条件将防止破坏。
一个重要的特性是永远不要忘记包括管理表单数据,这是通过使用{{ buyer_formset.management_form }}标签添加的。这将包括 Django 处理你的内联表单集所需的重要数据。然后我们使用{% for form in buyer_formset %}遍历buyer_formset对象中的每个表单。对于每个存在的表单,我们将所有内部 HTML 包裹在一个名为<div class="formset-container"></div>的节点中。这个类很重要,因为它将在我们使用 JavaScript 处理每个内联表单时区分它们。在内部,你可以按你喜欢的方式结构化你的字段。最后,在循环外部,就在提交按钮之前,我们需要添加一个type="button"的新<button>,以防止意外提交表单。给这个按钮一个属性id="add-formset"。
现在,访问我们之前访问的相同 URL,在http://localhost:8000/chapter-5/model-form-class/添加一个新的车辆。你应该看到一个类似于以下表单的界面:


图 5.7 – VehicleForm 内联表单集
目前只有一个潜在买家的实例。接下来,我们将添加 JavaScript 控件,使我们能够向此表单添加更多实例。
动态内联表单集
按照以下步骤配置 JavaScript,以便用户可以添加更多内联表单集的实例:
-
在你的
/chapter_5/base/base_template_1.html文件中,<head>部分已经引用了一个 JavaScript 文件。确保以下脚本包含在该文档的<head>中:# /becoming_a_django_entdev/chapter_5/templates/chapter_5/base/base_template_1.html ... <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> ... <script defer type="text/javascript" src="img/site-js.js' %}"></script> </head> ... </html>
这将加载一个用于使表单交互并允许用户添加另一个ProspectiveBuyerFormset内联表单集实例的单个 JavaScript 文件。
-
如果你之前在准备之前的练习时没有复制 JavaScript 文件,那么现在请创建
/chapter_5/static/chapter_5/和/chapter_5/static/chapter_5/js/文件夹以及site-js.js文件,使用你的 IDE、文件资源管理器或以下命令:(virtual_env) PS > mkdir becoming_a_django_entdev/chapter_5/static/chapter_5 (virtual_env) PS > mkdir becoming_a_django_entdev/chapter_5/static/chapter_5/js (virtual_env) PS > cd becoming_a_django_entdev/chapter_5/static/chapter_5/js (virtual_env) PS > touch site-js.js -
在你的
/chapter_5/js/site-js.js文件中,包括以下变量:# /becoming_a_django_entdev/chapter_5/static/chapter_5/js/site-js.js let formsetContainer = document.querySelectorAll( '.formset-container' ), form = document.querySelector('#form'), addFormsetButton = document.querySelector( '#add-formset' ), totalForms = document.querySelector( '#id_form-TOTAL_FORMS' ), formsetNum = formsetContainer.length - 1; -
将以下事件监听器添加到同一个 JavaScript 文件中:
# /becoming_a_django_entdev/chapter_5/static/chapter_5/js/site-js.js ... addFormsetButton.addEventListener( 'click', $addFormset ); -
将以下函数添加到同一个 JavaScript 文件中:
# /becoming_a_django_entdev/chapter_5/static/chapter_5/js/site-js.js ... function $addFormset(e) { e.preventDefault(); let newForm = formsetContainer[0].cloneNode(true), formRegex = RegExp(`form-(\\d){1}-`,'g'); formsetNum++ newForm.innerHTML = newForm.innerHTML.replace( formRegex, 'form-${formsetNum}-' ); form.insertBefore(newForm, addFormsetButton); totalForms.setAttribute( 'value', '${formsetNum + 1}' ); }
我们所做的是添加一个事件监听器,监听<div class="formset-container"></div>的点击事件,并在<button id="add-formset"></button>节点之前插入克隆的节点。由于 Django 也需要精确管理表单数据,我们需要确保每次添加或删除内联表单集时都更新相关数据。这就是为什么我们在执行克隆操作之前找到存在的内联表单集数量作为formsetNum变量。然后,我们使用正则表达式方法搜索具有formset-container CSS 类的节点的所有内部 HTML 节点,来增加这个数字,它从索引 0 开始。这个增加的数字用于更新所有节点属性到新插入的节点正确的索引。我们还更新了表单对象的值,将id="id_form-TOTAL_FORMS"更新为新存在的内联表单集总数。
如果成功,当我们点击添加另一个潜在买家按钮时,我们应该看到添加了额外的内联表单集,就像以下这样:

图 5.8 – VehicleForm 添加另一个内联表单集
摘要
到目前为止,我们已经完成了两个主要表单,一个用作联系表单,另一个用于处理在第三章中创建的车辆对象,模型、关系和继承。我们添加了各种字段,并讨论了这些字段类型之间的差异。通过反复使用电子邮件示例,我们见证了验证以多种不同的方式工作。根据为项目收集的需求,我们可以决定采用几种不同的编写模式来满足这些需求。例如,如果我们想完全消除 JavaScript 验证的需求,比如使用我最喜欢的库 jQuery Validate,我们可以在表单类中编写干净的方法来在后端执行所有验证。这将利用 Django 来提供错误消息。然而,如果我们确实在前端使用了基于 JavaScript 的验证,我们可以编写创建为我们创建节点属性的字段,例如<input>字段的required=""属性,这将防止表单在没有值的情况下提交。
无论项目的需求如何,我们也发现了一种非常简单的方法来创建我们自己的字段类。自定义字段类让我们可以预先格式化支持不要重复自己(DRY)风格的字段。我们探讨了视图类、表单类和字段类的差异,然后讨论了在模板中渲染这些表单的方法。
在下一章中,我们将探讨一个专门针对快速开发表单的用户界面,允许用户自行更新、创建和删除模型对象。这被称为 Django 管理站点,基本上是一种美化了的渲染与模型管理相关表单的方式。
第六章:第六章:探索 Django 管理站点
本章将介绍 Django 管理站点,这是一个功能,允许开发者将某些模型注册到一个以模型为中心的界面中,只有被允许的用户才能管理数据库内容。该功能旨在读取与模型相关的元数据以及设置在这些模型上的字段和字段约束,以构建一组用于搜索、排序、筛选、创建、编辑和删除那些表中记录的页面。
管理站点是 Django 框架的一个可选功能,可以在项目中使用。它允许我们使用 Django 框架内构建的用户基于的角色和权限设置,仅允许被允许的用户编辑、添加或删除对象。用户角色可以被修改,仅授予编辑某些模型的权限,甚至可以设置为更细粒度,例如仅允许用户编辑或查看数据,但不能添加或删除数据。如果项目不需要或不希望使用此功能,则可以禁用此功能。
未在 Django 管理站点中特别注册的模型将无法通过该界面访问,这为开发者提供了创建存储数据的表的选择,这些数据没有任何用户可以控制。在本章结束时,我们将注册我们在 第三章,模型、关系和继承 中创建的模型。这些模型将作为本章提供的多数练习的基础。
在本章中,我们将涵盖以下主题:
-
使用 Django 管理站点
-
配置
admin类选项 -
添加
admin类方法 -
编写自定义
admin form classes -
使用 Django 认证系统
技术要求
要与本章中的代码一起工作,以下工具需要在您的本地机器上安装:
-
Python 版本 3.9 – 作为项目的底层编程语言使用
-
Django 版本 4.0 – 作为项目的后端框架使用
-
pip 包管理器 – 用于管理第三方 Python/Django 包
我们将继续使用在 第二章,项目配置 中创建的解决方案。然而,并不需要使用 Visual Studio IDE。主要项目本身可以使用其他 IDE 运行,或者从项目根目录(其中包含 manage.py 文件)独立运行,使用终端或命令行窗口。无论您使用什么编辑器或 IDE,都需要一个虚拟环境来与 Django 项目一起工作。有关如何创建项目和虚拟环境的说明,请参阅 第二章,项目配置。您需要一个数据库来存储项目中的数据。在上一章的示例中选择了 PostgreSQL;然而,您可以为项目选择任何数据库类型来与本章的示例一起工作。
我们还将使用以 Django fixture 形式提供的数据,这些数据在第三章中“模型、关系和继承”小节标题为“加载 chapter_3 数据 fixture”中提供过。确保chapter_3 fixture 已加载到你的数据库中。如果这已经完成,则可以跳过下一个命令。如果你已经创建了第三章中“模型、关系和继承”中提到的表,并且尚未加载该 fixture,那么在激活你的虚拟环境后,运行以下命令:
(virtual_env) PS > python manage.py loaddata chapter_3
本章创建的所有代码都可以在本书的 GitHub 仓库github.com/PacktPublishing/Becoming-an-Enterprise-Django-Developer中找到。本章展示的大部分代码可以在/becoming_a_django_entdev/becoming_a_django_entdev/chapter_6/目录中找到。
查看以下视频,了解代码的实际应用:bit.ly/3ODUaAW。
准备本章内容
首先,按照第二章中“项目配置”小节标题为“创建 Django 应用”的步骤,在你的项目中创建一个名为chapter_6的新应用。如该节所述,不要忘记将位于/becoming_a_django_entdev/becoming_a_django_entdev/chapter_6/apps.py文件中的name =变量值更改为指向你安装应用路径。务必也将此应用包含在settings.py文件中的INSTALLED_APPS变量中。
在网站的主urls.py文件中,添加以下路径,该路径指向我们将要创建的此章节的 URL 模式:
# /becoming_a_django_entdev/urls.py
...
urlpatterns = [
path('', include('becoming_a_django_entdev.chapter_6.urls')),
]
现在我们已经创建了本章的应用,让我们开始使用 Django 管理站点来管理在第三章中“模型、关系和继承”创建的模型。
使用 Django 管理站点
Django 使得直接使用管理站点变得非常简单。为了使用此功能,我们需要在settings.py文件中添加一个应用并注册一个 URL 模式来处理任何项目的管理链接。默认情况下,当使用startproject命令或通过 IDE 创建项目时,这些设置应该已经存在于代码中。然而,某些工具和版本可能生成的代码略有不同,因此始终检查这些设置是否以这种方式配置是很好的。
激活 Django 管理站点
要确保 Django 管理站点在你的项目中已激活,请按照以下步骤操作:
-
在主
settings.py文件中,将以下应用添加到INSTALLED_APPS变量中,并确保它位于列表顶部:# /becoming_a_django_entdev/settings.py ... INSTALLED_APPS = [ 'django.contrib.admin', ... ] -
如果
/chapter_6/urls.py文件尚不存在,创建该文件并包含以下 URL 模式:# /becoming_a_django_entdev/chapter_6/urls.py ... from django.contrib import admin urlpatterns = [ path('admin/', admin.site.urls), ]
要停用 Django 管理站点,请删除本节中描述的设置。与 Django 管理站点相关的模板和静态文件也可以被覆盖以创建您自己的外观和感觉。您不受默认提供的用户界面的限制。由于本书空间有限,我们将不会演示如何覆盖 Django 管理站点模板;然而,您可以参考 第四章,URL、视图和模板,以了解更多关于覆盖第三方模板的信息。
小贴士
由于 Django 被设计用来做所有繁重的工作,因此我们只需要创建一个 URL 模式,就可以激活与 Django 管理站点相关的所有 URL 模式。
登录 Django 管理站点
现在 Django 管理站点应该已经激活,如果我们导航到 URL http://localhost:8000/admin/,我们应该看到标准的 Django 管理登录屏幕,如下面的截图所示:


图 6.1 – Django 管理站点登录
使用在 第二章,项目配置 中执行 createsuperuser 命令时提供的用户名和密码。您可以使用这些超级用户凭据登录,或者如果您希望使用 chapter_3 数据固定提供的超级用户,则可以使用以下凭据:
-
admin -
mynewpassword
登录后,您应该看到两个主要部分,标题为 认证和授权 和 CHAPTER_3,在右侧,您的仪表板上显示了一个 最近操作 侧边导航部分,如下面的截图所示:


图 6.2 – Django 管理站点仪表板
如果您在 第三章,模型、关系和继承 中安装了 django-address 包,您将在 settings.py 文件的 INSTALLED_APPS 变量中看到额外的 address 应用程序,那么该部分就不会出现在管理仪表板中。
最近操作面板显示的信息跟踪了当用户在管理站点中创建、更改或删除特定对象时的时间,这些操作中的任何一个发生时,该对象都会在管理站点中注册。
在 第三章,模型、关系和继承 中标题为 User 模型的部分,使用 settings.py 文件中的 AUTH_USER_MODEL 变量指向我们创建的自定义 Seller 类,我们将不得不在管理站点中注册 Seller 模型,以便它显示出来。
接下来,让我们注册那个 Seller 模型,以便它在仪表板上显示。
编写管理类
在这里,我们将讨论如何在django.contrib.admin库中找到的标准ModelAdmin类的编写。我们还将提供一个使用UserAdmin类的示例,因为我们已经在第三章中扩展了User模型,模型、关系和继承。
类 – ModelAdmin
ModelAdmin是 Django 为我们提供的类,它为我们处理了所有繁重的工作,特别是在非用户模型方面。在模型可以注册之前,需要创建一个使用ModelAdmin类的 admin 类,以便将模型链接到管理界面。
在/chapter_6/admin.py文件中,包含以下代码:
# /becoming_a_django_entdev/chapter_6/admin.py
from django.contrib.admin
import ModelAdmin
class SellerAdmin(ModelAdmin):
pass
在前面的代码中,SellerAdmin类是使用 Django 的contrib类ModelAdmin构建的。Django 通过为我们做很多繁重的工作而使我们变得容易。Django 将自动配置我们在这个类中没有指定的所有内容,例如我们将在本章中很快讨论的选项。这意味着如果我们在这个类中包含pass语句,我们就不需要写任何其他内容,Django 将根据默认参数为该模型创建管理站点。请继续编写这个类另外三次,包括剩余的车辆模型对象,并在编写时为每个类包含pass语句。将这些类命名为VehicleAdmin、VehicleModelAdmin和EngineAdmin。
注意
每个类名都以单词Admin结尾。在命名未来的 admin 类时,请遵循ModelNameAdmin的命名约定。
类 – UserAdmin
UserAdmin是 Django 提供的一个特殊类,它允许我们将基于用户的模型链接到管理界面。UserAdmin类是ModelAdmin类的扩展。接下来,我们将创建SellerAdmin类,它将导入 Django UserAdmin类中可用的所有功能。我们将以与上一个练习中相同的方式编写 admin 类,包括pass语句。所有其他与车辆相关的 admin 类将继续使用标准的ModelAdmin类。
以下代码展示了SellerAdmin类现在的样子:
# /becoming_a_django_entdev/chapter_6/admin.py
...
from django.contrib.auth.admin
import UserAdmin
class SellerAdmin(UserAdmin):
pass
现在我们已经编写了 admin 类,接下来让我们注册它们。
注册模型
我们需要编写一个注册语句,将 admin 类与其相应的模型链接,并自动创建与该模型相关的 URL 模式。有两种方法可以做到这一点,它们都做完全相同的事情:
-
第一种方法是在文档的底部写一个单独的语句,放在已经编写的任何 admin 类之后。
-
第二种方法是将每个 admin 类包裹在一个 register 装饰器函数中。
这两种方法将在以下示例中演示,但请选择您最喜欢的一种。同时实现两种方法可能会导致错误。
使用语句
使用语句很简单;首先编写管理类,然后在同一admin.py文件的底部,像以下代码块中对Seller所做的那样,写下注册语句:
# /becoming_a_django_entdev/chapter_6/admin.py
...
from django.contrib
import admin
from django.contrib.auth.admin
import UserAdmin
from ..chapter_3.models
import Seller
...
class SellerAdmin(UserAdmin):
pass
admin.site.register(Seller, SellerAdmin)
在这里,我们使用admin.site.register(Model, ModelAdmin)语句来注册管理类。再写三个语句来注册Vehicle、VehicleModel和Engine模型类到它们相应的管理类。
使用装饰器
使用装饰器的工作方式与我们在第三章中介绍的@property模型方法装饰器类似,模型、关系和继承。装饰器封装类,写在类声明之前的行上,就像这里的SellerAdmin类一样:
# /becoming_a_django_entdev/chapter_6/admin.py
...
from django.contrib
import admin
from ..chapter_3.models import (
Engine,
Seller,
Vehicle,
VehicleModel
)
@admin.register(Seller)
class SellerAdmin(UserAdmin):
pass
在这里,我们使用 Django 提供的@admin.register(Model)装饰器。当以这种方式使用时,这个函数只接受一个位置参数,即我们希望链接到该管理类的模型类名称。管理类是从使用装饰器的类名称派生的,在这种情况下,是SellerAdmin类。将此装饰器添加到剩余的三个管理类中,并相应地调整每个装饰器相关的模型。
现在我们已经注册了这些模型,当访问 Django 管理仪表板时,您现在应该会看到一个名为CHAPTER_3的第四个部分,包含四个项目,如下面的截图所示:


图 6.3 – Django – 注册模型
点击这些选项中的任何一个,都会带您进入所谓的变更列表视图,在这里会显示所有这些对象的列表或集合。点击这些项目中的任何一个,然后会进入所谓的变更视图。在变更列表视图页面上点击添加按钮,会将用户带到所谓的添加视图,这是一个创建该模型新实例的表单,而不是编辑现有实例。同样,删除一个对象会将用户带到该对象的删除视图页面。类似于如何使用那些类中编写的字段和方法来控制模型和表单,我们可以通过定义某些称为选项的自定义方法来以相同的方式控制管理类。接下来,让我们讨论一下这些选项是什么。
配置管理类选项
Django 为自定义 Django 管理站点界面提供了管理类选项。在本节中,我们将介绍一些最重要和最广泛使用的选项,并提供如何使用它们的示例。我们没有足够的空间详细讨论它们。
要详细了解如何使用任何可用的选项,请访问官方 Django 文档,网址为:docs.djangoproject.com/en/4.0/ref/contrib/admin/#modeladmin-options。
以下选项已根据它们相关的视图类型(更改列表视图、更改或添加视图以及仅添加视图)进行分类。
注意
在将任何选项添加到你的管理类之前,请记住删除之前为该类编写的pass语句作为占位符。
与更改列表视图相关的选项
这些选项与更改列表视图页面上的管理类相关,例如以下列出的:
-
http://localhost:8000/admin/chapter_3/engine/ -
http://localhost:8000/admin/chapter_3/seller/ -
http://localhost:8000/admin/chapter_3/vehicle_model/ -
http://localhost:8000/admin/chapter_3/vehicle/
选项 – 操作
actions选项与更改列表视图页面左侧每个项目旁边的复选框相关。默认情况下,Django 为每个创建并注册的模型自动提供至少一个操作;这个操作是删除操作。例如,导航到Sellers更改列表视图页面http://localhost:8000/admin/chapter_3/seller/,选择操作标签旁边的下拉菜单,你将只看到一个选项,即删除选项,如下面的截图所示:

图 6.4 – Django – 操作管理选项
如果我们在这个列表中有 20 个卖家,我们可以通过使用这个操作一次性删除所有 20 个卖家,而不是不得不进行很多不必要的点击。可以创建额外的操作以供自定义使用。一个可以使用此操作的例子是,如果你有一组页面,这些页面可以处于已发布或未发布状态,并存储在你的表中的一个字段中。可以创建一个自定义操作,该操作可以发布或取消发布所有选定的对象。
在这些练习中,我们不会创建自定义操作,因为这会是一个复杂的任务,超出了本书的范围。此外,你可能考虑的一些操作,例如编辑字段,也可以使用list_editable选项来完成,我们将在选项 – list_editable 部分中讨论。
Django 在这里提供了关于如何创建自定义操作的完整文档:docs.djangoproject.com/en/4.0/ref/contrib/admin/actions/。
选项 – actions_on_bottom
actions_on_bottom选项用于显示False。如果设置为True,如以下示例中对SellerAdmin类所做的那样,操作下拉菜单将出现在更改列表结果下方:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
actions_on_bottom = True
选项 – actions_on_top
与actions_on_bottom选项类似,actions_on_top选项将显示True。我们只需要写这个选项,如果我们希望在更改列表上方禁用它,但我们可以使用以下示例在SellerAdmin类中将它设置为True而不触发错误:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
actions_on_top = True
如果同时将此选项和actions_on_bottom选项设置为True,则操作下拉菜单将出现在更改列表结果上方和下方。
选项 – actions_selection_counter
actions_selection_counter与显示在操作下拉框右侧的计数器相关。此选项控制是否应出现在操作旁边,如下面的截图所示:

图 6.5 – Django – actions_selection_counter 管理选项
默认情况下,Django 将此值设置为True。除非你想通过将其值设置为False来禁用此功能,否则无需编写此选项。然而,包含此选项并使用True值是没有害处的,如下面的示例所示:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
actions_selection_counter = True
选项 – 列显示
list_display选项用于在模型的更改列表视图页面上显示垂直列。例如,导航到 URL http://localhost:8000/admin/chapter_3/seller/,你会看到目前存在的五个列是username、email、first_name、last_name和is_staff。Django 将为该表中存在的每一行显示每个字段的值。
如果我们想调整此列表以显示我们添加到Seller模型中的自定义字段,而这些字段在User模型中不存在,例如SellerAdmin类将如下所示:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
list_display = (
'username',
'email',
'first_name',
'last_name',
'name',
'is_staff',
'is_superuser',
)
在之前存在的五个列已经在UserAdmin类中定义的list_display选项中定义,该选项用于构建SellerAdmin类。导航回相同的 URL http://localhost:8000/admin/chapter_3/seller/,你现在应该看到页面倒数第三列的业务名称列和作为页面最后一列的超级用户状态列,因为它们在先前的列表中的位置就是这样。
list_display选项也可以以可调用函数的形式使用,允许你在该管理类中编写一个方法来格式化该列显示的数据。只需将自定义方法的名称添加到该列表中,就像使用可调用选项时添加任何其他字段名称一样。
选项 – list_display_links
list_display_links选项用于控制更改列表结果中哪些列在点击时将用户导航到该对象的更改视图页面。默认情况下,Django 将使此列表中的第一列可点击。如果你想使其他列可编辑,请使用此选项。
在SellerAdmin类中,添加选项如下:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
list_display_links = (
'username',
'name',
)
当使用 list_display_links 选项时,还必须使用 list_display 选项。只有添加到 list_display 选项中的字段可以添加到该选项。此外,如果定义了 list_display_links 选项,Django 将不再使第一列可点击。例如,如果我们想保持第一列可点击,即在此选项中的 username 和 name 字段。此外,它们必须按照它们在 list_display 选项中出现的顺序排列。
注意
如果您正在使用我们即将讨论的 list_editable 选项,则这些字段不能用于 list_display_links 选项,例如 first_name 和 last_name 字段,我们将为 list_editable 示例保留。
选项 – list_editable
list_editable 选项是一个允许我们指定在变更列表视图页面上哪些字段可以从该页面进行编辑的选项。这避免了打开特定对象的更改视图来编辑单个字段的必要性。此外,只有包含在 list_display 选项中的字段可以包含在 list_editable 选项中。这是因为列必须存在,我们才能编辑它。
例如,如果我们想在变更列表视图页面上使 first_name 和 last_name 字段可编辑,则 SellerAdmin 类可以这样编写:
# /becoming_a_django_entdev/chapter_6/admin.py
...
from django.contrib.auth.admin
import UserAdmin
class SellerAdmin(UserAdmin):
...
list_editable = (
'first_name',
'last_name',
)
现在,导航到位于 http://localhost:8000/admin/chapter_3/seller/ 的变更列表视图。我们可以看到 first_name 和 last_name 字段现在显示为一个带有底部保存按钮的输入框,如下面的截图所示:

图 6.6 – Django – list_editable 管理选项
选项 – list_filter
list_filter 选项是一个强大的工具,它在一个标签为 过滤 的框中创建一个标签,位于变更列表视图页面上的结果右侧。添加到该选项的字段将告诉 Django 从该结果 QuerySet 中找到的值创建一组相关选项的过滤器。
例如,如果我们想覆盖 UserAdmin 类中找到的默认 list_filter 选项,并添加名为 name 的 Seller 模型字段作为过滤器,那么我们将按照以下示例编写 SellerAdmin 类:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
list_filter = (
'is_staff',
'is_superuser',
'is_active',
'name',
'groups'
)
在前面的例子中,我们只是复制了位于 Django 的 django.contrib.auth.admin 库中的 UserAdmin 类中找到的 list_filter 选项,然后我们只修改了该值到期望的值。
现在,当我们导航回 URL http://localhost:8000/admin/chapter_3/seller/ 时,我们看到 按企业名称 过滤器,如下面的截图所示:

图 6.7 – Django – list_filter 管理选项
选项 – list_per_page
list_per_page选项指定了我们希望在变更列表视图页面上显示的项目数量。默认情况下,Django 会将此值设置为100。有时这个值对于用户来说可能太多,所以让我们继续练习将这个值设置为更友好的数字,比如在SellerAdmin类中使用以下示例将其设置为20:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
list_per_page = 20
如果你向你的表中添加超过 20 个项目,你将在变更列表视图页面的底部看到一组分页按钮。当总项目数少于 20 时,不会出现分页按钮,因为只有一个结果页面。
注意
如果你正在使用第三方包或自定义类而不是 Django 中使用的默认django.core.paginator.Paginator类,你可以通过使用paginator管理选项来实现这个自定义分页器类。如何做到这一点的说明可以在这里找到:docs.djangoproject.com/en/4.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.paginator/。
选项 – ordering
ordering选项与模型Meta类中的ordering选项执行方式相同。此选项接受一个字段列表,当页面首次加载时,按指定的字段进行排序。
例如,UserAdmin类中的默认排序选项设置为按username升序排序。请继续使用以下示例将其添加到SellerAdmin类中:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
ordering = ('username',)
在这里,我们可以添加减号符号,例如在('-username',)中,使其按降序排列,就像模型Meta类中的ordering选项一样。可以添加多个字段,Django 将按照此选项列表中出现的顺序对项目进行排序。此外,一旦页面加载,用户可以选择取消排序或按另一列排序,如果他们按下那些列的表头中的操作按钮。
选项 – preserve_filters
preserve_filters选项与用户访问变更列表视图页面时应用的过滤器有关。默认情况下,当用户决定为模型创建、更改或编辑对象时,当他们被带回变更列表视图页面时,过滤器将被保留。我们实际上会使用此选项的唯一时间是我们想禁用此功能,使得在执行添加、更改或删除操作后,所有过滤器都会重置。
要禁用此功能,将preserve_filters的值设置为False,如下例所示:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
preserve_filters = False
选项 – search_fields
search_fields选项在变更列表视图页面上启用搜索栏。在Sellers列表中应该已经有一个出现,因为UserAdmin类已经为我们定义了这个选项。然而,像name这样的字段目前是不可搜索的。导航到http://localhost:8000/admin/chapter_3/seller/并搜索Biz关键词,你应该不会得到任何结果。
现在,将以下代码块添加到 SellerAdmin 类中,使标记为商业名称的字段可搜索:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
search_fields = (
'username',
'first_name',
'last_name',
'name',
'email'
)
现在,刷新页面并使用 chapter_3 固件搜索相同的 Seller。
更改/添加视图相关选项
这些选项与添加或更改视图页面的管理类相关,例如以下列出的:
-
http://localhost:8000/admin/chapter_3/engine/add/ -
http://localhost:8000/admin/chapter_3/engine/1/change/ -
http://localhost:8000/admin/chapter_3/seller/add/ -
http://localhost:8000/admin/chapter_3/seller/1/change/ -
http://localhost:8000/admin/chapter_3/vehicle_model/add/ -
http://localhost:8000/admin/chapter_3/vehicle_model/1/change/ -
http://localhost:8000/admin/chapter_3/vehicle/add/ -
http://localhost:8000/admin/chapter_3/vehicle/1/change/
选项 – exclude
可以将 exclude 选项视为 fields 选项的反面。此选项将接受一个字段列表,我们希望从 Django 管理站点的表单中排除这些字段。添加到 exclude 选项中的字段不应出现在 fieldsets 或 add_fieldsets 选项中;否则,将触发一个错误。如果一个字段同时存在于 fields 和 exclude 选项中,该字段将被排除。
例如,如果我们想使用以下代码排除 first_name 字段,那么我们也必须从 fieldsets、add_fieldsets、list_display、list_editable、search_fields 和 prepopulated_fields 选项中删除 first_name 字段,因为它们在之前的和未来的示例中已经编写过:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
exclude = ('first_name',)
您现在可以删除此设置并将 first_name 字段重置为其以前的用法。
选项 – fields
fields 选项让我们明确指定要在管理表单中包含哪些字段。如果不声明此字段,Django 将自动包含为指定模型存在的所有字段。然而,也可以使用 '__all__' 值来明确指定应包含相关模型中存在的所有字段。
我们不会包括这个选项,因为我们希望这些表单中包含所有字段。然而,如果我们只想指定某些字段,那么这将通过一个列表来完成,其中模型中每个字段的名字由逗号分隔:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
fields = ('username', 'password', 'first_name', 'last_name',)
在这个示例中,只有这四个字段会出现在 Seller 对象的更改视图页面上。
注意
fields 选项不能与 fieldsets 或 add_fieldsets 选项结合使用,这样做会导致错误。但是,fieldsets 和 add_fieldsets 选项可以一起使用。在 SellerAdmin 类中,我们只能使用 fieldsets 和 add_fieldsets 选项,因为用于构建 SellerAdmin 类的 UserAdmin 父类已经使用了这些选项。在这种情况下,如果我们不想包括特定的字段,最好使用 exclude 选项。此外,如果一个字段没有包含在管理类中,但在任何类选项中指定了它,那么将会触发一个错误。
选项 – fieldsets
fieldsets选项与fields选项类似,因为它与自定义字段相关,但它会将类似字段分组到我们创建的指定类别中。Django 将为这些字段在更改和查看页面上添加 HTML 和特殊格式。
例如,因为我们扩展了 Django 的User模型在第三章“模型、关系和继承”,现在使用UserAdmin类来构建SellerAdmin类,我们将需要编写自己的fieldsets选项。我们必须这样做,因为 Django 将使用fieldsets选项,就像它在UserAdmin类中那样编写,目前不包括我们创建的额外字段,称为name和vehicles。为了使name和vehicles字段出现在更改和添加查看页面上,我们需要将它们添加到任何字段集组中;否则,只会出现原始的User字段。
首先,将 Django 的django.contrib.auth.admin库中找到的原始fieldsets选项复制到您的SellerAdmin类中,并修改组以现在包括name和vehicles字段,如下所示:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'username',
'password',
),
}),
(('Personal Info'), {'fields': (
'first_name',
'last_name',
'name',
'email',
)}),
(('Permissions'), {'fields': (
'is_active',
'is_staff',
'is_superuser',
'groups',
'user_permissions',
)}),
(('Important Dates'), {'fields': (
'last_login',
'date_joined',
)}),
(('Vehicles'), {
'description': ('Vehicles that this user is selling.'),
'fields': (
'vehicles',
),
}),
)
此选项接受一个元组列表。每个元组包含两个项目,该字段集的标签或显示无标签的None值,如之前的第一组所示。第二个项目是另一个元组列表,其中该元组定义了三个可用的键之一或全部:fields、description和classes。classes键用于给该字段集的容器赋予一个 HTML 类名,如'classes': ('wide',)所示。description键将添加一个可选的描述,该描述作为<p></p>HTML 对象渲染在管理表单中的字段上方,如之前在车辆组中所示。fields键将接受一个由逗号分隔的字段列表,以将该字段添加到字段集组中。这是必选键。
使用链接http://localhost:8000/admin/chapter_3/seller/1/change/访问名为admin的卖家更改视图,你应该看到五个部分。第一个部分没有标签,其他四个部分按以下顺序命名:
-
不显示标签
-
个人信息
-
权限
-
重要日期
-
车辆
新字段是vehicles字段中标记为name字段的独立组。
选项 – filter_horizontal
filter_horizontal选项与名为vehicles的ManyToMany字段相关联,该字段是为Seller模型创建的。导航到数据库中任何销售员的更改视图页面,例如http://localhost:8000/admin/chapter_3/seller/1/change/,你会看到标记为<select multiple="">的 HTML 对象字段。这实际上可能对一些用户来说难以交互,特别是当需要使用键盘时,无论是在 Windows 上按Ctrl按钮还是在 Mac 上按Command按钮来选择多个项目进行提交。Django 提供了一个基于 JavaScript 的用户界面,可以应用于任何ManyToMany字段。
例如,将以下选项应用到SellerAdmin类中,将vehicles字段转换为现在使用水平 JavaScript 用户界面:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
filter_horizontal = ('vehicles',)
接下来,刷新相同的更改视图页面,你会看到车辆现在看起来如下截图所示,其中框是水平并排堆叠的:


图 6.8 – Django – filter_horizontal 管理选项
现在,用户有更多种方式与这个字段交互。他们仍然可以使用键盘完成所有操作,他们可以使用鼠标完成所有操作,或者他们可以使用两者的组合。从视觉上看,这更加吸引人,更容易看到哪些被选中,哪些没有被选中。这甚至提供了一个搜索字段来帮助过滤结果,如果可选项太多,还可以减少冗余。两个框用于选择左侧可用的项目,并通过控制按钮将其移动到右侧的选中框中。额外的控制按钮允许用户选择并将一个框中找到的所有项目移动到另一个框中。
选项 – filter_vertical
filter_vertical选项与filter_horizontal选项完全相同,只是它将框垂直堆叠而不是水平并排。顶部框是可用的项目,底部框用于选中的项目。
选项 – 表单
当创建自定义管理表单时,我们将在本章后面进行,form选项用于指向我们希望使用的表单类。如果我们不使用此设置,Django 将为与该管理类注册的模型动态创建一个ModelForm。
在本章标题为编写自定义管理表单类的部分之前,将以下选项添加到EngineAdmin类中:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class EngineAdmin(ModelAdmin):
...
form = EngineForm
此选项将管理类链接到我们稍后称为EngineForm的管理表单。
注意
在我们实际创建EngineForm表单之前,你可能会遇到错误。为了防止这些错误,你可以取消注释此行代码并在EngineAdmin类下留下pass语句,或者现在创建EngineForm类并将pass语句添加到该EngineForm类中,直到我们在本章后面添加代码。
选项 – inlines
inlines选项是一个高级功能,允许我们在父模型的更改视图页面上作为内联表单集显示来编辑子模型。这个功能被认为是高级的,因为我们需要编写单独的类来使用和实现这个选项。这些类可以使用自定义这些表单集外观和感觉的选项进行编辑。每个内联对象都以InlineModelAdmin类开始。用于渲染内联表单集的两个InlineModelAdmin子类是StackedInline和TabularInline。这两个类执行与InlineModelAdmin类相同的行为,除了为每个类渲染不同的 HTML 模板。甚至可以创建和使用这些内联类的新表单类。请参阅位于docs.djangoproject.com/en/4.0/ref/contrib/admin/#inlinemodeladmin-objects的文档,以及第五章中提供的概念,Django 表单。
StackedInline和TabularInline类都用于与ForeignKey字段链接的模型。在车辆对象中,我们可以将内联选项应用于EngineAdmin和VehicleModelAdmin类。对于任何ManyToMany字段,我们都需要以稍微不同的方式将它们链接起来。
接下来,我们将逐一介绍这些类,并简要讨论它们的使用方法。
类 – InlineModelAdmin
InlineModelAdmin类是构建用于渲染内联表单集的两个类的父类。这个类的每个选项和方法都可在这两个子类中使用。
许多可用于ModelAdmin类的选项和方法也适用于InlineModelAdmin类。以下是InlineModelAdmin类中提供内容的完整列表:

我们将添加到所有内联类的一个常见选项是extra = 1,这个值设置为1以仅显示一个额外的空白表单集,使用户能够在需要时动态添加对象。如果这个值被设置为,比如说10,那么将出现 10 个额外的空白表单集。默认值是3,这就是为什么我们将所有类的默认值覆盖为1,以提供更好的用户体验。
要详细了解如何使用这些选项和方法,请访问docs.djangoproject.com/en/4.0/ref/contrib/admin/#inlinemodeladmin-options。
类 – StackedInline
StackedInline类用于使用/admin/edit_inline/stacked.html模板渲染内联表单集。
要在EngineAdmin类上练习使用此选项,请将以下类添加到/chapter_6/admin.py文件中,最好将所有内联表单集类放置在此文档的顶部:
# /becoming_a_django_entdev/chapter_6/admin.py
...
from django.contrib.admin
import ..., StackedInline
class VehicleInline(StackedInline):
model = Vehicle
extra = 1
接下来,将以下选项添加到EngineAdmin类中,放置在所有内联表单集类下方:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class EngineAdmin(ModelAdmin):
...
inlines = [VehicleInline,]
现在,导航到任何发动机对象的变更视图页面,例如http://localhost:8000/admin/chapter_3/engine/1/change/。我们可以看到,使用该特定发动机的每辆车都可以从编辑发动机页面进行编辑,如下面的屏幕截图所示:

图 6.9 – Django – 内联管理选项 – 堆叠内联
StackedInline类将垂直堆叠显示每个字段,如前面的屏幕截图所示。
类 – 表格内联
TabularInline类是以与之前的StackedInline示例完全相同的方式编写的和实现的,除了使用VehicleInline类,如下例所示:
# /becoming_a_django_entdev/chapter_6/admin.py
...
from django.contrib.admin
import ..., TabularInline
class VehicleInline(TabularInline):
model = Vehicle
extra = 1
现在,刷新相同的页面,http://localhost:8000/admin/chapter_3/engine/1/change/,当渲染到页面上时,字段将现在以整齐排列的列水平显示,如下面的屏幕截图所示:

图 6.10 – Django – 内联管理选项 – 表格内联
多对多字段内联
在车辆关系中最有用且最有意义的方法是将卖家在Seller变更视图页面上出售的所有车辆链接起来。如果我们以与为ForeignKey字段编写内联类相同的方式编写内联类,现在为ManyToMany字段关系编写,我们最终会得到一个错误。为了防止这个错误,编写你的内联类几乎与实现相关ForeignKey字段的内联类相同,但现在,你将添加一个through语句。
创建以下类,用于SellerAdmin类,使用TabularInline或StackedInline类作为其构造函数,对于模型,使用以下through语句:
# /becoming_a_django_entdev/chapter_6/admin.py
...
from django.contrib.admin
import ..., TabularInline
class VehiclesInline(TabularInline):
model = Seller.vehicles.through
extra = 1
在这里,我们把这个类命名为复数形式的VehiclesInline,带有一个s,而不是我们之前写的第一个类,叫做VehicleInline,按照命名约定。我们这样做是为了表明一个与ManyToMany字段相关,而另一个则不是。这个命名约定不是强制的;你的命名约定可以是任何你想要的,但这是有帮助的。我们通过vehicles字段直接将model选项链接到Seller模型,vehicles字段是ManyToMany字段,使用through属性。
在SellerAdmin类中,添加以下inlines选项:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
inlines = [VehiclesInline,]
现在,导航到 http://localhost:8000/admin/chapter_3/seller/1/change/ 的卖家更改视图页面,我们可以看到这位卖家正在销售的车辆现在渲染得与之前的内联表单集练习相同。使用 ManyToMany 字段的一个区别是,它们将渲染为相关对象。可以通过点击每个项目旁边的铅笔图标来编辑它们,此时将出现一个弹出窗口,允许用户编辑该项目。
小贴士
在实现车辆内联字段集时,为了获得最佳效果,请从 fieldsets、filter_horizontal 和 filter_vertical 选项中移除 vehicles 字段。
选项 – radio_fields
radio_fields 选项用于 ForeignKey、OneToOne 字段以及任何使用选择列表的 CharField。默认情况下,Django 将这些字段渲染为 <select> HTML 对象,用户可以从下拉列表中选择一个选项。通过将字段添加到 radio_fields 选项中,Django 将选择渲染为一系列 HTML 单选按钮,当用户查看此页面时,所有选项都会以视觉方式呈现给他们。对于有数十个甚至数百个选项的字段,这可能不是最佳选项,这也是为什么 Django 默认使用 <select> 框的原因。
将 radio_fields 添加到 VehicleAdmin 类中,我们有两个 ForeignKey 字段可以操作。将此选项应用于其中一个字段,即 engine 字段,而将 vehicle_model 字段保持不变,就像以下示例中那样。这样,当我们查看页面时,我们可以看到两种字段之间的区别:
# /becoming_a_django_entdev/chapter_6/admin.py
...
from django.contrib
import admin
from django.contrib.admin
import ModelAdmin
class VehicleAdmin(ModelAdmin):
radio_fields = {'engine': admin.HORIZONTAL,}
在前面的示例中,radio_fields 选项中的每个键都与我们将从 <select> 转换为单选按钮的字段名称相关。该键的值接受两种选择之一,admin.VERTICAL 或 admin.HORIZONTAL。此值用于在页面上垂直或水平显示单选按钮选项。
现在,导航到任何车辆的更改视图页面,例如 http://localhost:8000/admin/chapter_3/vehicle/1/change/,并查看 vehicle_model 和 engine 字段如何不同,如下面的截图所示,有一个选择框和单选按钮选项:

图 6.11 – Django – radio_fields 管理选项
选项 – save_on_top
save_on_top 选项用于在更改或添加视图页面的顶部显示一组操作按钮。这与 actions_on_bottom 或 actions_on_top 选项不同,后者仅与更改列表视图页面相关。默认情况下,Django 将此值设置为 False,只在页面底部显示操作按钮。将此值设置为 True 意味着按钮将出现在这些页面的顶部和底部。
例如,将以下选项添加到 SellerAdmin 类中,以在更改和添加视图页面的顶部和底部显示这些按钮:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
save_on_top = True
现在,导航到任何更改或添加视图页面,例如 http://localhost:8000/admin/chapter_3/seller/1/change/,我们将在页面顶部和底部看到操作按钮。
添加视图相关选项
这些选项覆盖了仅与添加视图页面相关而不与更改视图页面相关的管理类行为,例如以下列出的:
-
http://localhost:8000/admin/chapter_3/engine/add/ -
http://localhost:8000/admin/chapter_3/seller/add/ -
http://localhost:8000/admin/chapter_3/vehicle_model/add/ -
http://localhost:8000/admin/chapter_3/vehicle/add/
选项 – add_fieldsets
add_fieldsets 选项与 fieldsets 选项执行的功能完全相同,但这些字段仅与添加/创建视图表单相关,而不是与更改视图表单相关。例如,导航到 http://localhost:8000/admin/chapter_3/seller/add/,您将看到由 SellerAdmin 类构建的 Django UserAdmin 类仅提供三个字段,然后剩余的字段在我们创建新的 Seller 对象后出现。
如果我们想要提供 first_name、last_name、name 和 email 字段,我们需要修改 SellerAdmin 类的 add_fieldsets 变量,使其看起来像以下示例:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'username',
'password1',
'password2',
),
}),
(('Personal Info'), {'fields': (
'first_name',
'last_name',
'name',
'email',
)}),
)
在前面的示例中,我们看到编写和分组字段集的写法与之前 fieldsets 示例的写法完全相同。现在,当我们访问之前相同的添加 Seller URL,即 http://localhost:8000/admin/chapter_3/seller/add/ 时,我们可以看到四个额外的字段,这四个字段是我们之前在 个人信息 字段集中包含的。
选项 – prepopulated_fields
prepopulated_fields 选项告诉一个字段(或多个字段)在目标字段的值发生变化时监听事件,然后使用目标字段的值更新这些字段值。这在用户更改 title 字段值时很有用,因为 slug 字段将自动填充相同的值。例如,在 Seller 模型中,让我们将其连接起来以监听 first_name 和 last_name 字段的变化,然后使用从 first_name 和 last_name 值派生的值填充 username 字段值。
对于这个示例,将以下选项应用到 SellerAdmin 类:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class SellerAdmin(UserAdmin):
...
prepopulated_fields = {
'username': ('first_name', 'last_name',)
}
在前面的代码中,我们也可以像对 first_name 和 last_name 字段那样应用额外的字段。导航到 Seller 模型的添加视图页面 http://localhost:8000/admin/chapter_3/seller/add/ 并在 first_name 或 last_name 字段中开始输入。您将看到 username 字段会自动填充输入的值。空格将被破折号替换,字段的值将按照它们在 prepopulated_fields 选项中写入的顺序显示。例如,first_name 将根据我们之前如何编写它而出现在 last_name 值之前。
现在我们已经了解了所有不同的选项,并将其中许多应用到我们的管理类中,让我们深入了解我们可以使用的各种管理类方法。
添加管理类方法
管理类方法允许我们添加或更改ModelAdmin或UserAdmin类的默认行为。任何在管理类中可用的选项都可以通过编写一个方法来动态计算其值。这些方法使用get_命名约定,然后是选项的名称,例如get_ordering()或get_form()。Django 还提供了许多内置方法,当发生某些事件时,如使用save_model()或delete_model()方法保存或删除对象时,会添加额外的操作。
接下来,我们将探索其中的一些方法,并使用一个动态值提供演示,特别是针对form选项。这将为我们使用本章后面的单独表单类做准备。
要详细了解如何使用 Django 管理类方法,请访问官方 Django 文档:docs.djangoproject.com/en/4.0/ref/contrib/admin/#modeladmin-methods。
方法 – get_form()
get_form()方法用于获取在管理类内部使用的表单类。例如,在以下两个练习中,一个将检查对象是否存在,然后根据该条件的结果,我们将为更改视图提供一种表单类,为添加视图提供另一种表单类。在另一个练习中,我们将演示为超级用户显示一个表单,为普通用户显示另一个表单。
注意
我们尚未创建EngineForm、EngineSuperUserForm或AddEngineForm类。请继续在/chapter_6/forms.py文件中创建这些类,至少包含pass语句,以允许除引擎更改和添加视图页面以外的页面加载时不会出错。完成这些练习后,即使包含pass语句,您的引擎更改和添加视图页面也会出错。请等待我们完成本章标题为“编写自定义管理表单类”的部分,这将添加必要的组件以防止加载这些页面时出错。
修改/添加视图条件
在EngineAdmin类中,添加以下get_form()方法,并删除或注释掉我们之前为该类编写的先前form选项,如图所示:
# /becoming_a_django_entdev/chapter_6/admin.py
from .forms
import AddEngineForm, EngineForm
...
class EngineAdmin(ModelAdmin):
...
#form = EngineForm
def get_form(self, request, obj=None, **kwargs):
if obj:
return EngineForm
else:
return AddEngineForm
return super(EngineAdmin, self).get_form(request, obj, **kwargs)
如前例所示,我们用执行一点逻辑以提供两种不同表单之一的方法替换了form = EngineForm。使用井号符号,我们注释掉了form = EngineForm行。
超级用户条件
此方法的另一种用途是,如果用户具有超级用户状态,则提供一种表单,如果没有,则提供另一种表单,使用此处所示的条件语句:
# /becoming_a_django_entdev/chapter_6/admin.py
from .forms
import ..., EngineSuperUserForm
...
class EngineAdmin(ModelAdmin):
...
def get_form(self, request, obj=None, **kwargs):
if obj:
if request.user.is_superuser:
return EngineSuperUserForm
else:
return EngineForm
else:
return AddEngineForm
这里的想法是赋予超级用户一些普通用户无法编辑的额外字段,例如权限权利和权限组设置。
注意
尽管我们将 Django 的 User 模型扩展为卖家,但当前登录的卖家将作为 user 键出现在 request 字典中,就像前面代码块中显示的 request.user 一样。
方法 – save_model()
save_model() 方法用于在对象在更改或添加视图中保存前后添加操作。要将此方法添加到 EngineAdmin 类中,请包含以下代码:
# /becoming_a_django_entdev/chapter_6/admin.py
…
class EngineAdmin(ModelAdmin):
...
def save_model(self, request, obj, form, change):
print(obj.__dict__)
# Code actions before save here
super().save_model(request, obj, form, change)
# Code actions after save here
在这里,我们创建了一个具有五个位置参数的 save_model() 方法,这些参数依次是 self、request、obj、form 和 change。这五个参数使得在方法内部编写逻辑时可以使用相关数据。change 参数的值为 True 或 False。如果正在保存的对象来自添加视图页面,则 change 值将为 False;如果对象在更改视图页面上,则 change 值将为 True。super().save_model(request, obj, form, change) 行实际上是保存对象的方式,这与使用 Model.save() 操作相同。此行以上的操作将在对象保存之前执行。此行之后的操作将在对象保存之后执行,例如发送电子邮件或触发通知警报。
提示
在前面显示的 print 语句中使用 __dict__ 属性将显示 obj 中可用的键和值的字典。
方法 – delete_model()
delete_model() 方法的使用方式与 save_model() 方法类似,只是在对象被删除而不是保存时使用。在相同的 EngineAdmin 类中,添加以下方法:
# /becoming_a_django_entdev/chapter_6/admin.py
...
class EngineAdmin(ModelAdmin):
...
def delete_model(self, request, obj):
print(obj.__dict__)
# Code actions before delete here
super().delete_model(request, obj)
# Code actions after delete here
接下来,我们将创建和修改本章前面提到的自定义管理表单类,在标题为 方法 – get_form() 的子节中进行。
编写自定义管理表单类
管理表单可以像我们在 第五章 中讨论的标准表单类一样创建和使用,Django 表单。对于管理表单类,我们需要使用 Django 的 ModelForm 类而不是 django.forms 库中的标准 Form 类,因为这些表单中的字段将链接到模型类。请参阅 第五章 中的示例,Django 表单,以了解更多关于如何自定义和更改表单类行为的信息,无论是 Form 还是 ModelForm 类。在这里,我们将演示仅初始化您的管理表单并启用所有现有字段,以便允许任何引擎更改和添加视图页面加载时不会出现前面提到的错误。
初始化管理表单
如果您还没有这样做,在chapter_6应用文件夹中,创建一个名为forms.py的文件。我们需要创建本章前面示例中使用的三个不同的表单类,并将它们命名为EngineForm、AddEngineForm和EngineSuperUserForm。使用此处提供的示例创建它们,但将EngineForm的名称更改为您正在编写的类的名称,并将所有三个类的相关模型类也相应更改:
# /becoming_a_django_entdev/chapter_6/forms.py
...
from django.forms
import ..., ModelForm
from ..chapter_3.models
import Engine
class EngineForm(ModelForm):
def __init__(self, *args, **kwargs):
print('EngineForm Initialized')
super(EngineForm, self).__init__(*args, **kwargs)
class Meta:
model = Engine
fields = '__all__'
要在 admin 类中使ModelForm类工作所需的最少代码量可以通过提供__init__方法,初始化表单来实现。此外,我们还需要带有model和fields选项的Meta子类。在这三个类(EngineForm、AddEngineForm和EngineSuperUserForm)中,将model选项的值设置为Engine,将它们全部链接到Engine模型。对于fields选项,提供值为'__all__',让 Django 根据在Engine模型类中编写的字段为您创建字段。与我们所编写的 admin 类不同,我们实际上必须告诉 Django 在这个类中使用所有或某些字段。
您可以调整字段和/或添加其他选项,以自定义这些表单的外观和感觉,这样我们就可以看到它们在渲染时彼此之间的差异。或者,您可以在每个表单的__init__方法中使用print语句,就像之前所做的那样,以告知您逻辑正在正常工作。如果您访问了本章“配置 admin 类选项”部分提供的选项的任何引擎更改或添加视图页面,页面现在应该能够无错误地加载。
引擎更改和添加页面如下所示:
-
引擎添加视图 –
http://localhost:8000/admin/chapter_3/engine/add/ -
引擎更改视图 –
http://localhost:8000/admin/chapter_3/engine/1/change/
接下来,让我们讨论在 Django 管理站点中配置用户权限。
使用 Django 认证系统
Django 提供了一个非常强大的认证系统,用于授予用户权限。默认情况下,超级用户拥有做任何事的权限,这就是为什么我们不得不在 第二章 中创建至少一个超级用户,项目配置。这个超级用户在 Django 系统中的任何时候都是必需的,以维护对您的站点和数据的控制。超级用户授予了我们控制我们系统的能力,并为系统中的每个其他用户建立用户角色和组。创建用户和超级用户可以通过命令行使用 Django 管理命令或通过 IDE 完成,就像我们在 第二章 中探索这些主题时一样,项目配置。也可以通过 Django shell 完成,就像我们在 第三章 中创建和保存模型时一样,模型、关系和继承。user 和 Seller 只是其他我们创建和保存的模型对象。现在我们有了访问 Django 管理站点的权限,我们也可以通过此界面添加用户,或者在我们的情况下,添加卖家,并编辑他们的权限。
接下来,让我们向系统中添加一个普通用户,以便我们可以至少有一个普通用户和一个超级用户来比较和对比他们的角色。
添加卖家
在这里,我们将激活 Django shell,从 chapter_3 应用程序导入 Seller 模型,然后继续创建新的 Seller 对象,这是一个标准用户:
-
在任何终端或命令行窗口中激活 Django shell,就像我们在 第三章 中做的那样,模型、关系和继承。一旦激活,导入
Seller对象,如下所示:(virtual_env) PS > python3 manage.py shell >>> from becoming_a_django_entdev.chapter_3.models import Seller
Django 在 User 模型中包含了 create_user() 方法,用于轻松创建一个没有超级用户权限的新用户。此方法为我们完成了加密密码的重任,所以我们只需提供未加密的密码字符串即可。
-
由于我们在 第三章 中将 Django
User模型扩展为Seller,模型、关系和继承,因此我们必须在Seller模型上使用create_user()方法,如下所示代码所示。执行以下命令,记住你使用的密码,因为你将需要使用该用户登录 Django 管理站点:>>> seller = Seller.objects.create_user('test', 'testing@example.com', 'testpassword', is_staff=True) -
现在退出 Django shell,执行
exit()命令,然后再次运行项目,如下所示:>>> exit() (virtual_env) PS > python3 manage.py runserver
现在,访问 Seller 更改列表视图页面:http://localhost:8000/admin/chapter_3/seller/。你应该看到至少两个结果,如下面的截图所示:

图 6.13 – Django – 卖家管理站点测试
这是预期行为,因为我们还没有授予这个用户任何权限。接下来,让我们授予这个用户一些权限。
授予权限
为了授予用户权限,我们这次将使用管理界面。再次登出,然后以admin用户或您可能为超级用户提供的任何名称重新登录。导航到您的test用户的更改视图,网址为http://localhost:8000/admin/chapter_3/seller/2/change/,应该是 ID 号2,除非您自己创建了更多用户,那么找到您当前正在工作的用户 ID 的更改视图。
在这个页面上,授予test用户更改chapter_3应用中所有内容的权限,并保留其他所有内容未选中,如下所示:

图 6.14 – Django – 用户权限
使用chapter_3来仅显示与此任务相关的权限。不要忘记在继续之前保存此用户。登出并再次登录,这次使用test用户凭据。现在,我们应该看到与我们的车辆相关的模型,如下面的截图所示:

图 6.15 – Django – 卖家管理站点测试 2
可以基于每个组授予权限,而不是逐个用户地授予权限。
注意
现在Sellers对象对test用户可用。以标准用户身份登录时,导航到任何存在的Sellers的更改视图将显示权限字段。为了防止这种情况发生,您可以选择仅允许超级用户查看和编辑Seller模型对象,或者遵循本章中方法 – get_form()子节中描述的步骤,然后向SellerAdmin类添加自己的逻辑。这将只向超级用户显示与权限相关的字段。
权限组
权限组是一种通过定义一组权限来添加或从组中删除用户的方法。拥有数千个用户的管理系统将变得繁琐,更不用说它可能导致多少不一致性,因为我们考虑到了人为错误。
现在,请确保你已从test用户账户注销,然后使用admin用户账户重新登录,并在此导航到组添加视图页面:http://localhost:8000/admin/auth/group/add/。然后,创建一个名为test_group的新组,并将之前在练习中分配给test用户的相同权限授予这个组。接下来,返回到那个test用户的修改视图页面,移除之前的所有用户权限,并将它们分配给test_group组。这个test用户将获得与之前相同的权限。有了这个,你可以创建你想要的任何数量的组,根据你的需求将用户分配到每个组,并定制项目的权限。可以通过将用户分配到组并授予额外的基于用户的权限来临时性地给用户分配额外的权限,这组分配之外。
摘要
我们为项目激活并定制了 Django 管理站点,也称为管理面板。这个强大的工具帮助我们在这个网站上注册的所有模型上实现搜索、筛选、排序、创建、编辑和删除功能。根据本章提供的概念,你应该能够使你的管理面板成为一个非常有用的工具,你的用户会喜欢使用。
使用 Django 提供的认证系统,许多不同类型的用户都可以访问和使用同一个网站,但他们的角色和用途却非常不同。如果我们选择扩展模板或使用前几章中提供的概念构建模板系统,每种类型的用户甚至可以分配完全不同的模板和流程。
在下一章中,我们将讨论发送电子邮件、创建自定义电子邮件模板,以及使用 Django 模板语言创建 PDF 报告。
第七章:第七章:处理消息、电子邮件通知和 PDF 报告
在本章中,我们将使用 Django 消息框架、电子邮件通知和模板以及 PDF 文档。为了让我们知道电子邮件实际上是从我们的系统中发送的,并且在不同电子邮件客户端中正确渲染,我们将使用一个免费的第三方服务来捕获所有发出的电子邮件。通过捕获所有发出的电子邮件,我们可以防止开发测试电子邮件发送给尚未看到它们的人。我们将使用 HTML、CSS 和 Django 模板语言来创建电子邮件和 PDF 报告模板。两者都将使用上下文数据,就像我们在 第四章,URL、视图和模板 中传递上下文到模板一样。
在本章中,我们将涵盖以下内容:
-
创建一个用于捕获应用发送的所有电子邮件的测试环境
-
使用 Django 消息框架创建闪存消息和自定义消息级别
-
创建和发送 HTML 和/或纯文本格式的电子邮件
-
使用 HTML、CSS 和 Django 模板语言创建基于模板的电子邮件
-
使用 HTML、CSS 和 Django 模板语言生成 PDF 文档
技术要求
要处理本章中的代码,需要在您的本地机器上安装以下工具:
-
Python 版本 3.9 – 作为项目的底层编程语言
-
Django 版本 4.0 – 作为项目的后端框架
-
pip 包管理器 – 用于管理第三方 Python/Django 包
我们将继续使用在 第二章,项目配置 中创建的解决方案。然而,并不需要使用 Visual Studio IDE。主要项目本身可以使用其他 IDE 运行,或者从项目根目录(其中包含 manage.py 文件)独立运行,使用终端或命令行窗口。无论您使用什么编辑器或 IDE,都需要一个虚拟环境来与 Django 项目一起工作。如何创建项目和虚拟环境的说明可以在 第二章,项目配置 中找到。您需要一个数据库来存储项目中的数据。在上一章的示例中选择了 PostgreSQL;然而,您可以为项目选择任何数据库类型,以使用本章中的示例。
我们将不会使用 chapter_3 应用数据固定文件中的任何数据,但如果该数据已经加载,请不要担心!本章的所有练习都将使用用户交互的表单中获取的数据,而不是来自数据库的数据。
本章创建的所有代码都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Becoming-an-Enterprise-Django-Developer。本章中展示的大部分代码可以在/becoming_a_django_entdev/becoming_a_django_entdev/chapter_7/目录中找到。
查看以下视频,了解代码的实际应用:bit.ly/3OzpalD。
准备本章内容
首先,按照第二章中讨论的步骤,在您的项目中创建一个名为chapter_7的新应用。正如该节所述,不要忘记将/becoming_a_django_entdev/becoming_a_django_entdev/chapter_7/apps.py文件中您应用类中的name =变量的值更改为指向您安装应用的位置。务必还将此应用包含在settings.py文件中的INSTALLED_APPS变量中。
在网站的主要urls.py文件中,添加以下路径,该路径指向我们将在本章中创建的应用的 URL 模式:
# /becoming_a_django_entdev/urls.py
...
urlpatterns = [
path(
'',
include('becoming_a_django_entdev.chapter_7.urls')
),
]
将chapter_5应用中找到的 URL 模式、表单、字段、视图、模板、CSS 和 JavaScript 文件直接复制到新创建的chapter_7应用中。这样,我们可以将每个章节的练习分开,本章的练习将建立在第五章中Django 表单的练习之上。在您复制到本章应用的代码中,确保更新所有从chapter_5/chapter-5到chapter_7/chapter-7的文件/代码引用,如有必要。
创建 Mailtrap 账户
为了在本章中与电子邮件示例一起工作,您需要一个能够捕获从正在构建的系统发送的所有电子邮件的电子邮件测试服务,因为我们是在本地运行项目。目前市场上有很多不同的第三方服务,它们都提供这种解决方案。每个服务在提供的附加测试工具和功能以及与该服务相关的成本方面都有所不同。您可以使用本书未选择的服务。如果您选择这样做,您将不得不遵循该服务的说明来配置您项目的设置,而不是本节中找到的说明。
为了演示本章中的练习,我们将使用一个完全免费的名为 Mailtrap 的服务。创建账户时无需信用卡,并且对个人副项目免费。这不是基于试用期的计划;它终身免费,或者至少直到 Mailtrap 改变其政策和程序。Mailtrap 还提供升级的付费计划,如果你和你的团队决定使用这项服务,这将特别有用,尤其是在多个开发环境和大型测试团队中。
按照以下步骤创建和设置你的 Mailtrap 账户:
-
访问
mailtrap.io/register/signup创建新账户。按照他们网站上提供的步骤进行操作。 -
要激活你的账户,你需要回到你的邮箱收件箱,如果你在收件箱中没有看到它,请点击
垃圾邮件文件夹。 -
完成后,登录你的新账户,你将被带到 我的收件箱,第一个标签页是 SMTP 设置。
-
在你的项目
settings.py文件中,如下所示:

图 7.1 – Mailtrap – SMTP 设置
将这些变量,以及你在账户中提供的凭证,添加到 settings.py 文件的任何位置。
-
此步骤是可选的。为了在生产环境中分离这些变量,在你的
settings.py文件中使用以下条件语句。每个变量的值也保存在项目的.env文件中:# /becoming_a_django_entdev/settings.py ... if DEBUG: EMAIL_HOST = os.getenv('EMAIL_HOST') EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER') EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD') EMAIL_PORT = os.getenv('EMAIL_PORT') else: # Production Email Connection Settings Pass
确保在运行项目之前,将变量添加到你的本地 .env 文件中,当使用前面的示例时。
注意
为了使 Mailtrap 能够与你的 Heroku 项目一起工作,确保将你的本地 .env 文件中的四个电子邮件变量添加到每个 Heroku 应用的配置变量中。有关如何操作的说明,请参阅 第二章,项目配置下的远程变量小节。你可以为每个环境使用相同的连接设置,它们都将发送到同一个收件箱。
就这样。Mailtrap 现已配置,并将拦截来自你的 Django 项目的所有邮件。接下来,让我们继续使用 Django 消息框架创建闪存消息。
使用 Django 消息框架
让我们先来介绍 Django 消息框架,这是一个用于向用户提供基于会话的消息的框架。闪存消息是一种一次性通知消息,直接显示给用户,这正是该框架创建的消息类型。我们可以将消息渲染到用户在模板中放置代码的任何位置,无论是模态弹出窗口还是从页面顶部或底部下拉的消息,甚至可以出现在用户提交的表单上方或下方。
在本章中,chapter_7 FormClassView 类将是主要的工作类,因为它将主要用于触发我们将要编写的操作。我们将编写在相应的 ContactForm 类中执行这些操作的方法,该类用于 chapter_7 应用程序的 FormClassView 类。
在我们开始编写这些类之前,我们首先将启用 Django 消息框架。
启用 Django 消息框架
这些设置将启用 Django 消息框架。项目不需要此框架即可运行。如果需要,可以删除这些设置,但为了使用本章,它们是必需的。很可能在您创建项目时,这些设置已经自动为您生成。请再次检查以确保无误。
在您的 settings.py 文件中,请确保存在以下设置和值:
# /becoming_a_django_entdev/settings.py
...
INSTALLED_APPS = [
...
'django.contrib.sessions',
'django.contrib.messages',
]
MIDDLEWARE = [
...
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
]
TEMPLATES = [
{
...
'OPTIONS': {
'context_processors': [
...
'django.contrib.messages.context_processors.messages',
],
},
},
]
在前面的设置中,需要记住的一个重要事项是 SessionMiddleware 总是在 MIDDLEWARE 列表中的 MessageMiddleware 条目之前。同样适用于 INSTALLED_APPS 变量;请确保 django.contrib.sessions 应用程序在 django.contrib.messages 应用程序之前。在 TEMPLATES 配置下显示的 Django 消息框架上下文处理器也是必需的,以便在您的模板中从任何地方访问 Django 消息框架上下文。这意味着您不需要在每个页面的上下文中显式定义一个变量专门用于消息。相反,它将通过项目的全局上下文自动可用。
接下来,我们将讨论 Django 消息框架的额外配置/启用。
消息存储后端
settings.py 文件中的 MESSAGE_STORAGE 变量。
可用于 MESSAGE_STORAGE 的后端选项在此列出:
-
django.contrib.messages.storage.session.SessionStorage– 在请求会话中存储消息,并要求将django.contrib.sessions应用程序包含在INSTALLED_APPS变量中。 -
django.contrib.messages.storage.fallback.FallbackStorage– 此选项首先使用现在已过时的CookieStorage,然后,当 cookie 数据超过 2,048 字节阈值时,而不是像CookieStorage选项的默认操作那样删除那些较旧的 cookie,新消息将被放置在SessionStorage中。CookieStorage在 Django 4.0 中不再受支持,但仍然可在django.contrib.messages.storage库中使用。目前尚不清楚这种存储方法何时将被完全删除。 -
django.contrib.messages.storage.base.BaseStorage– Django 提供此类作为开发人员创建自己的存储系统的一种方式。本身,这不会工作,因为这个类是一个抽象类,旨在被扩展。这被认为是超出本书范围的进阶主题。
让我们先将此设置为使用 SessionStorage 选项:
# /becoming_a_django_entdev/settings.py
...
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
消息级别
消息级别表示消息的不同严重程度。这些级别有一个变量名、一个小写标签名和一个表示严重程度的数值,如下表所示:

默认情况下,Django 将MESSAGE_LEVEL变量设置为INFO;更具体地说,Django 将其设置为20的值。这意味着如果我们尝试发布一个值为10的DEBUG相关消息,它将永远不会渲染到页面上。有些人可能会认为这是 Django 框架中的错误;然而,这是有意为之的设计。原因是,在生产环境中,我们不希望这些消息出现在任何地方供用户看到。任何值低于20的自定义消息级别也不会出现。相反,我们只想让开发人员和可能的项目测试人员在一个开发或本地环境中看到这些消息。
要在您的环境中启用与DEBUG相关的消息,最好的方法是在settings.py文件中再次利用调试条件,如下所示:
# /becoming_a_django_entdev/settings.py
...
from django.contrib.messages
import constants as messages
...
if DEBUG:
MESSAGE_LEVEL = messages.DEBUG
else:
pass
在这里,我们明确地定义了MESSAGE_LEVEL设置,以便在项目的DEBUG变量设置为True时包含与DEBUG相关的消息。如果DEBUG设置为False,就像生产环境的.env文件中所做的那样,那么它将使用 Django 的默认设置messages.INFO为此变量。我们完全可以省略等式中的else条件;然而,它被编写为一个占位符,用于说明目的。
消息标签
在本书代码中提供的/becoming_a_django_entdev/chapter_7/static/chapter_7/css/site.css文件中,有一些 CSS 样式正是这样做的;它们将本章讨论的每个消息级别样式化为不同的颜色。将这些样式复制并粘贴到您的项目中,您可以在浏览器中看到这些图像中描述的相同颜色。
在这个例子中,让我们将INFO消息的标签从原始的info更改为information,使用以下示例:
# /becoming_a_django_entdev/settings.py
...
from django.contrib.messages
import constants as messages
...
MESSAGE_TAGS = {
messages.INFO: 'information',
}
在前面的代码中,DEBUG、SUCCESS、WARNING和ERROR消息标签都将继续使用它们的默认消息标签值,因为我们没有将它们包含在这个列表中。
自定义消息级别
在settings.py文件中,请添加三个具有数值的新变量,如下所示:
# /becoming_a_django_entdev/settings.py
...
MINOR = 50
MAJOR = 60
CRITICAL = 70
MESSAGE_TAGS = {
messages.INFO: 'information',
MINOR: 'minor',
MAJOR: 'major',
CRITICAL: 'critical',
}
每个新级别都使用这些数值定义。它们可以命名为任何不与其他设置变量冲突的名称。这些值可以是任何数字,例如19或199,尽管最好不使用任何默认值,如10、20、25、30或40,因为这些值已被其他级别使用。我们还添加了这些变量到MESSAGE_TAGS变量中,因为当我们有创建新消息的事件时,它也需要一个消息标签来在渲染 HTML 时添加 CSS 类。
现在 Django 消息框架的设置已经配置好了,我们可以使用该框架并创建消息。
创建消息
创建消息非常简单。为此练习,让我们修改 FormClassView 类中找到的 post() 方法,该方法已复制到 chapter_7 应用程序中。在这里,我们将添加在表单提交时创建消息的代码。Django 提供了两种编写消息的方式,一种是通过提供的 add_message() 方法,另一种是明确将消息添加到五个默认消息级别之一。
以下步骤展示了使用两种方式。请只使用其中一种:
-
在
/chapter_7/views.py文件中,将以下代码块中高亮显示的add_message()语句和上下文添加到FormClassView类的if form.is_valid():条件下。请记住取消注释或删除此条件中找到的return语句:# /becoming_a_django_entdev/chapter_7/views.py ... from django.contrib import messages from django.template.response import TemplateResponse from django.views.generic.edit import FormView class FormClassView(FormView): ... def post(self, request, *args, **kwargs): ... if form.is_valid(): messages.add_message( request, messages.SUCCESS, 'Your contact form submitted successfully' ) context = { 'title': 'FormClassView Page', 'page_id': 'form-class-id', 'page_class': 'form-class-page', 'h1_tag': 'This is the FormClassView Page Using ContactForm', 'form': form, } ... -
接下来,在
/chapter_7/views.py文件中,将以下代码块中高亮显示的add_message()语句和上下文添加到FormClassView类的else:条件下。请记住取消注释或删除此条件中找到的return语句,并添加此处所示的新return语句:# /becoming_a_django_entdev/chapter_7/views.py ... class FormClassView(FormView): ... def post(self, request, *args, **kwargs): ... if form.is_valid(): ... else: messages.add_message( request, messages.ERROR, 'There was a problem submitting your contact form.<br />Please review the highlighted fields below.' ) context = { 'title': 'FormClassView Page - Please Correct The Errors Below', 'page_id': 'form-class-id', 'page_class': 'form-class-page errors-found', 'h1_tag': 'This is the FormClassView Page Using ContactForm<br /><small class="error-msg">Errors Found</small>', 'form': form, } return TemplateResponse( request, self.template_name, context ) ... -
此步骤不是必需的;它只是展示了编写和使用之前显示的消息的另一种方式。使用以下代码块中高亮显示的
Success和Error级别语句作为之前显示语句的替代。只使用其中一种:# /becoming_a_django_entdev/chapter_7/views.py ... class FormClassView(FormView): ... def post(self, request, *args, **kwargs): ... if form.is_valid(): messages.success( request, 'Your contact form submitted successfully' ) ... else: messages.error( request, 'There was a problem submitting your contact form.<br />Please review the highlighted fields below.' ) ...
使用任何添加消息的示例都将执行相同的添加消息操作,而同时使用两个示例将导致相同的消息被添加两次到您的存储系统中。在前面所示的 post() 方法中,我们注释掉了旧的重定向语句,现在正在定义在表单提交时使用相同条件的成功和失败消息。消息本身可以接受一个字符串,并且该字符串可以包含 HTML,如图所示的失败消息。如果字符串中存在 HTML,则消息在使用模板中的消息时必须使用 |safe 过滤器。
接下来,让我们回顾一下在创建消息时可以做的额外事情。
使用自定义消息级别
如果我们想使用我们创建的自定义消息级别之一,例如 CRITICAL,那么我们只能使用 add_message() 方法。我们还需要导入 settings 来访问这些变量,如图所示:
# /becoming_a_django_entdev/chapter_7/views.py
from django.views.generic.edit
import FormView
...
from django.conf
import settings
class FormClassView(FormView):
...
def post(self, request, *args, **kwargs):
...
if form.is_valid():
messages.add_message(
request,
settings.CRITICAL,
'This is critical!'
)
...
消息的创建方式相同,只是使用 settings.LEVEL 而不是 messages.LEVEL。
使用额外标签
我们可以为消息及其仅有的消息传递额外的自定义 CSS 类。这是通过使用 extra_tags 属性将那个类(们)添加到添加消息操作中实现的。例如,让我们使用以下示例将我们的消息渲染为两个类,success 类会自动添加,以及一个额外的名为 bold 的类,以加粗渲染的文本,如下所示:
# /becoming_a_django_entdev/chapter_7/views.py
from django.contrib
import messages
from django.views.generic.edit
import FormView
from django.conf
import settings
...
class FormClassView(FormView):
...
def post(self, request, *args, **kwargs):
...
if form.is_valid():
messages.success(
request,
'Your contact form submitted successfully',
extra_tags = 'bold'
)
...
在完成本章标题为显示消息的部分后,当我们加载页面并检查消息时,我们应该在检查该特定元素时在屏幕上看到渲染的 CSS 类粗体和成功,如下面的截图所示:

图 7.2 – Django 消息框架 – extra_tags 属性
那个静默失败
创建一个静默失败的消息,简单地说就是创建一个可重用的应用程序,使用 Django 消息框架,不需要其他开发者在不同的项目中启用 Django 消息框架。这意味着如果他们已禁用此框架或尚未启用,添加消息操作将不会阻止他们的项目正常工作。
要使用此选项,请将fail_silently属性添加到您的添加消息操作中,如下所示:
# /becoming_a_django_entdev/chapter_7/views.py
from django.contrib
import messages
from django.views.generic.edit
import FormView
from django.conf
import settings
...
class FormClassView(FormView):
...
def post(self, request, *args, **kwargs):
...
if form.is_valid():
messages.success(
request,
'Your contact form submitted successfully',
fail_silently=True
)
...
将fail_silently属性设置为True时,应用程序将正常运行,而不会出现阻止代码运行的错误。如果开发人员已禁用 Django 消息框架且未包含fail_silently属性,则在运行应用程序时,应触发一个闪存消息,您将看到MessageFailure错误,如下所示:

图 7.3 – Django 消息框架 – fail_silently 属性
在下一节中,我们将我们的消息渲染成模板,即 HTML。
显示消息
为了让消息真正被用户看到,我们需要在 Django 模板中添加一些代码。使用chapter_7 FormClassView类使用的模板文件/chapter_7/templates/chapter_7/form-class.html,将以下代码添加到该模板中 HTML <form>对象的顶部:
# /becoming_a_django_entdev/chapter_7/templates/chapter_7/form-class.html
...
{% block body_content %}
...
<form method="post">
{% csrf_token %}
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
{{ message|safe }}
</li>
{% endfor %}
</ul>
{% endif %}
...
此文件中的所有其他代码都可以保持不变。在这里,我们使用一个简单的条件语句来检查存储系统中是否存在此请求的消息。如果存在,则创建一个<ul>列表,然后遍历每个存在的消息,在该列表中为每个消息创建一个单独的<li>项目。消息本身使用|safe过滤器,允许它渲染消息字符串中可能存在的 HTML。
访问 URL http://www.localhost:8000/chapter-7/form-class/ 并提交表单。有效或无效的消息将显示,具体取决于您是否实际触发了该表单的验证错误,如下面的截图所示:

图 7.4 – Django 消息框架 – 显示消息
如果您使用的是本书提供的 CSS 类,前面的消息将在浏览器中以绿色显示。
现在我们已经启用了 Django 消息框架,并在我们的视图类中添加了几条消息,接下来让我们练习发送电子邮件通知而不是显示闪存消息。
配置电子邮件通知
本节将帮助我们构建实际的电子邮件通知而不是闪存消息。我们将编写逻辑以在添加消息的同一FormClassView类中的post()方法中触发发送电子邮件操作。我们将利用本章开头创建的 Mailtrap 账户来捕获我们项目发送的所有电子邮件。如果您尚未这样做,请创建一个 Mailtrap 账户,并在您的settings.py文件中配置该连接。如果不这样做,您将难以执行本节中的代码。
电子邮件存在三种 MIME 类型,如下所示:
-
text/plain -
application/rtf -
text/html
虽然存在三种 MIME 类型,但在发送电子邮件时 Django 只使用其中两种:纯文本和 HTML。富文本电子邮件被视为 HTML 电子邮件,因为它们包含 HTML 标记。
作为纯文本电子邮件
纯文本电子邮件就像其名称所暗示的那样;它们只是文本,没有其他内容。我们将在我们使用的ContactForm类中创建一个方法,准备并发送电子邮件。触发发送电子邮件的方法可以从技术上放置在任何类或任何文件中。按照以下步骤创建您的:
-
在将
ContactForm类复制到/chapter_7/forms.py文件中时,使用以下代码添加一个名为send_email()的新方法:# /becoming_a_django_entdev/chapter_7/forms.py ... from django.core.mail import EmailMessage ... class ContactForm(Form): ... def send_email(self, request): data = self.cleaned_data msg_body = 'Hello World' email = EmailMessage( subject = 'New Contact Form Entry', body = msg_body, from_email = 'no-reply@example.com', reply_to = ['no-reply@example.com'], cc = [], bcc = [], to = [data['email_1']], attachments = [], headers = {}, ) email.content_subtype = 'plain' email.send()
此方法将处理与格式化和发送电子邮件相关的所有繁琐工作。当然,这是一个基本的text/plain电子邮件,仅由msg_body变量组成的短语。email.content_subtype语句是我们告诉 Django 我们希望将此电子邮件格式化为纯文本电子邮件的地方。我们还从django.core.mail库中导入并使用了EmailMessage类,用于构建电子邮件和格式化与该电子邮件相关的标题。Django 还提供了基于方法的简单函数,例如send_mail()或send_mass_mail(),以及其他少量方法。我们将专注于EmailMessage类,因为它将涵盖我们在本章中需要实现的所有方面。要了解更多关于 Django 提供的所有电子邮件方法,请访问官方文档,网址为:docs.djangoproject.com/en/4.0/topics/email/。
由于我们保持此示例非常基础,我们只定义了subject、body、from_email、reply_to和to属性。数据通过使用self.cleaned_data访问,我们将名为email_1的字段的值分配给to属性列表的值,即收件人的电子邮件地址。如果你要将电子邮件发送到多个地址,请在该列表中使用逗号分隔每个电子邮件地址,不要有空格。
-
在
/chapter_7/views.py文件中找到的FormClassView类中,保留与该视图类中发送消息相关的所有内容不变。要实际发送电子邮件,请将以下代码行添加到post()方法中,如图所示:# /becoming_a_django_entdev/chapter_7/views.py from django.template.response import TemplateResponse ... class FormClassView(FormView): ... def post(self, request, *args, **kwargs): form = self.form_class(request.POST) ... form.send_email(request) return TemplateResponse( request, self.template_name, context )
在这里,我们将发送电子邮件操作放置在return语句之上和检查表单是否有效的条件语句之下。我们目前没有将有效和无效表单提交的发送操作分开。
- 现在,访问 URL
http://www.localhost:8000/chapter-7/form-class/并提交表单,无论是有效还是无效状态。页面应该刷新而不会出现编程错误。你现在应该能在 Mailtrap 账户的收件箱中看到你的电子邮件,Mailtrap 账户的网址为mailtrap.io/inboxes/,如图所示:


图 7.5 – Mailtrap – 纯文本电子邮件
你还会注意到电子邮件现在显示在text/plain下。
作为 HTML 电子邮件
配置 HTML 或富文本电子邮件相当简单。使用上一节中的相同示例,作为纯文本电子邮件,将纯文本电子邮件转换为 HTML 电子邮件只需更改两行代码。第一行代码是将msg_body变量更改为等于'<b>Hello World</b>'。这样,我们实际上可以传递 HTML 来查看它是否工作。第二行是将email.content_subtype的值更改为等于'html',这样就完成了!
现在,再次访问相同的 URL,http://www.localhost:8000/chapter-7/form-class/,并提交表单。这次,当你访问 Mailtrap 收件箱mailtrap.io/inboxes/时,你应该能看到加粗的Hello World文本,如图所示:


图 7.6 – Mailtrap – HTML 电子邮件
在前面的屏幕截图中,你还会看到电子邮件现在以text/html MIME 类型显示,没有提供文本回退,这意味着此电子邮件只提供 HTML。点击Tech Info标签将揭示有关你的电子邮件的其他详细信息。在此标签中,你可以验证实际的内容类型。检查 Content-Type 的值是否有等于text/html; charset=utf-8的 MIME 类型。
注意
'html'的值用于富文本和 HTML 格式的电子邮件。它们都将被发送为text/html。这是因为我们无法明确告诉 Django 为富文本电子邮件使用'application/rtf'。Django 只是假设富文本电子邮件是 HTML,因为它们包含 HTML 标记。
作为具有纯文本替代的 HTML 电子邮件
具有纯文本替代的 HTML 电子邮件用于难以渲染电子邮件 HTML 格式版本的电子邮件客户端,或者如果垃圾邮件拦截器只显示文本。我们需要使用 Django 提供的EmailMultiAlternatives类而不是EmailMessage类来完成此操作。EmailMultiAlternatives是EmailMessage类的扩展,这意味着EmailMessage类中所有可用的方法和属性都可以在这个类中使用,还有更多。当我们使用这个类时,我们格式化电子邮件为text/html,然后使用EmailMultiAlternatives类中可用的新的attach_alternative()方法,该替代电子邮件格式化为text/plain。
使用与作为 HTML 电子邮件子节相同的代码,并做出以下突出更改:
# /becoming_a_django_entdev/chapter_7/forms.py
...
from django.core.mail
import (
EmailMessage,
EmailMultiAlternatives
)
...
class ContactForm(Form):
...
def send_email(self, request):
data = self.cleaned_data
msg_body = '<b>Hello World</b>'
email = EmailMultiAlternatives(
subject = 'New Contact Form Entry',
body = msg_body,
from_email = 'no-reply@example.com',
reply_to = ['no-reply@example.com'],
cc = [],
bcc = [],
to = [data['email_1']],
attachments = [],
headers = {},
)
email.content_subtype = 'html'
email.attach_alternative(
'Hello World',
'text/plain'
)
email.send()
在前面的例子中,我们只是将EmailMessage类替换为新的EmailMultiAlternatives类。然后,我们添加了email.attact_alternative()动作语句,该语句使用我们提供的文本Hello World格式化一个新的text/plain电子邮件,并将该新的纯文本格式化的电子邮件附加到原始 HTML 电子邮件上。我们这样做而不是使用EmailMessage类的attachment属性,因为我们实际上正在重构电子邮件的内容类型,使其现在成为multipart/alternative MIME 类型,而不是text/html或text/plain MIME 类型。
就这样;你现在有一个既是 HTML 又是纯文本的电子邮件。让我们验证一下。访问相同的 URL,http://www.localhost:8000/chapter-7/form-class/,并提交表单。这次,当你访问你的 Mailtrap 收件箱mailtrap.io/inboxes/时,你应该看到加粗的Hello World文本,并且也会看到文本标签现在突出显示并可点击,如图所示:

图 7.7 – Mailtrap – HTML 和纯文本电子邮件
点击文本标签将显示可用的纯文本格式。主要格式是 HTML,备用格式将是纯文本。在这个例子中,内容类型的顺序可以颠倒。当我们点击技术信息标签时,我们现在会看到内容类型显示为multipart/alternative,如图所示:

图 7.8 – Mailtrap – multipart/alternative
带有文件附件
发送带有文件附件的电子邮件也非常简单。Django 的 EmailMessage 类提供了一个名为 attach_file() 的方法,它允许我们通过传递该方法的两个位置参数(文件的路径和一个可选的 MIME 类型)轻松地附加文件。对于接下来的练习,请复制随本书代码一起提供的示例 PDF 文档,该文档位于 /becoming_a_django_entdev/chapter_7/static/chapter_7/pdf/ 目录中,文件名为 example.pdf。在遵循此示例之前,请将此文件复制到与您的项目相同的目录中,或者创建一个自己的虚拟 PDF 文件。
在接下来的示例中,我们将继续在“作为纯文本替代的 HTML 电子邮件”部分中刚刚完成的最后一个示例,并将 example.pdf 文档附加到该电子邮件中。Django 文档描述了使用 attach_file() 方法,路径写为 email.attach_file('static/chapter_7/pdf/example.pdf')。这就是在许多 Django 项目中使用此方法时路径的定义方式。然而,由于使用了 whitenoise 包,我们必须导入 settings.py 文件并使用 STATIC_ROOT 变量,如下所示:
# /becoming_a_django_entdev/chapter_7/forms.py
...
from django.conf
import settings
...
class ContactForm(Form):
...
def send_email(self, request):
...
email.attach_file(settings.STATIC_ROOT + '/chapter_7/pdf/example.pdf')
email.send()
注意
当使用 STATIC_ROOT 变量与 whitenoise 包结合使用时,我们现在必须运行 Django 管理命令 collectstatic,以便在本地运行项目时找到该文件。这不需要在每一个 Django 项目中执行,但在我们的项目中必须这样做。为此,首先停止项目运行。在终端或命令行窗口中,这可以通过在 Windows 上按 Ctrl + C 或在 Mac 上按 Cmd + C 并使用键盘来完成。然后,执行以下命令。当提示您这样做时,输入单词 yes 并按 Enter 键:
(virtual_env) python manage.py collectstatic
(virtual_env) python manage.py runserver
这是一个内置的 Django 命令,它将从您项目中加载的所有应用程序中收集静态文件,并将它们的副本放置到项目的 STATIC_ROOT 目录中,该目录被定义为 /becoming_a_django_entdev/staticfiles/ 文件夹,我们在 Git 仓库中已忽略该文件夹。
就这样。现在,如果您访问相同的 URL,http://www.localhost:8000/chapter-7/form-class/,并提交表单,这次当您访问 Mailtrap 邮箱 mailtrap.io/inboxes/ 时,您应该能在该电子邮件的右上角看到所附加的文件,如图所示:

图 7.9 – Mailtrap – PDF 附件
Mailtrap 允许您点击此文档以打开和查看它或下载它。打开文档以查看它是否正常工作。
那个失败是静默的
开发者可以编写包含发送电子邮件等操作的可重用应用程序,并且让它们在失败时静默处理,就像我们使用 Django 消息框架时做的那样。这意味着当开发者安装了你的应用程序但尚未配置电子邮件客户端连接时,项目不会出错。Django 将此选项作为 EmailMessage 或 EmailMultiAlternatives 类的 send() 方法的属性提供。
要激活我们刚才编写的发送电子邮件示例中的 fail_silently 选项,请向现有的 send() 动作添加以下属性,如下所示:
# /becoming_a_django_entdev/chapter_7/forms.py
...
class ContactForm(Form):
...
def send_email(self, request):
...
email.send(fail_silently=True)
这防止了当执行此代码时,email.send() 动作显示错误消息。
注意
Django 在使用前面提到的 send_mail() 和 send_mass_mail() 方法时也提供了此选项。要了解更多信息,请访问 docs.djangoproject.com/en/4.0/topics/email/。
现在我们对 Django 中电子邮件的发送方式有了更好的理解,让我们继续创建我们自己的电子邮件模板,以便为我们客户提供定制服务。
编写自定义电子邮件模板
将 HTML 作为字符串写入 Python 可能会变得非常混乱。我们可以将正文内容,如 '<b>Hello World</b>',作为 .html 模板文件编写。这将允许我们将多个电子邮件模板组织到 chapter_7 应用程序的 /templates/emails/ 目录中。编程工作也可以以这种方式在开发者之间共享。电子邮件模板还可以用于纯文本格式的电子邮件,只需在 .html 文件中放置文本,而不包含任何 HTML 代码。虽然这听起来可能对纯文本电子邮件没有吸引力,但在与大量开发者合作时,这确实有其好处。让我们从使用纯文本电子邮件的最简单模板开始。
Django 提供了 get_template() 方法,位于 django.template.loader 库中。此方法将在以下子节中的所有电子邮件模板示例中使用。
对于纯文本电子邮件
按照以下步骤创建一个纯文本电子邮件的模板:
-
在我们一直在使用的
ContactForm类中,修改send_email()方法为以下代码:# /becoming_a_django_entdev/chapter_7/forms.py ... from django.template.loader import get_template ... class ContactForm(Form): ... def send_email(self, request): data = self.cleaned_data template = get_template( 'chapter_7/emails/plain_text_format.html' ) msg_body = template.render() email = EmailMessage( subject = 'New Contact Form Entry', body = msg_body, from_email = 'no-reply@example.com', reply_to = ['no-reply@example.com'], cc = [], bcc = [], to = [data['email_1']], attachments = [], headers = {}, ) email.content_subtype = 'plain' email.send(fail_silently = True)
在前面的代码中,我们导入了 get_template() 方法,并使用它来构建模板变量,该变量指向 /chapter_7/emails/plain_text_format.html 文件。
-
现在,请在该目录下
/chapter_7/templates/文件夹中创建该文件。在该文件中,只需添加文本Hello World,不要添加其他内容。如果你在此文件中放置任何 HTML,它将作为纯文本正文内容中的字符串渲染,而不会作为 HTML 渲染。 -
现在,访问相同的 URL,
http://www.localhost:8000/chapter-7/form-class/,并提交表单。这次,当您访问您的 Mailtrap 收件箱mailtrap.io/inboxes/时,您应该会看到HTML标签已被禁用,只剩下文本标签来查看您的电子邮件。这也表明过程是成功的,如下面的截图所示:

图 7.10 – Mailtrap – 纯文本模板
对于 HTML 电子邮件
编写 HTML 模板的方式与之前加载纯文本示例模板的方式相同。只需进行以下修改:
-
首先,我们加载一个名为
html_format.html的新文件,并将content_subtype改回'html',如这里所示:# /becoming_a_django_entdev/chapter_7/forms.py ... from django.template.loader import get_template ... class ContactForm(Form): ... def send_email(self, request): data = self.cleaned_data template = get_template( 'chapter_7/emails/html_format.html' ) msg_body = template.render() email = EmailMessage( subject = 'New Contact Form Entry', body = msg_body, from_email = 'no-reply@example.com', reply_to = ['no-reply@example.com'], cc = [], bcc = [], to = [data['email_1']], attachments = [], headers = {}, ) email.content_subtype = 'html' email.send(fail_silently = True) -
现在,在您的
/chapter_7/templates/chapter_7/emails/目录中创建html_format.html文件。在此文件中,放置以下代码,其中我们实际上需要像 HTML 页面一样格式化文档,并提供标记的Hello World文本:# /becoming_a_django_entdev/chapter_7/templates/chapter_7/emails/html_format.html <!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8" /> <title>Hello World</title> </head> <body> <b>Hello World</b> </body> </html>
在此模板中,您可以按需格式化 HTML <head> 和 <body> 内容。甚至明智的做法是在此文档中包含响应式电子邮件和浏览器/客户端支持的语法,以确保在每种设备上都能正确渲染。您的电子邮件测试客户端通常会提供文档来帮助您处理这类事情。
- 现在,访问相同的 URL,
http://www.localhost:8000/chapter-7/form-class/,并提交表单。这次,当您访问您的 Mailtrap 收件箱mailtrap.io/inboxes/时,您应该会看到HTML标签现在已启用,而文本标签已被禁用。同样,这也表明过程是成功的,如以下所示:

图 7.11 – Mailtrap – HTML 模板
提供模板上下文
当我们将动态内容引入等式中时,使用基于模板的电子邮件可以变得更加有用。为此,我们需要将上下文发送到template.render()语句。通过这样做,我们甚至可以将已定义为data变量的表单数据直接传递到模板中,在该模板中访问表单字段值。
在接下来的练习中,我们将渲染一个模板,该模板将显示用户为表单的每个字段输入的确切内容。按照以下步骤进行操作:
-
在
ContactForm的send_email()方法中,进行以下突出显示的更改:# /becoming_a_django_entdev/chapter_7/forms.py ... from django.template.loader import get_template ... class ContactForm(Form): ... def send_email(self, request): data = self.cleaned_data template = get_template('chapter_7/emails/new_contact_form_entry.html') context = {'data': data} msg_body = template.render(context) email = EmailMessage( subject = 'New Contact Form Entry', body = msg_body, from_email = 'no-reply@example.com', reply_to = ['no-reply@example.com'], cc = [], bcc = [], to = [data['email_1']], attachments = [], headers = {}, ) email.content_subtype = 'html' email.send(fail_silently = True) -
现在,在您的
/chapter_7/templates/chapter_7/emails/目录中创建一个名为new_contact_form_entry.html的新文件,并将以下代码放入该文件中:# /becoming_a_django_entdev/chapter_7/templates/chapter_7/emails/new_contact_form_entry.html {% load static %} <!DOCTYPE html> <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <meta charset="utf-8"> <title>Contact Form Submitted</title> </head> <body> <center> <h1>New Contact Form Entry</h1> <h2>The field contents are listed below</h2> <ul> <li>Full Name: {{ data.full_name }}</li> <li>Email Field Example 1: {{ data.email_1 }}</li> <li>Email Field Example 2: {{ data.email_2 }}</li> <li>Email Field Example 3: {{ data.email_3 }}</li> <li>Conditionally Required Field: {{ data.conditional_required }}</li> <li>Multiple Emails Field: {{ data.multiple_emails }}</li> <li>Message: {{ data.message }}</li> </ul> </center> </body> </html>
在这个模板中,您可以使用 Django 模板语言的标准标签和过滤器,例如编写条件语句来检查字段值是否等于特定值。这意味着您可以为存在于您的 data 变量中的所有字段编写循环,然后使用提供的字段标签而不是之前显示的定制标签。您还可以使用之前显示的加载标签加载 static 和/或自定义 templatetags,就像使用任何其他 Django 模板一样。
- 现在,访问相同的 URL,
http://www.localhost:8000/chapter-7/form-class/,并提交表单。这次,当您访问 Mailtrap 邮箱mailtrap.io/inboxes/时,您应该看到新电子邮件中每个字段的值,如图中所示:

图 7.12 – Mailtrap – 模板上下文
接下来,让我们添加一个新的操作,在触发发送电子邮件操作的同一点创建 PDF 文档。
生成 PDF 报告
Django 依赖于第三方包的支持来生成 PDF 文档。他们自己的文档甚至建议使用 reportlab 包;然而,任何提供 PDF 支持的第三方包都可以使用。当使用除 reportlab 之外的内容时,请参阅该包的文档以获取如何使用该包的说明。reportlab 包甚至为开发者提供了示例 PDF 发票、报告、目录等,以便他们可以快速轻松地开始使用,前提是他们使用的是 reportlab 包的付费 Plus 版本。Plus 版本需要 rlextra 包,该包对公众不可用。要了解更多关于此服务和包可以提供的信息,请访问他们的文档 www.reportlab.com/dev/docs/。
在本节的所有练习中,我们将使用 xhtml2pdf 包,它也是免费的,但使用基于模板的 PDF 时更简单、更容易使用。我们将坚持为每个静态或动态 PDF 的内容创建一个单独的 .html 文件。
将 xhtml2pdf 包添加到您的 requirements.txt 文件中,并将其安装到您的虚拟环境或运行以下命令:
(virtual_env) pip install xhtml2pdf
小贴士
在我合作的所有包中,我发现一些人在渲染复杂的 HTML 表格时存在困难。我建议完全避免使用表格,或者如果您需要将数据结构化为表格,请使用简单的结构以防止在文档创建过程中出现渲染差异和错误。
现在我们已经安装了一个生成 PDF 文档的工具,让我们来练习使用它。
作为基于模板的 PDF
在这里,我们将使用相同的 Django 模板语言来构建我们称之为 PDF 模板 的内容。
按照以下步骤创建您的模板:
-
在
ContactForm类中创建一个名为generate_pdf()的新方法,并包含以下代码:# /becoming_a_django_entdev/chapter_7/forms.py ... from django.conf import settings from django.http import HttpResponse from django.template.loader import get_template from xhtml2pdf import pisa ... class ContactForm(Form): ... def generate_pdf(self, request): dest = open(settings.STATIC_ROOT + '/chapter_7/pdf/test.pdf', 'w+b') template = get_template('chapter_7/pdfs/pdf_template.html') html = template.render() result = pisa.CreatePDF( html, dest = dest, ) return HttpResponse(result.err)
在这里,我们从xhtml2pdf包中导入pisa库,并使用CreatePDF()方法。我们还在使用 Python 的open()方法来指定我们想要创建的文档的目标文件夹和文件名。我们使用与之前相同的STATIC_ROOT变量,这很可能是由于我们的项目堆栈中的whitenoise包。正在创建的文件将位于/becoming_a_django_entdev/staticfiles/chapter_7/pdf/目录中。然后,我们将result变量设置为运行CreatePDF()方法的结果,其中我们传递渲染的 HTML 模板作为该 PDF 的内容。更多详细信息可在以下文档中找到:xhtml2pdf.readthedocs.io/en/latest/format_html.html。
注意
由于我们正在创建此文件,因此无需运行之前必须运行的collectstatic命令。
-
接下来,在
FormClass_View类的post()方法中,我们在这里编写了send_email()操作,让我们在该语句下方添加generate_pdf()操作,如下所示并突出显示:# /becoming_a_django_entdev/chapter_7/views.py from django.template.response import TemplateResponse from django.views.generic.edit import FormView ... class FormClass_View(FormView): ... def post(self, request, *args, **kwargs): form = self.form_class(request.POST) ... form.send_email(request) form.generate_pdf(request) return TemplateResponse( request, self.template_name, context ) -
接下来,在指定为 PDF 正文内容模板的
pdf_template.html文件中,添加以下代码:# /becoming_a_django_entdev/chapter_7/templates/chapter_7/pdfs/pdf_template.html <!DOCTYPE html> <html> <head></head> <body> <div id="header_obj"><h1>Header</h1></div> <div id="footer_obj"> ©Footer - Page <pdf:pagenumber> of <pdf:pagecount> </div> <div class="body-content"> <h2>Hello World</h2> {% lorem 50 p %}<pdf:pdf-next-page />{% lorem 50 p %} </div> </body> </html>
在这里,我们编写标准的 HTML 代码来创建 PDF 的内容。前面的示例在文档的每一页上创建了一个可重复使用的页眉和页脚。我们还使用特殊的供应商特定标签来告诉我们诸如当前页或文档的总页数等信息,例如在文档页脚中使用的<pdf:pagenumber>和<pdf:pagecount>。我们还使用了 Django 提供的{% lorem %}模板标签,该标签生成 50 段拉丁文本,使用我们传递给该函数的50 p值。代表Lorem Ipsum的拉丁文本仅用于说明在没有实际编写该内容的情况下,当有超过一页的内容时会发生什么。
-
size属性用于指定我们想要指定的 HTML 文档大小,即 PDF 文档的物理大小和方向。接下来,使用@page和@frameCSS 对象来格式化你的 PDF 文档:# /becoming_a_django_entdev/chapter_7/templates/chapter_7/pdfs/pdf_template.html ... <head> <style> @page { size: a4 portrait; @frame header_frame { -pdf-frame-content: header_obj; top: 50pt; left: 50pt; width: 512pt; height: 40pt; } @frame content_frame { top: 90pt; left: 50pt; width: 512pt; height: 632pt; } @frame footer_frame { -pdf-frame-content: footer_obj; top: 772pt; left: 50pt; width: 512pt; height: 20pt; } } #header_obj { color: darkblue; text-align: center; } .body-content { color: black; text-align: left; } #footer_obj { color: green; text-align: right; } </style> </head> ...
-pdf-frame-content属性用于将@frame对象映射到具有与指定值匹配的 ID 属性的实际情况下的<div>。这必须是<div>而不是<header>或<footer>HTML 对象,否则你的内容将无法正确渲染。
- 现在,访问相同的 URL,
http://www.localhost:8000/chapter-7/form-class/,并提交表单。这次,您应该在您的/becoming_a_django_entdev/staticfiles/chapter_7/pdf/目录中看到一个名为test.pdf的新文件。当打开该文档时,您应该看到大约八页的随机生成的拉丁文文本,并且在每一页上,您都应该看到相同的页眉和页脚,如图所示:

Figure 7.13 – xhmtl2pdf – 静态 PDF
小贴士
当打开此文档查看其外观时,尤其是在 Windows 上,您必须在再次提交表单之前关闭此文档,这将触发它生成一个新的文档。如果您不这样做,可能会遇到权限错误,表明另一个人或应用程序已经在使用该文件。
让我们接下来为 PDF 模板添加上下文。
添加上下文
让我们将表单字段值的内容传递到那个 PDF 中作为上下文。这种方法不一定要存在于表单类中;同样的,这也适用于send_email()方法。它们可以存在于视图或模型类中,甚至可以作为一个独立的实用方法,可以在任何地方使用。
目前,修改前面的示例,使用以下步骤传递上下文:
-
在
ContactForm类的同一个generate_pdf()方法中,进行以下突出显示的更改:# /becoming_a_django_entdev/chapter_7/forms.py ... from django.conf import settings from django.template.loader import get_template from xhtml2pdf import pisa ... class ContactForm(Form): ... def generate_pdf(self, request): data = self.cleaned_data context = { 'data': data } dest = open(settings.STATIC_ROOT + '/chapter_7/pdf/test_2.pdf', 'w+b') template = get_template( 'chapter_7/pdfs/pdf_template.html' ) html = template.render(context) result = pisa.CreatePDF( html, dest = dest, ) return HttpResponse(result.err) -
接下来,在同一个
/chapter_7/pdfs/pdf_template.html文件中,在现有的两行代码之间添加以下突出显示的代码,如图所示:# /becoming_a_django_entdev/chapter_7/templates/chapter_7/pdfs/pdf_template.html ... <div class="body-content"> <h2>Hello World</h2> <h3>The field contents are listed below</h3> <ul> <li>Full Name: {{ data.full_name }}</li> <li>Email Field Example 1: {{ data.email_1 }}</li> <li>Email Field Example 2: {{ data.email_2 }}</li> <li>Email Field Example 3: {{ data.email_3 }}</li> <li>Conditionally Required Field: {{ data.conditional_required }}</li> <li>Multiple Emails Field: {{ data.multiple_emails }}</li> <li>Message: {{ data.message }}</li> </ul> {% lorem 50 p %}<pdf:pdf-next-page />{% lorem 50 p %} </div> ...
本章的提供模板上下文小节中编写的相同代码被使用。
- 现在,访问相同的 URL,
http://www.localhost:8000/chapter-7/form-class/,并提交表单。您应该在您的本地机器上的/becoming_a_django_entdev/staticfiles/chapter_7/pdf/目录中看到一个名为test_2.pdf的新文件。当您打开该文件时,应该仍然有八页的内容。在第一页上,将有一个包含我们刚刚传递到该 PDF 模板中的表单内容的列表,如图所示:![Figure 7.14 – xhmtl2pdf – 动态 PDF![Figure 7.14 – xhmtl2pdf – 动态 PDF]()
Figure 7.14 – xhmtl2pdf – 动态 PDF
现在我们知道了如何构建 PDF 模板和生成 PDF 文档,我们可以以非常干净和结构化的方式展示数据,这使得它们成为有价值的报告工具。
摘要
在完成本章中找到的练习后获得的技能之后,您现在可以创建和发送各种类型的信息、通知和报告。我们现在知道如何使用 Django 消息框架在每次页面加载或重新加载时提供闪存消息。我们可以创建和发送各种内容类型的电子邮件,甚至可以使用电子邮件测试客户端账户来捕获这些电子邮件,表明它们实际上正在工作。我们甚至安装了一个包并开始构建我们自己的 PDF 报告。
使用这些工具的任何组合来为您的项目增加价值。闪存消息、电子邮件通知和报告生成概念都有助于让用户了解并参与您的应用程序。始终记住,过多的信息可能会让用户感到不知所措,例如有成千上万的电子邮件通知涌入他们的收件箱。明智地使用它们!
Django 消息框架提供了一系列工具,可以为用户创建闪存消息。只需一点创意,Django 消息框架就可以与异步 JavaScript 和 XML(AJAX)一起使用,提供更类似于单页应用程序(SPA)的消息。在下一章,第八章,使用 Django REST 框架中,我们将讨论 Django REST 框架是什么以及如何使用它来处理 AJAX 请求。
第三部分 – 高级 Django 组件
在本部分,您将了解 Django 框架的更多高级组件。虽然 Django REST 框架是一个完全独立的包,但它是一个专门为扩展到 Django 框架而制作的依赖项,使我们能够构建 API。您将学习在 Django 环境中如何使用 REST 框架,以及它是如何与 Django 模板语言结合使用的。本部分还将介绍用于测试 Django 项目中发现的类和方法的包,使开发者能够编写自定义测试脚本。最后,您将了解如何优化数据库查询并在项目中管理数据。
本部分包括以下章节:
-
第八章,使用 Django REST 框架
-
第九章,Django 测试
-
第十章,数据库管理
第八章:第八章:使用 Django REST 框架
本章将专注于使用应用程序编程接口(API)。API 实际上是一套工具和通信协议,旨在允许两个不同的应用程序有效地相互通信;它是作为两个系统之间的中间人。REST API采用表示状态传输(REST)软件架构中提出的设计原则,并且通常与基于 Web 的应用程序一起使用。在本章中,每次提到 API 这个词时,我们实际上是在指 REST API,因为它们在技术上略有不同,但通常被视为同一事物。
Django 本身依赖于第三方包来与现有的 API 交互或自己创建 API。一个常见的 Python 包叫做requests包。requests包用于向服务器端现有的 API 发送和接收请求。有关此包的更多信息,请参阅pypi.org/project/requests/。另一方面,基于 JavaScript 的框架,如 React、AngularJS 或 Vue.js 等,都会在用户的浏览器中客户端执行这些请求。在客户端与服务器端选择操作 API 的工具方面,没有正确或错误的方式。这些决定是根据为您的项目获得的技术要求做出的。我们实际上不会使用requests包或任何客户端 JavaScript 框架;相反,我们将专注于Django REST 框架,它用于为我们之前创建的模型构建基于模型的 API。
Django REST 框架作为开源软件授权,允许开发者在他们的商业或私人应用程序中使用它。它用于根据 Django 项目的模型构建 API,其中端点执行 HTTP 请求,执行requests包,但如果您将此包与框架结合使用,这在项目中有时也会这样做,这也不会有害。本章将完全专注于使用 Django REST 框架为我们创建的车辆模型创建 API,这些模型在第三章“模型、关系和继承”中创建。我们将序列化这些模型,并将 URL 模式(即 API 端点)注册到我们编写的视图和视图集中。我们还将使用路由器为我们生成一些这些 URL 模式,完全基于我们数据库表中的数据。
在本章中,我们将涵盖以下内容:
-
安装和配置 Django REST 框架
-
序列化项目中的相关模型
-
使用 Django REST 框架提供的可浏览 API 工具
-
创建 SPA 风格的页面
-
创建自定义 API 端点
-
使用令牌认证措施执行 API 请求
技术要求
要使用本章中的代码,您需要在本地机器上安装以下工具:
-
Python 版本 3.9 – 作为项目的底层编程语言
-
Django 版本 4.0 – 作为项目的后端框架
-
pip 包管理器 – 用于管理第三方 Python/Django 包
我们将继续使用第二章,项目配置中创建的解决方案。然而,没有必要使用 Visual Studio IDE。主要项目本身可以使用其他 IDE 运行,或者从项目根目录(其中包含manage.py文件)独立运行,使用终端或命令行窗口。无论您使用什么编辑器或 IDE,都需要虚拟环境来与 Django 项目一起工作。有关如何创建项目和虚拟环境的说明可以在第二章,项目配置中找到。您需要一个数据库来存储项目中的数据。在上一章的示例中选择了 PostgreSQL;然而,您可以为项目选择任何数据库类型来使用本章中的示例。
我们还将使用以 Django fixture 形式提供的数据,这些数据包含在第三章,模型、关系和继承,的子标题加载 chapter_3 数据 fixture中。请确保chapter_3 fixture 已加载到您的数据库中。如果已经完成,则可以跳过下一个命令。如果您已经创建了第三章,模型、关系和继承中提到的表,并且尚未加载该 fixture,那么在激活您的虚拟环境后,请运行以下命令:
(virtual_env) PS > python manage.py loaddata chapter_3
本章中创建的所有代码都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Becoming-an-Enterprise-Django-Developer。本章中展示的大部分代码可以在/becoming_a_django_entdev/becoming_a_django_entdev/chapter_8/目录中找到。
观看以下视频以查看代码的实际应用:bit.ly/3Ojocdx。
为本章做准备
首先,按照在第二章中讨论的步骤,在项目中创建一个名为chapter_8的新应用。正如该部分所讨论的,不要忘记将/becoming_a_django_entdev/becoming_a_django_entdev/chapter_8/apps.py文件中您的应用类中的name =变量的值更改为指向您安装应用的位置。务必还将此应用包含在settings.py文件中的INSTALLED_APPS变量中。
在站点的urls.py主文件中,添加以下路径,该路径指向我们将要创建的本章的 URL 模式:
# /becoming_a_django_entdev/urls.py
...
urlpatterns = [
path(
‘’,
include(
‘becoming_a_django_entdev.chapter_8.urls’
)
),
]
安装 Django REST 框架
要在任何 Django 项目中安装 Django REST 框架并启用开始使用它所需的最小设置,请按照以下步骤操作:
-
将
djangorestframework、markdown和django-filter包添加到您的requirements.txt文件中,并使用您的 IDE 或命令行在虚拟环境中安装它们。您也可以运行以下单独的pip命令。使用以下命令中的第一个来激活您的虚拟环境:PS C:\Projects\Packt\Repo\becoming_a_django_entdev> virtual_env/Scripts/activate (virtual_env) PS > pip install djangorestframework (virtual_env) PS > pip install markdown (virtual_env) PS > pip install django-filter
在安装这三个包时,无需运行 Django 迁移命令,因为它们不会创建任何额外的表。
-
接下来,在您的
settings.py文件中,将以下应用添加到INSTALLED_APPS列表中。然后,添加包含在此处显示的DjangoModelPermissionsOrAnonReadOnly权限类的REST_FRAMEWORK字典:# /becoming_a_django_entdev/settings.py INSTALLED_APPS = [ ... ‘rest_framework’, ] REST_FRAMEWORK = { ‘DEFAULT_PERMISSION_CLASSES’: [ ‘rest_framework.permissions. DjangoModelPermissionsOrAnonReadOnly’ ], }
此权限类允许我们检查基于模型的 CRUD 权限,并允许匿名用户仅查看或读取项目。有关其他六个以上权限类的完整说明,请访问www.django-rest-framework.org/api-guide/permissions/。在本章中,我们只将与一个权限类一起工作。
-
您需要注册与此框架的认证机制相关的 URL 模式。在您的
/chapter_8/urls.py文件中,添加以下include模式,以及本章的主页和 Django 管理站点链接,如下所示:# /becoming_a_django_entdev/chapter_8/urls.py from django.contrib import admin from django.urls import include, path from django.views.generic import TemplateView urlpatterns = [ path(‘admin/’, admin.site.urls), path( ‘’, TemplateView.as_view( template_name = ‘chapter_8/index.html’ ) ), path(‘api-auth/’, include(‘rest_framework.urls’)) ] -
我们还需要像在第六章中那样包含管理站点 URL,即探索 Django 管理站点,对于本章也是如此。如果您已经在项目的
urls.py主文件中放置了这个包含语句,那么您就不需要在chapter_8应用中再次包含它。
现在,Django REST 框架已安装并准备好在您的项目中使用。目前,我们除了这个框架附带的身份验证 URL 外没有 API URL。目前,这些身份验证 URL 没有为我们提供任何操作。您可以通过访问 URL http://localhost:8000/api-auth/login/ 来导航到登录页面,以查看它是否正确加载。如果您使用超级用户账户登录,目前将没有任何内容显示,并且会显示一个404 页面未找到的消息。
注意
我们启用chapter_8应用的 Django 管理站点 URL 模式的原因是能够使用超级用户账户登录 Django 管理站点并在处理本章的一些练习时验证用户。对于这些练习,如果您未登录,您将在结果中看到一个消息,指出认证凭据未提供。对于本章末尾的其他练习,您不需要登录 Django 管理站点;将通过基于令牌的授权措施进行认证。
要开始使用此框架并创建新的 API 端点,我们将从为第三章中创建的每个模型创建序列化器类开始,模型、关系和继承。
序列化对象
创建 API 的起点是创建一个序列化器类,然后创建一个视图,特别是ModelViewSet视图类。序列化对象意味着将模型对象转换为 JSON 格式以表示该对象的数据。我们需要做的最后一件事是创建 URL 模式,这些模式映射到我们编写的视图类;这将通过 URL 路由器来完成。这些 URL 模式被认为是您的 API 端点。
在本节中需要注意的一点是,当使用相关字段时,我们需要为所有与其它模型相关的模型创建序列化器。这就是为什么以下练习将展示chapter_3应用中所有四个模型的示例。这必须做到,以确保我们在使用 Browsable API 时不会出错,我们将在本章后面介绍 Browsable API,以及进行 API 请求时不会出错。这意味着如果您有多个被分配了Group或Permission的Seller,那么Group和/或Permission对象也必须进行序列化。记住,当我们将AUTH_USER_MODEL设置更改为现在等于‘chapter_3.Seller’时,Seller对象替换了django.contrib.auth.models库中找到的默认User对象,如第三章中所述,模型、关系和继承。由于chapter_3数据固定中只提供了一个Seller,因此未显示序列化Group或Permission对象的示例:

图 8.1 – chapter_3 数据固定 – 卖家对象
此处的 Seller 没有分配到 Group 或 Permission,因此结果是我们不应该在以下练习中遇到错误。相反,该 Seller 的 is_superuser 字段被设置为 true,这允许我们在登录 Django 管理站点时执行所有 CRUD 操作。
注意
如果你遇到错误,要么删除之前显示的所有除 Seller 数据之外的内容,或者建议只创建 Group 和 Permission 对象的附加序列化器、视图集和路由器。遵循以下示例中使用的相同代码格式。同样适用于 django.contrib.contenttypes.models 库中找到的 ContentType 对象。如果你在该序列化器类的 Meta 子类中定义了 depth 属性,更具体地说,如果 depth 设置为 2 或更大的值,这将需要。我们很快将讨论此属性的作用。
接下来,让我们开始编写我们的序列化器,并了解更多可用的类。
序列化器类
rest_framework.serializers 库为我们提供了五个类,如下所示:
-
Serializer– 用于嵌套ManyToManyField、ForeignKey和OneToOneField关系时使用。 -
ModelSerializer– 用于创建具有直接映射到项目中模型字段的序列化器。 -
HyperlinkedModelSerializer– 用于执行ModelSerializer类所做的所有操作,但它在 Browsable API 中查看时将为每个相关对象生成一个可点击的链接,而不是显示这些对象的数字 ID。 -
ListSerializer– 用于在一个请求中序列化多个对象,并且通常在 Django 中使用,当Serializer、ModelSerializer或HyperlinkedModelSerializer已初始化为具有定义了many=True属性时。 -
BaseSerializer– 提供给开发者创建他们自己的序列化和反序列化风格的能力。这与在 Django 消息框架中使用的BaseStorage类类似,如在第七章“消息、电子邮件通知和 PDF 报告”的子标题[“消息存储后端”]中讨论的那样。
首先,按照以下步骤为在第三章中创建的每个模型创建 ModelSerializer 类,包括 Engine、Vehicle、VehicleModel 和 Seller 模型:
-
在你的
/becoming_a_django_entdev/chapter_8/文件夹中创建一个名为serializers.py的新文件。在此文件中,添加以下导入:# /becoming_a_django_entdev/chapter_8/serializers.py from rest_framework.serializers import ModelSerializer from ..chapter_3.models import ( Seller, Vehicle, Engine, VehicleModel ) -
在此文件中,添加以下所示的
EngineSerializer类:# /becoming_a_django_entdev/chapter_8/serializers.py ... class EngineSerializer(ModelSerializer): class Meta: model = Engine fields = ‘__all__’ -
在此文件中,添加以下所示的
VehicleModelSerializer类:# /becoming_a_django_entdev/chapter_8/serializers.py ... class VehicleModelSerializer(ModelSerializer): class Meta: model = VehicleModel fields = ‘__all__’ -
在此文件中,添加以下所示的
VehicleSerializer类:# /becoming_a_django_entdev/chapter_8/serializers.py ... class VehicleSerializer(ModelSerializer): class Meta: model = Vehicle fields = ‘__all__’ -
在此文件中,添加以下所示的
SellerSerializer类:# /becoming_a_django_entdev/chapter_8/serializers.py ... class SellerSerializer(ModelSerializer): class Meta: model = Seller fields = ‘__all__’
你可能会注意到前面的类与之前章节中练习中使用的某些类相似。在这里,我们使用 ‘__all__’ 值定义了字段,但我们可以提供所需字段的列表以及它们的顺序,就像在 第五章 的表单类中那样,Django 表单。
Meta 子类
Meta 子类提供了额外的选项,类似于我们在为 第三章 的 Models, Relations, and Inheritance 编写的模型中自定义 Meta 子类的方式。有关所有 Meta 类选项的完整说明以及有关序列化器的一般信息,请访问此处。其他选项包括以下内容:
-
model– 用于指定映射到该序列化器的模型类。 -
fields– 用于指定在序列化器中包含哪些字段或所有字段。 -
validators– 用于在创建或更新操作时添加验证。类似于在 第五章 的 Django 表单 中使用表单验证,Django 首先会依赖于在数据库级别设置的任何约束,然后它会检查序列化器级别上应用的验证。有关序列化器验证器的更多信息,请参阅此处。 -
depth– 用于将相关对象表示为嵌套 JSON,而不是使用指定深度的数字 ID。此选项的默认值为0。 -
read_only_fields– 用于指定在序列化器级别上只读的字段。 -
extra_kwargs– 用于在序列化器中的特定字段上指定额外的关键字参数。 -
list_serializer_class– 用于指定使用ListSerializer类创建的自定义ListSerializer。这通常在你需要修改ListSerializer类的行为时进行,例如,在整个集合上执行自定义验证,例如比较嵌套对象的值或执行字段级别的验证。
现在我们有了可以工作的序列化器类,我们需要为它们创建一个视图类。我们可以使用 Django REST 框架提供的 ModelViewSet 类来完成这个任务。
视图集类
与为每个 CRUD 操作创建视图/方法不同,Django REST 框架提供了一个将它们全部组合在一起的类。它首先在 views.py 文件中创建一个视图类,类似于我们在 第四章 的 URLs, Views, and Templates 中所做的那样,除了它们是使用以下视图集类之一构建的。Django REST 框架提供了以下四个视图集类:
-
GenericViewSet– 包含执行某些操作的方法,通常用于创建非基于模型的 API。 -
ModelViewSet– 这个类包括执行 CRUD 操作所需的所有方法,并旨在直接映射到您项目的模型。 -
ReadOnlyModelViewSet– 只提供读取操作,其他所有方法将不会提供。这个视图集也旨在与您项目的模型一起工作。 -
ViewSet– 开发者用来创建类似于BaseSerializer和BaseStorage类使用的自定义视图集。这个类不提供任何操作,这些方法将由开发者创建以便使用此类。
按照以下步骤准备您的视图集类:
-
在您的
/chapter_8/views.py文件中,添加以下导入:# /becoming_a_django_entdev/chapter_8/views.py from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import ModelViewSet from .serializers import ( EngineSerializer, SellerSerializer, VehicleSerializer, VehicleModelSerializer ) from ..chapter_3.models import ( Engine, Seller, Vehicle, VehicleModel ) -
在同一文件中,添加以下
EngineViewSet类,如下所示:# /becoming_a_django_entdev/chapter_8/views.py ... class EngineViewSet(ModelViewSet): queryset = Engine.objects.all().order_by(‘name’) serializer_class = EngineSerializer permission_classes = [IsAuthenticated] -
在同一文件中,添加以下
VehicleModelViewSet类,如下所示:# /becoming_a_django_entdev/chapter_8/views.py ... class VehicleModelViewSet(ModelViewSet): queryset = VehicleModel.objects.all().order_by( ‘name’ ) serializer_class = VehicleModelSerializer permission_classes = [IsAuthenticated] -
在同一文件中,添加以下
VehicleViewSet类,如下所示:# /becoming_a_django_entdev/chapter_8/views.py ... class VehicleViewSet(ModelViewSet): queryset = Vehicle.objects.all().order_by(‘price’) serializer_class = VehicleSerializer permission_classes = [IsAuthenticated] -
在同一文件中,添加以下
SellerViewSet类,如下所示:# /becoming_a_django_entdev/chapter_8/views.py ... class SellerViewSet(ModelViewSet): queryset = Seller.objects.all() serializer_class = SellerSerializer permission_classes = [IsAuthenticated]
在每个类中,我们只为这些类定义了三个属性,即 queryset、serializer_class 和 permission_classes 属性。在前面的示例中,我们只使用了 all() 方法来搜索该表中所有记录。除了使用 all() 方法外,还可以使用 filter() 和 get() 函数来查找特定记录。serializer_class 属性用于将视图映射到我们在前面的子节中构建的序列化器类;它映射到我们正在查询的模型类。permission_classes 属性用于定义该请求的权限。权限与我们在本章末尾将要讨论的认证令牌不同。权限确保访问系统的用户被允许在所讨论的模型上执行那些特定的 CRUD 操作。这些属性只有三个,并且在使用 ModelViewSet 或 GenericViewSet 类时,只有前两个是必需的;最后一个是可以选的。您还可以使用可调用方法自定义这些属性,甚至可以自己覆盖默认的操作方法。要了解更多关于视图集的信息,请访问 www.django-rest-framework.org/api-guide/viewsets/。
接下来,让我们配置这些 URL 路由器以映射到我们刚刚创建的视图集。这些将是您项目的 API 端点。
使用 URL 路由器
URL 路由器被用作一种防止开发者必须为 API 中每个模型相关的 CRUD 操作编写单独 URL 模式的手段。这可能会在一段时间后变得非常复杂,而 Django REST 框架提供这些 URL 路由器作为自动为您生成每个端点的一种方式。
按照以下步骤配置您的路由器:
-
在您的
/chapter_8/urls.py文件中,添加以下import语句:# /becoming_a_django_entdev/chapter_8/urls.py ... from rest_framework import routers from .views import ( EngineViewSet, SellerViewSet, VehicleViewSet, VehicleModelViewSet ) -
在同一文件中,添加以下
router和register语句:# /becoming_a_django_entdev/chapter_8/urls.py ... router = routers.DefaultRouter() router.register(r’engines’, EngineViewSet) router.register(r’sellers’, SellerViewSet) router.register(r’vehicles’, VehicleViewSet) router.register( r’vehicle-models’, VehicleModelViewSet ) -
在同一文件中,包含以下
include路径以供您的路由器使用:# /becoming_a_django_entdev/chapter_8/urls.py ... urlpatterns = [ ... path(‘chapter-8/’, include(router.urls)), path(‘api-auth/’, include(‘rest_framework.urls’)), ]
之前显示的include(router.urls)路径位于api-auth路径和您的管理页面路径之间。对于每个模型,使用 Django REST 框架提供的routers.DefaultRouter()类定义一个路由器变量。每个router.register()函数为该模型创建一组 URL 模式。例如,在这个 URL 中显示的engines路径,即http://localhost:8000/chapter-8/engines/,这就是在第一个注册函数的第一个参数中定义的,即r'engines'。路由器生成了一组 URL 模式,每个模式对应表中的一个对象,这些模式通过使用path('chapter-8/', include(router.urls))路径添加到您的urlpatterns列表中。将chapter-8添加到这个path()函数中,就是告诉 Django 为使用这组路由器创建的每个路径添加前缀http://localhost:8000/chapter-8/。
就这样;现在您已经有一个非常基础的 API 可以与在第三章“模型、关系和继承”中创建的四个模型一起使用了。为了测试您的 API 并查看每个请求发送和接收的数据,我们将使用 Django REST 框架提供的可浏览 API。
使用可浏览 API
在上一节中提到的chapter-8路径是 URL 路由器的路径,我们激活了该路径,称为http://localhost:8000/chapter-8/,以查看这些 URL,如图所示:

图 8.2 – 可浏览 API – API 根
当构建自定义 API 端点时,就像我们在本章后面将要做的那样,您可能不会在 API 根中看到它们。您会看到,组、权限和内容类型的序列化器都包含在这本书的代码中。在每个主要路由路径的右上角有一个下拉菜单,可以在这两种格式之间切换,即 API 和 JSON,如图所示:

图 8.3 – 可浏览 API – GET 格式
如果使用HyperlinkedModelSerializer来构建您的序列化器类,每个对象将显示为可点击的 URL,而不是不可点击的 ID。超链接版本如图所示,当访问路由器为Seller模型在http://localhost:8000/chapter-8/sellers/创建的主要 URL 路径时:

图 8.4 – 可浏览 API – 卖家列表
要查看与前面截图相同的结果,只需将所有序列化器类中的ModelSerializer更改为HyperlinkedModelSerializer。还将您的SellerSerializer类更改为排除以下代码中显示的字段,以防止出现指示配置错误的lookup_field的错误,这是一个超出本书范围的高级主题:
# /becoming_a_django_entdev/chapter_8/serializers.py
...
from rest_framework.serializers import (
HyperlinkedModelSerializer,
ModelSerializer
)
class SellerSerializer(HyperlinkedModelSerializer):
class Meta:
model = Seller
#fields = ‘__all__’
exclude = [‘groups’, ‘user_permissions’]
在前一小节中注册的每个路由器上,主 URL,例如之前显示的http://localhost:8000/chapter-8/sellers/链接,将允许您使用该页面底部的表单执行创建操作(POST请求)。仅查看此页面就执行读取操作(GET请求)。http://localhost:8000/chapter-8/sellers/1/包括一个位于该页面底部的表单,允许您对该视图中的对象执行PUT、PATCH和DELETE操作,如下所示:

图 8.5 – 可浏览的 API – 卖家详情
默认情况下,Django 将显示没有超级用户状态的Seller,然后该用户/卖家必须获得组或个人权限级别访问权限才能对该模型对象执行任何 CRUD 操作。如果用户/卖家只有执行一项操作而没有执行另一项操作的权限,例如更新但不删除或创建但不更新,那么只会出现那些操作按钮。如果您没有看到您预期看到任何操作按钮,请仔细检查该用户的权限设置。
现在我们已经有一个工作的 API 并探索了如何使用可浏览的 API,让我们构建不需要重新加载或重定向页面的内容更改页面。
构建类似 SPA 的页面
单页应用(SPA)页面是内容在容器/节点内更新而不是重新加载或重定向页面以显示数据的网页。通常,服务器的一些工作会卸载到客户端浏览器来执行这些请求和/或渲染 HTML,通常使用 JavaScript 或 jQuery。当触发事件时,例如点击按钮或提交表单,JavaScript 用于从服务器获取数据,然后将该内容渲染到页面上,无论我们希望在何处显示。
在这个练习中,我们将使用由路由器创建的Seller API 端点,在http://localhost:8000/chapter-8/sellers/1/,将 JSON 作为字符串渲染在查询页面体中的容器内。查询页面只是一个标准的页面,它使用 JavaScript 与 API 端点进行通信。
创建视图
在本小节中,我们将构建视图来处理用户可以输入与想要查询的Seller ID 相关的数字的页面。这将是GetSellerView类,它将作为本章剩余两个练习的骨干使用。
要开始,请执行以下步骤:
-
打开
/chapter_8/views.py文件,并添加以下GetSellerView类:# /becoming_a_django_entdev/chapter_8/views.py ... from django.template.response import TemplateResponse from django.views.generic import View ... class GetSellerView(View): template_name = ‘chapter_8/spa_pages/get_seller.html’ def get(self, request, *args, **kwargs): context = {} return TemplateResponse( request, self.template_name, context )
在前面的例子中,我们正在构建一个基于类的视图,就像我们在第四章,URLs, Views, and Templates中所做的那样。我们在这里所做的唯一不同的事情是我们没有包括post()方法;我们只提供了get()方法。因为我们不处理表单提交。操作按钮将通过 JavaScript 作为type=’button’类型的按钮来控制,而不是type=’submit’。完成这些后,就没有必要使用post()方法了。此外,我们正在创建一个标准的 Django 视图类,而不是 REST API 视图类,因为这个页面仅用于与 API 端点通信,而不是作为 API 端点本身。
现在,让我们创建一个模板来格式化要渲染到页面上的 HTML。
构建模板
前一个子节构建了这个练习的视图。本节将创建用于我们练习的模板。
按照以下步骤创建你的模板:
-
在
/chapter_8/templates/chapter_8/spa_pages/文件夹中创建get_seller.html模板文件。 -
在第四章,URLs, Views, and Templates中,我们创建了一个名为
/chapter_4/templates/chapter_4/base/base_template_1.html的文件。我们将重新使用这个文件为本章服务。请将此文件以及任何相关的模板文件复制到你的chapter_8应用中。相关的模板文件包括在base_template_1.html文件中引用的标题、页脚、JavaScript 和 CSS 文件。将它们放在与复制来源相同的子文件夹中的templates和static目录中,然后在文件中任何提及chapter_4和chapter-4的地方将其重命名为chapter_8和chapter-8。你可以始终复制本书代码中找到的chapter_8JavaScript、CSS 和模板文件。 -
接下来,在你在步骤 1中创建的
get_seller.html文件中,添加以下代码所示:# /becoming_a_django_entdev/chapter_8/templates/chapter_ 8/spa_pages/get_seller.html {% extends ‘chapter_8/base/base_template_1.html’ %} {% load static %} ... {% block body_content %} <form> <div class=”field-box input-box”> <label for=”seller-id”>Seller ID:</label> <div class=”form-group”> <input id=”seller-id” type=”text” /> <span class=”help-text”>Please enter the ID of the seller you want to lookup</span> </div> </div> <button type=”button” id=”get-sellers” onclick =”$gotoSPA_Page()”> Get Seller Details</button> </form> <div id=”details”> <p>!!! No Details to Display !!!</p> </div> {% endblock %} -
这个页面非常简单。我们所做的只是创建一个
type=”text”类型的输入字段,然后是一个type=”button”类型的按钮。我们给按钮添加了一个onclick属性,它会触发一个名为$gotoSPA_Page()的 JavaScript 函数。有一个带有id=”details”属性的<div>容器,其中包含一段文本,表明目前没有内容可以显示。
这里的想法是我们将用从 API 请求接收到的内容替换<div id=”details”>容器中的所有内容。在这个特定的设置中不需要<form>容器;它只是为了符合之前章节中编写的相同的 CSS 样式和 HTML 节点结构而添加的。可以偏离这种结构并创建自己的。请使用前面的结构来演示这个练习。
接下来,让我们添加执行 API 请求的 JavaScript。我们将在这个客户端而不是服务器端执行这个操作。
编写 JavaScript
我们不需要很多 JavaScript,只需要一个利用原生 JavaScript fetch()函数的小函数。这几乎与 jQuery 的.ajax()函数相同,但也有一些不同。fetch()函数的不同之处在于它不会发送cross-origin头信息,默认模式设置为no-cors,而.ajax()函数将默认模式设置为same-origin。这可能会根据你的项目需求而变得重要。请求的结果将随后在具有 CSS ID 属性details的容器中显示,更广为人知的是details容器。
如果你从chapter_4复制了你的 JavaScript 文件,那么现在这个文件应该是空的。请按照以下步骤准备你的 JavaScript。如果你从书中代码的chapter_8复制了这个文件,请确保只注释掉以下代码:
-
在你的
/chapter_8/static/chapter_8/js/site-js.js文件中,添加以下代码:# /becoming_a_django_entdev/chapter_8/static/chapter_8/js/site-js.js function $gotoSPA_Page() { const input = document.getElementById( ‘seller-id’ ); const container = document.getElementById( ‘details’ ); const id = input.value; var url = `/chapter-8/sellers/${id}/`; } -
在相同的
$gotoSPA_Page()函数中,在你的常量和变量下面添加fetch()方法,如下所示:# /becoming_a_django_entdev/chapter_8/static/chapter_8/js/site-js.js function $gotoSPA_Page() { ... fetch(url, { method: ‘GET’, headers: { ‘Content-Type’: ‘application/json’, }}).then(response => { return response.json(); }).then(data => { container.innerHTML = JSON.stringify(data); }); }
这是我们在前一小节中配置的$gotoSPA_Page()函数,当Get Seller Details按钮的onclick动作被触发时执行。就这样!这就是我们完成从数据库中检索单个记录的单个任务所需的全部 JavaScript。
在前面的代码中,我们编写了三个常量,一个名为input的用于定位input字段节点,另一个名为container的用于定位details容器节点。第三个,名为id的用于在函数执行时捕获输入字段的value。url变量用于使用该路径转换器的value作为关键字参数构造一个字符串。在 JavaScript 中,这被称为字符串连接,因为我们正在这样做,你需要确保使用反引号字符(`)而不是单引号字符(‘)。它们看起来几乎一模一样;如果你只是匆匆浏览前面的代码,请小心。在这里,我们告诉url变量指向由Seller API 的 router 创建的 URL。
fetch() 函数接受 url 变量作为该函数的第一个位置参数,这是一个必需的参数。然后我们传递额外的可选参数,例如接受这些值的方法(GET、POST、PUT、PATCH 和 DELETE)。目前我们只想展示如何获取数据,因此在这个练习中我们将使用 GET 方法。有时会使用头参数来指定 ‘Content-Type’;在这种情况下,它被设置为 ‘application/json’。之前显示的方法和头参数是使用 fetch() 函数的默认值。对于读取操作来说,它们不是必需的,因为它们是默认值,但提供它们是为了说明目的。
fetch() 函数还使用了之前显示的两个 then() 方法;它们各自返回一个 JSON 格式的响应对象作为承诺。简单来说,承诺是一个包含状态和结果的物体。第二个 then() 方法使用返回的承诺作为 data 变量,然后我们通过编写一个简单的语句将那个 data 放入 details 容器中。我们使用 JSON.stringify() 方法将那个 JSON 对象转换成可读的格式,特别是将一个字符串放入那个容器中。如果不使用 JSON.stringify() 函数,我们只会看到一个对象被打印到屏幕上,用括号括起来,这在我们查看时不会很有意义。我们将在本章标题为 First demo 的子节中看到这个操作的截图。
目前,我们只是在 <div> 容器中打印 JSON 字符串。我们没有为这些节点创建 HTML 元素和/或 CSS 样式。这就是你可能需要编写额外的 JavaScript 来手动完成这些操作,或者使用基于 JavaScript 的框架的强大功能的地方。让我们先完成这个练习,看看它是如何工作的,然后我们将向你展示如何在章节标题为 Writing custom API endpoints 的部分中在服务器端渲染 HTML 和 CSS。
使用 async 和 await 关键字
传统的 JavaScript 是同步和单线程的。它将依次运行一个进程,如果其中一个进程因为例如 API 请求(服务器响应时间过长)而挂起,那么后续的进程也会挂起。问题是当这种情况发生时,页面可能会变得无响应。异步 JavaScript 允许函数在等待服务器响应的同时并行运行。返回承诺的 then() 函数已经是一个异步函数,这也是我们倾向于使用 fetch() 函数的原因。JavaScript 提供了 async 和 await 关键字,这使得使用和操作异步函数变得更容易,尤其是在你的代码开始超出这些基本用法示例时。
采取以下步骤修改你的 JavaScript。
在以下代码块中,将高亮显示的更改应用到前面示例中的$gotoSPA_Page()函数:
# /becoming_a_django_entdev/chapter_8/static/chapter_8/js/site-js.js
function $gotoSPA_Page() {
...
fetch(url, {
method: ‘GET’,
headers: {
‘Content-Type’: ‘application/json’,
}
}).then(async(response) => {
return await response.json();
}).then(async(data) => {
const thisData = await data;
container.innerHTML = JSON.stringify(
thisData
);
});
}
变量和常量仍然需要。它们保持不变,并使用前三个点表示法表示。我们现在几乎可以运行我们的项目并演示这个练习的实际应用。我们只需要将 URL 模式映射到我们创建的视图。
映射 URL 模式
现在,我们将把创建的视图连接到一个 URL 模式,监听/chapter-8/get-seller/路径。
执行以下步骤来配置你的 URL 模式。
在你的/chapter_8/urls.py文件中,将以下路径添加到urlpatterns列表中:
# /becoming_a_django_entdev/chapter_8/urls.py
from .views import ..., GetSellerView
...
urlpatterns = [
...
path(
‘chapter-8/get-seller/’,
GetSellerView.as_view(),
name = ‘get-seller’
),
]
你还需要导入GetSellerView类以映射到前面的模式。
接下来,让我们演示这段代码的实际应用。
第一次演示
要演示构建 SPA-like 页面练习中展示的代码,请按照以下步骤操作:

图 8.6 – 获取卖家页面
-
接下来,在前面截图所示的输入字段中输入数字
1,这与你数据库中第一个Seller的 ID 相关。然后,点击标有获取卖家详情的按钮。 -
要见证正在发生的事情,在任何主要浏览器中,右键单击并选择在运行时发生的
console.log()消息或错误。如果此操作成功,你应该会看到!!! 无详细信息显示 !!!的单词被请求的结果所替换,如图所示:

图 8.7 – 获取卖家结果 – JSON.stringify()
小贴士
你需要始终打开网络标签,以便将数据记录到该标签。打开此标签,然后刷新页面,以在执行这些操作时获得准确的结果。
- 现在,再次查看网络标签,你应该会看到列表中显示了两个/chapter-8/get-seller/请求,如图所示:

图 8.8 – 获取卖家页面 – 网络标签
列表顶部的第一个请求是由浏览器在用户首次加载 http://localhost:8000/chapter-8/get-seller/页面时发起的。第二个site-js.js文件,这是我们编写$gotoSPA_Page()函数的文件。最后一列显示了执行每个请求所需的时间。所有介于这些文件之间的文件都是其他应用在项目中使用的其他资产,例如 CSS 和 JavaScript 文件。
注意
如果你没有看到这些文件,无需担心;这仅仅意味着它们由于某种原因尚未加载。
- 接下来,移除在
$gotoSPA_Page()函数中使用的JSON.stringify()函数,只需使用thisData变量即可。然后,刷新此页面并再次执行查询。我们应该看到单个对象,如下所示:

图 8.9 – 获取卖家结果 – 标准
- 正是这里,我们可以看到为什么我们必须使用
JSON.stringify()函数。没有这个函数,我们可以看到对象被描述为[object Object],这并不很有帮助。
既然我们的 API 客户端已经启动并运行,让我们来探索如何返回渲染的 HTML 而不是返回的 JSON 对象的字符串表示。
编写自定义 API 端点
创建我们自己的 API 端点就像编写另一个 URL 模式一样简单。本节将教会我们如何编写自己的 API 端点并练习发送预格式化的 HTML 回客户端。您不需要创建所有自定义 API 端点来返回预格式化的 HTML,但我们将练习这样做。预格式化 HTML 只有在与您的 API 通信的应用程序在收到 HTML 后不需要以任何方式重新结构或重新样式化 HTML 时才能很好地工作。这意味着服务器/开发者需要确切地知道客户端将如何使用它接收到的数据。除了之前在$gotoSPA_Page()函数中已经写过的 JavaScript 之外,不再需要更多的 JavaScript。我们将重用那个相同的函数,并在继续之前只更改一两个东西。我们将创建一个新的视图类,并添加权限逻辑来保护该端点免受不受欢迎的用户访问 API。
让我们按照之前练习的顺序开始这个练习,从视图开始。
创建视图
按照以下步骤创建您的APIView类:
-
在您的
/chapter_8/views.py文件中,添加以下import语句:# /becoming_a_django_entdev/chapter_8/views.py ... from django.shortcuts import render from ..chapter_3.models import ..., Seller from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView ... -
在同一个文件中,创建
GetSellerHTMLView类和get()方法,如下所示:# /becoming_a_django_entdev/chapter_8/views.py ... class GetSellerHTMLView(APIView): permission_classes = [IsAuthenticated] template_name = ‘chapter_8/details/seller.html’ def get(self, request, format=None, id=0, *args, **kwargs): if request.user.is_authenticated and request.user.has_perm (‘chapter_3.view_seller’): try: seller = Seller.objects.get(id=id) except Seller.DoesNotExist: seller = None else: seller = None context = {‘seller’: seller,} return render( request, self.template_name, context = context )
在这里,新的GetSellerHTMLView类模仿了我们之前练习中创建的GetSellerView类,但现在它使用的是 Django REST 框架提供的APIView类。我们只需要在这个类中指定get()方法即可;因为我们不处理表单对象,所以不需要post()方法。我们目前只创建一个处理GET API 方法的视图,用于查看/读取一个对象。我们将这个视图映射到的模板是/chapter_8/templates/chapter_8/details/seller.html文件,我们将在下一小节中创建它。我们需要将id=0传递给get()方法,正如前面代码中突出显示的那样,以期待我们如何编写这个 API 端点的 URL 模式。由于我们使用的是APIView类,我们必须在get()方法中显式设置id=0。如果你继承的是常规的View、FormView、CreateView、UpdateView或DeleteView类,你只需要写id,而不需要=0部分。同样适用于format=None参数,这个参数只有在与APIView类一起工作时才需要,而不是常规的View类。
这种方法依赖于用户登录到你的系统,通过使用request.user对象访问当前登录的用户。那些没有访问 Django 管理站点的组织外用户,将不得不使用授权令牌来登录,这将在本章后面讨论。尽管我们在第三章中更改了AUTH_USER_MODEL设置,使用Seller模型而不是 Django 的User模型,但我们仍然可以通过使用request.user在request对象中访问当前用户。你不需要使用request.seller;实际上,这将导致错误。当使用该用户的is_authenticated属性时,我们可以确定用户是否实际上已通过活动会话登录。
has_perm()方法用于检查该用户的权限。在这种情况下,我们使用‘chapter_3.view_seller’检查用户是否有对Seller模型对象的读取/查看权限。如果用户已认证并且具有正确的权限,我们将执行查询以查找由提供的 ID 指定的Seller对象。如果用户未认证,我们将设置seller变量为None,我们将在模板文件中使用它来比较它是否有值。
然后,seller 变量被传递到正在使用的模板的 context 中,这样我们就可以访问其数据。此外,我们需要将查询语句包裹在 try/except 块中,这是防止当用户搜索不存在的 Seller 时出现运行时错误所必需的。使用 try/except 块,我们可以将 seller 的值设置为 None,允许程序在没有错误的情况下继续运行。在模板中使用时,它将指示搜索没有返回任何内容。
我们正在使用 django.shortcuts 库提供的 render() 方法,而不是我们迄今为止一直在使用的 TemplateResponse 类。这是因为我们只想返回一小段 HTML,而不是整个 HTML 页面,页面可能包含所有可能的装饰。
现在我们已经创建了视图,接下来让我们构建使用该 seller 对象作为上下文的模板。
构建模板
按照以下步骤准备您的模板:
-
在
/chapter_8/templates/chapter_8/details/目录下创建一个名为seller.html的文件。 -
在该文件中,添加以下代码:
# /becoming_a_django_entdev/chapter_8/templates/chapter_8/details/seller.html {% load static %} <h1>Seller Details</h1> {% if seller %} <h2>{{ seller.first_name|safe }} {{ seller.last_name|safe }}</h2> <h3>{{ seller.name|safe }}</h3> {% if seller.vehicles %} <ul> {% for vehicle in seller.vehicles.all %} <li>{{ vehicle.fullname }}</li> {% endfor %} </ul> {% endif %} {% else %} <p> <b>No Seller to Display</b><br /> <em>or you <b>DO NOT</b> have permission</em> </p> {% endif %}
注意,我们在这个文件中并没有扩展任何其他模板。我们只是将简单的文本对象作为页面组件的一部分来显示,而不是整个页面。一个条件语句比较 seller 对象是否有值,使用 {% if seller %} 语句。如果没有 seller 对象,将渲染显示消息 seller does exist,然后另一个条件语句比较 seller 是否有任何 vehicles,使用 {% if seller.vehicles %} 语句。如果 vehicles 存在,我们将使用 {% for vehicle in seller.vehicles.all %} 语句遍历所有的车辆对象。非常重要的一点是,您需要在语句末尾添加 .all;否则,您将收到错误。这是在 Django 模板语言中访问单个对象中找到的任何嵌套对象列表的方法。我们使用在 第三章 中创建的 fullname 属性方法,模型、关系和继承,来打印车辆的全名作为一个 <li> HTML 节点对象。
现在我们有了模板,接下来让我们修改之前练习中创建的 $gotoSPA_Page() 函数。
修改 JavaScript
将现有 $gotoSPA_Page() 函数中的 url 变量更改为指向新的端点,我们将在下一小节中编写,即 `/chapter-8/seller/${id}/`,与之前练习中使用的复数 sellers 相比,这里是一个单数的 seller。
执行以下步骤来修改您的 JavaScript 函数。
在 $gotoSPA_Page() 函数中,进行以下突出显示的更改:
# /becoming_a_django_entdev/chapter_8/static/chapter_8/js/site-js.js
function $gotoSPA_Page() {
...
var url = `/chapter-8/seller/${id}/`;
fetch(url, {
method: ‘GET’,
headers: {
‘Content-Type’: ‘application/json’,
}}).then(async(response) => {
return await response.text();
}).then(async(data) => {
container.innerHTML = await data;
});
}
在前面的代码中,我们仍然使用了async和await关键字,但你不需要这样做。$gotoSPA_Page()函数的前三个常量container、input和id保持不变,并由前面的省略号表示。
就这样;现在我们只需要创建一个 URL 模式,它将作为 API 端点被使用。
映射 URL 模式
执行以下步骤来映射你的 URL 模式。
在你的/chapter_8/urls.py文件中,添加以下突出显示的模式,保留我们之前编写的get-seller路径:
# /becoming_a_django_entdev/chapter_8/urls.py
from .views import (
...,
GetSellerView,
GetSellerHTMLView
)
...
urlpatterns = [
...
path(
‘chapter-8/get-seller/’,
GetSellerView.as_view(),
name = ‘get-seller’
),
path(
‘chapter-8/seller/<int:id>/’,
GetSellerHTMLView.as_view(),
name = ‘seller-detail’
),
]
我们仍然需要第一个 URL 模式,因为它是触发 API 请求的页面,包含获取卖家详情按钮。
就这样。现在,让我们看看这个动作的实际效果。
第二个演示
为了演示这一动作,请按照以下步骤操作:
-
确保你目前登录到
http://localhost:8000/admin/的 Django 管理站点,使用你的超级用户账户。 -
然后,导航到获取卖家页面
http://localhost:8000/chapter-8/get-seller/,它应该与之前的图 8.6中的页面相同。 -
在此页面的输入字段中输入数字
1,然后点击此页面上的details容器,如图所示:

图 8.10 – 获取卖家页面 – 定制 API 端点
如果你打开了浏览器工具的网络标签页,你也会看到这个操作是在不重新加载或重定向你的页面的情况下完成的。你可以根据需要对其进行样式设置。在这个例子中,我们只是使用了简单的 HTML 节点,并进行了最小化的样式和格式化来演示这个练习。
-
接下来,使用位于
http://localhost:8000/admin/chapter_3/seller/的 Django 管理站点添加或创建一个新的超级用户账户。这也可以像我们在第二章的项目配置中做的那样,通过命令行完成。 -
在不同的浏览器或隐身窗口中,使用你刚刚创建的新超级用户登录。
-
接下来,导航到原始超级用户的编辑页面,位于
http://localhost:8000/admin/chapter_3/seller/1/change/。这是用户名为admin且 ID 为1的用户。 -
在此页面的权限部分,取消选中超级用户状态复选框,限制该用户执行任何操作。保留激活和员工状态复选框启用。确保选择用户权限框中没有选择,如图所示:

图 8.11 – 编辑超级用户权限
-
在你的第一个浏览器中,你已以
Seller身份登录,用户名为admin,请在不同的标签页中导航到http://localhost:8000/chapter-8/get-seller/,或者如果它仍然打开,请刷新现有标签页。 -
在输入字段中输入数字
1,然后再次点击 获取卖家详情 按钮。它应该显示你没有权限,如以下截图所示:

图 8.12 – 获取卖家页面 – 权限受限
-
这是因为我们移除了该用户的原始权限。前面的消息是我们写在
/chapter_8/details/seller.html模板中的 HTML,具体是在检查seller对象是否有值的条件。 -
为了确保
seller对象没有值是由于权限问题而不是由于不存在的查询,你可以在你的代码中写入print()语句来为你提供该指示器。回到另一个打开在http://localhost:8000/admin/chapter_3/seller/1/change/的浏览器窗口,你用为这次练习创建的新超级用户登录,并再次编辑admin用户。 -
按照以下截图所示,给那个用户分配 第三章 | 卖家 | 可以查看卖家 权限:

图 8.13 – 编辑超级用户权限 – 查看 Seller
通过这样做,我们给这个用户分配了我们正在 GetSellerHTMLView 类中检查的确切权限。记住在继续之前点击此页面的底部 保存 按钮。
- 在你的第一个浏览器中,在 http://localhost:8000/chapter-8/get-seller/ 的获取卖家页面上,确保你仍然使用原始的
admin用户登录,并再次点击 获取卖家详情 按钮。在这里,我们将看到与之前在 图 8.9 中看到相同的成果。
这个练习演示了如何使用 Django 模板语言来预格式化在 API GET 请求中返回的 HTML。正如我们在做这个练习时所发现的,我们实际上需要在执行此操作之前登录到该网站的 Django 管理站点。如果没有登录,这种方法将不起作用。这是通过在编辑用户时 权限 部分下的 员工状态 复选框控制的,它授予用户访问 Django 管理站点的权限。如果 员工状态 复选框未被勾选,则用户无法访问您的系统,因此将无法使用权限系统中的任何权限。
注意
将原始超级用户(用户名为 admin)的设置切换回原始状态,启用 员工状态 和 超级用户状态 复选框,并移除所有个人权限和组权限。确保这样做,并且在使用此用户登录之前完成此操作。
如果您需要构建一个不授予用户访问 Django 管理网站的 API,那么将需要认证令牌。在下一个练习中,我们将结合使用认证令牌和 Django REST 框架来完成这个任务。
使用令牌进行认证
在这个练习中,我们将把本章之前构建的 API 视为第三方提供的 API。假设您没有构建您的 API,我们将通过使用安全令牌来练习认证。除了我们在上一个练习中使用的单个模型权限之外,我们还将使用令牌安全。这将无论您是否授予用户访问 Django 管理网站都将执行。这也意味着我们将为这个练习创建一个新的用户/卖家,然后为了演示目的限制该用户对 Django 管理网站的访问。
我们将遵循与之前两个练习相同的步骤。
项目配置
在我们可以开始使用之前的步骤之前,这个练习需要在我们项目的 settings.py 文件中进行一些配置。
按照以下步骤配置您的项目:
-
在您的
settings.py文件中,将以下应用程序添加到您的INSTALLED_APPS列表中,以及REST_FRAMEWORK设置中突出显示的添加,如下所示:# /becoming_a_django_entdev/settings.py INSTALLED_APPS = [ ... ‘rest_framework’, ‘rest_framework.authtoken’, ] REST_FRAMEWORK = { ‘DEFAULT_AUTHENTICATION_CLASSES’: ( ‘rest_framework.authentication.TokenAuthentication’, ‘rest_framework.authentication.SessionAuthentication’, ), ‘DEFAULT_PERMISSION_CLASSES’: [ ‘rest_framework.permissions. DjangoModelPermissionsOrAnonReadOnly’ ], }
rest_framework.authtoken 应用程序已经安装在了您的虚拟环境中,因此您不需要安装任何额外的 pip 包。它在安装 djangorestframework 包时是标准配置的一部分,但仅使用该框架所需的基本设置并不会在您的项目中启用。如果我们实际上打算使用它,我们必须将之前显示的两个认证类添加到 REST_FRAMEWORK 设置中,告诉 Django REST 框架使用所有 APIView 类进行令牌认证。这意味着我们将需要使用该 APIView 类创建的任何自定义端点以及本章之前使用 router 方法创建的所有端点。
使用 router 方法创建的端点都是使用 APIView 类构建的。添加 SessionAuthentication 类意味着我们将启用用户登录 Django 管理网站以使用 Browsable API 测试该端点的功能。如果没有它,您将看到一条消息表明您尚未认证。我们还将保留之前显示的 DjangoModelPermissionsOrAnonReadOnly 权限类,以继续检查模型级别的权限。
请确保您遵循正确的 Python 缩进。在之前显示的代码中,没有足够的空间来正确显示这一点。
-
现在我们已经为这个项目添加了新的包到
settings.py文件中,我们需要运行以下迁移命令:(virtual_env) PS > python3 manage.py migrate -
接下来,确保你已使用
admin用户登录 Django 管理站点,并导航到http://localhost:8000/admin/chapter_3/seller/add/,以创建一个名为test的新用户/卖家。你可能已经有一个来自前几章的测试用户。如果是这样,只需删除该用户并重新创建它以供此练习使用。这次,不要勾选 Staff status 和 Superuser status 复选框。只给这个新用户一个权限,即之前使用的 chapter_3 | Seller | Can view Seller 权限。 -
接下来,导航到 URL
http://localhost:8000/admin/authtoken/tokenproxy/并为刚刚创建的用户添加一个新的令牌。这也可以在命令行窗口或终端中通过执行以下命令来完成:(virtual_env) PS > python manage.py drf_create_token test -
复制为该用户创建的令牌密钥,并将其保存以备后用,可以使用记事本或其他类似工具。
接下来,我们将按照与上次两个练习相同的顺序进行。
创建视图
现在,我们需要为这个练习创建一个新的视图类。它将被用于在将 API 视为他人为我们构建之前,我们添加到 API 中的一个新端点。此端点将仅返回标准 JSON 数据,而不会返回我们在上一个练习中练习过的预格式化 HTML。JSON 是 API 请求中传统上返回的内容。
按照以下步骤准备你的视图类:
-
在你的
/chapter_8/views.py文件中,添加以下高亮的import语句和GetSellerWithTokenView类:# /becoming_a_django_entdev/chapter_8/views.py ... from django.http import JsonResponse from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView from .serializers import SellerSerializer from ..chapter_3.models import ..., Seller ... class GetSellerWithTokenView(APIView): permission_classes = [IsAuthenticated] -
在同一个
GetSellerWithTokenView类中,添加以下get()方法和条件语句:# /becoming_a_django_entdev/chapter_8/views.py ... class GetSellerWithTokenView(APIView): ... def get(self, request, format=None, id=0, *args, **kwargs): seller = None req_user = request._user if req_user.has_perm(‘chapter_3.view_seller’): perm_granted = True try: seller = Seller.objects.get(id=id) except Seller.DoesNotExist: pass else: perm_granted = False -
在同一个
get()方法中,在刚刚添加到该方法的以下context、serializer、new_context和return语句下方添加以下内容:JsonResponse() object to return data as formatted JSON in your endpoint in this way, your endpoint will not be readily available in the Browsable API tool. If you wish for it to be accessible via that tool, use Response() instead. Keep in mind that it may alter the way developers work with the returned data.
在前面的类中,我们遵循了与上一个练习中编写的 GetSellerHTMLView 类相同的逻辑格式。我们添加了一个名为 permission_classes 的属性,它使用 IsAuthenticated 类。这是与令牌身份验证一起工作所需的。我们在 get() 方法中添加了一个额外的查询。这里的逻辑是我们使用在请求发送时添加到请求头部的两个项目,使用 fetch() JavaScript 函数。这两个头是 HTTP_AUTHORIZATION 和 HTTP_USER,我们很快将添加到我们的 JavaScript 函数中。
request._user项用于查找与该request关联的用户,无论该用户是否登录到 Django 管理站点,还是通过HTTP_USER头传递给请求,即为本练习创建的test用户,我们将将其与 API 请求关联。我们查找该用户以使用与之前练习中相同的has_perm()方法比较单个模型权限。如果找到 API 请求用户,则我们执行与之前相同的逻辑来检查该用户是否有权查看seller对象。这次,我们从那个条件语句中移除了is_authenticated属性,因为我们现在依赖于这个类的令牌认证。如果你授予了test用户查看seller对象的能力,逻辑将继续查找在输入字段中提供的 ID 的seller,与之前相同。如果你的test用户没有被授予查看seller对象的能力,那么perm_granted上下文项将返回False,以在返回给我们的数据中提供指示。
上下文被拆分为两个不同的项目,如步骤 3所示,因为在使用SellerSerializer时需要在该上下文中请求。然后,我们从最终返回的JsonResponse()中移除该请求。
构建模板
这个练习不需要一个新的模板。它将只返回 JSON,并且不遵循预格式化的 HTML 示例。
修改 JavaScript
我们将使用在编写自定义 API 端点部分下的修改 JavaScript子部分中提供的相同 JavaScript 示例。
执行以下步骤来修改你的 JavaScript 以完成这个练习。
在同一个 JavaScript 文件中,对现有的$gotoSPA_Page()函数进行以下突出显示的更改:
# /becoming_a_django_entdev/chapter_8/static/chapter_8/js/site-js.js
function $gotoSPA_Page() {
...
var url = `/chapter-8/sellertoken/${id}/`;
fetch(url, {
method: ‘GET’,
headers: {
‘Content-Type’: ‘application/json’,
‘Authorization’: ‘Token your_token’,
‘User’: ‘test’
}}).then(async(response) => {
return await response.text();
}).then(async(data) => {
container.innerHTML = await data;
});
}
在这个例子中,我们保留了前三个常量container、input和id,它们与之前示例中的写法相同,并由前面的三个点符号表示。我们将url变量更改为指向我们将要创建的新路径,即`/chapter-8/sellertoken/${id}/`。fetch()函数的其余部分与之前相同,我们返回的结果是预格式化的 HTML 而不是 JSON。唯一不同的是,我们向这个请求的headers中添加了‘Authorization’和‘User’项。‘Authorization’项的值是创建的令牌的值,即你之前被要求复制的那个;将此粘贴到之前显示的your_token的位置。‘User’项的值是新用户/卖家的用户名,即分配给你要提供的令牌的用户。
注意
标记永远不应该保存在 JavaScript 文件中,就像前面示例中所做的那样。为什么在前面示例中这样做的原因在练习末尾的第三演示子部分中提供了解释。
映射 URL 模式
我们几乎完成了!我们只需要将我们正在通信的端点映射到我们新的视图类。
按照以下步骤映射你的 URL 模式。
在你的 /chapter_8/urls.py 文件中,添加以下路径。你可以保留已经创建的其他路径,如图所示:
# /becoming_a_django_entdev/chapter_8/urls.py
from .views import ..., GetSellerView, GetSellerHTMLView,
GetSellerWithTokenView
...
urlpatterns = [
...
path(
‘chapter-8/get-seller/’,
GetSellerView.as_view(),
name = ‘get-seller’
),
path(
‘chapter-8/seller/<int:id>/’,
GetSellerHTMLView.as_view(),
name = ‘seller-detail’
),
path(
‘chapter-8/sellertoken/<int:id>/’,
GetSellerWithTokenView.as_view(),
name = ‘seller-token-detail’
),
]
就这些了;让我们接下来演示这段代码的实际应用。
第三次演示
按照以下步骤查看实际效果:
-
打开一个新的无痕窗口,并导航到
http://localhost:8000/chapter-8/get-seller/。我要求你打开无痕窗口是为了确保你在这次测试运行中未以任何用户登录到 Django 管理站点。你也可以导航到http://localhost:8000/admin/来双重检查,确保你没有登录。 -
接下来,在输入字段中输入数字
1,并点击 JSON 格式的seller数据,包括我们传递的额外perm_granted上下文,如下截图所示:

图 8.14 – 获取卖家页面 – 带令牌认证的自定义 API 端点
你也可以在你的代码中添加 print() 语句来验证每个条件是否实际满足。本书代码中已包含额外的 print() 语句和提供详细信息的注释。
注意
如果你正在你的序列化器中继承 HyperlinkedModelSerializer 类,你将在前一个示例中看到超链接车辆。如果你仍然使用 ModelSerializer 类,则只会显示数字 ID。
- 如果你没有成功或者你在 JavaScript 文件中输入了错误的令牌,那么你将看到一个
fetch()函数,再次将1输入到输入字段中,并点击 获取卖家详情 按钮。无效令牌消息应该看起来像以下截图所示:

图 8.15 – 获取卖家页面 – 无效令牌
重要提示
在现实世界的例子中,如果使用requests包,则不应将令牌直接保存在 JavaScript 文件或 Python 文件中。相反,您应考虑创建一个额外的 API 端点,该端点使用内置的令牌生成器obtain_auth_token,如本节所述:www.django-rest-framework.org/api-guide/authentication/#generating-tokens。令牌生成器通过接受附加在第一个 API 请求头部的用户名和密码来工作,然后返回一个新创建的令牌。然后,使用从第一个请求中接收到的令牌附加到第二个请求的头部来执行所需的操作。Django REST 框架将执行剩余的工作,使用提供的凭据来验证该请求。本练习中提供的示例仅用于演示在收到令牌后如何执行请求。生成令牌的方法需要使用和了解信号,这超出了本书的范围。
如果使用前述信息框中提到的双请求方法,现在您可以允许第三方应用的开发者与您的 API 通信,而无需创建用户账户。然而,您仍然可以在系统中为该第三方用户创建一个用户,以便继续使用本练习中一直使用的细粒度权限级别。您采取的路径取决于您项目的需求。
摘要
本章中提供的示例展示了构建和使用您新创建的 API 的简单方法,以多种方式!如果您想给您的应用带来类似 SPA(单页应用)的感觉,最简单的实现方式是使用纯 JavaScript 的fetch()函数或 jQuery 的ajax()函数。您不必使用这两个函数中的任何一个来编写自己的操作,而可以考虑使用基于 JavaScript 的框架,例如 React、AngularJS 或 Vue.js,仅举几个例子。基于 JavaScript 的框架可以在客户端格式化和样式化您的 HTML。本章提供的基于模板的方法之一还展示了如何将这项工作从客户端转移到服务器端。这为您提供了在构建和使用 API 方面的众多工具。
我们还学习了如何处理认证令牌,并发现我们可以在服务器端格式化 HTML 时仍然使用令牌。然而,在实站上完全实现这种方法之前,需要了解 Django 的更多高级主题和安全措施。Django REST 框架旨在成为 API 的后端,并设计为与团队确定的任何前端一起工作。
在下一章中,我们将探讨如何测试我们的项目并确保所编写的代码确实能够工作。为了做到这一点,我们将学习如何编写自动化测试脚本,然后安装一个新的包,该包提供了更多的工具,以及学习如何使用这些工具。
第九章:第九章:Django 测试
本章致力于测试和调试 Django 项目。Django 在其框架中内置了广泛的测试类,用于编写自动化测试脚本。随着我们构建每个应用程序和/或项目的每个组件,我们可以在任何时候运行一个命令来确保每个组件仍然按预期工作。这对于 回归测试 非常有用,这意味着测试新的或更改的组件,确保它不会影响现有组件或整个系统的预期行为。在本章中我们将涵盖的大部分内容中,我们不需要安装任何第三方包。我们将最后涵盖的是 Django 调试工具栏(DjDT),它确实需要我们安装一个第三方包才能使用。
在本章中,我们将涵盖以下主题:
-
编写自动化测试脚本
-
创建单元测试用例
-
测试视图类及其 get 和 post 方法
-
测试需要用户认证的视图类
-
测试 Django REST API 端点
-
安装 DjDT,一个用于调试的工具
技术要求
要使用本章中的代码,你需要在本地机器上安装以下工具:
-
Python 版本 3.9 – 作为项目的底层编程语言
-
Django 版本 4.0 – 作为项目的后端框架
-
pip 包管理器 – 用于管理第三方 Python/Django 包
我们将继续使用在 第二章 中创建的解决方案,项目配置。然而,使用 Visual Studio IDE 并非必需。主要项目本身可以使用其他 IDE 运行,或者从项目根目录(其中包含 manage.py 文件)独立使用终端或命令行窗口运行。无论你使用什么编辑器或 IDE,都需要一个虚拟环境来与 Django 项目一起工作。如何创建项目和虚拟环境的说明可以在 第二章 的 项目配置 中找到。你需要一个数据库来存储项目中的数据。前几章的示例选择了 PostgreSQL;然而,你可以为你的项目选择任何数据库类型来与本章的示例一起工作。
我们还将使用在 第三章 的 模型、关系和继承 部分提供的 Django 固件数据,标题为 加载 chapter_3 数据固件。请确保 chapter_3 固件已加载到你的数据库中。如果这已经完成,则可以跳过下一个命令。如果你已经创建了在 第三章 的 模型、关系和继承 中找到的表,并且尚未加载该固件,那么在激活你的虚拟环境后,运行以下命令:
(virtual_env) PS > python manage.py loaddata chapter_3
本章创建的所有代码都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Becoming-an-Enterprise-Django-Developer。本章中展示的大部分代码可以在/becoming_a_django_entdev/becoming_a_django_entdev/chapter_9/目录中找到。
查看以下视频,了解代码的实际应用:bit.ly/3yh0tW6。
准备本章内容
首先,按照第二章,项目配置中标题为创建 Django 应用的小节中讨论的步骤,在你的项目中创建一个名为chapter_9的新应用。正如该节所述,不要忘记将/becoming_a_django_entdev/becoming_a_django_entdev/chapter_9/apps.py文件中你的应用类的name =变量的值更改为指向你安装应用的位置。务必还将此应用包含在settings.py文件中的INSTALLED_APPS变量中。
在网站的主要urls.py文件中,添加以下两个路径:
# /becoming_a_django_entdev/urls.py
...
urlpatterns = [
path(
'',
include(
'becoming_a_django_entdev.chapter_9.urls'
)
),
path(
'',
include(
'becoming_a_django_entdev.chapter_8.urls'
)
),
]
这些指向我们将为本章创建的第九章 URL 模式,并包括我们为第八章,使用 Django REST 框架创建的所有 URL 模式。我们将需要上一章中创建的 API 端点来进行一些 REST API 测试练习。确保将第九章的 URL 放在列表的第一位,以便它们优先于其他 URL。
在你的/chapter_9/urls.py文件中,你应该添加以下路径。这些路径在第四章的练习中使用了,第四章,URLs, Views, and Templates:
# /becoming_a_django_entdev/chapter_9/urls.py
from django.urls import path, register_converter
from django.views.generic import TemplateView
from ..chapter_4.converters import YearConverter
from ..chapter_4.views import (
practice_year_view,
VehicleView
)
register_converter(YearConverter, 'year')
urlpatterns = [
path(
'',
TemplateView.as_view(
template_name = 'chapter_9/index.html'
)
),
path(
'my_year_path/<year:year>/',
practice_year_view,
name = 'year_url'
),
path(
'vehicle/<int:id>/',
VehicleView.as_view(),
name = 'vehicle-detail'
),
]
我们没有包括来自第四章,URLs, Views, and Templates的所有 URL,而是只提供所需的那些。这样做的原因是,在第四章中,我们讨论了为了学习目的而编写相同路径的几种变体。为了避免混淆,仅包括满足即将进行的测试类型所需的 URL 模式变体,这些变体包含在上面的代码中。
将位于/chapter_9/templates/chapter_9目录中的index.html文件从本书的代码中复制到你的项目中相同的目录下。同时,将本书代码中的chapter_9 CSS 和 JavaScript 文件复制到你的项目中。
注意
如果你克隆了本书提供的整个代码库,并且 DjDT 已经开启/启用,请在运行我们即将创建的任何测试用例之前将其禁用。在整个设置和 URL 文件中查找 Turn Off/Comment Out For the First Half of Chapter 9 的注释。该工具将在处理一些测试用例之后讨论。
接下来,让我们讨论一下在 Django 中自动化测试是什么以及它是如何被使用的。
理解 Django 中的自动化测试
自动化测试有很多好处。开发者在重构需要修改的旧组件时使用它。测试脚本用于回归测试旧组件,以查看它们是否受到任何新添加内容的负面影响。Django 提供了几个测试类,这些类是标准 Python 库 unittest 的扩展。您可以在以下链接中了解更多关于此包的信息:docs.python.org/3/library/unittest.html。Django 测试类都位于 django.test 库中。最常用的类是 TestCase。
以下列表描述了 django.test 库中可用的所有测试类:
-
unittest库。这个类不会与数据库交互。 -
SimpleTestCase类并允许进行数据库事务。 -
TransactionTestCase类并包含允许与数据库更好交互的功能。这是最常用的测试类。 -
TransactionTestCase类允许使用除 Django 提供的测试客户端以外的测试客户端,例如 Appium、Cypress、Selenium、Serenity 或其他数十种可用的客户端。它实际上会在后台启动一个 live Django 服务器来运行测试,并在测试完成后销毁该服务器。 -
LiveServerTestCase类。这个类专门为使用 Selenium 测试框架作为测试客户端而构建,因为它的流行。
编写测试类就像编写任何其他 Python 类一样。它们必须至少包含一个测试方法,并且通常包含一个 setUp() 方法,但这个方法不是必需的。测试方法的名字前缀为 test_,例如 test_one() 和 test_two()。setUp() 方法用于为该类中的任何测试方法准备环境或数据库。如果一个类有多个测试方法,一个测试方法中创建的对象将无法在另一个测试方法中使用。如果你需要在类的两个测试方法中都需要一个对象,你需要将这个逻辑放在 setUp() 方法中。
测试类还可以有一个tearDown()方法,该方法将在执行测试后并在进行下一个测试之前执行任何必要的清理任务。由于 Django 会在测试完成后自动销毁测试期间创建的任何服务器和数据库,所以tearDown()方法并不常用。还有其他方法可用,你可以在docs.djangoproject.com/en/4.0/topics/testing/tools/了解更多关于它们的信息。
小贴士
Selenium 是一个第三方工具库,它模拟实际浏览器,允许你在许多不同的浏览器类型和版本上运行自动化测试。执行基本/标准测试用例并不需要 Selenium,这被认为是本书范围之外的进阶主题。要了解更多关于 Selenium 的信息,请访问www.selenium.dev/、pypi.org/project/selenium/和django-selenium.readthedocs.io/en/latest/。
每次在 Django 中创建新应用时,例如在本章之前创建的所有章节应用,你可能已经注意到在那个应用目录中自动为你创建了一个tests.py文件。无论你是使用 IDE 还是命令行创建新应用,这个文件都会被创建。我们之前一直忽略这个文件,因为它对我们没有任何作用,直到现在。本章的代码几乎全部位于tests.py文件中。如果你使用的是 Visual Studio IDE,你可能也注意到它已经在你的tests.py文件中创建了一个SimpleTest(TestCase)类。通过命令行窗口或终端创建的应用不会为你创建这个类。在继续之前,请先注释掉或删除它,这样我们只看到与当前测试相关的结果。
现在我们已经更好地理解了测试是如何执行的,让我们深入探讨并开始测试。
开始单元测试
Visual Studio IDE 为我们创建的SimpleTest类实际上是在测试的。这些可以是实用方法、条件或比较语句、Django 模型、表单、电子邮件消息等等。
让我们练习编写一个简单的测试脚本,然后编写另一个包含我们模型的脚本。
基本单元测试脚本
在这个练习中,我们将编写一个非常基础的测试类,该类执行两种不同的测试方法。这些测试不会与数据库交互,仅用于比较True和False语句。整个类可以作为创建新测试类时的模板,并根据需要修改。
按照以下步骤操作:
-
在你的
/chapter_9/tests.py文件中,添加类的结构,如下所示:# /becoming_a_django_entdev/chapter_9/tests.py from django.test import SimpleTestCase class TestingCalibrator(SimpleTestCase): def setUp(self): pass def tearDown(self): pass def test_pass(self): '''Checks if True == True, Value set to True''' self.assertTrue(True) def test_fail(self): '''Checks if False == False, Value set to True''' self.assertFalse(True)
test_pass(self) 方法用于比较当将 True 传递给函数时,True 是否实际上等于 True;它旨在进行一次成功的测试。test_fail(self) 方法用于比较当将 True 传递给函数时,False 是否等于 False;它旨在产生一个失败。
-
现在,在你的命令行窗口或终端中,导航到你的项目根目录并激活你的虚拟环境,但此时不要运行项目。相反,执行以下代码中的 Django 测试命令,这将仅执行
chapter_9应用中找到的测试:(virtual_env) PS > python manage.py test becoming_a_django_entdev.chapter_9
如果在这个练习中一切按预期进行,它应该在命令行窗口中告诉你进行了两次测试,以及哪个失败了,如下所示:
Found 2 test(s).
System check identified no issues (0 silenced).
F.
======================================================
FAIL: test_fail (becoming_a_django_entdev.chapter_9.tests.TestingCalibrator)
Checks if False == False, Value set to True
------------------------------------------------------
Traceback (most recent call last):
File "C:\Projects\Packt\Repo\becoming_a_django_entdev\becoming_a_django_entdev\chapter_9\tests.py", line 49, in test_fail
self.assertFalse(True)
AssertionError: True is not false
------------------------------------------------------
Ran 2 tests in 0.001s
FAILED (failures=1)
Destroying test database for alias 'default'...
在前面的输出中的第三行,它打印了 F.。大写 F 代表有一个测试失败了,而点代表有一个测试成功了。然后它打印出下面那一行中失败的每个测试。对于它打印出的每个测试,Django 会包含为该测试用例编写的注释,例如 '''Checks if False == False, Value set to True'''。当你的测试失败时,请使用三重双引号或单引号注释记号来包含有用的信息。然后它提供跟踪信息,指示错误或失败的原因和位置。如果你想在测试方法中提供有关特定测试的额外信息,你还可以在这些测试方法中包含 print() 语句。
小贴士
要运行项目中包含的所有应用的测试,请运行以下命令:(虚拟环境)PS > python manage.py test
此外,如果你将单行双引号或单引号注释拆分成多行注释,那么在你的命令行窗口中只会显示该注释的第一行。
现在,在继续下一个练习之前,先注释掉或删除 TestingCalibrator 类。
测试 Django 模型
在这个练习中,我们将使用 TestCase 类,因为我们将会连接到数据库。测试客户端为我们启动的数据库将不会影响所有本地或远程数据库中找到的任何数据。请按照以下步骤操作:
-
在你的
/chapter_3/models.py文件中,确保(8, 'Jeep')值存在于MAKE_CHOICES列表的选择中:# /becoming_a_django_entdev/chapter_3/models.py ... MAKE_CHOICES = ( ... (8, 'Jeep'), ... ) -
在你的
/chapter_9/tests.py文件中,添加以下import语句:# /becoming_a_django_entdev/chapter_9/tests.py from django.test import ..., TestCase from djmoney.money import Money from ..chapter_3.models import ( Engine, Seller, Vehicle, VehicleModel ) -
在相同的文件中,添加以下类和
setUp()方法:# /becoming_a_django_entdev/chapter_9/tests.py ... class ModelUnitTestCase(TestCase): def setUp(self): model = VehicleModel.objects.create( name = 'Grand Cherokee Laredo 4WD', make = 8 ) engine = Engine.objects.create( name = '3.6L FI FFV DO', vehicle_model = model ) vehicle = Vehicle.objects.create( vin = 'aa890123456789012', sold = False, price = Money(39875, 'USD'), make = 8, vehicle_model = model, engine = engine ) seller = Seller.objects.create_user( 'test', 'testing@example.com', 'testpassword', is_staff = True, is_superuser = True, is_active = True, name = 'Chapter 9 Seller 1' ) seller.vehicles.set([vehicle]) -
在相同的
ModelUnitTestCase类中,添加以下测试方法:# /becoming_a_django_entdev/chapter_9/tests.py ... class ModelUnitTestCase(TestCase): ... def test_full_vehicle_name(self): vehicle_1 = Vehicle.objects.get( vin = 'aa890123456789012' ) self.assertEqual( vehicle_1.full_vehicle_name(), 'Jeep Grand Cherokee Laredo 4WD - 3.6L FI FFV DO' )
之前的 setUp(self) 方法将创建一个 VehicleModel、Engine、Vehicle 和 Seller 模型对象,这些对象是从 chapter_3 应用程序中导入的。setUp() 方法在执行该类中的任何测试用例之前创建这些对象。我们创建每个相关对象作为变量,然后使用该变量将下一个创建的对象的相关对象分配给它。Seller 对象使用在 第六章,Exploring the Django Admin Site 中引入的相同的 create_user() 方法来创建一个新的 Seller,带有为我们格式化的散列密码和日期字段。我们只创建了一个测试,名为 test_full_vehicle_name(),它通过 vin 字段值查找在设置时创建的车辆。它使用我们在 第三章,Models, Relations, and Inheritance 中创建的 full_vehicle_name() 方法,返回新创建车辆的定制格式化名称。预期值是 Jeep Grand Cherokee Laredo 4WD - 3.6L FI FFV DO,其格式为 {{ make }} {{ model }} – {{ engine }}。如果返回的值与该值不匹配,测试将失败。
-
现在,执行这里显示的运行测试命令:
(virtual_env) PS > python manage.py test becoming_a_django_entdev.chapter_9
如果你注释掉了之前的所有测试,你应该会看到这里显示的结果:
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
------------------------------------------------------
Ran 1 test in 0.229s
OK
Destroying test database for alias 'default'...
-
在之前步骤 4 中的
self.assertEqual()函数中找到的预期值更改为一个不存在的值,然后再次运行你的测试命令。现在,你应该会看到一个失败消息,如图所示:Found 1 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). F ====================================================== FAIL: test_full_vehicle_name ------------------------------------------------------ Traceback (most recent call last): File "C:\Projects\Packt\Repo\becoming_a_django_entdev\becoming_a_django_entdev\chapter_9\tests.py", line 88, in test_full_vehicle_name self.assertEqual(vehicle_1.full_vehicle_name(), 'Jeep Grand Cherokee Laredo 4WD - 3.6L FI FFV DO asdfasdfas') # Incorrect Value AssertionError: 'Jeep Grand Cherokee Laredo 4WD - 3.6L FI FFV DO' != 'Jeep Grand Cherokee Laredo 4WD - 3.6L FI FFV DO asdfasdfas' - Jeep Grand Cherokee Laredo 4WD - 3.6L FI FFV DO + Jeep Grand Cherokee Laredo 4WD - 3.6L FI FFV DO asdfasdfas ? +++++++++++ ------------------------------------------------------ Ran 1 test in 0.227s FAILED (failures=1) Destroying test database for alias 'default'...
在进行下一个练习之前,先注释掉 ModelUnitTestCase 类。现在我们已经了解了如何编写简单的测试用例以及测试模型 CRUD 操作的用例,接下来,我们将测试一个在 第四章,URLs, Views, and Templates 中编写的自定义视图类。
测试 HTTP 视图请求
在本节中,我们将扩展我们之前编写的基线测试用例,以包括 HTTP 视图请求。在测试视图类时,无论是基于方法的视图还是基于类的视图,它们都将使用我们迄今为止一直在使用的相同的 TestCase 类。
在接下来的子节中,我们将执行两个测试,一个基于方法的视图测试,另一个基于类的视图测试。
测试基于方法的视图
在这个练习中,我们将测试在 第四章,URLs, Views, and Templates 中编写的 practice_year_view() 方法。在这个测试中,我们比较的是返回的响应代码是否等于 200 的值,这意味着成功的响应。
按照以下步骤创建你的测试用例:
-
在你的
/chapter_9/tests.py文件中,添加以下YearRequestTestCase类和方法:# /becoming_a_django_entdev/chapter_9/tests.py ... from django.contrib.auth.models import AnonymousUser from django.test import ..., RequestFactory, TestCase from ..chapter_4.views import practice_year_view class YearRequestTestCase(TestCase): def setUp(self): self.factory = RequestFactory() def test_methodbased(self): request = self.factory.get( '/my_year_path/2022/' ) request.user = AnonymousUser() response = practice_year_view(request, 2022) self.assertEqual(response.status_code, 200)
这个测试使用了一个保存为self.factory变量的RequestFactory()对象。然后使用该工厂构建一个实际的request对象。我们想要测试的路径是通过self.factory.get()方法传入的/my_year_path/2022/。由于我们在practice_year_view()中不需要认证,我们将request.user对象设置为django.contrib.auth.models库中提供的AnonymousUser()类对象。响应是通过practice_year_view(request, 2022)方法构建的。在这里,我们传入request对象和我们试图访问的年份关键字参数的值。最后一行检查response.status_code是否实际上等于200。
-
接下来,运行以下测试命令以执行
chapter_9应用测试用例:(virtual_env) PS > python manage.py test becoming_a_django_entdev.chapter_9
如果成功,你应该会看到以下信息:
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
------------------------------------------------------
Ran 1 test in 0.003s
OK
Destroying test database for alias 'default'...
返回到步骤 1,将所有年份2022的实例更改为12(在两个地方找到),然后重新运行你的测试命令。你应该会看到这里显示的失败/错误信息:
Found 1 test(s).
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E
======================================================
ERROR: test_methodbased (becoming_a_django_entdev.chapter_9.tests.YearRequestTestCase)
Checks if the path http://localhost:8000/my_year_path/2022/ actually exists and returns a 200 response code (Valid)
------------------------------------------------------
Traceback (most recent call last):
File "C:\Projects\Packt\Repo\becoming_a_django_entdev\becoming_a_django_entdev\chapter_9\tests.py", line 115, in test_methodbased
response = practice_year_view(request, 12)
File "C:\Projects\Packt\Repo\becoming_a_django_entdev\becoming_a_django_entdev\chapter_4\views.py", line 37, in practice_year_view
raise Http404('Year Not Found: %s' % year)
django.http.response.Http404: Year Not Found: 12
------------------------------------------------------
Ran 1 test in 0.004s
FAILED (errors=1)
Destroying test database for alias 'default'...
这个测试用例失败是因为我们在第四章中编写的practice_year_view()方法中写入了条件,该条件检查用户是否输入了一个大于或等于1900的年份。你还会看到,对于这个测试,它没有打印F或句点,而是打印了大写字母E,代表错误。错误与失败的区别在于我们正在检查的自定义参数,这意味着 URL 模式是正确的,但视图本身执行了额外的逻辑,触发了页面找不到错误。
在进行下一个练习之前,请先注释掉YearRequestTestCase类。
让我们在下一节测试基于类的视图。
测试基于类的视图
在这个练习中,我们将测试在第四章中编写的VehicleView类,URLs, Views, and Templates。我们将加载chapter_3数据固定文件,而不是在setUp()方法中创建对象,就像我们在ModelUnitTestCase类中所做的那样。我们已经在创建对象是否成功方面进行了测试。现在我们可以通过只加载固定文件来节省时间和精力。
按照以下步骤创建你的测试用例:
-
在你的
/chapter_9/tests.py文件中,添加VehicleRequestTestCase类和方法,如下所示:# /becoming_a_django_entdev/chapter_9/tests.py ... from django.contrib.auth.models import AnonymousUser from django.test import ..., RequestFactory, TestCase from ..chapter_4.views import ..., VehicleView class VehicleRequestTestCase(TestCase): fixtures = ['chapter_3'] def setUp(self): self.factory = RequestFactory() def test_classbased(self): request = self.factory.get('/vehicle/1/') request.user = AnonymousUser() response = VehicleView.as_view()(request, 1) self.assertEqual(response.status_code, 200)
我们仍然需要之前使用的RequestFactory()对象和AnonymousUser(),因为VehicleView类也不需要认证。我们使用VehicleView.as_view()(request, 1)创建了此测试的响应对象。它看起来类似于任何urls.py文件中找到的 URL 模式映射到视图类的.as_view()方法。我们再次检查response.status_code是否等于200,表示成功。
-
现在,运行以下代码中的测试命令,你应该再次看到成功的测试:
(virtual_env) PS > python manage.py test becoming_a_django_entdev.chapter_9 -
现在,将
'/vehicle/1/'和(request, 1)中的步骤 1中的数字1更改为99。这个数字代表我们试图访问的车辆的索引,这个索引目前不应该存在。然后,重新运行你的test命令,你应该看到以下消息:Found 1 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). E ====================================================== ERROR: test_classbased (becoming_a_django_entdev.chapter_9.tests.VehicleRequestTestCase) Checks if the path http://localhost:8000/vehicle/1/ actually exists and returns a 200 response code (Valid) ------------------------------------------------------ Traceback (most recent call last): File "C:\Projects\Packt\Repo\becoming_a_django_entdev\becoming_a_django_entdev\chapter_4\views.py", line 68, in get vehicle = Vehicle.objects.get(id=id) File "C:\Projects\Packt\Repo\becoming_a_django_entdev\virtual_env\lib\site-packages\django\db\models\manager.py", line 85, in manager_method return getattr(self.get_queryset(), name)(*args, **kwargs) File "C:\Projects\Packt\Repo\becoming_a_django_entdev\virtual_env\lib\site-packages\django\db\models\query.py", line 439, in get raise self.model.DoesNotExist( becoming_a_django_entdev.chapter_3.models.Vehicle.DoesNotExist: Vehicle matching query does not exist. During handling of the above exception, another exception occurred: Traceback (most recent call last): File "C:\Projects\Packt\Repo\becoming_a_django_entdev\becoming_a_django_entdev\chapter_9\tests.py", line 143, in test_classbased response = VehicleView.as_view()(request, 99) File "C:\Projects\Packt\Repo\becoming_a_django_entdev\virtual_env\lib\site-packages\django\views\generic\base.py", line 69, in view return self.dispatch(request, *args, **kwargs) File "C:\Projects\Packt\Repo\becoming_a_django_entdev\virtual_env\lib\site-packages\django\views\generic\base.py", line 101, in dispatch return handler(request, *args, **kwargs) File "C:\Projects\Packt\Repo\becoming_a_django_entdev\becoming_a_django_entdev\chapter_4\views.py", line 70, in get raise Http404('Vehicle ID Not Found: %s' % id) django.http.response.Http404: Vehicle ID Not Found: 99 ------------------------------------------------------ Ran 1 test in 0.062s FAILED (errors=1) Destroying test database for alias 'default'...
你会注意到,在前面的跟踪消息中,它表明没有找到具有该 ID 的对象。你的跟踪消息底部是你可能找到关于问题原因答案的地方,但这并不总是如此。这是因为我们在chapter_3固定中找到的车辆项目少于 10 个。
在进行下一个练习之前,请注释掉VehicleRequestTestCase类。
现在我们已经学会了如何测试请求响应并将数据固定加载到测试用例中,让我们在此基础上添加身份验证措施。
测试已验证的视图请求
在本节中,我们将基于我们刚刚构建的相同请求测试用例来移除AnonymousUser类并执行我们自己的身份验证,只要求允许的用户。我们编写了一些在第八章中,使用 Django REST 框架中需要用户身份验证的视图类。让我们创建测试脚本,以便在执行自动化测试时使用实际用户进行身份验证。这正是我们在准备本章时加载chapter_8/urls.py文件的原因。Django 提供了一个名为Client的类,位于django.test库中,它允许我们在测试视图类时执行用户身份验证。
在下面的子节中,我们将实现Client类以进行身份验证。
使用 Client()类
在这个练习中,我们将测试在第八章中编写的自定义 API 端点,即GetSellerHTMLView类。这是我们编写的用于通过 ID 查询卖家并返回预格式化的 HTML 而不是预期的 JSON 的传统 API 端点。我们将测试的是seller上下文对象是否具有我们正在查找的 ID 所期望的商号。当使用Client类时,不再需要RequestFactory类,也不需要AnonymousUser类。
按照以下步骤实现自己的身份验证:
-
在你的
/chapter_9/tests.py文件中,添加这里显示的SellerClientTestCase类和setUp()方法:# /becoming_a_django_entdev/chapter_9/tests.py ... from django.test import ..., Client, TestCase from ..chapter_3.models import ..., Seller class SellerClientTestCase(TestCase): fixtures = ['chapter_3'] def setUp(self): self.user = Seller.objects.get(id=1) self.client = Client() self.client.login( username = self.user.username, password = 'mynewpassword' )
我们首先将 self.user 值设置为等于一个单独的 Seller。提供的 ID 是数字 1,与使用用户名 admin 创建的第一个超级用户相关联。这是在 chapter_3 固件中为你提供的唯一卖家对象。接下来,我们将 self.client 值设置为一个新的 Client() 对象。setUp(self) 方法的最后一行是我们模拟登录系统的地方。我们使用 self.user.username 来获取我们查询的 Seller 的用户名。不要使用 self.user.password 作为密码;相反,使用手动写入代码中的未散列密码作为字符串。这是因为没有方法可以检索用户的未散列密码,这是出于安全原因的设计。
注意
当编写自己的测试用例时,明智的做法是将测试用户凭据存储在 .env 文件中,并将其导入项目作为 settings.py 变量,这样就可以引用它,而不是像之前那样硬编码密码。
-
在相同的
SellerClientTestCase类中,添加以下测试方法:# /becoming_a_django_entdev/chapter_9/tests.py ... class SellerClientTestCase(TestCase): ... def test_get(self): response = self.client.get( '/chapter-8/seller/1/' ) self.assertEqual(response.status_code, 200) seller = response.context['seller']
self.assertEqual(seller.name, 'Test Biz Name')
在前面的 test_get(self) 方法中,我们使用 self.client.get() 创建了响应对象。在该方法内部,我们传递了我们要测试的路径,即 http://localhost:8000/chapter-8/seller/1/。在这个测试用例中,我们执行了两个检查而不是一个;第一个检查 response.status_code 是否实际上等于 200,以指示成功。另一个检查卖家业务名称是否是我们正在查找的对象所期望的,即 Test Biz Name。
这就是为什么我们要创建小写的 seller 变量,它从请求返回的上下文中获取 seller。在创建小写的 seller 变量之前添加 self.assertEqual(response.status_code, 200) 语句也很重要。如果我们由于任何原因没有得到成功的响应,显然 seller 对象将不存在,因此测试将失败。当这种情况发生时,它可能会将你引向错误的方向,告诉你真正的问题可能是什么。
-
现在,运行以下
test命令,你应该再次看到成功的测试:(virtual_env) PS > python manage.py test becoming_a_django_entdev.chapter_9 -
接下来,将 步骤 1 中找到的
password值更改为一个错误的密码,例如mynewpassword1,这将强制产生失败的响应。重新运行test命令,你应该看到以下消息:Found 1 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). F ====================================================== FAIL: test_get (becoming_a_django_entdev.chapter_9.tests.SellerClientTestCase) Tests a custom-built REST-API Endpoint using the Client() class. ------------------------------------------------------ Traceback (most recent call last): File "C:\Projects\Packt\Repo\becoming_a_django_entdev\becoming_a_django_entdev\chapter_9\tests.py", line 171, in test_get self.assertEqual(response.status_code, 200) AssertionError: 401 != 200 ------------------------------------------------------ Ran 1 test in 0.356s FAILED (failures=1) Destroying test database for alias 'default'...
我们可以看到失败的原因是 AssertionError: 401 != 200。一个 401 响应表示未授权的响应,这意味着请求的资源没有有效的认证凭据,访问将不会获得批准。
-
将密码改回正确的
mynewpassword,然后返回到 步骤 2,将最后一行中的业务名称更改为不正确的名称,例如Test Biz Name1。 -
再次重新运行
test命令,现在你应该看到以下失败的错误信息:Found 1 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). F ====================================================== FAIL: test_get (becoming_a_django_entdev.chapter_9.tests.SellerClientTestCase) Tests a custom-built REST-API Endpoint using the Client() class. ------------------------------------------------------ Traceback (most recent call last): File "C:\Projects\Packt\Repo\becoming_a_django_entdev\becoming_a_django_entdev\chapter_9\tests.py", line 175, in test_get self.assertEqual(seller.name, 'Test Biz Name1') AssertionError: 'Test Biz Name' != 'Test Biz Name1' - Test Biz Name + Test Biz Name1 ? + ------------------------------------------------------ Ran 1 test in 0.561s FAILED (failures=1) Destroying test database for alias 'default'...
这也表明我们实现了200成功响应代码,因为这是第二个失败的断言,而不是第一个。
在进行下一个练习之前,您可以取消注释SellerClientTestCase类。现在,我们已经更好地了解了如何将身份验证措施添加到我们的测试用例中,我们将使用身份验证测试 Django REST API 端点。
测试 Django REST API 端点
本节将介绍编写测试用例以测试 Django REST 框架端点。当测试使用 Django REST 框架创建的任何 REST API 端点时,我们需要使用rest_framework.test库提供的APITestCase类。我们还应该在需要身份验证时使用该库提供的APIClient()类,而不是像之前那样使用Client()类。
在以下练习中,我们将创建一个测试类,该类执行两个测试:第一个将创建一个引擎对象,另一个将更新一个对象。
创建对象测试用例
此测试将使用POST请求方法向localhost:8000/chapter-8/engines/端点发送数据并在数据库中创建一个引擎对象。由于我们正在加载一个只包含 ID 为1和2的两个引擎对象的数据固定文件,我们应该期望新对象在索引3处创建,但您的结果可能会有所不同。我们将在更新对象测试用例子节中回过头来讨论这一点。
按照以下步骤创建您的测试用例:
-
在您的
/chapter_9/tests.py文件中,添加以下EngineAPITestCase类、setUp()方法和import语句:# /becoming_a_django_entdev/chapter_9/tests.py ... from rest_framework.test import APITestCase, APIClient class EngineAPITestCase(APITestCase): fixtures = ['chapter_3'] def setUp(self): self.user = Seller.objects.get(id=1) self.client = APIClient() self.client.login( username = self.user.username, password = 'mynewpassword' )
上述类的结构非常类似于标题为使用 Client()类的子节中执行的模式。在这里,我们将self.client的值设置为使用rest_framework.test库提供的APIClient类。登录与之前相同,在self.client.login()声明中完成。
-
在相同的
EngineAPITestCase类中,添加以下test_post()方法:# /becoming_a_django_entdev/chapter_9/tests.py ... class EngineAPITestCase(APITestCase): ... def test_post(self): response = self.client.post( '/chapter-8/engines/', {'name': 'New Engine'}, format = 'json' ) self.assertEqual(response.status_code, 201) self.assertEqual(response.data['name'], 'New Engine')
对于响应对象,我们不是使用self.client.get(),而是使用self.client.post(),因为我们想向测试客户端服务器发送信息。内部是我们要发送的数据,注意最后一个参数是数据的格式,在这个例子中设置为 JSON 格式。然后我们检查response.status_code值,这次是要看它是否等于201而不是200。201响应代码表示对象已成功创建。最后一行检查返回给我们的数据,即创建的对象,是否具有我们期望的引擎名称。在这种情况下,我们期望的新引擎名称是New Engine。
-
现在,运行以下
test命令,您应该再次看到成功的测试:virtual_env) PS > python manage.py test becoming_a_django_entdev.chapter_9 -
接下来,回到步骤 1并添加一个错误的密码,例如
mynewpassword1,然后再次运行你的test命令。你应该会看到以下消息:Found 1 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). F ====================================================== FAIL: test_post (becoming_a_django_entdev.chapter_9.tests.EngineAPITestCase) Checks if it returns a 201 response code (Created). ------------------------------------------------------ Traceback (most recent call last): File "C:\Projects\Packt\Repo\becoming_a_django_entdev\becoming_a_django_entdev\chapter_9\tests.py", line 203, in test_post self.assertEqual(response.status_code, 201) AssertionError: 401 != 201 ------------------------------------------------------ Ran 1 test in 0.363s FAILED (failures=1) Destroying test database for alias 'default'...
在这个测试中,我们可以看到我们被警告AssertionError: 401 != 201而不是这次200。你可以通过更改引擎名称的预期值来实现相同的效果,你会看到它警告你那个断言。
接下来,让我们添加到这个类中,以便我们可以测试更新对象。
更新对象测试用例
此测试将使用PUT请求方法向localhost:8000/chapter-8/engines/1/端点发送数据,这是数据库中要更新的特定引擎对象。
按照以下步骤更新你的类以适应此测试用例:
-
在同一个
EngineAPITestCase类中,添加以下test_put()方法:# /becoming_a_django_entdev/chapter_9/tests.py ... from rest_framework.test import APITestCase, APIClient class EngineAPITestCase(APITestCase): ... def test_put(self): response = self.client.put( '/chapter-8/engines/1/', {'name': 'My Changed Engine Name'}, format = 'json' ) self.assertEqual(response.status_code, 200) self.assertEqual( response.data['name'], 'My Changed Engine Name' )
请保持setUp()和test_post()方法不变。在先前的test_put()方法中,我们使用self.client.put()方法来创建响应对象。我们发送的数据是相同的 JSON 格式。注意,在先前的示例中,我们指定路径为'/chapter-8/engines/1/',这指的是 ID 索引为1的第一个引擎对象。该对象被插入通过chapter_3固定值创建的虚拟数据库中,该固定值仍在本类中使用。我们再次检查response.status_code是否等于200,成功。我们不需要检查201,因为没有创建任何内容,只是更新。然后我们检查确保预期的对象名称等于My Changed Engine Name。
-
现在,运行以下测试命令,你应该会看到两个测试都成功了:
(virtual_env) PS > python3 manage.py test becoming_a_django_entdev.chapter_9 -
为了演示在一个测试中创建的项目不能从同一个类中的另一个测试中检索,请在
test_put()方法中将 ID 的1改为3,如'/chapter-8/engines/3/'。
当我们在test_post(self)方法中创建对象时,你可能会期望新创建的对象具有 ID 索引3,因为chapter_3固定值中只有两个对象。我们找不到更新该对象的新对象的原因是,当test_post(self)方法完成时,在该操作期间创建的任何内容都会在完成时被销毁。
-
重新运行你的
test命令,现在你应该会看到这里显示的失败消息:Found 2 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). .F ====================================================== FAIL: test_put (becoming_a_django_entdev.chapter_9.tests.EngineAPITestCase) Checks if it returns a 200 response code (Success). ------------------------------------------------------ Traceback (most recent call last): File "C:\Projects\Packt\Repo\becoming_a_django_entdev\becoming_a_django_entdev\chapter_9\tests.py", line 219, in test_put self.assertEqual(response.status_code, 200) AssertionError: 404 != 200 ------------------------------------------------------ Ran 2 tests in 1.037s FAILED (failures=1) Destroying test database for alias 'default'...
我们看到失败的原因是 Django 不会在同一个测试类中保留其他测试用例之间创建的对象。Django 会保留最后创建的对象 ID 的计数器,这意味着如果或当测试用例完成并且对象被销毁时,计数器将继续计数。这可能会在测试时造成挫败感,因此,我们加载了一个固定值,这样我们就可以确信 ID 是我们正在测试的对象应有的 ID。
现在我们对 Django 中的自动化测试工作方式有了更好的理解,接下来介绍 DjDT,这是一套强大的工具,帮助开发者在开发过程中进行调试。
使用 DjDT
DjDT 是一个第三方包,它集成了可配置的面板,可以实时向开发者显示调试信息。可以安装其他第三方包以向此工具栏添加更多面板。考虑到这一点,您也可以构建自己的面板。我们只将安装 DjDT 包本身,然后解释其最常见功能,指导您使用它,解释它向您展示的内容。要了解更多关于其所有功能的信息,请访问 pypi.org/project/django-debug-toolbar/ 和 django-debug-toolbar.readthedocs.io/en/latest/。
安装 DjDT
要开始安装 DjDT,请按照以下步骤操作:
-
将
django-debug-toolbar包添加到您的requirements.txt文件中,并通过该文件或运行以下pip命令将其安装到您的虚拟环境中,确保您的虚拟环境已经激活:PS C:\Projects\Packt\Repo\becoming_a_django_entdev> virtual_env/Scripts/activate (virtual_env) PS > pip install django-debug-toolbar -
在您的
settings.py文件中,将以下项目添加到您的INSTALLED_APPS列表中:# /becoming_a_django_entdev/settings.py ... INSTALLED_APPS = [ ... 'debug_toolbar', ... ] -
在相同的
settings.py文件中,将以下中间件添加到您的MIDDLEWARE列表顶部:# /becoming_a_django_entdev/settings.py ... MIDDLEWARE = [ 'debug_toolbar.middleware.DebugToolbarMiddleware', ... ] ...
应该只将 debug_toolbar 应用和 MIDDLEWARE 项目添加到该文件中。
-
在 第二章 中,当我们在创建和配置项目时,已经添加了
django.contrib.staticfiles应用、INTERNAL_IPS列表和STATIC_URL变量,即 Chapter 2,项目配置。请注意,它们对于此工具栏的正常工作是必需的。如果您正在处理一个不遵循本书settings.py规范的自己的项目,请确保包含这些项目:# /becoming_a_django_entdev/settings.py ... INTERNAL_IPS = [ '127.0.0.1', ] INSTALLED_APPS = [ ... 'django.contrib.staticfiles', 'debug_toolbar', ... ] STATIC_URL = '/staticfiles/' ... -
接下来,您需要导入与该第三方应用相关的 URL 模式。为了确保此工具栏可以在本书的所有章节中使用,请在您的主
urls.py文件中添加以下include模式:# /becoming_a_django_entdev/urls.py ... from django.urls import ..., include, re_path ... if settings.DEBUG: ... import debug_toolbar urlpatterns = [ re_path( r'^__debug__/', include(debug_toolbar.urls) ), ] + urlpatterns
注意,在前面的示例中,我们将导入放在 settings.DEBUG 条件语句下,检查我们的环境是否是 DEBUG 环境。我们绝对不希望这个工具栏出现在生产或类似生产的环境,如预发布环境。开发环境通常是可接受的。
就这样;到目前为止,这个工具栏应该已经安装并正常工作。接下来,让我们讨论如何调整以与我们的远程环境一起使用。
调整 DjDT 设置
你即将了解的任何面板都有可以在你的settings.py文件中定义的行为设置。一个常见的例子是我们需要使用SHOW_TOOLBAR_CALLBACK设置来允许我们在 Heroku 环境中看到 DjDT。要了解更多关于所有可用设置的信息,请访问django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config。
按照以下步骤激活此设置:
-
在
settings.py文件中,添加以下代码:# /becoming_a_django_entdev/settings.py ... def show_toolbar(request): return True if os.getenv('SHOW_TOOLBAR_CALLBACK') == 'True': DEBUG_TOOLBAR_CONFIG = { 'SHOW_TOOLBAR_CALLBACK': show_toolbar, }
我们必须在.env文件中使用一个可调用对象和一个变量,因为使用DEBUG_TOOLBAR_CONFIG字典并本地运行你的自动化 Django 测试命令会导致由于 DjDT 包的错误。单独使用工具栏或 Django 测试命令时,没有前面的代码也可以,但当它们一起使用时,这个代码是必需的。
-
要在你的 Heroku 托管环境中显示此工具栏,请在
.env文件中将以下值设置为True。请参阅位于第二章,“项目配置”中标题为远程变量的子部分:# .env SHOW_TOOLBAR_CALLBACK=True -
在你的本地环境中,将此值保留为
False。
现在,让我们使用这个工具栏。
如何使用 DjDT
确保你的虚拟环境已激活,然后按照以下步骤使用 DjDT:
-
使用以下命令运行你的项目,或者你可以使用 IDE,如第二章,“项目配置”中讨论的那样:
PS C:\Projects\Packt\Repo\becoming_a_django_entdev> virtual_env/Scripts/activate (virtual_env) PS > python3 manage.py runserver -
在你的浏览器中,导航到你的主页
localhost:8000/,你应该看到带有第九章副标题的经典主页图片,现在,在窗口右上角有一个选项卡,如下面的截图所示,箭头指向它:

图 9.1 – DjDT – 选项卡
- 点击这个工具栏以打开它,看看隐藏的秘密,如下面的截图所示:

图 9.2 – DjDT – 打开
工具栏中的每个项目都可以点击以进一步展开。以下子部分为你分解了每个面板实际上显示的内容。
历史
历史面板显示在这个浏览器选项卡内发出的每个请求的列表。每次你刷新页面或在你网站上导航到新的路径时,这些信息都会在这个面板中记录。在这个面板中的每个项目都有一个切换按钮。当点击该按钮时,与该请求相关的其他选项卡中的调试信息将更新,如下面的截图所示,其中用箭头突出显示:

图 9.3 – DjDT – 历史选项卡
当你点击准备本章的chapter_8应用 URL 模式时。在这个例子中,我实际上登录到了我的管理面板,在SQL标签页中显示有三个查询,然后当我切换到主页面的一个 URL 时,SQL标签页更新并告诉我现在有两个查询。我将在接下来的SQL子节中解释这些查询的含义。现在,你至少可以看到这些数据是如何变化的。
版本
requirements.txt文件与开发者共享,用于自动化安装项目所需的包及其版本。以下截图显示了此标签页的外观,验证我们确实在使用 Django 4.0:

图 9.4 – DjDT – 版本标签页
时间
时间标签页仅显示执行当前请求所需的时间。它实际上并没有像前两个标签页那样打开一个新的面板。它只是一个占位符标签页,显示有用的信息。
设置
settings.py变量及其计算值。如果你有计算值的函数,或者如果你正在将许多不同的settings.py文件链接到其他包中,这些包覆盖或更改了父文件中的值,它们都可以在这里查看。以下截图显示了它的样子:

图 9.5 – DjDT – 设置标签页
标题
标题面板显示与你的 HTTP 请求和响应头相关的所有信息。它还在此面板的底部显示你的 WSGI 或 ASGI 环境变量。当你与 API 端点一起工作时,这非常有帮助,你需要确保请求和响应头中的信息是你预期的。以下截图显示了它的样子:

图 9.6 – DjDT – 标题标签页
请求
请求面板显示了与你的请求相关的所有数据,例如关键字参数和 cookie 和会话数据。此标签页非常有用,可以检查确保这些信息是你预期的。以下截图显示了此面板的样子:

图 9.7 – DjDT – 请求标签页
SQL
SQL面板展示了每个查询的非常详细和分解的视图,以及显示特定请求中涉及的所有查询。例如,如果你在未登录 Django 管理站点的情况下访问主页,SQL标签页将告诉你没有查询,如图图 9.2所示。然而,如果你已登录 Django 管理站点并访问主页,你应该在SQL标签页下看到至少两个查询。当你点击此标签页时,你会看到这两个查询是什么,如下面的截图所示:

图 9.8 – DjDT – SQL 标签页
在这里,我们可以看到第一个查询为当前请求建立了一个会话,然后查询了系统登录用户的对象。由于我们在 第三章,模型、关系和继承 中扩展了 User 模型,该用户对象是一个 Seller 模型对象。每个查询都有一个 Sel 和 Expl 按钮,它们提供了有关该查询的其他详细信息。点击任何这些查询左侧的加号,可以进一步展开,显示有关该查询的信息,包括跟踪信息,如下所示:

图 9.9 – DjDT – SQL 标签页展开
静态文件
静态文件面板显示所有已安装且实际包含静态文件的程序。中间部分列出与当前请求相关的所有静态文件。您实际上可以点击它们以在当前标签页或新标签页中打开和查看它们。此面板的最后部分显示所有已安装的应用程序中找到的所有静态文件的列表。如果您正在比较覆盖另一个应用程序相同静态文件的静态文件,这可能有时很有帮助,您可以看到哪个被使用,哪些被忽略。以下截图显示了此面板的外观:

图 9.10 – DjDT – 静态文件标签页
在这里,我们可以看到该页面上正在使用的唯一静态文件是 home_page.jpg 文件。由于 index.html 文件没有扩展 base_template_1.html 文件,因此我们在这个页面上没有使用任何 CSS 或 JavaScript 文件;它们不会被加载。如果您激活 DjDT 并返回到前面的章节,您可能会看到那些额外的资源,因为我们使用了 base_template_1.html 文件。任何与 DjDT 相关的调试工具,如 CSS 和 JavaScript 文件,都不会显示在中间部分。我们的想法是这些是调试相关的资源,我们不需要在调试的页面上知道这些。如果您使用浏览器工具检查页面,您将看到与 DjDT 相关的资源;它们不会出现在生产环境中。
模板
模板面板显示与使用的模板和可用上下文相关的信息。中间部分显示所有模板。如果您使用了包含或扩展的局部 HTML 文件,则每个模板都会按使用顺序出现在此列表中。当您点击下面的截图所示的切换上下文箭头时,它将显示与该文件相关的所有上下文变量及其值的列表:

图 9.11 – DjDT – 模板标签页
以下截图展示了点击 切换上下文 按钮的样子,显示了在该特定模板或部分文件中可用的上下文:

图 9.12 – DjDT – 显示上下文的模板选项卡
在此面板的底部是所有上下文处理器所在的位置。你可以查看每个上下文处理器中可用的上下文。
缓存
缓存面板显示了与该页面相关的所有缓存对象。这是如果你正在使用帮助提高数据库性能的工具时。这被认为是一个超出本书范围的进阶主题。
注意
DjDT 发布者也在他们的文档中指出,此面板与 Django 的按站点缓存不兼容。
信号
信号面板显示了应用程序内相互通信的通知者和接收者。这些可以与 WebSocket 进行紧密比较。这被认为是一个超出本书范围的进阶主题。
记录
logging 库。在展示此面板的外观之前,让我们连接一个实际的日志来查看。与 Django 消息框架一样,日志系统有不同的消息级别。默认的最小日志级别是 WARNING,但你也可以以相同的方式显示 DEBUG 和 INFO 级别的日志,无论是通过在 settings.py 文件中设置它,还是通过在设置日志的地方声明它,就像我们将在以下步骤中做的那样。要了解更多关于使用日志系统所有功能的信息,请访问 docs.djangoproject.com/en/4.0/topics/logging/ 和 docs.python.org/3/library/logging.html。
按以下步骤练习使用日志系统:
-
在你的
/chapter_4/views.py文件中,在现有的practice_year_view()函数开始处添加以下日志语句:# /becoming_a_django_entdev/chapter_4/views.py ... import logging def practice_year_view(request, year): logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) logger.info('The Requested Year Is: %s' % year) ...
前两行将日志级别降低,以显示所有 INFO 级别及以上的日志,允许我们使用 logger.info() 方法创建日志消息。否则,默认情况下我们能够使用的最低级别将是 logger.warning() 方法。当我们访问 URL 时,期望看到的日志消息应该是 The Requested Year Is: 2022。
- 现在,在浏览器中导航到
localhost:8000/my_year_path/2022/并展开调试工具栏。打开 记录 选项卡,现在你应该能看到我们创建的日志,如以下截图所示:

图 9.13 – DjDT – 记录选项卡
拦截重定向
拦截重定向复选框用于在执行重定向时显示一个中间页面,以显示有关该重定向的信息,在浏览器更新重定向页面之前。
性能分析
配置文件复选框允许在页面加载时收集额外数据。它是对内存和 CPU 进程的详细分析。每个进程都被分解成可能的最小测量值。默认情况下,此复选框保持未选中状态。以下截图展示了其外观示例:

图 9.14 – DjDT – 配置文件选项卡
现在,我们对 DjDT 中所有可用的选项卡及其用途有了更深入的了解。这为我们提供了丰富的工具,帮助我们轻松地制作世界级的应用程序。
摘要
到目前为止,我们已经对 Django 中自动测试的执行方式有了坚实的理解。我们编写了几个测试用例,测试了前几章中完成的许多练习。我们练习编写模拟成功的测试用例和其他故意触发失败的测试用例,以更好地理解正在发生的事情。我们甚至发现了如何编写与 Django REST 框架一起工作的测试用例。在处理自动测试后,我们安装了我认为所有工具中最强大的工具,即 DjDT。DjDT 用于开发者在编写代码和本地运行项目时进行实时调试。
在下一章中,我们将学习如何使用 DjDT 来监控性能,同时学习如何优化数据库查询。
第十章:第十章:数据库管理
在编程中,数据库管理的主题涵盖了广泛的各种子类别。其中许多类别在早期的章节中已经介绍过,例如在 第二章 项目配置 中,我们讨论了使用数据库管理工具的概念,或者在 第三章 模型、关系和继承 中,我们探讨了模型管理器的概念。虽然这些主题可以被认为是本章的主题,但它们在早期章节中被引入,是为了更好地适应该章节的主题讨论,或者作为适合早期章节练习的工具。在 第三章 模型、关系和继承 中介绍并使用的 Django 固定文件也可以被视为数据库管理工具,并将在本章中更深入地探讨。
Django 固定文件用于导入和导出与 Django 项目连接的数据库中的数据。在本章之前提供的 chapter_3 数据固定文件,在每一章中都得到了使用,它通过提供必要的测试/虚拟数据来帮助演示这些练习。我们将介绍如何导出数据,为项目和开发者创建自己的数据固定文件。我们还将更深入地解释我们一直在使用的导入过程以及可用的选项。
在本章中,我们将探讨其他执行查询的方法,这些方法可以增强您系统的整体性能。常用的两种方法是 select_related() 和 prefetch_related(),在 Django 中分别被称为 Vehicle 和 Seller 模型类数据,这些数据存在于我们的数据库中。我们将使用在 第九章 Django 测试 中介绍的 Django Debug Toolbar (DjDT) 来监控性能的变化。
在本章中,我们将涵盖以下内容:
-
将数据导出到数据固定文件
-
从数据固定文件导入数据
-
使用
select_related()方法提升查询性能 -
使用
prefetch_related()方法提升查询性能 -
使用
Prefetch()类提升查询性能
技术要求
要在此章节中与代码一起工作,您需要在本地机器上安装以下工具:
-
Python 版本 3.9 – 作为项目的底层编程语言
-
Django 版本 4.0 – 作为项目的后端框架
-
pip 包管理器 – 用于管理第三方 Python/Django 包
我们将继续使用第二章中 Project Configuration 部分创建的解决方案。然而,没有必要使用 Visual Studio IDE。主要项目本身可以使用其他 IDE 运行,或者从项目根目录(其中包含 manage.py 文件)独立使用终端或命令行窗口运行。无论你使用什么编辑器或 IDE,都需要一个虚拟环境来与 Django 项目一起工作。有关如何创建项目和虚拟环境的说明可以在第二章 Project Configuration 中找到。你需要一个数据库来存储项目中的数据。在上一章的示例中选择了 PostgreSQL;然而,你可以为你的项目选择任何数据库类型来与本章的示例一起工作。
我们还将使用第三章中提供的 Django fixture 格式的数据,该章节标题为 Models, Relations, and Inheritance,子标题为 Loading the chapter_3 data fixture。确保 chapter_3 fixture 已加载到你的数据库中。如果这已经完成,则可以跳过下一个命令。如果你已经创建了第三章中 Models, Relations, and Inheritance 部分提到的表,并且尚未加载该 fixture,那么在激活你的虚拟环境后,运行以下命令:
(virtual_env) PS > python manage.py loaddata chapter_3
本章创建的所有代码都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Becoming-an-Enterprise-Django-Developer。本章中展示的大部分代码可以在 /becoming_a_django_entdev/becoming_a_django_entdev/chapter_10/ 目录中找到。
查看以下视频以查看 Code in Action:bit.ly/3zYgbqd。
准备本章内容
首先,按照第二章中讨论的步骤,在你的项目中创建一个名为 chapter_10 的新应用。正如该部分所述,不要忘记将 /becoming_a_django_entdev/becoming_a_django_entdev/chapter_10/apps.py 文件中你的应用类中 name = 变量的值更改为指向你安装应用的位置。务必还将此应用包含在 settings.py 文件中的 INSTALLED_APPS 变量中。
在网站的主要 urls.py 文件中,添加以下路径,该路径指向我们将要创建的本章 URL 模式:
# /becoming_a_django_entdev/urls.py
...
urlpatterns = [
path(
'',
include(
'becoming_a_django_entdev.chapter_10.urls'
)
),
]
接下来,将本书提供的代码中的 /chapter_10/urls.py 文件复制到你的项目中同一目录下。
在以下练习中,我们将使用在 第九章 中介绍的 DjDT,即 Django 测试,来监控性能。请确保在继续之前,您已经在项目中安装了 DjDT。说明可以在该章节的标题为 安装 DjDT 的子节中找到。
既然我们已经为这一章创建了应用程序,让我们首先创建我们自己的数据固定文件。
将数据导出到数据固定文件
每个 Django 应用程序中都有一个名为 fixtures 的文件夹。此目录也可以通过修改名为 FIXTURE_DIRS 的 settings.py 变量来更改,但如果您打算使用默认目录和行为,则这不是必需的。Django 固定文件可以编写为 JSON、JSONL、XML 或 YAML 文件格式。这意味着如果您可以将数据导出为这些格式之一,即使该系统不是 Django 项目,您也可以轻松地从其他系统导出数据。请记住,如果要从旧系统进行干净的导出并将其导入到新系统,对象的表结构必须完全匹配。
通常,在从旧版遗留系统导出并导入到新更新的系统时,涉及大量的数据处理。另一种选择是在导出或导入数据时使用一个或多个命令选项的组合,以防止数据结构不匹配时出现错误。有时,使用提供的选项可能不足以完成对数据进行所需的所有操作。数据处理是将数据从一种格式或数据类型转换为另一种格式的过程。有时我们必须处理数据,因为较新的系统改变了旧数据结构及其/或对旧数据结构设置的约束。当这种情况发生时,我们有时会在导入过程中遇到错误。有时,数据导入没有问题,但在运行时,由于数据库中找到的不正确格式的数据,您的用户可能会遇到奇怪的行为。在本章中,我们将仅介绍使用提供的选项;如果您的系统中的数据如此复杂,以至于需要处理,您将不得不查找编写自己的 Python 脚本来根据需要转换数据。
让我们练习使用 Django 的 dumpdata 管理命令。
使用 dumpdata 命令
dumpdata 管理命令是我们在导入 chapter_3 固定数据时使用的 loaddata 命令的反义词。它用于将连接到您的 Django 项目的数据库中的数据导出到数据固定文件中。默认情况下,Django 会将数据固定文件导出为 JSON 格式,但您可以在运行 dumpdata 命令时使用 --format 选项指定不同的格式。
我们将首先使用-o或--output选项将所有应用的表导出到我们的项目中的chapter_10应用目录,以保持章节练习的整洁组织。本章中的每个dumpdata练习都将使用--output选项,因为这个选项不是必需的。
关于这些选项以及其他未涵盖的选项的更多信息,可以在此处找到:docs.djangoproject.com/en/4.0/ref/django-admin/#dumpdata。
在进行此练习之前,请确保你的数据库中有数据,无论是手动添加的还是从chapter_3应用导入的。我们将通过以下步骤将所有现有数据导出到chapter_10数据固定文件夹以进行练习:
-
确保你位于你的项目根目录中,即你的
manage.py文件所在的同一个文件夹。然后,打开你的命令行窗口或终端,激活你的虚拟环境,但在此时不运行你的项目。 -
使用以下命令在你的
/becoming_a_django_entdev/chapter_10/目录中创建一个名为fixtures的新文件夹:(virtual_env) PS > mkdir becoming_a_django_entdev/chapter_10/fixtures -
使用
-o或--output输出选项执行dumpdata命令,将导出的数据放入我们刚刚创建的文件夹中的chapter_10.json文件中:(virtual_env) PS > python manage.py dumpdata -o becoming_a_django_entdev/chapter_10/fixtures/chapter_10.json
如果你操作成功,你现在应该能在你的/becoming_a_django_entdev/chapter_10/fixtures/文件夹中看到一个名为chapter_10.json的新文件,如下面的截图所示,使用 Visual Studio IDE 中的解决方案资源管理器:

图 10.1 – 使用dumpdata命令导出所有数据
我们之所以必须首先创建fixtures文件夹,是因为如果我们指定了一个不存在的文件夹,dumpdata命令将会失败。实际上,.json文件不必存在,Django 会为你创建该文件。请记住,如果你的.json文件已经存在,并且你运行dumpdata命令指定该文件作为输出选项,那么你现有的所有数据都将被覆盖并丢失。
在创建的chapter_10.json文件中,你会注意到它是以压缩文件格式存在的。你可以将这份文档格式化成可读的形式。在 Visual Studio 中,你可以在文档内右键点击并选择格式化文档来完成此操作。你还可以将数据复制粘贴到在线格式化工具中,例如我最喜欢的jsonlint.com/。格式化文档不是必需的,但如果你想直接在该文件中读取数据对象或编辑它们,这将很有帮助。
您还会注意到,在创建的 chapter_10.json 文件中,您将拥有项目中每个应用程序的数据,包括 auth、authtoken 和 chapter_3 数据表以及任何与 Django 相关的数据表,如 admin 和 contenttypes 表。这比您在 chapter_3.json 数据 fixture 中提供的信息要多得多。很可能会发现,您不需要包含诸如 admin.logentry 和 contenttypes.contenttype 对象的信息,因为它们通常在将数据导入到不同的系统时会引起冲突。
在下一个子节中,我们将通过指定我们想要包含的数据的 app_name 和/或 model_name 来练习仅导出项目中特定应用程序中找到的表。
导出特定应用程序
当使用 dumpdata 命令且未指定任何应用程序时,就像我们在之前的练习中所做的那样,Django 将导出项目中所有应用程序中所有表的数据。没有选项语法可以这样做;如果有,它将是 dumpdata {{ app_name }} 或 dumpdata {{ app_name.model_name }} 格式,当然不包括花括号。
要指定应用程序或表,请按照以下步骤操作:
-
确保您位于项目根目录内,即您的
manage.py文件所在的文件夹。然后,打开一个命令行窗口或终端并激活您的虚拟环境,但在此时尚未运行您的项目。 -
执行以下
dumpdata命令,该命令将指定仅在chapter_3应用程序中找到的所有表:(virtual_env) PS > python manage.py dumpdata chapter_3 -o becoming_a_django_entdev/chapter_10/fixtures/chapter_3_models.json
现在,应该为我们创建的文件是一个名为 chapter_3_models.json 的单个文件,位于 /chapter_10/fixtures/ 文件夹中。它应仅包含 vehiclemodel、engine、vehicle 和 seller 表的数据。之前在 fixture 文件中看到的其它所有数据将不再出现在这个新文件中。
- 在该文件中,格式化数据,以便您可以阅读其中的内容。如果您使用的是 Visual Studio,请在文档内部右键单击并选择 格式化文档,或者将数据复制粘贴到位于
jsonlint.com/的在线工具中。您的数据应类似于以下截图所示:

图 10.2 – 导出 chapter_3 应用程序 dumpdata 命令
默认情况下,所有相关对象都使用该相关对象的键来显示。我们可以通过先前的截图中的所有七个相关 Vehicle 对象来看到这一点,表示为从 1 到 7 的数字列表。顺序取决于该模型的默认排序。
注意
JSON 导出不一定遵循相同的或逻辑的顺序。您的结果可能会有所不同。在先前的截图中,每个导出的对象都包括其原始主键,表示为 "pk" 字段。我们可以使用 --natural-primary 选项来删除它,我们将在稍后讨论。
-
要练习仅导出特定表,请使用以下命令导出
Seller模型数据,通过指定chapter_3.seller作为源:(virtual_env) PS > python manage.py dumpdata chapter_3.seller -o becoming_a_django_entdev/chapter_10/fixtures/chapter_3_sellers.json
如果想进行额外的练习,可以使用相同的点表示法来指定其他模型和/或应用程序。
接下来,让我们再次练习导出所有内容,就像我们在第一个练习中所做的那样。这次,我们将使用--exclude选项来排除我们不想包含的应用程序。
使用--exclude选项
--exclude或-e选项用于告诉 Django 在dumpdata命令的给定包含中排除特定的应用程序或模型。在本练习中,我们将执行与本章标题为使用 dumpdata 命令的子节中早些时候执行相同的导出所有内容操作,并排除所有与 Django 相关的表。我们希望通过使用--exclude选项而不是告诉 Django 包含什么来产生与导出特定应用程序子节中相同的输出结果。
按照以下步骤执行您的--exclude操作:
-
确保您位于项目根目录内,即与您的
manage.py文件相同的文件夹。然后,打开命令行窗口或终端并激活您的虚拟环境,但在此阶段不要运行您的项目。 -
执行以下
dumpdata命令,该命令排除以下应用程序:dumpdata commands in this chapter are very long single-line commands. Anything broken down onto a new line is likely separated by a single space, as is the case with the preceding command.
选项也可以使用等号字符来编写,例如-e=app_name或--exclude=app_name。
新创建的chapter_10_exclude.json文件的内容应与我们在上一节标题为导出特定应用程序中创建的chapter_3_models.json文件的内容相匹配。这是因为我们在技术上执行了相同的行为,第一次我们告诉 Django 包含什么,第二次我们告诉 Django 排除什么。比较您文件的输出以查看结果。
接下来,让我们练习将数据导出为除默认 JSON 格式之外的其他格式。
使用--format选项
使用--format选项来告诉 Django 将数据输出到指定的格式。在导出数据时,我们可以指定的四种格式是 JSON、JSONL、XML 和 YAML。如果没有指定此选项,默认为 JSON。
按照以下步骤以可能的每种格式类型导出您的数据,每一步一个格式:
-
确保您位于项目根目录内,即与您的
manage.py文件相同的文件夹。然后,打开命令行窗口或终端并激活您的虚拟环境,但在此阶段不要运行您的项目。 -
执行以下
dumpdata命令,该命令将Sellers对象以 XML 格式导出:(virtual_env) PS > python manage.py dumpdata chapter_3.seller --format xml -o becoming_a_django_entdev/chapter_10/fixtures/chapter_3_sellers.xml -
执行以下
dumpdata命令,该命令将Sellers对象以 JSONL 格式导出:(virtual_env) PS > python manage.py dumpdata chapter_3.seller --format jsonl -o becoming_a_django_entdev/chapter_10/fixtures/chapter_3_sellers.jsonl -
要使用 YAML 格式,您需要安装名为
pyyaml的pip包。将此包添加到您的requirements.txt文件中,并从该文件安装它或运行以下命令将此包手动安装到您的虚拟环境中:(virtual_env) PS > pip install pyyaml -
执行以下
dumpdata命令,该命令将Sellers对象作为 YAML 导出:(virtual_env) PS > python manage.py dumpdata chapter_3.seller --format yaml -o becoming_a_django_entdev/chapter_10/fixtures/chapter_3_sellers.yaml
你现在应该有三个额外的 chapter_3_sellers 文件,每个格式一个:.xml、.jsonl 和 .yaml。打开这些文档,查看数据在每个格式中的表示方式以及它们与默认的 .json 格式的区别。
接下来,让我们练习在导出数据到 fixture 时使用 --natural-primary 选项删除主键,即 "pk" 字段。
使用 --natural-primary 选项
使用 --natural-primary 选项,该选项用于生成一个不包含每个导出对象的 "pk" 字段的 fixture。如果你有一个已经包含数据的系统,并且需要将数据追加到现有数据中,这将非常有用。假设主键被包含在内,它可能与具有相同主键但不是同一对象的现有对象冲突。这可能导致丢失或更改数据,从而产生不理想的结果。
按照以下步骤使用 --natural-primary 选项:
-
确保你位于项目的根目录中,即你的
manage.py文件所在的文件夹。然后,打开命令行窗口或终端,激活你的虚拟环境,但在此时不运行你的项目。 -
执行以下
dumpdata命令,指定chapter_3应用程序中找到的seller表:(virtual_env) PS > python manage.py dumpdata chapter_3.seller --natural-primary -o becoming_a_django_entdev/chapter_10/fixtures/chapter_3_sellers_natural_primary.json
应该在你的 /chapter_10/fixtures/ 文件夹中创建一个名为 chapter_3_sellers_natural_primary.json 的新文件。
- 在该文件中,格式化数据,以便你可以阅读其中的内容。如果你正在使用 Visual Studio,请在文档内部右键单击并选择 格式化文档,或者将数据复制并粘贴到此处找到的在线工具中:
jsonlint.com/。
现在,你应该看到与上一个子节完全相同的数据,只是所有 "pk" 字段都已从你的数据中删除,如下所示:

图 10.3 – dumpdata --natural-primary 选项
你也应该仍然看到所有 Vehicles 的数值外键值,如之前在 图 10.2 中所示。如果我们使用 --natural-primary 选项导出所有数据,这可能会引起问题。可能发生的情况是,在新数据库中创建了一个具有自己的主键的车辆,该主键与指定的外键不匹配。为了克服这个问题,我们还应该使用 --natural-foreign 选项,我们将在下一节中讨论。
使用 --natural-foreign 选项
--natural-foreign 选项将打印出所有相关对象的字符串表示形式,而不是该对象的数字外键值。我们还需要为所有相关对象编写一个新的模型类方法,以便在以这种方式使用时格式化和结构化该对象的字符串表示形式。输出可能与我们之前在 第三章,模型、关系和继承 中讨论的 __str__() 方法不同。
按照以下步骤将新的模型方法添加到你的 Vehicle 模型中,然后再次使用 --natural-foreign 选项导出 Seller 数据:
-
在你现有的
/chapter_3/models.py文件中,在现有的Vehicle模型类中,添加以下natural_key()方法:# /becoming_a_django_entdev/chapter_3/models.py from django.db import models ... class Vehicle(models.Model): ... def natural_key(self): return self.full_vehicle_name()
此方法依赖于在 第三章,模型、关系和继承 中创建的现有 full_vehicle_name() 方法,在标题为 自定义模型方法 的子节中。请确保在继续下一步之前,该方法存在于你的 Vehicle 模型类中。
-
确保你位于项目根目录中,即你的
manage.py文件所在的文件夹。然后,打开一个命令行窗口或终端,激活你的虚拟环境,但在此时尚未运行你的项目。 -
执行以下
dumpdata命令,指定chapter_3应用程序中找到的所有表:(virtual_env) PS > python manage.py dumpdata chapter_3.seller --natural-foreign -o becoming_a_django_entdev/chapter_10/fixtures/chapter_3_sellers_natural_foreign.json
应该在你的 /chapter_10/fixtures/ 文件夹中创建一个名为 chapter_3_sellers_natural_foreign.json 的新文件。
- 在该文件中,格式化数据,以便你可以阅读其中的内容。如果你使用 Visual Studio,在文档中右键单击并选择 格式化文档,或者将数据复制并粘贴到此处找到的在线工具中:
jsonlint.com/。
你现在应该看到类似于以下截图的内容,其中车辆列表不再由数字表示;现在它显示了由我们创建的 natural_key() 方法返回的字符串:

图 10.4 – dumpdata 的 --natural-foreign 选项
如果你看到重复的字符串条目,就像你在前面的截图中所看到的那样,这是因为自然键字符串表示形式使用了在这种情况下恰好具有相同值的数据模型,尽管它们是不同的对象。你可能想回去配置 natural_key() 方法,使其返回更独特的内容。
你可以为chapter_3应用中存在的每个模型创建一个natural_key()方法,然后再次运行这些命令的组合以进行练习。在本书提供的代码中,/chapter_10/fixtures/文件夹内有许多预先为你生成的不同固定文件,所有这些文件都使用了chapter_3.json固定文件中提供的初始数据。在/chapter_10/readme.md文件中,你可以找到一个命令列表,这些命令扩展了本章提供的示例。提供的每个命令都会生成一个不同的chapter_10固定文件。
注意
你可以将选项组合起来,例如在单个命令中结合--natural-foreign --natural-primary。这样做会产生图 10.4中所示的结果,但不会包含"pk"字段。
接下来,让我们通过使用loaddata Django 管理命令来练习导入数据。
从数据固定文件导入数据
使用loaddata Django 管理命令从固定文件导入数据。只要数据存在于以下四种文件格式之一,JSON、JSONL、XML 或 YAML,就可以使用此命令导入。即使数据不是从 Django 项目导出的,也可以导入数据。loaddata管理命令没有dumpdata命令那么多选项,但它们确实共享了大部分相同的选项。
我们一直在本书的大部分内容中使用此命令,以确保在处理前几章的练习时我们有可用的测试数据。我们不会深入探讨如何使用此命令的示例,而是简要地提醒自己如何使用它,然后描述每个可用的选项及其用途。
使用importdata命令
按照以下步骤练习加载本章 earlier 创建的/chapter_10/fixtures/chapter_3_sellers.json固定文件。如果我们成功,我们应该看到数据没有变化,因为我们正在导入相同的数据,覆盖了自身。如果你想在导入前练习更改字段值和/或添加新对象到文件中,以便在数据库管理工具中看到数据变化,你可以这样做:
-
确保你位于项目根目录中,即你的
manage.py文件所在的文件夹。然后,打开一个命令行窗口或终端,激活你的虚拟环境,但在此阶段不要运行你的项目。 -
执行以下
loaddata命令,告诉 Django 只加载chapter_3_sellers.json固定文件:chapter_3_sellers, and different file extensions. Because of this, you will have to include the file extension when executing your command. If you are using anything other than JSON file formats, don't forget to always include the --format option. If you do not have multiple file formats that share the same name, it is not necessary to include the file extension when using the loaddata command.
这里列出了每个可用的选项及其用途:
-
--app– 用于告诉 Django 只在该选项指定的应用目录中搜索固定文件,而不是在 Django 的每个应用目录中搜索。如果你有两个具有相同名称的固定文件存在于两个不同的 Django 应用目录中,这有时很重要。 -
--database– 告诉 Django 使用在您的settings.py文件中配置的数据库,而不是指定的默认数据库。Django 使用您提供的名称在settings.py文件中标识该数据库。这也可以与dumpdata命令一起使用。 -
--format– 用于告诉 Django 在导入数据文件时使用除默认 JSON 格式以外的格式。 -
--exclude,-e– 用于告诉 Django 在导入数据时省略提供的app_name或model_name。 -
--ignorenonexistent,-i– 用于省略自固定文件创建以来可能已删除的特定字段或模型。
接下来,让我们开始使用性能提升器。
使用 select_related() 方法
select_related() 方法用于查询所有相关 ForeignKey 和 OneToOneField 关系时的性能提升。此方法主要用于获取与父对象相关联的单个对象的数据。此方法不适用于 ManyToManyField 关系。在 SQL 层面上,此方法通常使用左外连接来查找相关数据。要了解有关 select_related() 方法的完整信息,请访问 docs.djangoproject.com/en/4.0/ref/models/querysets/#select-related。
在这里,我们将监控显示车辆列表及其每个车辆的详细信息(包括相关字段数据)的页面性能。使用以下子节来创建所需的视图类、模板和 URL 模式,以展示这一概念的实际应用。
创建视图
按照以下步骤创建您的 VehicleView 类:
-
在您的
/chapter_10/views.py文件中,添加以下VehiclesView类和import语句:# /becoming_a_django_entdev/chapter_10/views.py from django.http import Http404 from django.template.response import ( TemplateResponse ) from django.views.generic import View from ..chapter_3.models import Vehicle class VehiclesView(View): template_name = 'chapter_10/vehicles.html'
在这个视图类中,我们告诉 Django 使用我们即将创建的 /chapter_10/vehicles.html 文件作为模板。
-
将以下
get()方法添加到您的VehiclesView类中:# /becoming_a_django_entdev/chapter_10/views.py ... class VehiclesView(View): ... def get(self, request, *args, **kwargs): try: vehicles = Vehicle.objects.all() except Vehicle.DoesNotExist: raise Http404('No Vehicles Found') return TemplateResponse( request, self.template_name, {'vehicles': vehicles} )
为了解释这个 get() 方法的作用,我们在 Vehicle 模型对象上执行了一个 all() 查询。如果没有找到车辆,我们随后引发一个 Http404 未找到响应。如果找到车辆,我们随后返回一个 TemplateResponse,将车辆查询集作为上下文传递到要渲染的模板中。
注意
在 get() 方法中执行查询目前没有性能提升。跳到本章标题为 第一演示 的子节,以查看性能提升后的查询。
接下来,让我们构建模板。
构建模板
按照以下步骤创建您的车辆列表模板文件:
-
在
/chapter_10/templates/chapter_10/目录下创建一个名为vehicles.html的文件。在此文件中,添加以下代码:# /becoming_a_django_entdev/chapter_10/templates/chapter_10/vehicles.html {% load static chapter_4 %} <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <title>All Vehicles Page</title> </head> <body style="text-align:center" class="chapter_10"> <h1>All Vehicles</h1> </body> </html>
{% load %} 标签导入我们在 第四章 中创建的 templatetags 文件,即 URLs, Views, and Templates 小节下的 自定义标签和过滤器。如果你此时还没有创建 chapter_4.py 文件,只需从前面代码块中的 {% load %} 标签中移除 chapter_4,并移除 步骤 2 中的 |vehicle_make。
-
在同一文件中,在关闭
</body>标签之前,添加以下条件和for循环,这将用vehicles查询集中每个vehicle的信息填充你的页面:# /becoming_a_django_entdev/chapter_10/templates/chapter_10/vehicles.html ... <body ...> ... {% if vehicles %} {% for vehicle in vehicles %} <br /> <p>VIN #: {{ vehicle.vin }}</p> <p>Price: {{ vehicle.price }}</p> <p>Make: {{ vehicle.make|vehicle_make }}</p> <p>Model: {{ vehicle.vehicle_model }}</p> <p>Engine: {{ vehicle.engine }}</p> <p>Is Sold? {{ vehicle.sold }}</p> <br /><hr /> {% endfor %} {% endif %} </body> ...
前面的条件语句将检查 vehicles 查询集对象中是否有任何车辆。尽管我们在视图类中使用 try/except 语句来做这件事,但仍然在模板中这样做是个好习惯,以防视图类/方法中没有设置异常处理程序来检查对象未找到的情况。我们正在显示每个对象存在的所有字段数据。前面代码块中显示的 Engine 和 VehicleModel 数据是我们将监控其性能的相关对象。
让我们映射我们需要的下一个 URL 模式。
映射 URL 模式
按照以下步骤映射我们将用于访问此测试页面的 URL 模式:
-
在你的
/chapter_10/urls.py文件中,将以下路径添加到urlpatterns列表中:from django.urls import path from .views import VehiclesView urlpatterns = [ ... path( 'all-vehicles/', VehiclesView.as_view(), name = 'all-vehicles' ), ]
我们刚刚映射到 VehiclesView 类的路径将指向 URL http://localhost:8000/all-vehicles/。
接下来,让我们使用 DjDT 检查车辆列表页的性能。
第一次演示
按照以下步骤查看车辆列表页并使用 DjDT 检查其数据库查询性能:
-
确保你的项目正在虚拟环境中运行,并导航到 URL
http://localhost:8000/all-vehicles/。如果你使用的是chapter_3应用程序的fixture提供的数据,你应该看到至少七组车辆,每组车辆之间用一条实线分隔。 -
打开 DjDT 并查看 SQL 部分。你应该看到至少 15 个查询,如下面的截图所示:

图 10.5 – 不使用 select_related() 方法的查询
-
现在,在你的
/chapter_10/views.py文件中,在VehiclesView类的get()方法下,将你的查询更改为以下片段中突出显示的查询,其中我们向上次使用的查询中添加了select_related()方法:# /becoming_a_django_entdev/chapter_10/views.py ... class VehiclesView(View): ... def get(self, request, *args, **kwargs): try: vehicles=Vehicle.objects.select_related( 'vehicle_model', 'engine' ).all() ...
在 select_related() 方法中,我们告诉 Django 获取相关的 vehicle_model 和 engine 字段数据。
- 在 http://localhost:8000/all-vehicles/ 上刷新页面,并再次检查 DjDT 的 SQL 部分。这次,你应该只看到至少 1 个查询,如下面的截图所示:

图 10.6 – 使用 select_related() 方法进行查询
如我们所见,对于这组特定的数据,我们能够从这次搜索任务中减少大约 14 个 SQL 查询操作。这也减少了 10.16 毫秒的原始时间。虽然 10.16 毫秒看起来可能很小,但请记住,我们只有大约一打与这个特定数据集相关的记录;想象一下在包含几十万条记录的数据集中的差异。时间会累积起来。
当我们打开 LEFT OUTER JOIN 操作以获取所有相关对象时。

图 10.7 – 检查 select_related() 查询
让我们看看 prefetch_related() 方法接下来会做什么。
使用 prefetch_related() 方法
prefetch_related() 方法被用作与 ManyToManyField 关系相关的查询的性能提升器。此方法也可以用于 ForeignKey 和 OneToOneField 关系,并允许进行正向和反向查找,正如我们很快将要练习的那样。在 SQL 层面上,此方法通常使用 WHERE 或 INNER JOIN 语句来执行查找操作。与 select_related() 方法不同,prefetch_related() 方法将为每个相关对象集执行单独的 SQL 查询。例如,如果我们查找了一个 Seller 并想要相关的 Vehicles 以及它们的 VehicleModel 和 Engine 对象,那么 Django 将执行四个单独的查询来查找所有相关数据。要了解更多关于 prefetch_related() 方法的完整信息,请访问 docs.djangoproject.com/en/4.0/ref/models/querysets/#prefetch-related。
以下是与 车辆视图 和 卖家视图 相关的两个练习,用于练习以不同方式使用 prefetch_related() 方法。
车辆视图
在这个练习中,我们将修改本章 使用 select_related() 方法 部分中创建的现有 VehiclesView 类。在那个练习中,我们创建了一个页面,显示了系统中所有的车辆,然后提升了查找相关 VehicleModel 和 Engine 对象的性能。使用 prefetch_related() 方法,我们将查找相关的 Seller 对象以显示谁在销售那辆特定的车辆。
使用以下子节来为这次演示准备你的模板。视图和 URL 模式将与之前保持相同。
检查视图
保持现有的 VehiclesView 类与之前相同,它使用的是上次演示中的性能提升查询,如下所示:
# /becoming_a_django_entdev/chapter_10/views.py
...
Vehicles = Vehicle.objects.select_related(
'vehicle_model',
'engine'
).all()
...
我们很快就会修改它,但首先我们想要监控在模板中显示 seller 对象将如何改变我们现在拥有的性能提升查询。
修改模板
按照以下步骤修改你的车辆列表模板:
-
在你的
/chapter_10/vehicles.html文件中,在<br /><hr />行上方和最后一个vehicle详细信息项下方添加以下突出显示的代码:# /becoming_a_django_entdev/chapter_10/templates/chapter_10/vehicles.html ... {% if vehicles %} {% for vehicle in vehicles %} ... <p>Is Sold? {{ vehicle.sold }}</p> {% for seller in vehicle. vehicle_sellers.all %} {{ seller.username }} {% endfor %} <br /><hr /> {% endfor %} {% endif %} ... -
重要的是要注意,用于访问
seller的名称,如在vehicle.vehicle_sellers.all中的vehicle_sellers,是在Seller模型类的vehicles字段上设置的,使用related_name参数。确保在你的/chapter_3/models.py文件中,在Seller模型类下,vehicles字段使用以下片段中突出显示的参数和值:related_name or related_query_name argument of a field on a model class ever changes, you will need to rerun your Django migration commands once again to reflect those changes in your database.
让我们看看这如何改变我们的性能。
第二个演示
按照以下步骤查看这些更改如何影响 DjDT 中 SQL 选项卡的结果:
-
确保你的项目正在你的虚拟环境中运行,并导航到或刷新 URL
http://localhost:8000/all-vehicles/一次。 -
检查
select_related()方法。现在,我们至少看到 8 个查询,如下面的截图所示:

图 10.8 – 使用 select_related() 方法进行查询,显示相关卖家
在我们的搜索中现在看到了七个额外的查询,每个查询对应于与找到的七个车辆相关的每个卖家。
-
在你的
/chapter_10/views.py文件中,在相同的VehiclesView类中,将查询更改为以下代码片段所示:# /becoming_a_django_entdev/chapter_10/views.py ... Vehicles = Vehicle.objects.prefetch_related( 'vehicle_sellers' ).select_related( 'vehicle_model', 'engine' ).all() ...
我们刚刚将 prefetch_related('vehicle_sellers') 方法添加到之前的查询中,同时保留之前的 select_related() 操作。确保你在这个地方遵循正确的 Python 缩进。在上面的示例中,正确显示的空间有限。
- 再次刷新 URL
http://localhost:8000/all-vehicles/并再次检查 DjDT 的 SQL 部分。你现在应该至少看到 2 个查询,如下面的截图所示:

图 10.9 – 使用 select_related() 和 prefetch_related() 方法进行查询
如果我们检查添加了 INNER JOIN 和 WHERE 查找的 prefetch_related() 方法,如下面的截图所示:

图 10.10 – 检查 select_related() 和 prefetch_related() 查询
我们可以从之前的内容中看到,Django 在尝试显示页面上的 vehicle_sellers 对象的用户名时,对每个对象执行了额外的查找。这就是我们在将 {% for seller in vehicle.vehicle_sellers.all %} 循环添加到 /chapter_10/vehicles.html 模板文件后,最终得到八个查询的原因。当我们将 prefetch_related() 方法添加到 VehiclesView 类中的查询操作时,它只是添加了一个额外的查找操作,检索了与这个数据集相关的所有七个 vehicle_sellers 对象,从而我们现在有了两个查询,并减少了多余的查询。在您的模板文件中添加更多字段查找和使用上下文有时会增加查询次数。
接下来,让我们将 prefetch_related() 方法应用于卖家列表页面,并查看它在反向查找时的行为如何。
卖家视图
在这个练习中,我们将创建一个新的 URL 模式、视图类和模板,用于显示卖家及其所售车辆的相关列表。
使用以下子节来创建所需的视图类、模板和 URL 模式,以构建卖家列表页面。
创建视图
按照以下步骤创建您的 SellersView 类:
-
在您的
/chapter_10/views.py文件中,添加以下SellersView类和import语句:# /becoming_a_django_entdev/chapter_10/views.py from django.http import Http404 from django.template.response import ( TemplateResponse ) from django.views.generic import View from ..chapter_3.models import Seller, Vehicle class SellersView(View): template_name = 'chapter_10/sellers.html'
在这个视图类中,我们告诉 Django 使用 /chapter_10/sellers.html 文件作为模板,这是我们很快将要创建的。我们还在之前使用的相同导入基础上添加了 Seller 模型类作为新的导入。
-
将以下
get()方法添加到您的SellersView类中:# /becoming_a_django_entdev/chapter_10/views.py ... class SellersView(View): ... def get(self, request, *args, **kwargs): try: sellers = Seller.objects.all() except Seller.DoesNotExist: raise Http404('No Sellers Found') return TemplateResponse( request, self.template_name, {'sellers': sellers} )
get() 方法结构与 VehiclesView 类相同。唯一的区别是我们正在使用 Seller 模型类来执行查询。同样,这个查询目前还没有进行性能优化;我们将在测量这个查询操作后进行。
让我们接下来构建模板。
构建模板
按照以下步骤构建卖家列表页面模板:
-
在
/chapter_10/templates/chapter_10/目录下创建一个名为sellers.html的文件。在此文件中,添加以下代码:# /becoming_a_django_entdev/chapter_10/templates/chapter_10/sellers.html {% load static chapter_4 %} <html lang="en" xmlns="http://www.w3.org/1999/xhtml"> <head> <title>All Sellers Page</title> <style type="text/css"> ...Found With the Code of this Book... </style> </head> <body style="text-align:center" class="chapter_10"> <h1>All Sellers</h1> </body> </html>
{% load %} 标签导入了我们在 第 4 章 4 中创建的 templatetags 文件,在标题为 自定义标签和过滤器 的子节中。如果您在此时尚未创建 chapter_4.py 文件,只需从前面代码块中的 {% load %} 标签中删除 chapter_4,并在 步骤 3 中显示的位置删除 |vehicle_make。
本书中的代码还提供了额外的 CSS 类样式。您可以在与本书代码相同的文件中找到这些样式,并将它们复制粘贴到您的文档中,以便以更组织化的方式查看这些对象。这不是继续进行的必要步骤。
-
在关闭
</body>标签之前,添加以下条件和for循环,这将使用sellers查询集中的每个seller的信息填充你的页面:# /becoming_a_django_entdev/chapter_10/templates/chapter_10/sellers.html ... <body ...> ... {% if sellers %} {% for seller in sellers %} <p>First Name: {{ seller.first_name }}</p> <p>Last Name: {{ seller.last_name }}</p> <p>Username: {{ seller.username }}</p> <p>Business Name: {{ seller.name }}</p> <p>Email: {{ seller.email }}</p> <p>Last Login: {{ seller.last_login }}</p> <p>Date Joined: {{ seller.date_joined }}</p> <p>Is Staff? {{ seller.is_staff }}</p> <p>Is Active? {{ seller.is_active }}</p> <p>Is Superuser? {{ seller.is_superuser }}</p> <br /><hr /><br /> {% endfor %} {% endif %} </body> ... -
在
<br /><hr /><br />代码片段行之后,插入以下条件和for循环:# /becoming_a_django_entdev/chapter_10/templates/chapter_10/sellers.html ... {% for seller in sellers %} ... <p>Is Superuser? {{ seller.is_superuser }}</p> {% if seller.vehicles.all %} <h2>Seller Vehicles</h2> {% for vehicle in seller. vehicles.all %} <div class="vehicle-box"> <p>VIN #: {{ vehicle.vin }}</p> <p>Price: {{ vehicle.price }}</p> <p>Make: {{ vehicle.make |vehicle_make }}</p> <p>Model: {{ vehicle. vehicle_model }}</p> <p>Engine: {{ vehicle.engine }}</p> <p>Is Sold? {{ vehicle .sold }}</p> </div> {% endfor %} {% endif %} <br /><hr /><br /> {% endfor %} ...
这里的逻辑基本上与之前构建的车辆列表页面相同。我们在 sellers 循环内部添加了一个额外的循环层,该循环遍历每个 seller 相关的 vehicles。
让我们映射我们需要的下一个 URL 模式。
映射 URL 模式
执行以下步骤以映射我们将使用的访问此列表页面的 URL 模式。
在你的 /chapter_10/urls.py 文件中,将以下路径添加到你的 urlpatterns 列表中:
from django.urls import path
from .views import SellersView, VehiclesView
urlpatterns = [
...
path(
'all-sellers/',
SellersView.as_view(),
name = 'all-sellers'
)
]
我们刚刚映射到 SellersView 类的路径将指向 URL http://localhost:8000/all-sellers/。
让我们使用 DjDT 检查卖家列表页面的性能。
第三次演示
按照以下步骤查看卖家列表页面,并使用 DjDT 检查其数据库查询性能:
- 确保你的项目正在你的虚拟环境中运行,并导航到 URL http://localhost:8000/all-sellers/。如果你使用的是
chapter_3应用程序固件提供的数据,你应该看到至少一组seller数据和第一个卖家的七辆车。
如果你一直跟随这本书学习,你应该在你的数据库中有大约三个 sellers,这是由于之前章节中执行练习的结果。chapter_3 固件中只提供了一个卖家。对于以下步骤中显示的性能结果,假设你有三个 sellers 和七辆车,因为你的实际数据可能会有所不同。
- 打开 DjDT 并查看 SQL 选项卡。你应该看到至少 19 个查询,如下面的截图所示:

图 10.11 – 卖家列表页面未进行性能优化
因为 Django 一直在重复查找每一辆车及其相关的 vehicle_model 和 engine,所以执行了如此多的查询。我们编写的 {% if seller.vehicles.all %} 条件,检查是否存在 vehicles,并且还添加了一个查询到页面性能中。
-
现在,在你的
/chapter_10/views.py文件中,在SellersView类的get()方法下,将查询更改为以下代码片段中突出显示的查询,我们在之前的基础上添加了prefetch_related()方法:# /becoming_a_django_entdev/chapter_10/views.py ... class SellersView(View): ... def get(self, request, *args, **kwargs): try: sellers = Seller.objects.prefetch_related( 'vehicles', 'vehicles__vehicle_model', 'vehicles__engine' ).all() ...
在 prefetch_related() 方法中,我们告诉 Django 获取相关的 vehicles,然后,在单独的操作中,获取每个 vehicle 对象的相关 vehicle_model 和 engine。每当我们需要指定其他相关字段时,就像我们在前面的代码块中所做的那样,我们使用双下划线 __ 来在一系列关系中向上或向下导航。请确保你遵循了正确的 Python 缩进,这里有限的空间来正确显示之前示例中的内容。
- 请再次刷新 URL http://localhost:8000/all-sellers/ 并再次检查 DjDT 的 SQL 选项卡。你现在应该至少看到 4 个查询,如下面的截图所示:

图 10.12 – 卖家列表页面性能提升
我们还使用性能提升的方法在这个查询查找任务中节省了 6.95 毫秒的时间。如果我们检查 Seller 对象,一个用于 Vehicle 对象,一个用于 VehicleModel 对象,最后一个用于 Engine 对象。Django 使用 WHERE 子句和 INNER JOIN 操作的组合来检索相关数据。

图 10.13 – 检查卖家列表页面查询
Django 还提供了一个 Prefetch() 类,可以用来执行具有性能提升能力的更复杂的查询。接下来,让我们使用这个类来对与 seller 相关的 vehicles 进行高级过滤。
使用 Prefetch() 类
在 django.db.models 库中提供的 Prefetch() 类用于控制 prefetch_related() 操作的执行方式。例如,我们将使用它来过滤并仅显示等于 "Blazer LT" 的 VehicleModel 的 vehicles。我们还可以在以这种方式进行过滤时预取所有相关对象。要深入了解如何使用此类,请访问 docs.djangoproject.com/en/4.0/ref/models/querysets/#prefetch-objects。
使用以下子节来准备你的视图类和模板以进行此演示。URL 模式将与本章 Sellers 视图 子节中找到的演示相同。
修改视图
按照以下步骤修改你的现有 SellersView 类以进行下一个练习:
-
在你的
/chapter_10/views.py文件中,添加以下import语句,最好在现有的import语句之前:# /becoming_a_django_entdev/chapter_10/views.py from django.db.models import Prefetch ... -
在此文件中找到的
SellersView类中,将你的查询语句更改为以下代码片段:# /becoming_a_django_entdev/chapter_10/views.py ... class SellersView(View): ... sellers = Seller.objects.prefetch_related( Prefetch( 'vehicles', to_attr = 'filtered_vehicles', queryset = Vehicle.objects.filter( vehicle_model__name = 'Blazer LT' ) ), 'filtered_vehicles__vehicle_model', 'filtered_vehicles__engine' ).all() ...
请确保你遵循了正确的 Python 缩进,这里有限的空间来正确显示之前示例中的内容。
我们编写的查询语句与之前写的类似。这次我们不同的地方是将Prefetch()类放在prefetch_related()方法的第一个参数中。Prefetch()类本身接受三个参数。第一个是查找参数,通常是一个字段,但也可以使用双下划线__在字符串中遍历上下游关系。第二个和第三个参数是可选的,不需要按确切顺序排列。to_attr参数用于将结果 QuerySet 存储为具有指定名称的对象列表。QuerySet 参数用于对那些项目的子集执行特定查询。在先前的示例中,我们执行了filter()操作,仅搜索具有vehicle_model__name为"Blazer LT"的车辆。
在Prefetch()类之后,在之前使用的prefetch_related()方法中,我们添加了两个额外的字段查找,分别是filtered_vehicles__vehicle_model和filtered_vehicles__engine对象。这将预取与我们刚刚创建的定制过滤列表相关的相关对象。
接下来,我们需要修改现有的模板文件,以便与filtered_vehicles对象列表一起工作。
修改模板
按照以下步骤修改现有的卖家列表模板文件,以便与创建的filtered_vehicles列表一起使用:
-
在你现有的
/chapter_10/sellers.html文件中,将使用seller.vehicles.all的两行更改为现在使用seller.filtered_vehicles,如下所示:# /becoming_a_django_entdev/chapter_10/templates/chapter_10/sellers.html ... {% for seller in sellers %} ... {% if seller.filtered_vehicles %} <h2>Seller Vehicles</h2> {% for vehicle in seller.filtered_vehicles %} ... {% endfor %} {% endif %} <br /><hr /><br /> {% endfor %} ...
这就是我们需要修改的所有内容。让我们看看这如何影响我们的性能。
第四个演示
按照以下步骤查看卖家列表页面,并使用Prefetch()类方法检查其数据库查询性能:
-
在浏览器中刷新 URL http://localhost:8000/all-sellers/。
-
打开 DjDT 并查看SQL标签。你应该看到至少4 个查询,如下面的截图所示:

图 10.14 – 使用 Prefetch 类的卖家列表页面
这应该也是我们在之前的示例中看到的相同数量的查询,如图 10.12 所示。在这个数据集中,唯一的区别是我们看到页面上的第一个卖家下有五个vehicles,而之前显示的是七个。现在结果只显示Blazer LT车辆,如下面的截图所示:

图 10.15 – 使用 Prefetch 类的卖家列表页面结果
虽然结果数量可能不同,但执行的查询数量保持不变。使用这种方法,我们可以定义一个非常细粒度的搜索查询,并且性能得到提升。
摘要
我们通过学习如何导入和导出数据以及将性能提升技巧应用于所有查询操作,得以完成如何使用 Django 构建企业级系统的旅程。了解如何处理数据与构建它们所在的数据孤岛一样重要。当你与现有系统合作时,总有从旧系统导出现有数据并将其导入新系统的需求。我们现在知道了如何做到这一点。我们还可以将这项知识与在第二章,“项目配置”,标题为“Heroku 数据库推送/拉取操作”的子节中学到的技能结合起来,以便在远程测试和生产环境中处理数据。在整个项目生命周期中,根据需要使用每个工具来执行不同的任务。
本章介绍的性能提升方法旨在应用于任何查询操作。参考第三章“模型、关系和继承”中讨论的主题,了解如何构建你的数据以及执行其他查询操作。在同一章节中,你还可以在模型管理器内部查询时应用性能提升器。
这本书没有涵盖与 Django 世界相关的丰富知识、技巧和窍门。我希望你喜欢到目前为止所学的阅读和实践内容,并希望你能继续你的学习之旅,学习更多。利用书中散布的链接、资源和包及工具的引用来扩展你的学习范围。如果你有任何问题,想要指出本书中的错误,或者分享赞美之词,请随时通过提供的任何联系方式联系我。虽然我投入了大量的时间和精力来撰写这本书并检查我的工作,但我们都是凡人,错误难免会出错。我也很乐意听到你阅读这本书后所创造的奇迹。感谢大家抽出时间阅读这本书!



浙公网安备 33010602011771号