Django2 和 Channel2 实践教程(一)
一、Django 简介
在过去的十年里,互联网对我们社会的影响非常深刻。它改变了一切,从我们社交和工作的方式到我们约会和度假的方式。互联网已经从让我们能够虚拟旅行并与大洋彼岸的人们联系的工具,扩展到让我们能够实际旅行的工具,包括打车服务,这是它变得多么有影响力的一个主要例子。
这一趋势没有停止的迹象。未来可能会更有影响力。如今,传统机构不仅需要在线业务,还需要出色的在线业务,为客户提供在线和线下同样的服务。不这样做的传统机构最终会被以互联网为骨干的更精简的组织所瓦解。
用户生成的数据也越来越普遍。在线用户评论现在是决定如何花费我们的时间和金钱的一个关键因素。在线社区数量众多且欣欣向荣。最近,机器学习取得了很多进展,如果没有网络作为一种为这些系统生成数据的方式,这一切都不会发生。
作为软件工程师,我们如何参与所有这些激动人心的活动?传统的计算机科学教育只会给你基础知识。考虑到用户期望和技术工具发展的速度,这种差距是不可避免的。但是,考虑到网上有那么多工具和教程,如何弥合这种差距并不明显。
如果您正在阅读这本书,我假设您熟悉 Python,并且已经选择使用它进行 web 开发。Python 是一种非常流行的编程语言。它是一种解释性语言,具有动态、强类型系统。在我看来(可能也是你的),它在速度和安全之间取得了很好的平衡。Django 是用 Python 编写的,它是使用最广泛的 Python web 框架,尽管不是唯一的。
Django 的结构使得创建复杂的、数据库驱动的网站变得尽可能容易。它提供了许多组件,这些组件都经过了实战测试,可以根据您的使用情况进行定制。现代网站中有许多常见的模式可以重用,本着实用的精神,这样做是明智的。
页状构造
这本书将使用大量实际例子来解释 Django。我选择电子商务作为整本书的主题,因为现在网上购物非常普遍,因此你可能已经知道术语和这些网站是如何构建的。
本书的每一章都将介绍足够理解和建立一个电子商务网站的概念。我们将从基础、一些演示页面开始,展示产品、结账流程和更多高级功能。这几章是我个人电子商务经验和用来以越来越复杂的方式介绍 Django 组件的起点之间的折衷。
这不会是一本电子商务圣经;这不是本书的目的。这本书的目的是让你在熟悉概念的帮助下学习。我希望它能成功地帮助你这样做,我期待着听到你的反馈。
Django 是什么
Django 是一个设计用于协同工作的组件集合,它们涵盖了 web 开发的许多公共领域。当你在网上浏览时,你会遇到许多不同的网站,表面上看起来不同,但实际上,它们包含相同的模式。
每个网站都提供 HTML(表示网站内容的语言)、CSS(声明内容呈现方式的语言)、图像等等。
大多数网站都可以登录到一个私人区域,在那里你可以看到为用户做的定制和可能不公开的数据。Django 用它的组件支持所有这些。
Django 的一个重要方面是,它的许多组件都是独立的,因此可以单独使用,而不必加载所有组件。也就是说,一些更高级别的组件利用了现有的组件。
Django 框架诞生于堪萨斯州劳伦斯市出版的《劳伦斯世界日报》的办公室。你可以在 Django 网站的在线教程中看到这种影响。它提供了许多按日期和时间过滤的内容视图。诚然,在第一个版本中,设计者(阿德里安·霍洛瓦蒂和西蒙·威廉森)可能专注于解决特定于报纸的问题,但如今该框架能够处理任何领域。
我个人与 Django 有多年的交往。我在许多领域使用过它——从可视化财务数据、创建在线医疗问卷、建立电子商务商店,到抓取仪表盘、在线目录和商业智能工具——并且一直认为它的价值无可置疑。每次我使用它,它都让我专注于业务问题,而不是重新发明轮子。
Django 不是一个微观框架;它带有行李。如果你的在线业务增长到有多个工程团队在软件的不同领域工作,并且有非常具体的需求,Django 可能不再适合你。但是对于大多数网站来说,它工作得很好。
Django 是一个遵循模型-模板-视图模式的框架,这与许多其他框架的模型-视图-控制器模式非常相似。模型层代表数据库层,用于数据存储。Django 将您从编写 SQL 查询中抽象出来。不使用 SQL,而是使用 Python 对象,加载/保存操作是为您处理的。
模板层负责编写 HTML 响应。它允许您将 HTML 分成几个部分,将这些部分合并到多个页面中,进行动态转换和数据生成,以及许多其他操作。
视图层位于数据库和 HTML 之间,通常是应用中最大的一层。它是大多数业务逻辑所在的地方。它负责告诉模板层要生成什么输出,将需要返回的数据传递给用户,处理表单提交,并与模型层接口以持久化数据。
领域驱动设计
领域驱动设计(DDD)是一种编写软件的技术。它关注领域,领域是软件需要建模和支持的知识或活动。在开发过程中,来自该领域的概念将在业务专家和工程师的帮助下进行整合。
Django 能带来的是专注。由于工程师可以利用现有代码实现许多非特定领域的功能,因此交付特性通常会更快。
Django 有 DDD 教的一些概念。实体、存储库、集合、值对象在某种程度上类似于 ORM 模型、管理器或 HTTP 请求对象的抽象。Django 缺少的代码是您的业务特有的代码,这正是 DDD 可以提供帮助的地方。
开始前你需要什么
在进入 Django 之前,您需要确保安装了一些工具,包括 Python、数据库和 Pipenv。
计算机编程语言
Django 2.0 和所有后续版本都需要 Python 3。Django 2.0 特别要求至少安装 Python 3.4。因此,你首先需要检查的是 Python 是否安装,版本是否正确。如果您还没有这样做,请到 Python 主站点的下载页面( www.python.org/downloads )下载最新的 3.x 版本。在撰写本文时,Python 3.7 已经推出,并且得到了 Django 2 的支持。
如果你用的是 Linux 或者 macOS,不用从官方网站下载,你可以用你的 OS 包管理器下载 Python。如果操作系统存储库中有 Python 的最新版本,我建议您通过这种方法安装它。它更干净,因为它将检查冲突,并且,如果需要,它使卸载 Python 更容易。
如果你不确定,我建议你在网上搜索如何安装 Python 3 的教程,最好使用你的发布工具,比如brew、apt或者yum。
数据库ˌ资料库
虽然不是绝对必要的,但我推荐安装的第二个东西是数据库。Django 可以使用 SQLite,这是一个嵌入式数据库引擎,但是不建议在生产环境中使用,因为这不是它的目的。我建议你从一开始就使用你在网上发布作品时会用到的工具。通过使用相同的工具,您将降低只出现在产品中的错误的几率。
谈到数据库,您有几个选择。最常用的都是开源的:PostgreSQL 和 MySQL。从历史上看,它们经历了不同的演变。PostgreSQL 更注重提供许多特性,而 MySQL 更注重核心功能的性能。如今,两者之间没有明确的赢家。
然而,在 Django 社区,人们更喜欢 PostgreSQL。一些核心提交者已经创建了依赖于标准 SQL 上的 PostgreSQL 扩展的 Django contrib包(虽然它们被包含在内,但不被认为是核心的)。除此之外,Heroku 提供了一个 PostgreSQL 免费层,对我们来说很方便,Heroku 是主机提供商,将在关于生产部署的章节中使用(第十章)。
Pipenv
Pipenv 是一个管理 Python 包的新工具,是这一系列工具中最新的一个。与其前身 Pip 不同,Pipenv 与 Python virtualenv有着更紧密的集成。它还会自动跟踪您安装了什么,并为您锁定版本,因此您总是知道哪些 Python 库正在运行。
为了进行跟踪,Pipenv 生成两个文件。Pipfile是一个文件,其中列出了项目的所有直接依赖项。它还生成另一个名为Pipfile.lock的文件,其中列出了所有依赖项的所有版本和散列。后一个文件用于生成确定性的构建,这意味着 Pipenv 每次都能准确地复制环境。它也不意味着手动编辑。
您可以使用操作系统软件包管理器或使用 Pip 来安装 Pipenv。就我个人而言,我总是更喜欢 OS 软件包管理器,但鉴于 Pipenv 是一个年轻的项目,在这一点上,我建议使用 Pip。
如果你喜欢坚持使用标准的pip,你没有理由不使用它。Python 包格式是相同的。我将在本书中使用pipenv作为参考,但同样可以使用pip。
入门指南
第一步是安装 Django。我们将在接下来的章节中继续使用这个项目,所以我们需要一个好的项目名称。因为我们要卖书,所以将这个项目命名为 BookTime。转到您的个人文件夹(或您想要的任何文件夹)并键入以下内容:
$ mkdir booktime
$ cd booktime
$ pipenv --three install Django
我们现在有了一个安装了 Django 的环境。因为 virtualenvs 在使用前需要激活,所以我们必须这样做:
$ pipenv shell
现在它是活动的,我们将创建项目的初始框架。我们将重新使用之前选择的名称,因为这是我们将在配置文件和文件夹结构中使用的名称:
$ django-admin startproject booktime .
此时,我们应该有一个初始文件夹结构(在当前文件夹中),如下所示:
-
manage.py:允许您与 Django 项目交互的命令行实用程序。在整本书中,你会非常频繁地用到它。 -
booktime:包含每个 Django 项目需要的文件的 Python 包,这些文件是-
booktime/__init__.py:这是一个空文件,只需要使其他文件可以导入。 -
booktime/settings.py:这个文件包含了我们项目的所有配置,可以随意定制。 -
booktime/urls.py:这个文件包含了所有到 Python 函数的 URL 映射。项目需要处理的任何 URL 都必须在这里有一个条目。 -
这是将我们的站点部署到生产环境时将使用的入口点。
-
-
Pipfile:项目正在使用的 Python 库列表。在这一点上只有 Django。 -
Pipfile.lock:内部 Pipenv 文件。
启动开发服务器
此时,我们应该能够看到 Django 的初始网页。为此,我们需要启动开发服务器,如下所示。这就是我们将在本书中看到的任何编码的结果。
$ ./manage.py runserver
如果一切正常,您将看到如下输出:
Performing system checks...
System check identified no issues (0 silenced).
You have 14 unapplied migration(s). Your project may not ...
Run 'python manage.py migrate' to apply them.
March 19, 2018 - 18:16:54
Django version 2.0.3, using settings 'booktime.settings'
Starting development server at http://127.0.0.1:8000/ Quit the server with CONTROL-C.
在 Django 中开发时,您将广泛地使用和重用这个命令,所以不要忘记它!
如果您导航到输出中显示的链接,您将看到类似于图 1-1 所示的初始页面。这是一个临时页面。我们的目标将是改变这是我们的项目。
图 1-1
Django 初始页面
Django 项目与应用
如果你在网上读过任何关于 Django 的介绍,你应该已经看到了这两个概念。Django 项目是我们刚刚创建的东西。例如,它是一个完整的网站。
Django 应用代表了一个独立的网站部分。例如,当考虑电子商务时,订单管理可以被认为是相当独立的。然而,实际上,由于不断变化的需求,很难预见哪些部门在未来会有帮助,哪些部门会由于认知开销而减缓开发。
现在,我们将开始创建一个“主”应用,如下所示,它将包含我们构建的所有内容。在开发过程中,将会清楚是否有足够有界的上下文值得更清晰的分离。
$ ./manage.py startapp main
下面将创建一个名为main的附加文件夹,以及上一节列出的所有现有文件。
main/:
admin.py apps.py __init__.py migrations models.py tests.py views.py
main/migrations:
__init__.py
上面列出的每个文件都使用了相应的同名 Django 组件。在整本书中,我们将逐一介绍它们。除此之外,我们将在书中做的大部分编辑都将在这个文件夹中完成;所以,只提文件名的时候,文件就会在这个main文件夹里。
设置
Django 的许多部分可以配置,或者需要配置,这取决于组件。当您稍早创建 Django 项目时,一个名为settings.py的文件被初始化。这是一个定义了许多常量的 Python 文件。这里所有大写的都被认为是配置,并且通过简单地导入django.conf.settings就可以在项目的每个部分使用。
向该文件添加 Python 代码时请小心。这不是添加函数或复杂代码的地方。这个文件的加载需要保持快速,所以请记住将代码的复杂性限制在不超过赋值操作。
另一件要避免的事情是在运行时改变它的任何内容。每次你在settings.py中改变任何东西,应用都应该被重新部署(或者至少重新启动)。
下面是您在继续操作之前应该知道的最重要的配置变量。在说 Django 之前,把这些看作是你应该知道的一些基本单词。
调试
这是一个打开/关闭调试模式的布尔值。调试模式在开发过程中提供了许多额外的信息。当我们编写最终产品时,我们将不可避免地编写一些不工作的代码。我们的代码中会有错误,调试模式非常有用,可以告诉我们在代码的什么地方触发了错误条件。
当出现错误时,将会出现一个调试页面,而不是我们期望的浏览器输出。这个页面将可视化失败的代码,并提供大量的上下文信息,比如当前变量、HTTP 请求信息、当前数据库、会话信息等等。
图 1-2 显示了在我写了一些试图将一个整数除以零的代码之后的调试页面的例子。
图 1-2
决哥调试页
正如您所看到的,调试页面顶部的第一件事是异常类型 ZeroDivisionError,后面是一些基本信息。第二个也是最有用的部分是回溯部分。
除了您在命令行中使用traceback看到的内容之外,就像您在命令行中使用 Python 一样,在调试页面上,您还可以单击并查看所有帧的所有局部变量。
在 Traceback 部分之后,有一个很长的部分,包含所有 HTTP 请求变量、所有元信息(运行开发服务器的 shell 的环境变量)和所有当前的 Django 设置。
已安装的应用
这是在启动时加载的应用列表,既有 Django 内部的,也有外部库的。Django 将初始化它们,加载和管理它们的模型,并使它们在应用注册中心可用。
对于只有一个“主”应用的 Django 项目,它应该是这样的:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'main.apps.MainConfig',
]
过去您可以只输入main,现在您应该指定AppConfig子类。通常只有一个,并且已经由startapp命令为你创建好了,它位于 Django 应用的apps.py文件中。
正如我们所看到的,Django 本身有一些可以随意启用和禁用的应用。例如,如果我们对消息框架不感兴趣,我们可以通过从列表中删除条目来禁用它。
记录
日志记录是应用的基础部分。日志记录的目的是在问题发生时节省时间,为了做到这一点,您需要能够跟踪运行时发生了什么。日志对于开发和生产站点都很重要。
如果您不熟悉 Python 中日志记录的概念,我建议您查看日志记录模块文档。在整本书中,我们将使用 logger 对象来记录正在发生的事情,并且我们将使用适当的日志级别。
首先,这个日志 Django 设置是关于使用logging.config.dictConfig()配置日志系统的。如果您想了解这是如何构成的,请考虑以下示例:
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
},
'loggers': {
'main': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': True,
},
'booktime': {
'handlers': ['console'],
'level': 'DEBUG', 'propagate': True,
},
},
}
这将打印来自名为booktime和main的记录器的任何记录语句。以 Python 模块名命名日志记录器是常见的做法,这是我们将使用的约定。
不管配置如何,配置可以快速改变,但是很难将日志记录实践改进到现有项目中。我们将在向项目添加代码时添加日志记录。
静态根目录/静态 URL
每个站点都需要一种方式来提供静态内容,无论是图像、CSS、JavaScript 等等。开发和生产环境之间的工作方式是不同的。在开发模式中,您放在 Django 应用的static文件夹中的所有东西(在我们的例子中是main)都将在STATIC_URL下自动可用。这是该环境中唯一重要的配置变量。
对于性能和安全性更为重要的生产环境,Django 不会服务于静态资产。相反,Django 将通过在一个唯一的目录中收集所有静态文件来支持更有效的 HTTP 服务器(如 Nginx)或类似的服务器。这个唯一的目录是在STATIC_ROOT中指定的目录。
媒体根目录/媒体 URL
Django 区分了与 Django 项目捆绑在一起的静态内容和由网站用户上传的内容。用户生成的内容是单独管理的,其配置类似于静态文件的配置。MEDIA_ROOT是本地驱动器上所有用户文件上传的位置。所有这些文件也将自动可供下载,它们的 URL 将以MEDIA_URL为前缀。
上一节中的推理同样适用于这里:如果您配置开发服务器,它将向您提供媒体文件,但是在生产环境中不建议这样做。稍后将对此进行更多介绍。
从现在开始,我们将使用以下配置:
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
中间件
中间件是 Django 的一个强大特性。它允许您插入额外的代码,这些代码将在 HTTP 请求/响应周期的特定点执行。这方面的一个例子是SessionMiddleware,它用于将会话与用户相关联。
指定中间件组件的顺序很重要。一些中间件组件依赖于其他一些组件的结果,它们只能在一个方向上工作。
中间件组件的例子将在本书的后面介绍。就目前而言,了解什么是最重要的就足够了。
SessionMiddleware和AuthenticationMiddleware为有用户概念的项目提供基本功能。如果网站上没有用户自定义,您可以删除它们。
如果您想在视图层使用缓存,您可能想看看缓存中间件。如果您将提供多种语言的内容,LocaleMiddleware类将在客户机和服务器之间进行语言协商。还有很多其他的;更多细节请看 Django 中间件文档。
模板
该变量用于配置 Django 的模板引擎。如果您没有特定的互操作性需求,您可能会继续使用 Django 模板系统。在这种情况下,您可以配置一些东西。我平时改动最多的设置是context_processors。
上下文处理器是在模板范围内注入额外变量的一种方式。通过这样做,您将不必在需要这些变量的每个视图中都这样做。
一个很好的例子是在settings.py中将一些值定义为常量,并在模板中使用。在这种情况下,一种方便的方法是定义一个上下文处理器,从全局配置中读取它们,并将它们推送到模板中。这方面的一个例子将在本书的第十章中介绍。
数据库
您的项目可能会使用数据库来存储数据。使用 Django 而不是较小的框架的一个主要好处是,这方面完全是为您管理的。这是使用 PostgreSQL 进行存储的初始配置:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydatabase',
'USER': 'mydatabaseuser',
'PASSWORD': 'mypassword',
'HOST': '127.0.0.1',
'PORT': '5432',
}
}
请记住,用您在配置数据库时选择的细节替换上面的细节。如果您继续进行此配置,安装 PostgreSQL 驱动程序是很重要的。在Pipfile的同一个目录中,您需要运行pipenv install psycopg2,其示例输出如下所示:
Installing psycopg2...
Collecting psycopg2
Downloading psycopg2-2.7.4-cp36-cp36m-manylinux1_x86_64.whl(2.7MB)
Installing collected packages: psycopg2
Successfully installed psycopg2-2.7.4
Adding psycopg2 to Pipfile's [packages]...
Pipfile.lock (374a8f) out of date, updating to (843434)...
Locking [dev-packages] dependencies...
Locking [packages] dependencies...
Updated Pipfile.lock (843434)!
Installing dependencies from Pipfile.lock (843434)...
如果您现在不想安装 PostgreSQL,您可以随时使用 SQLite。在这种情况下,您不需要安装额外的驱动程序:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'mydatabase',
}
}
但是,如前所述,不建议将 SQLite 用于生产。
电子邮件 _ 后端
Django 有一个发送电子邮件的图书馆。它提供了几个后端,这是它的配置。一般来说,我使用控制台后端进行开发,使用 SMTP 后端进行生产。
在开发过程中,使用控制台后端允许您查看电子邮件,而不用担心电子邮件被您指定为接收者的人接收。在生产中,这将是您想要的行为。
秘密密钥
这是在安全敏感的上下文中使用的随机字符串。目前,知道这需要保密就足够了。
基于类和函数的视图
历史上,Django 使用函数来表示视图。基于函数的视图(FBV)只是一个 Python 函数,它将一个request object作为参数,并返回一个响应对象。当请求一个特定的 URL 时,这个函数将被调用。
很长一段时间,当其他 Python 框架在使用类的时候,Django 一直在使用函数。函数的美妙之处在于它们是最小的。没有隐藏的行为——所见即所得。可以用 decorators 添加额外的行为,但是代码仍然保持可读性。
Django 1.7 中引入了基于类的视图(cbv)。这种添加背后的原因是,有了文档化的结构和继承,开发人员将能够定制基于类的视图,而不是基于函数的视图。
对于基于函数的视图,您必须添加代码来处理您想要的所有行为,而对于基于类的视图,方法会有所不同。Django 提供了许多 cbv 作为进一步定制的起点,您可以使用继承来实现。
可以想象,方法是完全不同的。函数是纯加法的。您需要添加代码来添加行为。另一方面,对于 CBVs,您可能必须添加代码来更改默认行为,或者添加代码来禁用扩展。
基于函数的视图和基于类的视图之间没有明显的赢家。如果您想编写一个不像 Django 中的 CBV 那样的视图,请使用基于函数的视图。如果您正在修改的视图代码变得超出了 CBV 的目的,也许是时候重新考虑用基于函数的视图来编写它了。
总是使用你的判断是否你应该写一个函数或一个类。在本书中,我将介绍这两种情况。如果在任何时候你意识到你用 cbv 构建了你的代码,这使得代码难以阅读,请记住拥有自动化测试将使你能够以有限的风险改变 FBV 的视图。
测试
在完成开发后,我们应该渴望向世界展示我们的工作,如果我们关心其他人使用我们的软件,它需要工作。为了确保它能正常工作,在 web 开发中,你只需要打开你的浏览器并检查它。如果你对互动感到满意,你会把它标记为完成。
自动化测试是介于“它有效”和“它总是有效”之间的东西如果你想让人们依赖你的软件,你需要确保任何新的修改不会破坏现有的功能。虽然理论上您可以手动完成,但实际上您很少在每次发布时手动测试所有的东西。
除了添加功能,自动化测试是确信你的新版本不会破坏网站关键部分的唯一方法。类似地,自动化测试是确保你的重构没有出错的唯一方法。
测试很容易做到,但是要让它发挥作用,它需要成为我们编码程序的一部分。重要的是,我们的测试要覆盖尽可能多的代码,并且足够细粒度,以便在出现问题时能够告诉我们问题的确切位置。
在本书中,我们将使用标准的unittest包。使用这个包编写的测试适用于每一个测试运行者。除了 Python 默认的测试运行器之外,还有其他的测试运行器,比如pytest,但它们不会在本书中讨论。就我们的目的而言,标准方式对我们来说已经足够好了。
摘要
这一章阐述了 Django 的基础知识:它是什么以及安装和使用它需要什么。建议该项目安装 Python 3.7 和 PostgreSQL 10.5。
我们已经讨论了 Django 最重要的设置和如何配置它们,以及一些其他的主题,比如不同类型的视图和测试方法,我们将贯穿全书。
在下一章中,我们将从一个用例开始,一个虚构的销售书籍的公司网站。
二、从简单的公司网站开始
在这一章中,我们将开始建立网站的第一页。这些页面将提供网站应有的基本功能,如展示页面和收集线索的表单。
我们将讨论
-
基本模板
-
提供模板
-
服务于 CSS 和 JS
-
如何添加 URL
-
创建发送电子邮件的表单
从主页开始
我们要做的第一步是为我们的网站写主页。它将为我们提供一些具体的开始,我们将利用这个机会介绍一些 Django 概念。
为了提供主页,我们将从在模板中编写 HTML 开始。我们将使用在第一章中创建的应用。我们需要将templates文件夹添加到其中:
$ mkdir main/templates/
为了从比基本 HTML 更时尚的东西开始,我们将使用 Bootstrap 实现一个基本模板。Bootstrap 是一个前端框架,它为我们提供了一套良好的风格和组件。如果您不在乎,可以忽略这一点,但是本着使用框架的精神,我们将把它作为一个例子。
让我们给main/templates/home.html添加一些 HTML:
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
<script
src="https://code.jquery.com/jquery-3.2.1.slim.min.js">
</script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js">
</script>
<script
src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js">
</script>
</body>
</html>
这只是 Bootstrap 文档中的一个“Hello world”示例。它的内容并不重要,因为我们很快就会改变它,但这是一个开始!
这个页面还不可见,因为它需要一个 URL。考虑到这是主页,我们希望这个页面能够满足任何对/的请求。为此,我们需要配置 URL 路由层,将它添加到booktime/urls.py:
from django.contrib import admin
from django.urls import path
from django.views.generic import TemplateView
urlpatterns = [
path('admin/', admin.site.urls),
path(", TemplateView.as_view(template_name="home.html")),
]
现在我们终于可以进入浏览器并看到我们的结果了,如图 2-1 所示。
图 2-1
你好,世界!例子
这足以看到页面,但我们还有两个问题要解决:
-
文件从外部内容交付网络(CDN)加载,这可能对生产站点有好处,但在开发中,我们希望能够完全离线工作。
-
它没有自动测试。
我们的存储库中需要 Bootstrap 的副本。为了快速做到这一点,我们可以从 HTML 中已经有的链接下载。我们可以使用curl(或wget)来完成这项任务:
$ mkdir main/static/
$ mkdir main/static/css/
$ mkdir main/static/js/
$ curl -o main/static/css/bootstrap.min.css \ https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css
...
$ curl -o main/static/js/jquery.min.js \ https://code.jquery.com/jquery-3.2.1.slim.min.js
...
$ curl -o main/static/js/popper.min.js \ https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js
...
$ curl -o main/static/js/bootstrap.min.js \ https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js
...
所有链接的资产现在都可以脱机使用。最后要做的是用本地链接替换外部网站的链接。为此,我们将更改main/templates/home.html以使用static模板标签:
{% load static %}
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet"href="{% static "css/bootstrap.min.css" %}">
<title>Hello, world!</title>
</head>
<body>
<h1>Hello, world!</h1>
<script src="{% static "js/jquery.min.js" %}"></script>
<script src="{% static "js/popper.min.js" %}"></script>
<script src="{% static "js/bootstrap.min.js" %}"></script>
</body>
</html>
测试
现在唯一缺少的就是测试。我们想确保我们的主页一直正常运行。
编写测试时要记住的一个关键概念是,您想要测试的是行为而不是内部实现。不过,在这个具体的例子中,我们将重点测试 HTTP 级别的行为,而不是浏览器级别的行为。
我们想确保
-
该页面的 HTTP 状态代码是 200。
-
模板
home.html已被使用。 -
响应包含我们商店的名称。
我们在 Django 中的测试最初将存储在main/tests.py中。这将使我们很快开始,但我们将改变它,一旦我们写了更多的代码。
from django.test import TestCase
class TestPage(TestCase):
def test_home_page_works(self):
response = self.client.get("/")
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'home.html')
self.assertContains(response, 'BookTime')
在 Django 中,自动化测试类似于标准的 Python 单元测试,但是它们继承自一组不同的基类。有相当多的基类,每一个都为基类unittest.TestCase增加了更多的功能。现在,我们将坚持使用django.test.TestCase。
您现在可以使用命令./manage.py test运行这个测试,它会产生以下输出:
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
F
===============================================================
FAIL: test_home_page_works (main.tests.TestPage)
---------------------------------------------------------------
Traceback (most recent call last):
File "/home/flagz/workspace/django-book/booktime/main/tests.py",...
self.assertContains(response, 'Booktime')
File "/home/flagz/.local/share/virtualenvs/booktime-8-1qP2a4/lib/...
self.assertTrue(real_count != 0, msg_prefix + "Couldn't find %s...
AssertionError: False is not true : Couldn't find 'BookTime' in response
---------------------------------------------------------------
Ran 1 test in 0.011s
FAILED (failures=1)
Destroying test database for alias 'default'...
糟糕,我们失败了。该测试期望找到单词“BookTime”,这在前面的 HTML 文件中是没有的。这需要改变。
如果你遵循本书中的例子,这种活动循环——编写代码、编写测试、运行测试、失败和修复——将会是非常常见的。对于不习惯这种方法的开发人员来说,一开始可能会显得很慢。然而,一旦你熟悉了机制,你可能会发现你在编码时付出的认知努力会减少,你会走得更快。
更改了main/templates/home.html的内容后,您的测试将会成功:
Creating test database for alias 'default'... System check identified no issues (0 silenced).
.
---------------------------------------------------------------
Ran 1 test in 0.010s
OK
Destroying test database for alias 'default'...
在我们开始创建下一个网页之前,让我们回顾一下到目前为止我们已经涉及到的领域。
模板
以下是模板的默认 Django 配置,您可以在booktime/settings.py中看到:
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
在这种配置中,模板加载的工作方式是在INSTALLED_APPS中列出的所有应用中寻找一个templates目录,并试图在其中找到home.html。这是因为APP_DIRS就是True。
通过使用默认的DjangoTemplates后端,你将有一个可用的“标签”列表,由{%和%}包围的单词,你可以在模板中使用它们。其中一些标签是内置的,而另一些标签只有在加载外部模块后才可用。
在上一节中,我们看到了两个标签:{% load %}和{% static %}。在我们的例子中,我们必须将 URL 组合到我们的main/static/文件夹中的 CSS 文件。为此,正确的方法是使用static标签。然而这个标签在默认情况下是不会被加载的,这就是为什么我们必须使用load的原因。在本书的后面,我们会看到我们可以定义自己的标签,它们也是可加载的。
模板视图
TemplateView是我们在这个项目中看到的第一个基于类的视图。非常简单:它呈现一个模板。它可以接受几个关键字参数,但最重要的是template_name。该参数指定要呈现的模板的路径。其他可能的关键字参数可以是extra_content或content_type。
我们可以对这个视图进行一定程度的定制,而不需要对它进行子类化。这就是使用 cbv 的好处:在某些情况下,它允许您编写比 fbv 更少的代码。
为了完整起见,如果您想在一个函数中实现它,您不必编写比这更多的内容:
def home(request):
return render(request, "home.html", {})
这实际上就是TemplateView所做的。
资源定位符
Django 中的 URL 匹配机制从加载settings.py中的ROOT_URLCONF变量中引用的文件开始。一旦加载,它将遍历一个名为urlpatterns的变量中列出的所有条目。
遵循之前显示的文件内容,urlpatterns中有两个条目:admin/是为了使用内置的 Django admin,这将在本书后面介绍,第二个是空条目。空条目表示没有路径,因此是主页。
在需要表达的模式比第一个函数所允许的更复杂的情况下,urlpatterns中的每个条目都是django.urls.path()或django.urls.re_path()的一个实例。
Django 将遍历模式,尝试将 HTTP 请求中的请求路径与现有路径进行匹配,在我们的例子中,第一个匹配将被接受。一旦匹配,遍历就完成了。
如果我们需要,这可以让我们进行一些基于优先级的匹配:
urlpatterns = [
path('product/95/', views.product_95),
path('product/<int:id>/', views.product),
re_path(r'^product/(?P<id>[^/]+)/$', views.product_unknown),
]
第一条路径只与产品 95 匹配。第二个路径将匹配任何以整数作为标识符的产品。由于正则表达式的要求,第三个路径将匹配长度至少为一个字符的任何内容。如果你不知道什么是正则表达式,不要担心,因为我们不会在本书中用到它们。
添加“关于我们”页面
我们想要建立的网站将是一个电子商务网站,但在我们开始之前,我们需要添加一个网页来描述该公司。每个值得尊敬的公司都有一个关于我们的页面,这就是我们在这里要谈论的。
这是我们网站的第二页,这是重要的一步,因为它将允许我们引入更多的结构。从模板开始,我们希望新页面和现有页面的初始部分相同,只有核心内容不同。
为了实现这个目标,我们将开始使用模板继承。模板可以从基础模板继承,并用新数据覆盖某些部分。在 Django 的术语中,这些部分被称为“块”。在处理新页面之前,我们需要更改现有的模板。
这将是新的结构,一个名为main/templates/base.html的新文件:
{% load static %}
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="{% static "css/bootstrap.min.css" %}">
<title>BookTime</title>
</head>
<body>
<h1>BookTime</h1>
{% block content %}
{% endblock content %}
<script src="{% static "js/jquery.min.js" %}"></script>
<script src="{% static "js/popper.min.js" %}"></script>
<script src="{% static "js/bootstrap.min.js" %}"></script>
</body>
</html>
这个文件将包含以前在main/templates/home.html中的大部分 HTML。main/templates/home.html将使用上面的文件并扩展它:
{% extends"base.html" %}
{% block content %}
<h2>Home</h2>
<p>this will be the content of home</p>
{% endblock content %}
新的main/templates/about_us.html文件将如下所示:
{% extends"base.html" %}
{% block content %}
<h2>About us</h2>
<p>BookTime is a company that sells books online.</p>
{% endblock content %}
这是一个足够好的开始,但是仍然缺少一些东西:一个导航栏。我们将在这里使用引导 navbar,但是这个概念在任何前端框架中都很常见。将这段 HTML 代码插入到main/templates/base.html中,替换h1头:
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="/">BookTime</a>
<button
class="navbar-toggler" type="button"
data-toggle="collapse"
data-target="#navbarSupportedContent">
<span class="navbar-toggler-icon"></span>
</button>
<div
class="collapse navbar-collapse"
id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
<li
class="nav-item">
<a class="nav-link" href="/">Home</a>
</li>
<li
class="nav-item">
<a class="nav-link" href="/about-us/">About us</a>
</li>
</ul>
</div>
</nav>
这将给我们一个导航栏,使我们的网站更接近普通网站。现在,我们想在导航栏中突出显示当前页面。有许多方法可以做到这一点。我们将选择一个允许我们探索 Django 的另一个概念,即上下文处理器:
...
<li
class="nav-item {% if request.path == "/" %}active{% endif %}">
<a class="nav-link" href="/">Home</a>
</li>
<li
class="nav-item {% if request.path =="/about-us/" %}active{% endif %}">
<a class="nav-link" href="/about-us/">About us</a>
</li>
...
因为我们的 context_processors 中有django.template.context_processors.request,所以变量request可以在模板中使用。它是代表当前 HTTP 请求的django.http.HttpRequest的一个实例。它有很多属性,比如使用的 HTTP 方法、请求的路径、GET 参数等等。
另一个我们之前没有见过的是{% if %}模板标签。它的行为与 Python 中的if语句完全一样。
请注意,前面的代码片段使用了大量硬编码的 URL。有一个更好的方法,你将在下一章看到。
现在我们需要用新模板连接 URL /about-us/。与此同时,我们将看到 Django 的另一个新特点。正如我们对模板所做的那样,我们将进行一些重组。下面是新的booktime/urls.py:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path(", include('main.urls')),
]
include()函数允许嵌套各种 URL 模式。在我们的例子中,我们希望将 Django URLs 与应用 URL 分开。同时,我们还为新创建的模板添加了一个新的 URL。函数的参数是下面文件的路径,将是main/urls.py。
from django.urls import path
from django.views.generic import TemplateView
urlpatterns = [
path(
"about-us/",
TemplateView.as_view(template_name="about_us.html")),
path(
"",
TemplateView.as_view(template_name="home.html")),
]
这足以看到我们的新创作,如图 2-2 所示。
图 2-2
关于我们页面
现在是时候确保它总是工作了,我们通过向现有的测试类添加一个测试来做到这一点。该测试与上一个类似:
...
def test_about_us_page_works(self):
response = self.client.get("/about-us/")
self.assertEqual(response.status_code,200)
self.assertTemplateUsed(response, 'about_us.html')
self.assertContains(response, 'BookTime')
最后,值得介绍一种更好的管理 URL 的方法,而不是对它们进行硬编码,这种方法将在以后给我们更多的自由来改变 URL 的结构。为此,我们必须命名urls.py文件中的所有 URL:
from django.urls import path
from django.views.generic import TemplateView
urlpatterns = [
path(
"about-us/",
TemplateView.as_view(template_name="about_us.html"),
name="about_us",
),
path(
"",
TemplateView.as_view(template_name="home.html"),
name="home",
),
]
这将允许我们使用一个名为reverse()的函数,它将我们刚刚插入的名称映射到实际的 URL 路径。如果将来您想要调整 URL,而不改变页面包含的信息,这是非常有用的。
我们需要删除测试中的硬编码 URL。这将是main/tests.py的最终版本:
from django.test import TestCase
from django.urls import reverse
class TestPage(TestCase):
def test_home_page_works(self):
response = self.client.get(reverse("home"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'home.html')
self.assertContains(response, 'BookTime')
def test_about_us_page_works(self):
response = self.client.get(reverse("about_us"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'about_us.html')
self.assertContains(response, 'BookTime')
创建联系我们表单
在这一点上,我们已经有两个网页在网站上,我们想增加一点互动性。许多公司都有联系方式,我们想在网站上添加一个。
在 Django 中,表单由forms库管理,这是一组管理 HTML 表单呈现和处理的函数。要使用这个库,第一步是在从基窗体继承的类中声明窗体的字段。我们将在一个新文件main/forms.py中这样做:
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(label='Your name', max_length=100)
message = forms.CharField(
max_length=600, widget=forms.Textarea
)
这个类声明了一个带有两个名为name和message的文本字段的表单。当这个表单呈现为 HTML 时,它将生成两个输入小部件。在我们的例子中,将会有一个<input type="text">和一个<textarea>。
我们希望这个表单负责处理通信,在这种情况下,使用电子邮件是最简单的方法。还有其他方法,比如触发内部聊天系统的消息,但是电子邮件是无处不在的,而且 Django 非常支持它。为此,我们需要向上面的类添加一个额外的方法:
from django.core.mail import send_mail
import logging
logger = logging.getLogger(__name__)
class ContactForm(forms.Form):
...
def send_mail(self):
logger.info("Sending email to customer service")
message = "From: {0}\n{1}".format(
self.cleaned_data["name"],
self.cleaned_data["message"],
)
send_mail(
"Site message",
message,
"site@booktime.domain",
["customerservice@booktime.domain"],
fail_silently=False,
)
这里有两个新功能:Django 电子邮件库和日志。如前所述,日志记录是一种实践,如果操作正确,可以显著减少故障发生时的恢复时间。这里我们触发了一个电子邮件发送,这可能会导致崩溃。我们希望记录足够的信息来快速定位。
Django 提供了一个名为send_mail()(以及其他一些)的功能来发送电子邮件。这与 Django 紧密集成,我建议使用它,而不是使用其他 Python 邮件函数。这样做的一个主要优点是它被集成到了 Django 测试库中。
测试
和其他代码一样,contact 表单也需要进行集成测试。在继续之前,我们必须进行一些重组。我们将把main/tests.py文件分成多个文件,每个 Django 层一个。
请执行以下操作:
-
创建一个
main/tests/__init__.py空文件来表示它是一个包。 -
将当前文件
main/tests.py移动到main/tests/test_views.py。 -
在主应用中添加一个名为
main/tests/test_forms.py的新文件。
这个新的文件结构还是会被 Django 发现并运行,而且会更清晰一点。
在继续之前,还要注意关于如何命名测试的一些规则:
-
测试文件需要以前缀
test_命名。 -
出现在继承自
TestCase的类中的测试方法需要test_前缀。
注意到这一点,是时候写形式测试了。表单测试将在main/tests/test_forms.py中进行,需要测试有效和无效数据:
from django.test import TestCase
from django.core import mail
from main import forms
class TestForm(TestCase):
def test_valid_contact_us_form_sends_email(self):
form = forms.ContactForm({
'name': "Luke Skywalker",
'message': "Hi there"})
self.assertTrue(form.is_valid())
with self.assertLogs('main.forms', level="INFO") as cm:
form.send_mail()
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(mail.outbox[0].subject, 'Site message')
self.assertGreaterEqual(len(cm.output), 1)
def test_invalid_contact_us_form(self):
form = forms.ContactForm({
'message': "Hi there"})
self.assertFalse(form.is_valid())
使用./manage.py test -v 2运行测试套件,现在应该显示 Django 中构建的测试发现正在查找视图测试和表单测试:
Creating test database for alias 'default' ('test_booktime')...
...
System check identified no issues (0 silenced).
test_invalid_contact_us_form (main.tests.test_forms.TestForm) ... ok
test_valid_contact_us_form_sends_email (main.tests.test_f...) ... ok
test_about_us_page_works (main.tests.test_views.TestPage) ... ok
test_home_page_works (main.tests.test_views.TestPage) ... ok
---------------------------------------------------------------
Ran 4 tests in 0.028s
OK
Destroying test database for alias 'default' ('test_booktime')...
这就结束了我们在联系表单上要做的工作。在使该表单在站点中可见之前,我们将更详细地回顾一下我们所看到的概念。
测试发现
默认情况下,Django 使用对unittest发现功能的一些定制来查找要运行的测试。它将遍历当前文件夹中的所有子文件夹,寻找名为test*.py的文件。在这些文件中,包含在其中的unittest.TestCase的所有子类都将被包含在内。
测试套件可以以几种方式运行。默认情况下,它会运行所有的测试,当我们想要对一些特定的重构有快速的反馈时,这并不理想。以下是一些例子:
$ # Run all the views tests
$ ./manage.py test main.tests.test_views.TestPage
$ # Run the homepage test
$ ./manage.py test main.tests.test_views.TestPage.test_home_page_works
$ # Run all tests
$ ./manage.py test
除了位置参数之外,您还可以传递一些选项:--failfast如果您希望测试套件在失败时立即停止,以及--keepdb如果您希望在运行之间保留测试数据库。它们都非常有助于加速前面列出的测试命令的执行。
表单和字段
Django 表单可能非常复杂和吓人。在本节中,我们将更详细地讨论它们。
正如您在前面看到的,要创建一个从django.forms.Form继承的表单,您需要添加django.forms.fields.Field的实例。这些字段代表一条信息。在一个典型的 HTML 表单中有许多输入,因此在 Django 表单中通常会发现许多字段。
Django 表单中一些最常见的字段如下:
-
BooleanField:典型的复选框 -
CharField:文本输入框(一般为<input type="text">或<textarea>) -
ChoiceField:一组选项之间的选择器 -
DecimalField:小数 -
EmailField:一个文本输入,使用一个特殊的小部件只接受电子邮件地址 -
FileField:文件输入 -
ImageField:类似 FileField,但仅验证图像格式 -
IntegerField:整数
还有其他字段,但是这些字段为我们提供了足够多的选项来处理最常见的情况。如果您需要一个或多个这里没有列出的字段,请查阅 Django 文档,它比这个列表更全面。
每个领域都有一套被接受的核心论点。值得记住的论点是
-
required(真/假):该值是必需的还是可选的? -
label(字符串):输入的友好名称。 -
help_text(字符串):较长的描述。 -
widget:用于呈现字段的django.forms.Widget子类。有些字段可以用多种方式呈现。
除了这个参数列表,还有一些方法可以添加到 form 子类中:
-
clean_<fieldname>():如果定义了这些函数,将会调用这些函数在相应的字段上运行自定义验证。例如,如果您必须检查 fieldname 是否是一个有效的 VAT 号,那么您应该在这里进行检查。 -
clean():这是您可以执行跨字段验证规则的唯一地方,跨字段验证规则是跨多个字段验证表单的规则。
在声明表单时,您可以指定更多选项;同样,如果您感兴趣,网上有很多 Django 文档。
数据提交后,下面是一些标准方法,您可以使用这些方法从视图层与表单进行交互。根据您使用的是基于函数的视图还是基于类的视图,有些工作可能会由 Django 隐式完成。
-
运行所有的验证器和清理函数,检查所有的东西是否正确验证。
-
errors:如果表单无效,这个数组将包含所有的错误。
您也可以在模板中使用这些方法,以及所有声明的字段。有时,为了精确控制模板的呈现方式,您需要这样做。稍后将详细介绍。
发送电子邮件
我们已经介绍了函数send_mail()。此功能可用于向一个或多个收件人发送普通电子邮件,邮件正文和可选的 HTML 版本。对于比这更复杂的事情,比如附加文件,类django.core.mail.EmailMessage是可用的。
电子邮件可以通过几种方式发送,最常见的是 SMTP。大多数时候,这是您想要使用的后端,尽管在运行开发服务器时发送电子邮件可能并不理想。你最终会向人们发送垃圾邮件。
在开发过程中,最好使用console后端。或者,您可以继续使用 SMTP,但使用专门用于测试的服务,如 MailHog 或其他服务。
对我有用的一个配置片段是基于DEBUG的值选择后端:
if not DEBUG:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST_USER = "username"
EMAIL_HOST = 'smtp.domain.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_PASSWORD = "password"
else:
EMAIL_BACKEND = (
"django.core.mail.backends.console.EmailBackend"
)
请将前面的代码添加到您的booktime/settings.py中。
将表单添加到“联系我们”页面
声明了这个表单之后,现在我们需要为联系我们页面创建一个模板和一个 URL 映射。
联系我们模板将被命名为main/templates/contact_form.html,其内容如下所示。我们将依靠表单的默认呈现方法,只需添加{{ form }}。
{% extends "base.html" %}
{% block content %}
<h2>Contact us</h2>
<p>Please fill the form below to contact us</p>
<form method="POST">
{% csrf_token %}
{{ form }}
<button type="submit" class="btn btn-primary">Submit</button>
</form>
{% endblock content %}
{% csrf_token %}标签有助于防止跨站点请求伪造(CSRF)攻击,在这种攻击中,用户被误导到一个站点,该站点具有提交到外部站点的表单,该站点可能是我们的。默认的 Django 配置需要这个标签。我鼓励您在 Django 在线文档中阅读更多关于它的内容。
现在我们需要一个 URL 映射。我们将在main/urls.py中添加另一个条目,但在此之前,还有一些基础工作要做。这个页面不仅需要呈现一个表单,还需要处理表单提交。
为此,我们可以使用名为FormView的基于类的视图,但是 Django 要求我们指定一些过多的参数,以便直接在 URL 文件中使用它。让我们在main/views.py中创建第一个定制视图:
from django.views.generic.edit import FormView
from main import forms
class ContactUsView(FormView):
template_name = "contact_form.html"
form_class = forms.ContactForm
success_url = "/"
def form_valid(self, form):
form.send_mail()
return super().form_valid(form)
完成这些之后,是时候添加 URLs 条目了。我们将把我们的main/views.py文件导入到带有urlpatterns的文件中。为简洁起见,此处仅显示发生变化的部分:
...
from main import views
urlpatterns = [
path(
"contact-us/",
views.ContactUsView.as_view(),
name="contact_us",
),
...
]
此时,我们有了一个带有表单的工作页面。你可以自己打开浏览器查看http://localhost:8000/contact-us/。
为了完成这一部分,我们最不需要的就是一个测试。表单本身已经有一个测试。剩下要测试的是点击 URL 时视图的呈现,页面包含联系人表单。
from main import forms
...
def test_contact_us_page_works(self):
response = self.client.get(reverse("contact_us"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'main/contact_form.html')
self.assertContains(response, 'BookTime')
self.assertIsInstance(
response.context["form"], forms.ContactForm
)
现在,我们应该能够通过其 URL 导航到联系人表单。或者,我们可以将它添加到基本模板的导航栏中,该链接将出现在所有页面中。参见图 2-3 。
图 2-3
联系我们页面
填写网站上的表单并单击 Submit 将按预期触发电子邮件。您可以在日志中看到:
INFO Sending email to customer service
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: Site message
From: site@booktime.domain
To: customerservice@booktime.domain
Date: Sat, 07 Apr 2018 17:37:14 -0000
Message-ID: <152312263451.9182.16061866787963579063@computer>
From: hello
just want to say hello world.
---------------------------------------------------------------
在本节中,我们已经看到了许多新的 Django 函数。接下来依次对它们进行描述。
FormView(形状视图)
FormView是一个非常重要的基于类的视图,它也是与模型交互的更高级视图的基础,这些视图将在后面介绍。
除了最初呈现模板之外,FormView还使用 Post/Redirect/Get 模式处理提交。1POST 请求一进来,就运行验证,然后调用几个函数:如果表单验证,就调用form_valid(),反之则调用form_invalid()。默认情况下form_valid()重定向到success_url。
这个类也实现了和TemplateView一样的功能,就是模板渲染。它可以采用相同的关键字参数。template_name指定要渲染的模板的路径。
为了完整起见,如果您想在函数中实现联系我们视图,您将不得不多写一点:
def contact_us(request):
if request.method == 'POST':
form = forms.ContactForm(request.POST)
if form.is_valid():
form.send_mail()
return HttpResponseRedirect('/')
else:
form = forms.ContactForm()
return render(request, 'contact_form.html', {'form': form})
表单渲染
历史上,表单只提供了几种在 HTML 中呈现自己的方式;{{ form.as_p }}和{{ form.as_ul }}就是很好的例子,除了默认。
如今,许多网站用 JavaScript 和 CSS 来增强表单,这需要更注重语义而不是布局的呈现。这意味着有时您可能需要单独呈现字段。
假设我们使用 Bootstrap 作为 CSS 框架,那么可以使用 Bootstrap 表单的样式对前面的示例进行更多的定制:
<form method="POST">
{% csrf_token %}
<div class="form-group">
{{ form.name.label_tag }}
<input
type="text"
class="form-control {% if form.name.errors %}is-invalid{% endif %}"
id="id_name"
name="name"
placeholder="Your name"
value="{{ form.name.value|default:"" }}" >
{% if form.name.errors %}
<div class="invalid-feedback">
{{ form.name.errors }}
</div>
{% endif %}
</div>
<div class="form-group">
{{ form.message.label_tag }}
<textarea
class="form-control {% if form.message.errors %}is-invalid{% endif %}"
id="id_message"
name="message"
rows="3">
{{ form.message.value|default:""}}
</textarea>
{% if form.message.errors %}
<div class="invalid-feedback">
{{ form.message.errors }}
</div>
{% endif %}
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
在 Django 中呈现表单时有很大的灵活性。您可以使用{{ form.name }}直接访问和呈现字段,或者使用{{ form.name.label_tag }}呈现特定的内容,比如标签标记。
当一个表单被提交并且无效时,Django 将用一个包含无效状态信息的表单呈现原始页面。在 Bootstrap 中,有显示错误状态的约定,您可以在前面的清单中看到如何构造这些约定。
摘要
在这一章中,我们开始创建每个人都会在公司网站上看到的基本页面:主页、关于我们的页面、联系表单等等。为了创造这些,我们采取了循序渐进的过程。
本章举例说明了基本的基于类的视图,如TemplateView和FormView,URL 路由系统,以及 Django 的电子邮件发送功能。
大多数情况下,我们还会对项目中添加的特性进行测试,因为这是良好软件开发的重要一步。
对于联系我们页面,我们介绍了 Django 表单以及如何使用它们来管理数据提交和电子邮件发送。
在下一章,我们将讨论数据库以及如何利用它们来构建一个更复杂的 web 应用。
Footnotes 1https://en.wikipedia.org/wiki/Post/Redirect/Get
三、将产品目录添加到网站
在这一章中,我们将开始为我们的 BookTime 网站构建产品页面,这些页面将由数据库中包含的数据驱动。我们还将看到如何操作数据以及如何从 CSV 文件导入数据。
本章介绍
-
Django 奥姆
-
迁移
-
管理命令
-
信号
-
ListView和DetailView -
上传文件管理
创建第一个模型
Django 有一个叫做 ORM 的层,代表对象关系映射器。这是软件中的一种已知模式,已经存在多年了。它包括将从数据库加载的所有行包装到一系列模型中。模型是 Python 对象,其属性对应于数据库行中的列。
模型有与底层数据库行交互的方法:save()将模型属性的任何更改写回数据库,delete()删除数据库行。
这些模型首先被声明为 Python 类,继承自django.db.models.Model。为了让 Django 检测添加到系统中的任何新模型,它们必须包含在文件models.py中(或者包含在模型包中,就像我们在第二章中对测试所做的那样)。
产品
我们公司需要可视化关于其产品的数据,所以我们将从一个Product模型开始。要创建它,请在空文件models.py中写入以下内容:
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=32)
description = models.TextField(blank=True)
price = models.DecimalField(max_digits=6, decimal_places=2)
slug = models.SlugField(max_length=48)
active = models.BooleanField(default=True)
in_stock = models.BooleanField(default=True)
date_updated = models.DateTimeField(auto_now=True)
分析前面的模型,您会注意到它是一个具有许多属性的类。那里的所有属性最终都是从django.db.models.fields.Field继承的,它们大致映射到数据库类型。例如,CharField映射到 SQL 类型VARCHAR,IntegerField映射到 SQL 整数,BooleanField映射到 SQL 布尔值,等等。
传递给字段构造函数的许多属性表示进一步的 SQL 说明符。以CharField为参考,max_length是允许的最大字符数,它作为参数传递给VARCHAR:这个最大值是在数据库级强制的。
Django 的其他部分也使用这些属性。例如,blank由 Django admin 使用。auto_now(仅适用于日期/日期时间字段)由 Django 管理:它自动使用模型的最后修改时间来更新字段。每个字段都有自己可能的配置。
一旦我们在文件中声明了模型,它仍然不能工作。我们需要首先创建一个迁移。迁移是一些特殊的文件,包含一系列创建表、添加列、删除列等数据库指令。所有数据定义语言(DLL)命令(创建和更改模式的命令)都封装在迁移文件中使用的 Python API 中。
虽然这些迁移文件可以手动创建,但是 Django 提供了makemigrations命令来自动创建它们。在大多数情况下,它像预期的那样工作,如下所示。对于一些特别困难的情况,自动创建可能无法创建正确的迁移。在这些情况下,必须手动纠正。
$ ./manage.py makemigrations
Migrations for 'main':
main/migrations/0001_initial.py
- Create model Product
为了说明我之前提出的关于包含 DDL 命令的迁移的观点,下面是我们刚刚运行的一个例子:
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Product',
fields=[
('id', models.AutoField(auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID')),
('name', models.CharField(max_length=32)),
('description', models.TextField(blank=True)),
('price', models.DecimalField(decimal_places=2,
max_digits=6)),
('slug', models.SlugField(max_length=48)),
('active', models.BooleanField(default=True)),
('in_stock', models.BooleanField(default=True)),
('date_updated', models.DateTimeField(auto_now=True)),
],
),
]
注意文件中有一个额外的id字段。当您的模型中没有字段明确具有属性primary_key=True时,这是由 Django 自动添加的。ORM 需要一个主键来将 Python 对象映射到数据库行。
产品图像
对于任何产品目录来说,每个产品都必须有一个图片。在我们的例子中,我们希望每个产品可以有任意数量的图像。为了实现这一点,关于图像的信息需要放在一个单独的表中,我们可以通过外键关系将它链接回Product模型:
class ProductImage(models.Model):
product = models.ForeignKey(
Product, on_delete=models.CASCADE
)
image = models.ImageField(upload_to="product-images")
与之前的模型相比,这里有几个更复杂的字段。ForeignKey是存储链接Product模型主键的字段。ORM 使用它在被访问时自动运行连接操作。
ImageField是FileField的子类,它专门为上传的图像提供了一些额外的功能。这些额外的功能需要一个叫做Pillow的额外的库。要安装它,需要运行pipenv install Pillow。
此时,我们可以再次运行 Django ./manage.py makemigrations命令。它将为这个模型生成一个新的迁移。迁移将包含一个CreateModel指令和三个字段,两个刚刚引入的字段和一个id字段。
ProductTag(产品标签)
我们要介绍的最后一个模型是作为类别概括的“标签”概念:一个产品可能有一个或多个标签,一个标签可能包含一个或多个产品。
class ProductTag(models.Model):
products = models.ManyToManyField(Product, blank=True)
name = models.CharField(max_length=32)
slug = models.SlugField(max_length=48)
description = models.TextField(blank=True)
active = models.BooleanField(default=True)
Django 提供了一种特殊类型的字段ManyToManyField,它自动在两个表之间创建一个链接表,在本例中是 Product Tags 和 Products。这个链接表允许您创建任何标签可以与任何产品相关联的关系,反之亦然。
这是目前的最后一种型号。记住要为此生成迁移,然后您可以通过运行migrate命令来运行它们:
$ ./manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, main, sessions
Running migrations:
...
Applying main.0001_initial... OK
Applying main.0002_productimage... OK
Applying main.0003_producttag... OK
后面的章节将介绍更多的模型,但是在本章的剩余部分,我们将关注在这三个模型之上实现功能。
缩略图生成
在这个阶段,我们已经有足够的东西来构建新的网页,但在此之前,我们将为ProductImage模型添加一些额外的功能:缩略图。
我们不想为网络客户提供太大的图像,因为加载时间太长,客户会因为我们的网站无响应而离开。
我们需要向模型添加一个新的ImageField字段,它将保存缩略图:
class ProductImage(models.Model):
...
thumbnail = models.ImageField(
upload_to="product-thumbnails", null=True
)
在其上运行makemigrations命令将生成一个带有AddField指令的新迁移:
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0003_producttag'),
]
operations = [
migrations.AddField(
model_name='productimage',
name='thumbnail',
field=models.ImageField(null=True,
upload_to='product-thumbnails'),
),
]
添加之后,这个字段的内容需要自动填充,为此,我们将使用 Django 信号。信号是在特定事件发生时运行代码的一种方式,Django 提供了许多钩子将它们连接到 ORM 操作。
信号在某些情况下非常有用,但你应该谨慎使用。使用大量信号的应用可能会变得更难调试,因为它们的执行顺序是不确定的。
编写信号处理程序时的惯例是将它们放在相关 Django 应用中的一个名为signals.py的文件中,在我们的例子中是main。这里我们将放入生成缩略图的代码:
from io import BytesIO
import logging
from PIL import Image
from django.core.files.base import ContentFile
from django.db.models.signals import pre_save
from django.dispatch import receiver
from .models import ProductImage
THUMBNAIL_SIZE = (300, 300)
logger = logging.getLogger(__name__)
@receiver(pre_save, sender=ProductImage)
def generate_thumbnail(sender, instance, **kwargs):
logger.info(
"Generating thumbnail for product %d",
instance.product.id,
)
image = Image.open(instance.image)
image = image.convert("RGB")
image.thumbnail(THUMBNAIL_SIZE, Image.ANTIALIAS)
temp_thumb = BytesIO()
image.save(temp_thumb, "JPEG")
temp_thumb.seek(0)
# set save=False, otherwise it will run in an infinite loop
instance.thumbnail.save(
instance.image.name,
ContentFile(temp_thumb.read()),
save=False,
)
temp_thumb.close()
一旦我们完成了这些,我们需要确保当 Django 应用由内部的 Django 应用注册中心启动时,这个文件被初始化。建议的方法是在main/apps.py内的应用配置中添加一个名为ready()的方法:
from django.apps import AppConfig
class MainConfig(AppConfig):
name = 'main'
def ready(self):
from . import signals
这足以确保信号被记录。现在,将为上传到网站的每个新产品图像调用处理程序。
鉴于这是我们第一次管理用户上传的文件,我们需要确保 Django 知道在哪里存储这些文件,以及从哪里提供这些文件。这两个值需要在booktime/settings.py中指定:
...
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"
为了测试信号,我们将编写一个 Django 测试。我们将把这个测试放在一个名为main/tests/test_signals.py的文件中。
下面的测试,以及本章中的许多测试,将依赖于一些引导数据(图像和 CSV)。这些数据包含在本书的代码库中。
from django.test import TestCase
from main import models
from django.core.files.images import ImageFile
from decimal import Decimal
class TestSignal(TestCase):
def test_thumbnails_are_generated_on_save(self):
product = models.Product(
name="The cathedral and the bazaar",
price=Decimal("10.00"),
)
product.save()
with open(
"main/fixtures/the-cathedral-the-bazaar.jpg", "rb"
) as f:
image = models.ProductImage(
product=product,
image=ImageFile(f, name="tctb.jpg"),
)
with self.assertLogs("main", level="INFO") as cm:
image.save()
self.assertGreaterEqual(len(cm.output), 1)
image.refresh_from_db()
with open(
"main/fixtures/the-cathedral-the-bazaar.thumb.jpg",
"rb",
) as f:
expected_content = f.read()
assert image.thumbnail.read() == expected_content
image.thumbnail.delete(save=False)
image.image.delete(save=False)
在测试中,我们正在创建一些数据库内容,假设测试基类继承自TransactionTestCase,那么任何创建的模型都会在测试结束时被重置。对于需要手动删除的文件,情况就不一样了。
查询集和管理器
在上一节中,我们讨论了模型,包括我们如何定义它们以及如何用它们创建数据库行。我们将研究的下一组 SQL 操作包括选择和更新,它们依赖于查询集和管理器。
管理器是负责针对模型构建查询的类。每个模型至少需要一个经理。默认情况下,django.db.models.Manager的实例通过模型中名为objects的属性连接到每个模型。管理器的所有标准方法都返回查询集。
QuerySet 是从数据库加载的模型的集合。查询集由管理器实例构造和填充。QuerySets 也有可以用来进一步过滤模型的方法,比如filter()或exclude()。查询集类似于 Python 列表:它们可以被切片,并且可以用一个for循环进行迭代。
这两个方法接受表示字段查找的关键字参数,Django 在查询时将其转换为 WHERE 子句。字段查找以field__lookuptype=value的形式表示。如果您省略了lookuptype部分,则假设您想要检查相等性。
下面是一些使用默认管理器对之前创建的模型进行查询的示例:
-
Product.objects.all():返回所有产品 -
Product.objects.filter(price__gt=Decimal("2.00")):返回价格大于 2.00 的产品 -
Product.objects.filter(price__lte=Decimal("2.00")):返回价格小于(或等于)2.00 的产品 -
Product.objects.exclude(price__gt=Decimal("12.00")):返回价格大于 12.00 的产品 -
Product.objects.filter(price=Decimal("2.00")):返回价格正好为 2.00 的产品 -
Product.objects.filter(name__icontains="cathedral"):返回名称中包含单词“cathedral”的产品(不区分大小写) -
...filter(name__startswith="The",price__gt=Decimal("9.00")):返回带有 AND 的两个子句的组合
所有前面的查询都限于一个表。如果需要匹配其他表中的字段,Django 提供了一种在查询时构建连接的方法。Product.objects.filter(producttag__name="sci-fi")就是这样的一个例子。它将返回与标签“sci-fi”相关联的产品,为了进行计算,它构建了两个额外的连接:一个连接到链接表,另一个连接到标签表。
如果您想构建更高级的查询,如 OR 查询或引用其他字段的查询,您需要使用django.db.models.Q对象或django.db.models.F对象。以下是一些例子:
-
...filter(Q(name__startswith="The") | Q(price__gt=Decimal("9.00"))):返回名称以“The”开头或价格大于 9.00 的产品。 -
...filter(price__gt=F("price") – 1):返回价格大于价格本身减 1 的产品。这是一个愚蠢的例子来证明它是如何工作的。
现在我们知道了经理是如何工作的,我们将创建一个。这样做的原因是为了添加额外的方法来返回经过筛选的查询集。假设我们在Product模型中有一个active字段,我们将添加一个带有过滤器的管理器:
class ActiveManager(models.Manager):
def active(self):
return self.filter(active=True)
声明之后,我们将通过覆盖一个由约定objects调用的属性来将其连接到模型:
class Product(models.Model):
...
objects = ActiveManager()
这个方法将可以像前面的例子一样使用:Product.objects.active()只返回活动的产品。为了完成这个功能,我们需要编写测试。这里有一个有效的方法:
from decimal import Decimal
from django.test import TestCase
from main import models
class TestModel(TestCase):
def test_active_manager_works(self):
models.Product.objects.create(
name="The cathedral and the bazaar",
price=Decimal("10.00"))
models.Product.objects.create(
name="Pride and Prejudice",
price=Decimal("2.00"))
models.Product.objects.create(
name="A Tale of Two Cities",
price=Decimal("2.00"),
active=False)
self.assertEqual(len(models.Product.objects.active()), 2)
迁移
考虑到数据库模式管理的影响力,它应该有自己的章节。我们已经看到,迁移是 Django 在模型更改后应用数据库更改的方式。这些迁移文件一旦生成,就是项目的一部分。它们需要被置于源代码控制之下,并在参与项目的每个人之间共享。
执行迁移时,需要了解一些命令:
-
makemigrations:生成移植文件(Python 语言),以后可以修改 -
migrate:运行未按顺序应用的迁移文件 -
showmigrations:列出已应用或未应用的迁移 -
sqlmigrate:显示迁移的 SQL
记住这些命令,值得再看一看我们到目前为止生成的迁移文件,并讨论迁移依赖关系。如果您打开任何迁移文件,您会注意到一个名为dependencies的属性。这用于构建依赖图。我们的如图 3-1 所示。
图 3-1
迁移
方向性
使用命令migrate,迁移可以向前或向后运行,这意味着应用更改或恢复已经应用的更改。为了让这种双向性发挥作用,在创建迁移时,应该以可逆的方式构建迁移。反向操作的一些示例如下
-
添加字段时,与删除字段相反。
-
添加表格时,与删除表格相反。
-
当删除一个可空的字段时,反过来就是将它添加回去。
然而,有些情况是不可逆转的。一些例子是不可空字段移除和将文本数据转换回数字。进行迁移时,我们应该避免进行不可逆的迁移。
作为双向性给我们的可能性的概述,我们将从分析当前数据库状态开始:
$ ./manage.py showmigrations main
main
[X] 0001_initial
[X] 0002_productimage
[X] 0003_producttag
[X] 0004_productimage_thumbnail
我们已经应用了所有迁移,但是如果我们愿意,我们可以恢复它们。使用migrate命令可以恢复它们,或者向后应用它们:
$ ./manage.py migrate main 0003_producttag
Operations to perform:
Target specific migration: 0003_producttag, from main
Running migrations:
Rendering model states... DONE
Unapplying main.0004_productimage_thumbnail... OK
在多个分支上工作时,恢复/应用迁移非常有用。如果不同版本的代码需要不同的数据库状态,这允许您切换它。要重新应用,我们只需使用migrate命令,无需指定迁移:
$ ./manage.py migrate main
Operations to perform:
Apply all migrations: main
Running migrations:
Applying main.0004_productimage_thumbnail... OK
合并迁移
当使用 Git 这样的版本控制系统时,创建分支并独立地处理相同代码库的副本是非常容易的。鉴于 Django 中的数据库结构是通过 Python 代码管理的,两个分支可能包含两组不同的模型。
两个源代码控制系统都有合并代码的方法,但是在这样做之后,我们可能会遇到这样的情况:多个迁移位于依赖图的顶部。一个例子是说明这一点的最好方法。假设在分支“A”上,有人将产品的名称字段编辑为 40 个字符,而不是当前的 32 个字符。makemigrations的结果将是
./manage.py makemigrations -n productname_40
Migrations for 'main':
main/migrations/0005_productname_40.py
- Alter field name on product
但是在另一个分支“B”上,其他人编辑了同一个 Django 应用的模型(在我们的例子中,main)。此人将标签的名称改为 40 个字符长:
$ ./manage.py makemigrations -n producttagname_40
Migrations for 'main':
main/migrations/0005_producttagname_40.py
- Alter field name on producttag
当需要将其合并到分支时,我们将有两个“0005”迁移,它们都依赖于同一个基础。如果我们试图在这种情况下运行migrate,我们会得到一个错误:
$ ./manage.py migrate
CommandError: Conflicting migrations detected;...
...multiple leaf nodes in the migration graph:
(0005_productname_40, 0005_producttagname_40 in main).
To fix them run 'python manage.py makemigrations --merge'
这是因为 Django 不允许迁移图有两个头部,这是当前状态(如图 3-2 所示)。
图 3-2
双重移民
如果我们运行推荐的makemigrations --merge命令,我们将通过一个特殊的空迁移来纠正这个问题,该迁移依赖于我们在分支 A 和 B 中创建的每个迁移(参见图 3-3 )。
图 3-3
合并后的迁移
数据迁移
除了模式更改之外,还可以使用迁移来加载数据或转换现有数据。例如,在通过三步过程更改字段类型时,通常会进行类型转换:
-
添加目标字段的模式迁移。
-
数据迁移,将数据转换并保存到新字段。
-
架构迁移以删除原始字段。
为了说明它们的用法,我们将关注一个更简单的用例。让我们创建一个将产品名称大写的数据迁移。为此,我们从空迁移开始:
$ ./manage.py makemigrations main --empty -n productname_capitalize
生成的文件需要填充我们的代码:
from django.db import migrations
def capitalize(apps, schema_editor):
Product = apps.get_model('main', 'Product')
for product in Product.objects.all():
product.name = product.name.capitalize()
product.save()
class Migration(migrations.Migration):
...
operations = [
migrations.RunPython(
capitalize,
migrations.RunPython.noop
),
]
我们没有使用像CreateModel或AlterField这样的操作,而是使用一个叫做RunPython的底层操作,它采用一个向前函数和一个向后函数。在我们的例子中,我们指定了一个 noop backward 函数,它将允许我们不做任何事情而返回,而不是引发一个异常。
一旦我们应用这一点,所有当前产品的名称都将大写。认识到这种迁移只应用一次是很重要的。如果我们的数据转换必须独立于数据库更改来应用,那么迁移可能不是最好的方法。
需要注意的一点是,在向前/向后函数中,我们必须使用那种特殊的方式来导入模型。这是因为在执行迁移时,数据库模式将不同于模型文件中声明的模式。apps.get_model()方法将返回旧模型,从它的自定义方法中剥离出来。
通过 Django admin 管理数据
Django 经常被引用的杀手级特性之一是它的管理界面。这是一个创建、更新和删除系统中任何模型的 UI。它还提供了一个认证和许可系统,用于为不同的用户分配不同级别的权限。
Admin 是一个非常有用的免费工具;它只需要被激活。为此,我们将首先创建一个名为main/admin.py的文件:
from django.contrib import admin
from . import models
admin.site.register(models.Product)
admin.site.register(models.ProductTag)
admin.site.register(models.ProductImage)
一旦我们完成了这些,我们需要创建 Django 项目的第一个用户。它将是管理员用户。Django 没有任何初始数据,因此这一步需要手动完成。为此,有一个名为createsuperuser的 Django 命令:
$ ./manage.py createsuperuser
Username (leave blank to use 'flagz'): admin
Email address: me@site.domain
Password:
Password (again):
Superuser created successfully.
这个应该够开始用了。默认情况下,可以在http://localhost:8000/admin/访问管理界面。
当使用管理界面时,用户将登陆其登录页面,一旦登录,将看到 Django 应用划分的模型列表,如图 3-4 所示。可以单击每个模型名称,这将导致一个视图,其中列出了模型的所有实例。
图 3-4
Django 管理初始页面
在这个阶段,您还没有任何数据。要完全尝试管理界面,您应该至少插入一个产品。继续插入你最喜欢的书的书名。您可以在产品列表视图中执行此操作。单击产品,然后单击右上角的添加按钮。
对于任何模型列表视图,页面上都有一些东西。右上角有一个按钮,用于添加所选模型的新实例。在左侧,有一个模型列表,以及每个模型的复选框。单击复选框后,可以对选定的模型应用操作。
管理操作是可以应用于一组模型(同类)的操作。型号列表上方的下拉框中列出了可用的操作。默认情况下,唯一可用的操作是删除。
简单定制
在这一点上,再多一点努力,我们就可以让管理界面对我们更有用。以下是列表视图的一些简单自定义:
-
list_display是字段列表,如果指定,将用于在模型列表视图中创建列。除了字段,这些也可以是函数。 -
list_filter是将用作该列表过滤器的字段列表。选中时,将只显示所有模型的子集。 -
search_fields是一个选项,当它出现时,会指示 Django 添加一个搜索框,可以用来搜索指定的字段。 -
list_editable是一个字段列表,它将使list_display中指定的一些列可编辑。
当可视化单个项目而不是项目列表时,请使用以下内容:
-
如果您有一个字段引用了另一个包含许多实体的表,那么
autocomplete_fields非常有用。由于选项的数量,这可能会使使用标准的选择框变得困难。 -
prepopulated_fields对段塞油田有用。它告诉管理界面从另一个字段自动创建 slug。 -
readonly_fields是不可编辑的字段列表。
使用所有这些定制,我们可以组合出比默认配置更友好的东西,其结果如图 3-5 所示:
图 3-5
我们的产品管理员
from django.contrib import admin
from django.utils.html import format_html
from . import models
class ProductAdmin(admin.ModelAdmin):
list_display = ('name', 'slug', 'in_stock', 'price')
list_filter = ('active', 'in_stock', 'date_updated')
list_editable = ('in_stock', )
search_fields = ('name',)
prepopulated_fields = {"slug": ("name",)}
admin.site.register(models.Product, ProductAdmin)
class ProductTagAdmin(admin.ModelAdmin):
list_display = ('name', 'slug')
list_filter = ('active',)
search_fields = ('name',)
prepopulated_fields = {"slug": ("name",)}
autocomplete_fields = ('products',)
admin.site.register(models.ProductTag, ProductTagAdmin)
class ProductImageAdmin(admin.ModelAdmin):
list_display = ('thumbnail_tag', 'product_name')
readonly_fields = ('thumbnail',)
search_fields = ('product__name',)
def thumbnail_tag(self, obj):
if obj.thumbnail:
return format_html(
'<img src="%s"/>' % obj.thumbnail.url
)
return "-"
thumbnail_tag.short_description = "Thumbnail"
def product_name(self, obj):
return obj.product.name
admin.site.register(models.ProductImage, ProductImageAdmin)
这种配置将
-
列出产品时,显示列
name, slug, in_stock,和price。在这四列中,in_stock将是可编辑的。更改产品是否有库存后,单击保存按钮保存更改。 -
使产品在指定的字段上可过滤。这些过滤器列在右侧。
-
在产品表上方添加搜索框。无论您搜索什么,Django 都会查找包含在指定字段中的字符串,忽略字母的大小写。
-
添加产品时,在输入产品名称的同时,即时计算 slug。
-
While working on the product tags, provide a similar configuration to the one of products (see Figure 3-6).
图 3-6
标签管理
-
在选择属于特定标签的产品时添加自动完成功能。
-
在两列中显示图像,其内容将是 admin 类中指定的两个函数的返回值,其中一个函数返回 HTML。
-
Add a search box in the images list view. This search will be applied on the linked product table (using a JOIN) rather than the productimage table (see Figure 3-7).
图 3-7
图像管理
此时,有几个问题需要解决。Django admin 在构建选择器或一般可视化项目时,依赖于它的字符串表示。在我们的例子中,自动完成需要一个好的字符串表示。要解决这个问题,我们需要向所有模型添加方法。这里有一个例子:
class Product(models.Model):
...
def __str__(self):
return self.name
最后要解决的是提供用户上传的图片。此时,虽然提供了正确的图像和缩略图 URL,但是这些 URL 返回 404。我们需要在主 URL 文件booktime/urls.py中解决这个问题。
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('main.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
这就是我们现在要定制的。在接下来的章节中,我们将讨论更高级的特性。
管理命令
到目前为止,你已经在书中看到了几个命令,如./manage.py runserver或./manage.py migrate。在 Django,这些工具被称为管理命令。这些可以随意添加,但是有一些已经包含在内,随时可以使用。您可以使用./manage.py打印可用命令的完整列表。
makemessages和compilemessages用于管理 Django 中的翻译文件。如果您的项目的用户界面需要支持多种语言,您可以使用这些。
check是 Django 系统检查框架的接口,在启动任何 Django 命令时使用,以确保满足项目运行所需的所有条件。例如,如果您的项目依赖于环境中指定的一些配置变量,这将非常有用。
dbshell和shell是交互式命令行提示。dbshell将启动数据库命令行客户端,而shell将启动 Python 解释器,Django 项目已经初始化。
loaddata和dumpdata作为简单的数据加载机制非常有用。它们被用来引导一个数据库,其中包含来自设备的数据,这些数据是 Django 能够理解的格式的简单数据文件。它们可以是 JSON 或 XML,只要结构符合 Django 的要求,就可以不用任何额外的代码来管理它们。
我们将利用loaddata和dumpdata命令来管理标签。标签将是一个固定的集合,由开发人员管理,并且这个固定装置将被提交到存储库中。我们还将介绍 Django 的另一个功能,叫做自然键。
我们可以使用 Django admin 来创建它们。添加六个标签后(见图 3-8 ,我们将使用dumpdata命令导出它们。
图 3-8
标签列表
序列化和自然键
序列化,在 Django 中,意思是将存储在数据库表中的数据转换成可以写入文件的东西。Django 支持 XML、JSON 和 YAML 文件格式。反序列化是相反的操作,从文件到填充的数据库表。
Django 通过助手函数和管理命令提供序列化和反序列化功能。我们现在将集中讨论管理命令。以下是如何使用dumpdata命令的示例:
$ ./manage.py dumpdata --indent 2 main.ProductTag
[{
"model": "main.producttag",
"pk": 2,
"fields": {
"name": "Finance",
"slug": "finance",
"description": "",
"active": true,
"products": [
16
]
}
},
...
不幸的是,这不适合与我们的库一起发布。它包含许多特定于数据库实例的内部 id。
解决这个问题的一个方法是使用独立的 id,比如 UUIDs,但是它们对人不太友好。相反,我们将使用自然键,因此我们需要对当前模型做一些事情:
-
将
ManyToManyField移出标签并移入Product模型 -
确保定义了标签的
__str__()方法 -
定义标签
natural_key()的方法
通过删除ProductTag模型中的products字段,并将其重新添加到Product模型中,可以很容易地完成第一个要点:
class Product(models.Model):
...
tags = models.ManyToManyField(ProductTag, blank=True)
...
在数据库级别,没有太大的变化:我们仍然有一个链接表。现在的区别是,这个链接更容易从产品中穿过。这个变化影响了 Django 管理,特别是autocomplete_fields设置,它也需要移动:
class ProductAdmin(admin.ModelAdmin):
...
autocomplete_fields = ('tags',)
第二个调整是添加__str__()方法,我们这样做的原因和我们已经做的一样,那就是可用性。
作为最后一步,我们将添加一个名为natural_key()的方法,它将返回标签自然键。在我们的例子中,我们将使用 slug 作为自然键。这背后的基本原理是,作为 URL 的一部分,slugs 不太可能改变(这里简化了一点)。
按照所有建议的更改,该类现在看起来应该是这样的:
class ProductTag(models.Model):
name = models.CharField(max_length=40)
slug = models.SlugField(max_length=48)
description = models.TextField(blank=True)
active = models.BooleanField(default=True)
def __str__(self):
return self.name
def natural_key(self):
return (self.slug,)
有了这个模型,就有可能使用自然键而不是内部数据库键来运行dumpdata。我们还将把下面的内容输出到一个文件中,供以后使用。生成的文件将独立于内部标识符,因此在不同的数据库环境之间更容易移植。
$ ./manage.py dumpdata --indent 2 --natural-primary main.ProductTag
[
{
"model": "main.producttag",
"fields": {
"name": "Finance",
"slug": "finance",
"description": "",
"active": true
}
},
...
$ ./manage.py dumpdata --indent 2 --natural-primary main.ProductTag \
> main/fixtures/producttags.json
如何使用自然键加载
当使用自然键加载数据时,Django 不能使用我们已经定义的natural_key()方法,因为模型加载是通过管理器进行的,而不是模型本身。为了能够加载回标签,我们需要为该模型创建一个管理器,并实现get_by_natural_key()方法:
class ProductTagManager(models.Manager):
def get_by_natural_key(self, slug):
return self.get(slug=slug)
class ProductTag(models.Model):
...
objects = ProductTagManager()
...
如果您遵循了前面的所有步骤,加载(或重新加载)它应该只是一个命令操作。为了测试这一点,我们可以在文件中添加一些标签描述并重新加载它们。如果数据已经存在于数据库中,它将被更新。
$ ./manage.py loaddata main/fixtures/producttags.json
Installed 6 object(s) from 1 fixture(s)
使用管理命令导入数据
除了已经包含的管理命令之外,项目还可以定义新的命令。一旦创建,就可以使用manage.py脚本启动它们。
我们将创建一个专用命令来导入产品数据。
为了创建管理命令,您需要在main/management/commands/中添加一个文件。您选择的文件名将成为命令名。每个文件将有一个管理命令;如果您想要创建多个命令,您将需要多个文件。
每个管理命令都可以接受选项:这些选项是用argparse解析的,Django 在指定这些选项时也有一些约定。
要开始创建管理命令的任务,我们首先需要设置一些基本文件夹:
$ mkdir main/management
$ touch main/management/__init__.py
$ mkdir main/management/commands
$ touch main/management/commands/__init__.py
这些命令将使这些文件夹成为 Python 模块,使 Django 能够检查和执行它们的内容。接下来我们将创建main/management/commands/import_data.py,它将包含进口商:
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = 'Import products in BookTime'
def handle(self, *args, **options):
self.stdout.write("Importing products")
这足以让命令执行,这里的约定清晰可见。这些文件需要包含一个继承自django.core.management.base.BaseCommand并实现handle()功能的 Python 类。这也会让 Django 发现它。
以下是一些交互示例:
$ ./manage.py
Type 'manage.py help <subcommand>' for help on a specific subcommand.
Available subcommands:
...
[main]
import_data
...
$ ./manage.py import_data
Importing products
handle()方法的主体是导入逻辑所在的地方。假设我们需要从 CSV 文件中填充数据,如下所示:
name,description,tags,image_filename,price
The cathedral and the bazaar,A book about open source methodologies,Open source|Programming,cathedral-Siddhartha,A novel by Hermann Hesse,Religion|Narrative,siddhartha.jpg,6.00
Backgammon for dummies,How to start playing Backgammon,Games|Manual,backgammon.jpg,13.00
要在本节稍后测试该命令,请将前面的数据保存在一个文件中。
这个文件包含我们数据库结构中分散在不同表中的字段。因此,我们不能在这里使用loaddata命令。另一个原因是 CSV 不是 Django 处理的序列化格式之一。这是一个导入前面文件的实现:
from collections import Counter
import csv
import os.path
from django.core.files.images import ImageFile
from django.core.management.base import BaseCommand
from django.template.defaultfilters import slugify
from main import models
class Command(BaseCommand):
help = "Import products in BookTime"
def add_arguments(self, parser):
parser.add_argument("csvfile", type=open)
parser.add_argument("image_basedir", type=str)
def handle(self, *args, **options):
self.stdout.write("Importing products")
c = Counter()
reader = csv.DictReader(options.pop("csvfile"))
for row in reader:
product, created = models.Product.objects.get_or_create(
name=row["name"], price=row["price"]
)
product.description = row["description"]
product.slug = slugify(row["name"])
for import_tag in row["tags"].split("|"):
tag, tag_created = models.ProductTag.objects.get_or_create(
name=import_tag
)
product.tags.add(tag)
c["tags"] += 1
if tag_created:
c["tags_created"] += 1
with open(
os.path.join(
options["image_basedir"],
row["image_filename"],
),
"rb",
) as f:
image = models.ProductImage(
product=product,
image=ImageFile(
f, name=row["image_filename"]
),
)
image.save()
c["images"] += 1
product.save()
c["products"] += 1
if created:
c["products_created"] += 1
self.stdout.write(
"Products processed=%d (created=%d)"
% (c["products"], c["products_created"])
)
self.stdout.write(
"Tags processed=%d (created=%d)"
% (c["tags"], c["tags_created"])
)
self.stdout.write("Images processed=%d" % c["images"])
这个新的import_data命令的代码中有很多需要解释的地方。首先,add_arguments函数:管理命令可以接受命令行选项,Django 提供了一些对所有命令都可用的选项(例如 verbosity),但是这个列表可以扩展。
我们还在所有 Django 选项之上添加了两个位置参数。第一个位置参数是要导入的 CSV 文件的路径,第二个是 images 目录的路径。add_argument的语法在argparse模块文档中有解释,这是 Django 正在使用的 Python 模块。
脚本参数的使用,而不是硬编码的变量,为这个脚本提供了灵活性。当在不同于您的机器环境的环境中运行这些导入时,您可以将它与其他命令(如wget或gunzip)结合使用,以下载和解压缩具有动态生成名称的临时文件夹中的档案。
打开 CSV 文件后,脚本循环遍历这些行,并尝试加载(或生成)具有相同名称/价格组合的产品。get_or_create函数返回两个值:一个模型和一个指示它是否是新模型的布尔标志。
一旦我们加载了一个产品,我们就通过循环浏览 CSV 文件中tags字段的所有标签来更新它的标签列表。鉴于 CSV 是一种平面格式,这个列表需要从其压缩形式(管道分隔列表)扩展。
一旦标签被插入,脚本试图通过用os.path.join()连接basedir和指定的文件名来打开图像。通过传递包装在一个ImageFile对象中的产品和打开的文件,创建了一个ProductImage模型的新实例,它添加了关于文件的额外信息。
在命令中,有几个对self.stdout.write的调用。这会写入标准输出。类似地,如果标准误差更可取,也可以使用self.stderr.write。
至于到目前为止出现的所有相当大的程序代码,这段代码需要测试,以确保它永远不会出错:
from io import StringIO
import tempfile
from django.conf import settings
from django.core.management import call_command
from django.test import TestCase, override_settings
from main import models
class TestImport(TestCase):
@override_settings(MEDIA_ROOT=tempfile.gettempdir())
def test_import_data(self):
out = StringIO()
args = ['main/fixtures/product-sample.csv',
'main/fixtures/product-sampleimages/']
call_command('import_data', *args, stdout=out)
expected_out = ("Importing products\n"
"Products processed=3 (created=3)\n"
"Tags processed=6 (created=6)\n"
"Images processed=3\n")
self.assertEqual(out.getvalue(), expected_out)
self.assertEqual(models.Product.objects.count(), 3)
self.assertEqual(models.ProductTag.objects.count(), 6)
self.assertEqual(models.ProductImage.objects.count(), 3)
如您所见,Django 提供了函数call_command()从 Python 本身调用管理命令,这在测试中非常方便。
在我们将示例 CSV 文件(以及一些图像)放在指定的位置后,就可以运行这个测试了。该测试断言 stdout 等于预期的结果,并且在导入之后出现的模型数量是应该的。
在测试中,使用了装饰器override_settings。它的目的是覆盖特定测试的 Django 设置。在本例中,我们创建了一个新的临时文件夹MEDIA_ROOT,因为我们要处理很多上传的文件。与数据库不同,Django 不清理这些文件。使用临时文件夹可以确保它最终会被操作系统清除。
对于所有涉及数据库的测试,Django 在运行这些测试之前,会创建一个临时使用的测试数据库。在测试运行结束时,此测试数据库将被删除。在我们的例子中,测试数据库将是一个新的 PostgreSQL 数据库,其名称与指定名称相同,只是添加了一个前缀test_。这种管理是自动的。
这个测试并不详尽:它没有测试 csv 文件是否存在,图像是否出现在basedir中,等等。这是留给你的练习。
添加产品列表页面
在这一章中,到目前为止,我们还没有做任何网站的工作,只有数据基础。现在是时候创建我们的第一个数据库驱动的网页了,从列出产品开始。我们可以利用另一个名为ListView的基于类的视图。Django 有很多 cbv 来帮助构建数据库驱动的视图,我们将在适当的时候使用它们。下面是我们将要使用的视图:
from django.views.generic.list import ListView
from django.shortcuts import get_object_or_404
from main import models
class ProductListView(ListView):
template_name = "main/product_list.html"
paginate_by = 4
def get_queryset(self):
tag = self.kwargs['tag']
self.tag = None
if tag != "all":
self.tag = get_object_or_404(
models.ProductTag, slug=tag
)
if self.tag:
products = models.Product.objects.active().filter(
tags=self.tag
)
else:
products = models.Product.objects.active()
return products.order_by("name")
我们的视图利用了ListView,但是增加了一个定制:一个额外的过滤参数(tag)。根据kwargs的内容,它返回属于该标签的活动产品列表,或者如果指定了标签all,则返回所有活动产品。
ListView和我们目前看到的 cbv 一样,使用template_name进行渲染。它将在呈现视图时寻找该模板。请注意,与最初的视图不同,我们添加了一个额外的main。这是一个惯例,我们将开始使用它来反映其他数据库视图所做的事情。
这个视图还使用paginate_by参数透明地管理分页。
当这个视图的实例被创建时,属性args和kwargs被来自 URL 路由的信息填充。在我们的例子中,这个视图期望用 URL 路径中指定的标记来调用,而不是在 GET 参数中。另一方面,如果标签是一个 GET 参数,可以使用self.request.GET字典访问它。
函数get_object_or_404是一个非常有用的快捷方式:它返回一个对应于指定过滤器的对象,或者引发一个 404 异常(django.http.Http404)。Django 将捕获这个消息,并创建一个 404 状态的 HTML 响应(404 是“未找到”HTTP 状态代码),如果可用的话,使用404.html模板。
要使用这个新视图,它需要 urlpatterns 中的一个条目:
from main import views
...
urlpatterns = [
path(
"products/<slug:tag>/",
views.ProductListView.as_view(),
name="products",
),
...
]
这是项目中第一个具有可变路径的 URL。Django 将尝试使用接受字母、数字、连字符和下划线的slug转换器来转换由<和>包围的部分中的任何内容。
产品列表页面需要的最后一样东西是一个模板,它将存储在main/templates/main/product_list.html中:
{% extends "base.html" %}
{% block content %}
<h1>products</h1>
{% for product in page_obj %}
<p>{{ product.name }}</p>
<p>
<a href="{% url "product" product.slug %}">See it here</a>
</p>
{% if not forloop.last %}
<hr>
{% endif %}
{% endfor %}
<nav>
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a
class="page-link"
href="?page={{page_obj.previous_page_number}}">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#">Previous</a>
</li>
{% endif %}
{% for pagenum in page_obj.paginator.page_range %}
<li
class="page-item{% if page_obj.number == pagenum %} active{% endif %}">
<a class="page-link" href="?page={{pagenum}}">{{pagenum}}</a>
</li>
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{page_obj.next_page_number}}">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<a class="page-link" href="#">Next</a>
</li>
{% endif %}
</ul>
</nav>
{% endblock content %}
这个模板中有一些新的东西值得解释。我们在已经看到的现有基础模板和标签的基础上构建,并使用一个新的for标签。这个标记做 Python 中的等效标记所做的事情:它重复一个部分。我们将生成与列表中的元素一样多的 HTML 部分。
在for循环中,我们可以使用一个名为forloop的变量,它包含一系列我们可以使用的动态值。这个变量由 Django 自动设置。在我们的例子中,除了最后一个周期,我们为每个周期打印一个水平分隔符。
模板中的最后一部分打印分页部分。视图填充的page_obj变量包含一部分需要可视化的结果,以及一些页面信息,比如当前页面是什么,是否有下一个页面。图 3-9 显示了上面的模板在渲染时的样子。
图 3-9
产品列表视图
作为产品列表视图的最后一步,我们将添加它的测试。我们可以将它们添加到test_views.py文件中:
...
from decimal import Decimal
from main import models
class TestPage(TestCase):
...
def test_products_page_returns_active(self):
models.Product.objects.create(
name="The cathedral and the bazaar",
slug="cathedral-bazaar",
price=Decimal("10.00"),
)
models.Product.objects.create(
name="A Tale of Two Cities",
slug="tale-two-cities",
price=Decimal("2.00"),
active=False,
)
response = self.client.get(
reverse("products", kwargs={"tag": "all"})
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "BookTime")
product_list = models.Product.objects.active().order_by(
"name"
)
self.assertEqual(
list(response.context["object_list"]),
list(product_list),
)
def test_products_page_filters_by_tags_and_active(self):
cb = models.Product.objects.create(
name="The cathedral and the bazaar",
slug="cathedral-bazaar",
price=Decimal("10.00"),
)
cb.tags.create(name="Open source", slug="opensource")
models.Product.objects.create(
name="Microsoft Windows guide",
slug="microsoft-windows-guide",
price=Decimal("12.00"),
)
response = self.client.get(
reverse("products", kwargs={"tag": "opensource"})
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "BookTime")
product_list = (
models.Product.objects.active()
.filter(tags__slug="opensource")
.order_by("name")
)
self.assertEqual(
list(response.context["object_list"]),
list(product_list),
)
假设产品列表页面总是返回数据库数据,那么每个测试都需要填充这些表。两个主要的执行路径是是否有带标签的查询,每个都有一个测试。
两个测试的最后一个断言是测试是否将正确的产品传递给模板进行呈现的断言。这里我们用object_list代替page_obj,但原理差不多。
我们使用product_list作为最后一个断言的预期结果。如您所见,在创建时,所有返回 querysets 的方法都是可链接的。这就是为什么可以将active()与filter()和order_by()一起使用。
路径转换器
我在前面简单提到了 URL 路径和转换器,以及slug转换器。路径转换器和path()函数是 Django 在 2.0 版本中的新增功能。它们是一种非常方便的快捷方式,而不是重复指定长而难读的正则表达式。
格式为<type:name>。Django 已经包含了一些路径转换器:
-
str匹配除字符/以外的任何非空字符串。 -
int匹配任何大于或等于零的整数。 -
slug匹配字母、数字、连字符和下划线。 -
uuid匹配 UUIDs。 -
path像str但也配/。
如果您有一些经常出现在项目中的标识符,并且不属于任何列出的类,那么您可以创建自己的转换器。在线文档解释了在需要时如何创建它们。
添加单个产品页面
为了完善这一章,我们将为单个产品创建一个可视化效果,比产品列表页面更详细。与ListView类似,我们可以利用另一个名为DetailView的视图。
与之前的视图不同,我们的详细视图版本不需要很多定制。我们将只定制模板和连接网址。我们将依赖它的约定,包括它构造模板名称的方式。在我们的例子中是main/templates/main/product_detail.html。
{% extends "base.html" %}
{% block content %}
<h1>products</h1>
<table class="table">
<tr>
<th>Name</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th>Cover images</th>
<td>
{% for image in object.productimage_set.all %}
<img src="{{ image.thumbnail.url }}" alt="cover"/>
{% endfor %}
</td>
</tr>
<tr>
<th>Price</th>
<td>{{ object.price }}</td>
</tr>
<tr>
<th>Description</th>
<td>{{ object.description|linebreaks }}</td>
</tr>
<tr>
<th>Tags</th>
<td>{{ object.tags.all|join:","|default:"No tags available" }}</td>
</tr>
<tr>
<th>In stock</th>
<td>{{ object.in_stock|yesno|capfirst }}</td>
</tr>
<tr>
<th>Updated</th>
<td>{{ object.date_updated|date:"F Y" }}</td>
</tr>
</table>
<a href="{% url "add_to_basket" %}?product_id={{ object.id }}">Add to basket</a>
{% endblock content %}
模板本身非常简单:它只显示变量object的一些属性,这是DetailView为模型实例定义的。然而,这个模板展示了模板过滤器的一个很好的例子。
模板过滤器是将一些值转换成另一个值的操作,这在数据有一些原始形式不能满足的可视化需求的情况下很有帮助。
linebreaks获取description字段的内容,并为字段中的每一个新行生成一个<p>标签。join用指定的分隔符连接字符串列表。default在当前输出为 None 的情况下输出其参数。
yesno将 true/false 转换为字符串 yes/no。capfirst将第一个单词大写。date在将日期对象转换成字符串时很有用,并且可以指定格式。这种情况下的格式仅为月和年。
使用过滤器的常见格式是variable|filtername:arg1,arg2...。有些过滤器接受参数,有些不接受。
模板中另一个值得一提的是相关模型是如何被遍历的。如果一个模型有另一个表的外键,只需指定相关管理器的名称就可以调用它们的方法。例如,模板中的object.tags.all相当于 Python 中的object.tags.all()。
Django 还有很多其他的过滤器。也有创建我们自己的过滤器的方法,我们将在本书的后面看到。
为了连接起来,下面是正确的 urlpattern:
from django.views.generic.detail import DetailView
from main import models
urlpatterns =
path(
"product/<slug:slug>/",
DetailView.as_view(model=models.Product),
name="product",
),
...
使用没有子类化的DetailView对于我们的用例来说已经足够好了。正因为如此,我们避免了编写任何 Python 代码,因此,没有必要为它编写自动化测试。图 [3-10 显示了该页面的外观。
图 3-10
产品明细视图
摘要
在这一章中,我们定义了我们的第一个模型。我们对虚构公司在线销售的产品进行建模,包括描述性标签和所有链接到特定产品的图像。除了模型之外,我们还包括视图和 URL 来向网站用户显示这些数据。
我们谈到了移民。在 Django 中,迁移是数据库管理的一个重要部分。它曾经是一个独立的库,但在最近的版本中已经集成到框架中。
本章还介绍了 Django admin,这是一个供内部用户管理数据库内容的管理仪表板。在我们的例子中,我们创建面板来添加、更改和删除关于我们产品的数据。
我们还讨论了管理命令、它们是什么以及它们如何工作,以及模型序列化和反序列化。我们利用这两个概念从 CSV 文件创建了一个产品导入器。
在下一章中,我们将讨论如何在网上列出这些待售产品,以便可以通过网络直接下订单。