Django2 Web 应用构建指南(一)

原文:zh.annas-archive.org/md5/18689E1989723338A1936B680A71254B

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

谁没有一个想要推出的下一个伟大应用或服务的想法?然而,大多数应用程序、服务和网站最终都依赖于服务器能够接受请求,然后根据这些请求创建、读取、更新和删除记录。Django 使得构建和启动网站、服务和后端变得容易。然而,尽管它在大规模成功的初创公司和企业中被使用的历史,但要收集实际将一个想法从空目录到运行生产服务器所需的所有资源可能是困难的。

在三个项目的过程中,《构建 Django Web 应用程序》指导您从一个空目录到创建全功能应用程序,以复制一些最受欢迎的网络应用程序的核心功能。在第一部分,您将创建自己的在线电影数据库。在第二部分,您将创建一个让用户提问和回答问题的网站。在第三部分,您将创建一个用于管理邮件列表和发送电子邮件的 Web 应用程序。所有三个项目都将最终部署到服务器上,以便您可以看到自己的想法变为现实。在开始每个项目和部署它之间,我们将涵盖重要的实用概念,如如何构建 API、保护您的项目、使用 Elasticsearch 添加搜索、使用缓存和将任务卸载到工作进程以帮助您的项目扩展。

《构建 Django Web 应用程序》适用于已经了解 Python 基础知识,但希望将自己的技能提升到更高水平的开发人员。还建议具有基本的 HTML 和 CSS 理解,因为这些语言将被提及,但不是本书的重点。

阅读完本书后,您将熟悉使用 Django 启动惊人的 Web 应用程序所需的一切。

这本书是为谁准备的

这本书是为熟悉 Python 的开发人员准备的。读者应该知道如何在 Bash shell 中运行命令。假定具有一些基本的 HTML 和 CSS 知识。最后,读者应该能够自己连接到 PostgreSQL 数据库。

充分利用本书

要充分利用本书,您应该:

  1. 对 Python 有一定了解,并已安装 Python3.6+

  2. 能够在计算机上安装 Docker 或其他新软件

  3. 知道如何从计算机连接到 Postgres 服务器

  4. 可以访问 Bash shell

下载示例代码文件

您可以从www.packtpub.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support并注册,以便文件直接通过电子邮件发送给您。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packtpub.com

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的解压缩或提取文件夹:

  • WinRAR/7-Zip 适用于 Windows

  • Zipeg/iZip/UnRarX 适用于 Mac

  • 7-Zip/PeaZip 适用于 Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Building-Django-2.0-Web-Applications。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有来自我们丰富的图书和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!

使用的约定

本书中使用了许多文本约定。

CodeInText:指示文本中的代码字词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。以下是一个示例:“它还提供了一个create()方法,用于创建和保存实例。”

代码块设置如下:

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

当我们希望引起您对代码块的特定部分的注意时,相关的行或项目会以粗体显示:

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

任何命令行输入或输出都以以下方式书写:

$ pip install -r requirements.dev.txt

粗体:表示一个新术语,一个重要的词,或者屏幕上看到的词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“点击 MOVIES 将显示给我们一个电影列表。”

警告或重要说明会以这种方式出现。

提示和技巧会以这种方式出现。

第一章:启动 MyMDB

我们将构建的第一个项目是一个基本的互联网电影数据库IMDB)克隆,名为我的电影数据库(MyMDB),使用 Django 2.0 编写,我们将使用 Docker 部署。我们的 IMDB 克隆将有以下两种类型的用户:用户和管理员。用户将能够对电影进行评分,添加电影图片,并查看电影和演员阵容。管理员将能够添加电影、演员、作家和导演。

在本章中,我们将做以下事情:

  • 创建我们的新 Django 项目 MyMDB,一个 IMDB 克隆

  • 创建一个 Django 应用程序并创建我们的第一个模型、视图和模板

  • 了解并使用我们模型中的各种字段,并在模型之间创建关系

该项目的代码可在以下网址在线获取:github.com/tomaratyn/MyMDB

最后,我们将能够在我们的项目中添加电影、人物和角色,并让用户在易于定制的 HTML 模板中查看它们。

启动我的电影数据库(MyMDB)

首先,让我们为我们的项目创建一个目录:

$ mkdir MyMDB
$ cd MyMDB

我们所有未来的命令和路径都将相对于这个项目目录。

启动项目

一个 Django 项目由多个 Django 应用程序组成。Django 应用程序可以来自许多不同的地方:

  • Django 本身(例如,django.contrib.admin,管理后台应用程序)

  • 安装 Python 包(例如,django-rest-framework,一个从 Django 模型创建 REST API 的框架)

  • 作为项目的一部分(我们将要编写的代码)

通常,一个项目会使用前面三个选项的混合。

安装 Django

我们将使用pip安装 Django,Python 的首选包管理器,并在requirements.dev.txt文件中跟踪我们安装的包:

django<2.1
psycopg2<2.8

现在,让我们安装这些包:

$ pip install -r requirements.dev.txt

创建项目

安装了 Django 后,我们有了django-admin命令行工具,可以用它来生成我们的项目:

$ django-admin startproject config
$ tree config/
config/
├── config
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

settings.py文件的父级称为config,因为我们将项目命名为config而不是mymdb。然而,让顶级目录继续被称为config是令人困惑的,所以让我们将其重命名为django(一个项目可能会包含许多不同类型的代码;再次称呼 Django 代码的父级目录为django,可以让人清楚地知道):

$ mv config django 
$ tree .
.
├── django
│   ├── config
│   │   ├── __init__.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   └── manage.py
└── requirements.dev.txt

2 directories, 6 files

让我们仔细看看其中一些文件:

  • settings.py:这是 Django 默认存储应用程序所有配置的地方。在缺少DJANGO_SETTINGS环境变量的情况下,Django 默认在这里查找设置。

  • urls.py:这是整个项目的根URLConf。你的 Web 应用程序收到的每个请求都将被路由到这个文件内匹配路径的第一个视图(或urls.py引用的文件)。

  • wsgi.pyWeb Server Gateway InterfaceWSGI)是 Python 和 Web 服务器之间的接口。你不会经常接触到这个文件,但这是你的 Web 服务器和 Python 代码知道如何相互通信的方式。我们将在第五章中引用它,使用 Docker 部署

  • manage.py:这是进行非代码更改的命令中心。无论是创建数据库迁移、运行测试,还是启动开发服务器,我们经常会使用这个文件。

请注意,缺少的是django目录不是 Python 模块。里面没有__init__.py文件,也不应该有。如果添加了一个,许多东西将会出错,因为我们希望添加的 Django 应用程序是顶级 Python 模块。

配置数据库设置

默认情况下,Django 创建一个将使用 SQLite 的项目,但这对于生产来说是不可用的,所以我们将遵循在开发和生产中使用相同数据库的最佳实践。

让我们打开django/config/settings.py并更新它以使用我们的 Postgres 服务器。找到settings.py中以DATABASES开头的行。默认情况下,它看起来像这样:

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

要使用 Postgres,请将上述代码更改为以下代码:

DATABASES = {
    'default': {
 'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mymdb',
        'USER': 'mymdb',
        'PASSWORD': 'development',
        'HOST': '127.0.0.1',
        'PORT': '5432',    }
}

如果您以前连接过数据库,大部分内容都会很熟悉,但让我们回顾一下:

  • DATABASES = {: 这是数据库连接信息的字典常量,并且是 Django 所必需的。您可以连接到不同数据库的多个连接,但大部分时间,您只需要一个名为default的条目。

  • 'default': {: 这是默认的数据库连接配置。您应该始终具有一组default连接设置。除非另有说明(在本书中我们不会),否则这是您将要使用的连接。

  • 'ENGINE': 'django.db.backends.postgresql ': 这告诉 Django 使用 Postgres 后端。这反过来使用psycopg2,Python 的 Postgres 库。

  • 'NAME': 'mymdb',: 您想要连接的数据库的名称。

  • ‘USER': 'mymdb',: 您的连接用户名。

  • ‘PASSWORD': 'development',: 您的数据库用户的密码。

  • ‘HOST': '127.0.0.1’,: 您要连接的数据库服务器的地址。

  • ‘PORT': '5432',: 您要连接的端口。

核心应用程序

Django 应用程序遵循模型视图模板MVT)模式;在这种模式中,我们将注意以下事项:

  • 模型负责从数据库保存和检索数据

  • 视图负责处理 HTTP 请求,启动模型上的操作,并返回 HTTP 响应

  • 模板负责响应主体的外观

在 Django 项目中,您可以拥有任意数量的应用程序。理想情况下,每个应用程序应该具有像任何其他 Python 模块一样紧密范围和自包含的功能,但在项目开始时,很难知道复杂性将出现在哪里。这就是为什么我发现从core应用程序开始很有用。然后,当我注意到特定主题周围存在复杂性集群时(比如说,在我们的项目中,如果我们在那里取得进展,演员可能会变得意外复杂),那么我们可以将其重构为自己的紧密范围的应用程序。其他时候,很明显一个站点有自包含的组件(例如,管理后端),并且很容易从多个应用程序开始。

制作核心应用程序

要创建一个新的 Django 应用程序,我们首先必须使用manage.py创建应用程序,然后将其添加到INSTALLED_APPS列表中:

$ cd django
$ python manage.py startapp core
$ ls
config      core        manage.py
$tree core
core
├─  472; __init__.py
├── admin.py
├── apps.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py

1 directory, 7 files

让我们更仔细地看看核心内部有什么:

  • core/__init__.py: 核心不仅是一个目录,还是一个 Python 模块。

  • admin.py: 这是我们将在其中使用内置管理后端注册我们的模型。我们将在电影管理部分进行描述。

  • apps.py: 大部分时间,您会将其保持不变。这是您将在其中放置任何在注册应用程序时需要运行的代码的地方,如果您正在制作可重用的 Django 应用程序(例如,您想要上传到 PyPi 的软件包)。

  • migrations: 这是一个带有数据库迁移的 Python 模块。数据库迁移描述了如何从一个已知状态迁移数据库到另一个状态。使用 Django,如果您添加了一个模型,您只需使用manage.py生成并运行迁移,您可以在本章后面的迁移数据库部分中看到。

  • models.py: 这是用于模型的。

  • tests.py: 这是用于测试的。

  • views.py: 这是用于视图的。

安装我们的应用程序

现在我们的核心应用程序存在了,让我们通过将其添加到settings.py文件中的已安装应用程序列表中,让 Django 意识到它。您的settings.py应该有一行看起来像这样的:

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

INSTALLED_APPS是 Django 应用程序的 Python 模块的 Python 路径列表。我们已经安装了用于解决常见问题的应用程序,例如管理静态文件、会话和身份验证以及管理后端,因为 Django 的 Batteries Included 哲学。

让我们将我们的core应用程序添加到列表的顶部:

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

添加我们的第一个模型 - 电影

现在我们可以添加我们的第一个模型,即电影。

Django 模型是从Model派生的类,具有一个或多个Fields。在数据库术语中,Model类对应于数据库表,Field类对应于列,Model的实例对应于行。使用像 Django 这样的 ORM,让我们利用 Python 和 Django 编写表达性的类,而不是在 Python 中编写我们的模型,然后再在 SQL 中编写一次。

让我们编辑django/core/models.py来添加一个Movie模型:

from django.db import models

class Movie(models.Model):
    NOT_RATED = 0
    RATED_G = 1
    RATED_PG = 2
    RATED_R = 3
    RATINGS = (
        (NOT_RATED, 'NR - Not Rated'),
        (RATED_G,
         'G - General Audiences'),
        (RATED_PG,
         'PG - Parental Guidance '
         'Suggested'),
        (RATED_R, 'R - Restricted'),
    )

    title = models.CharField(
        max_length=140)
    plot = models.TextField()
    year = models.PositiveIntegerField()
    rating = models.IntegerField(
        choices=RATINGS,
        default=NOT_RATED)
    runtime = \
        models.PositiveIntegerField()
    website = models.URLField(
        blank=True)

    def __str__(self):
        return '{} ({})'.format(
            self.title, self.year)

Movie派生自models.Model,这是所有 Django 模型的基类。接下来,有一系列描述评级的常量;我们将在查看rating字段时再看一下,但首先让我们看看其他字段:

  • title = models.CharField(max_length=140): 这将成为一个长度为 140 的varchar列。数据库通常要求varchar列的最大大小,因此 Django 也要求。

  • plot = models.TextField(): 这将成为我们数据库中的一个text列,它没有最大长度要求。这使得它更适合可以有一段(甚至一页)文本的字段。

  • year = models.PositiveIntegerField(): 这将成为一个integer列,并且 Django 将在保存之前验证该值,以确保在保存时它是0或更高。

  • rating = models.IntegerField(choices=RATINGS, default=NOT_RATED): 这是一个更复杂的字段。Django 将知道这将是一个integer列。可选参数choices(对于所有Fields都可用,不仅仅是IntegerField)接受一个值/显示对的可迭代对象(列表或元组)。对中的第一个元素是可以存储在数据库中的有效值,第二个是该值的人性化版本。Django 还将在我们的模型中添加一个名为get_rating_display()的实例方法,它将返回与存储在我们的模型中的值匹配的第二个元素。任何不匹配choices中的值的内容在保存时都将是一个ValidationErrordefault参数在创建模型时提供默认值。

  • runtime = models.PositiveIntegerField(): 这与year字段相同。

  • website = models.URLField(blank=True): 大多数数据库没有本机 URL 列类型,但数据驱动的 Web 应用程序通常需要存储它们。URLField默认情况下是一个varchar(200)字段(可以通过提供max_length参数来设置)。URLField还带有验证,检查其值是否为有效的 Web(http/https/ftp/ftps)URL。blank参数由admin应用程序用于知道是否需要值(它不影响数据库)。

我们的模型还有一个__str__(self)方法,这是一种最佳实践,有助于 Django 将模型转换为字符串。Django 在管理 UI 和我们自己的调试中都会这样做。

Django 的 ORM 自动添加了一个自增的id列,因此我们不必在所有模型上重复。这是 Django 的不要重复自己(DRY)哲学的一个简单例子。随着我们的学习,我们将看更多的例子。

迁移数据库

现在我们有了一个模型,我们需要在数据库中创建一个与之匹配的表。我们将使用 Django 为我们生成一个迁移,然后运行迁移来为我们的电影模型创建一个表。

虽然 Django 可以为我们的 Django 应用程序创建和运行迁移,但它不会为我们的 Django 项目创建数据库和数据库用户。要创建数据库和用户,我们必须使用管理员帐户连接到服务器。连接后,我们可以通过执行以下 SQL 来创建数据库和用户:

CREATE DATABASE mymdb;
CREATE USER mymdb;
GRANT ALL ON DATABASE mymdb to "mymdb";
ALTER USER mymdb PASSWORD 'development';
ALTER USER mymdb CREATEDB;

上述 SQL 语句将为我们的 Django 项目创建数据库和用户。GRANT语句确保我们的 mymdb 用户将能够访问数据库。然后,我们在mymdb用户上设置密码(确保与您的settings.py文件中的密码相同)。最后,我们授予mymdb用户创建新数据库的权限,这将在运行测试时由 Django 用于创建测试数据库。

要为我们的应用程序生成迁移,我们需要告诉manage.py文件执行以下操作:

$ cd django
$ python manage.py makemigrations core
Migrations for 'core':
  core/migrations/0001_initial.py
    - Create model Movie

迁移是我们 Django 应用程序中的一个 Python 文件,描述了如何将数据库更改为所需的状态。Django 迁移不绑定到特定的数据库系统(相同的迁移将适用于支持的数据库,除非我们添加特定于数据库的代码)。Django 生成使用 Django 的迁移 API 的迁移文件,我们不会在本书中研究它,但知道它存在是有用的。

请记住,有迁移的是应用程序而不是项目(因为有模型的是应用程序)。

接下来,我们告诉manage.py迁移我们的应用程序:

$ python manage.py migrate core 
Operations to perform:
  Apply all migrations: core
Running migrations:
  Applying core.0001_initial... OK

现在,我们的数据库中存在我们的表:

$ python manage.py dbshell
psql (9.6.1, server 9.6.3)
Type "help" for help.

mymdb=> \dt
             List of relations
 Schema |       Name        | Type  | Owner 
--------+-------------------+-------+-------
 public | core_movie        | table | mymdb
 public | django_migrations | table | mymdb
(2 rows)

mymdb=> \q

我们可以看到我们的数据库有两个表。Django 模型表的默认命名方案是<app_name>_<model_name>。我们可以看出core_moviecore应用程序的Movie模型的表。django_migrations是 Django 内部用于跟踪已应用的迁移的表。直接修改django_migrations表而不使用manage.py是一个坏主意,这将在尝试应用或回滚迁移时导致问题。

迁移命令也可以在不指定应用程序的情况下运行,在这种情况下,它将在所有应用程序上运行。让我们在没有应用程序的情况下运行migrate命令:

$ python manage.py migrate 
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, core, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying sessions.0001_initial... OK

这将创建用于跟踪用户、会话、权限和管理后端的表。

创建我们的第一个电影

与 Python 一样,Django 提供了一个交互式 REPL 来尝试一些东西。Django shell 完全连接到数据库,因此我们可以在 shell 中创建、查询、更新和删除模型:

$ cd django
$ python manage.py shell
Python 3.4.6 (default, Aug  4 2017, 15:21:32) 
[GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.42)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from core.models import Movie
>>> sleuth = Movie.objects.create(
... title='Sleuth',
... plot='An snobbish writer who loves games'
... ' invites his wife\'s lover for a battle of wits.',
... year=1972,
... runtime=138,
... )
>>> sleuth.id
1
>>> sleuth.get_rating_display()
'NR - Not Rated'

在前面的 Django shell 会话中,请注意我们没有创建的Movie的许多属性:

  • objects是模型的默认管理器。管理器是查询模型表的接口。它还提供了一个create()方法来创建和保存实例。每个模型必须至少有一个管理器,Django 提供了一个默认管理器。通常建议创建一个自定义管理器;我们将在添加人员和模型关系部分中看到这一点。

  • id是此实例的行的主键。如前一步骤中所述,Django 会自动创建它。

  • get_rating_display()是 Django 添加的一个方法,因为rating字段给定了一个choices元组。我们在create()调用中没有为rating提供值,因为rating字段有一个default值(0)。get_rating_display()方法查找该值并返回相应的显示值。Django 将为具有choices参数的每个Field属性生成这样的方法。

接下来,让我们使用 Django Admin 应用程序创建一个管理电影的后端。

创建电影管理

能够快速生成后端 UI 让用户在项目的其余部分仍在开发中时开始构建项目的内容。这是一个很好的功能,可以帮助并行化进度并避免重复和乏味的任务(读取/更新视图共享许多功能)。提供这种功能是 Django“电池包含”哲学的另一个例子。

为了使 Django 的管理应用程序与我们的模型一起工作,我们将执行以下步骤:

  1. 注册我们的模型

  2. 创建一个可以访问后端的超级用户

  3. 运行开发服务器

  4. 在浏览器中访问后端

让我们通过编辑django/core/admin.py来注册我们的Movie模型,如下所示:

from django.contrib import admin

from core.models import Movie

admin.site.register(Movie)

现在我们的模型已注册!

现在让我们创建一个可以使用manage.py访问后端的用户:

$ cd django
$ python manage.py createsuperuser 
Username (leave blank to use 'tomaratyn'): 
Email address: tom@aratyn.nam
Password: 
Password (again): 
Superuser created successfully.

Django 附带了一个开发服务器,可以为我们的应用提供服务,但不适合生产:

$ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).
September 12, 2017 - 20:31:54
Django version 1.11.5, using settings 'config.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

还可以在浏览器中打开它,导航到http://localhost:8000/

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

要访问管理后端,请转到http://localhost:8000/admin

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一旦使用凭据登录,我们必须管理用户和电影:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击 MOVIES 将显示我们的电影列表:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

请注意,链接的标题是我们的Movie.__str__方法的结果。点击它将为您提供一个 UI 来编辑电影:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在主管理屏幕和电影列表屏幕上,您可以找到添加新电影的链接。让我们添加一个新电影:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在,我们的电影列表显示了所有电影:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们有了一种让团队填充电影数据库的方法,让我们开始为用户编写视图。

创建 MovieList 视图

当 Django 收到请求时,它使用请求的路径和项目的URLConf来匹配请求和视图,后者返回 HTTP 响应。Django 的视图可以是函数,通常称为基于函数的视图FBVs),也可以是类,通常称为基于类的视图CBVs)。CBVs 的优势在于 Django 附带了丰富的通用视图套件,您可以对其进行子类化,以轻松(几乎是声明性地)编写视图以完成常见任务。

让我们编写一个视图来列出我们拥有的电影。打开django/core/views.py并将其更改为以下内容:

from django.views.generic import ListView

from core.models import Movie

class MovieList(ListView):
    model = Movie

ListView至少需要一个model属性。它将查询该模型的所有行,将其传递给模板,并返回渲染后的模板响应。它还提供了许多我们可以使用的钩子来替换默认行为,这些都有完整的文档记录。

ListView如何知道如何查询Movie中的所有对象?为此,我们需要讨论管理器和QuerySet类。每个模型都有一个默认管理器。管理器类主要用于通过提供方法(例如all())来查询对象,返回QuerySetQuerySet类是 Django 对数据库查询的表示。QuerySet有许多方法,包括filter()(例如SELECT语句中的WHERE子句)来限制结果。QuerySet类的一个很好的特性是它是惰性的;直到我们尝试从QuerySet中获取模型时,它才会被评估。另一个很好的特性是filter()等方法采用查找表达式,可以是字段名称或跨关系模型。我们将在整个项目中都这样做。

所有管理器类都有一个all()方法,应返回一个未经过滤的Queryset,相当于编写SELECT * FROM core_movie;

那么,ListView如何知道它必须查询Movie中的所有对象?ListView检查它是否有model属性,如果有,它知道Model类具有默认管理器,带有all()方法,它会调用该方法。ListView还为我们提供了放置模板的约定,如下所示:<app_name>/<model_name>_list.html

添加我们的第一个模板 - movie_list.html

Django 附带了自己的模板语言,称为Django 模板语言。Django 还可以使用其他模板语言(例如 Jinja2),但大多数 Django 项目发现使用 Django 模板语言是高效和方便的。

在我们的settings.py文件中生成的默认配置中,Django 模板语言配置为使用APP_DIRS,这意味着每个 Django 应用程序都可以有一个templates目录,该目录将被搜索以找到模板。这可以用来覆盖其他应用程序使用的模板,而无需修改第三方应用程序本身。

让我们在django/core/templates/core/movie_list.html中创建我们的第一个模板:

<!DOCTYPE html>
<html>
  <body>
    <ul>
      {% for movie in object_list %}
        <li>{{ movie }}</li>
      {% empty %}
        <li>
          No movies yet.
        </li>
      {% endfor %}
    </ul>
    <p>
      Using https? 
      {{ request.is_secure|yesno }}
    </p>
  </body>
</html>

Django 模板是标准的 HTML(或者您希望使用的任何文本格式),其中包含变量(例如我们的示例中的object_list)和标签(例如我们的示例中的for)。变量将通过用{{ }}括起来来评估为字符串。过滤器可以用来在打印之前帮助格式化或修改变量(例如yesno)。我们还可以创建自定义标签和过滤器。

Django 文档中提供了完整的过滤器和标签列表(docs.djangoproject.com/en/2.0/ref/templates/builtins/)。

Django 模板语言在settings.pyTEMPLATES变量中进行配置。DjangoTemplates后端可以使用很多OPTIONS。在开发中,添加'string_if_invalid': 'INVALID_VALUE',可能会有所帮助。每当 Django 无法将模板中的变量匹配到变量或标签时,它将打印出INVALID_VALUE,这样更容易捕捉拼写错误。请记住,不要在生产中使用此设置。完整的选项列表可以在 Django 的文档中找到(docs.djangoproject.com/en/dev/topics/templates/#django.template.backends.django.DjangoTemplates)。

最后一步将是将我们的视图连接到一个URLConf

使用 URLConf 将请求路由到我们的视图

现在我们有了模型、视图和模板,我们需要告诉 Django 应该将哪些请求路由到我们的MovieList视图使用 URLConf。每个新项目都有一个由 Django 创建的根 URLConf(在我们的情况下是django/config/urls.py文件)。Django 开发人员已经形成了每个应用程序都有自己的 URLConf 的最佳实践。然后,项目的根 URLConf 将使用include()函数包含每个应用程序的 URLConf。

让我们通过创建一个django/core/urls.py文件并使用以下代码来为我们的core应用程序创建一个 URLConf:

from django.urls import path

from . import views

app_name = 'core'
urlpatterns = [
    path('movies',
         views.MovieList.as_view(),
         name='MovieList'),
]

在其最简单的形式中,URLConf 是一个带有urlpatterns属性的模块,其中包含一系列pathpath由描述路径的字符串和可调用对象组成。CBV 不是可调用的,因此基本的View类有一个静态的as_view()方法来返回一个可调用对象。FBV 可以直接作为回调传递(不需要()运算符,这会执行它们)。

每个path()都应该被命名,这是一个有用的最佳实践,当我们需要在模板中引用该路径时。由于一个 URLConf 可以被另一个 URLConf 包含,我们可能不知道我们的视图的完整路径。Django 提供了reverse()函数和url模板标签,可以从名称转到视图的完整路径。

app_name变量设置了这个URLConf所属的应用程序。这样,我们可以引用一个命名的path,而不会让 Django 混淆其他应用程序具有相同名称的path(例如,index是一个非常常见的名称,所以我们可以说appA:indexappB:index来区分它们)。

最后,让我们通过将django/config/urls.py更改为以下内容来将我们的URLConf连接到根URLConf

from django.urls import path, include
from django.contrib import admin

import core.urls

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include(
        core.urls, namespace='core')),
]

这个文件看起来很像我们之前的URLConf文件,只是我们的path()对象不是取一个视图,而是include()函数的结果。include()函数让我们可以用一个路径前缀整个URLConf并给它一个自定义的命名空间。

命名空间让我们区分path名称,就像app_name属性一样,但不需要修改应用程序(例如,第三方应用程序)。

您可能会想为什么我们使用include()而 Django 管理网站使用propertyinclude()admin.site.urls都返回格式类似的 3 元组。但是,您应该使用include(),而不是记住 3 元组的每个部分应该具有什么。

运行开发服务器

Django 现在知道如何将请求路由到我们的 View,View 知道需要显示哪些模型以及要呈现哪个模板。我们可以告诉manage.py启动我们的开发服务器并查看我们的结果:

$ cd django
$ python manage.py runserver

在我们的浏览器中,转到http://127.0.0.1:8000/movies

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

干得好!我们制作了我们的第一个页面!

在这一部分,我们创建了我们的第一个模型,生成并运行了它的迁移,并创建了一个视图和模板,以便用户可以浏览它。

现在,让我们为每部电影添加一个页面。

单独的电影页面

现在我们有了项目布局,我们可以更快地移动。我们已经在跟踪每部电影的信息。让我们创建一个视图来显示这些信息。

要添加电影详细信息,我们需要做以下事情:

  1. 创建MovieDetail视图

  2. 创建movie_detail.html模板

  3. 在我们的URLConf中引用MovieDetail视图

创建 MovieDetail 视图

就像 Django 为我们提供了一个ListView类来执行列出模型的所有常见任务一样,Django 还提供了一个DetailView类,我们可以子类化以创建显示单个Model详细信息的视图。

让我们在django/core/views.py中创建我们的视图:

from django.views.generic import (
    ListView, DetailView,
)
from core.models import Movie

class MovieDetail(DetailView):
    model = Movie

class MovieList(ListView):
    model = Movie

DetailView要求path()对象在path字符串中包含pkslug,以便DetailView可以将该值传递给QuerySet以查询特定的模型实例。slug是一个短的、URL 友好的标签,通常在内容丰富的网站中使用,因为它对 SEO 友好。

创建 movie_detail.html 模板

现在我们有了 View,让我们制作我们的模板。

Django 的模板语言支持模板继承,这意味着您可以编写一个包含网站外观和感觉的模板,并标记其他模板将覆盖的block部分。这使我们能够创建整个网站的外观和感觉,而无需编辑每个模板。让我们使用这个功能创建一个具有 MyMDB 品牌和外观的基本模板,然后添加一个从基本模板继承的电影详细信息模板。

基本模板不应该与特定的应用程序绑定,因此让我们创建一个通用的模板目录:

$ mkdir django/templates

Django 还不知道如何检查我们的templates目录,因此我们需要更新settings.py文件中的配置。找到以TEMPLATES开头的行,并更改配置以在DIRS列表中列出我们的templates目录:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            os.path.join(BASE_DIR, 'templates'),
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            # omittted for brevity
        },
    },
]

我们唯一做的改变是将我们的新templates目录添加到DIRS键下的列表中。我们避免使用 Python 的os.path.join()函数和已配置的BASE_DIR来将路径硬编码到我们的templates目录。BASE_DIR在运行时设置为项目的路径。我们不需要添加django/core/templates,因为APP_DIRS设置告诉 Django 检查每个应用程序的templates目录。

虽然settings.py是一个非常方便的 Python 文件,我们可以在其中使用os.path.join和所有 Python 的功能,但要小心不要太聪明。settings.py需要易于阅读和理解。没有什么比不得不调试你的settings.py更糟糕的了。

让我们在django/templates/base.html中创建一个基本模板,其中有一个主列和侧边栏:

<!DOCTYPE html>
<html lang="en" >
<head >
  <meta charset="UTF-8" >
  <meta
    name="viewport"
    content="width=device-width, initial-scale=1, shrink-to-fit=no"
  >
  <link
    href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css"
    integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M"
    rel="stylesheet"
    crossorigin="anonymous"
  >
  <title >
    {% block title %}MyMDB{% endblock %}
  </title>
  <style>
    .mymdb-masthead {
      background-color: #EEEEEE;
      margin-bottom: 1em;
    }
  </style>

</head >
<body >
<div class="mymdb-masthead">
  <div class="container">
    <nav class="nav">
      <div class="navbar-brand">MyMDB</div>
      <a
        class="nav-link"
        href="{% url 'core:MovieList' %}"
      >
        Movies
      </a>
    </nav>
  </div>
</div>

<div class="container">
  <div class="row">
    <div class="col-sm-8 mymdb-main">
     {% block main %}{% endblock %}
    </div>
    <div
        class="col-sm-3 offset-sm-1 mymdb-sidebar"
    >
      {% block sidebar %}{% endblock %}
    </div>
  </div>
</div>

</body >
</html >

这个 HTML 的大部分实际上是 bootstrap(HTML/CSS 框架)样板,但我们有一些新的 Django 标签:

  • {% block title %}MyMDB{% endblock %}:这创建了一个其他模板可以替换的块。如果未替换该块,则将使用父模板中的内容。

  • href="{% url 'core:MovieList' %}"url标签将为命名的path生成 URL 路径。URL 名称应该被引用为<app_namespace>:<name>;在我们的情况下,core是核心应用程序的命名空间(在django/core/urls.py中),而MovieListMovieList视图的 URL 的名称。

这样我们就可以在django/core/templates/core/movie_detail.html中创建一个简单的模板:

{% extends 'base.html' %}

{% block title %}
  {{ object.title }} - {{ block.super }}
{% endblock %}

{% block main %}
<h1>{{ object }}</h1>
<p class="lead">
{{ object.plot }}
</p>
{% endblock %}

{% block sidebar %}
<div>
This movie is rated:
  <span class="badge badge-primary">
  {{ object.get_rating_display }}
  </span>
</div>
{% endblock %}

这个模板的 HTML 要少得多,因为base.html已经有了。MovieDetail.html所要做的就是为base.html定义的块提供值。让我们来看看一些新标签:

  • {% extends 'base.html' %}:如果一个模板想要扩展另一个模板,第一行必须是一个extends标签。Django 将寻找基本模板(它可以扩展另一个模板)并首先执行它,然后替换块。一个扩展另一个的模板不能在block之外有内容,因为不清楚将内容放在哪里。

  • {{ object.title }} - {{ block.super }}:我们在title模板block中引用block.superblock.super返回基本模板中title模板block的内容。

  • {{ object.get_rating_display }}:Django 模板语言不使用()来执行方法,只需通过名称引用它即可执行该方法。

将 MovieDetail 添加到 core.urls.py

最后,我们将MovieDetail视图添加到core/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('movies',
         views.MovieList.as_view(),
         name='MovieList'),
    path('movie/<int:pk>',
         views.MovieDetail.as_view(),
         name='MovieDetail'),
]

MovieDetailMovieListpath()调用几乎看起来一样,只是MovieDetail字符串有一个命名参数。path路由字符串可以包括尖括号,给参数一个名称(例如,<pk>),甚至定义参数的内容必须符合的类型(例如,<int:pk>只匹配解析为int的值)。这些命名部分被 Django 捕获并按名称传递给视图。DetailView期望一个pk(或slug)参数,并使用它从数据库中获取正确的行。

让我们使用python manage.py runserver来启动dev服务器,看看我们的新模板是什么样子的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

快速回顾本节

在本节中,我们创建了一个新视图MovieDetail,学习了模板继承,以及如何将参数从 URL 路径传递给我们的视图。

接下来,我们将为我们的MovieList视图添加分页,以防止每次查询整个数据库。

分页和将电影列表链接到电影详情

在这一部分,我们将更新我们的电影列表,为每部电影提供一个链接,并进行分页,以防止我们的整个数据库被倾倒到一个页面上。

更新 MovieList.html 以扩展 base.html

我们原来的MovieList.html是一个相当简陋的事情。让我们使用我们的base.html模板和它提供的 bootstrap CSS 来更新它,使它看起来更好看:

{% extends 'base.html' %}

{% block title %}
All The Movies
{% endblock %}

{% block main %}
<ul>
  {% for movie in object_list %}
    <li>
      <a href="{% url 'core:MovieDetail' pk=movie.id %}">
        {{ movie }}
      </a>
    </li>
  {% endfor %}
  </ul>
{% endblock %}

我们还看到url标签与命名参数pk一起使用,因为MovieDetail URL 需要一个pk参数。如果没有提供参数,那么 Django 在渲染时会引发NoReverseMatch异常,导致500错误。

让我们来看看它是什么样子的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

设置订单

我们当前视图的另一个问题是它没有排序。如果数据库返回的是无序查询,那么分页就无法帮助导航。而且,每次用户更改页面时,内容都不一致,因为数据库可能会返回一个不同顺序的结果集。我们需要我们的查询有一致的顺序。

对我们的模型进行排序也可以让开发人员的生活更轻松。无论是使用调试器、编写测试,还是运行 shell,确保我们的模型以一致的顺序返回可以使故障排除变得更简单。

Django 模型可以选择具有一个名为Meta的内部类,它让我们指定有关模型的信息。让我们添加一个带有ordering属性的Meta类:

class Movie(models.Model):
   # constants and fields omitted for brevity 

    class Meta:
        ordering = ('-year', 'title')

    def __str__(self):
        return '{} ({})'.format(
            self.title, self.year)

ordering接受一个列表或元组,通常是字段名称的字符串,可选地以-字符为前缀,表示降序。('-year', 'title')相当于 SQL 子句ORDER BY year DESC, title

ordering添加到模型的Meta类中意味着来自模型管理器的QuerySets将被排序。

添加分页

现在我们的电影总是以相同的方式排序,让我们添加分页。Django 的ListView已经内置了对分页的支持,所以我们只需要利用它。分页由控制要显示的页面的GET参数page控制。

让我们在我们的main模板block底部添加分页:

{% block main %}
 <ul >
    {% for movie in object_list %}
      <li >
        <a href="{% url 'core:MovieDetail' pk=movie.id %}" >
          {{ movie }}
        </a >
      </li >
    {% endfor %}
  </ul >
  {% if is_paginated %}
    <nav >
      <ul class="pagination" >
        <li class="page-item" >
          <a
            href="{% url 'core:MovieList' %}?page=1"
            class="page-link"
          >
            First
          </a >
        </li >
        {% if page_obj.has_previous %}
          <li class="page-item" >
            <a
              href="{% url 'core:MovieList' %}?page={{ page_obj.previous_page_number }}"
              class="page-link"
            >
              {{ page_obj.previous_page_number }}
            </a >
          </li >
        {% endif %}
        <li class="page-item active" >
          <a
            href="{% url 'core:MovieList' %}?page={{ page_obj.number }}"
            class="page-link"
          >
            {{ page_obj.number }}
          </a >
        </li >
        {% if page_obj.has_next %}
          <li class="page-item" >
            <a
              href="{% url 'core:MovieList' %}?page={{ page_obj.next_page_number }}"
              class="page-link"
            >
              {{ page_obj.next_page_number }}
            </a >
          </li >
        {% endif %}
        <li class="page-item" >
          <a
              href="{% url 'core:MovieList' %}?page=last"
              class="page-link"
          >
            Last
          </a >
        </li >
      </ul >
    </nav >
  {% endif %}
{% endblock %}

让我们看一下我们的MovieList模板的一些重要点:

  • page_objPage类型,知道有关此结果页面的信息。我们使用它来检查是否有下一页/上一页,使用has_next()/has_previous()(在 Django 模板语言中,我们不需要在()中放置(),但has_next()是一个方法,而不是属性)。我们还使用它来获取next_page_number()/previous_page_number()。请注意,在检索下一页/上一页数字之前使用has_*()方法检查其存在性非常重要。如果在检索时它们不存在,Page会抛出EmptyPage异常。

  • object_list仍然可用并保存正确的值。即使page_obj封装了此页面的结果在page_obj.object_list中,ListView也方便地确保我们可以继续使用object_list,而我们的模板不会中断。

我们现在有分页功能了!

404-当事物丢失时

现在我们有一些视图,如果在 URL 中给出错误的值(错误的pk将破坏MovieDetail;错误的page将破坏MovieList),它们将无法正常运行;让我们通过处理404错误来解决这个问题。Django 在根 URLConf 中提供了一个钩子,让我们可以使用自定义视图来处理404错误(也适用于403400500,都遵循相同的命名方案)。在您的根urls.py文件中,添加一个名为handler404的变量,其值是指向您自定义视图的 Python 路径的字符串。

但是,我们可以继续使用默认的404处理程序视图,并只编写一个自定义模板。让我们在django/templates/404.html中添加一个404模板:

{% extends "base.html" %}

{% block title %}
Not Found
{% endblock %}

{% block main %}
<h1>Not Found</h1>
<p>Sorry that reel has gone missing.</p>
{% endblock %}

即使另一个应用程序抛出404错误,也将使用此模板。

目前,如果您有一个未使用的 URL,例如http://localhost:8000/not-a-real-page,您将看不到我们的自定义 404 模板,因为 Django 的settings.py中的DEBUG设置为True。要使我们的 404 模板可见,我们需要更改settings.py中的DEBUGALLOWED_HOSTS设置:

DEBUG = False

ALLOWED_HOSTS = [
    'localhost',
    '127.0.0.1'
]

ALLOWED_HOSTS是一个设置,限制 Django 将响应的 HTTP 请求中的HOST值。如果DEBUGFalse,并且HOST不匹配ALLOWED_HOSTS值,则 Django 将返回400错误(您可以根据前面的代码自定义此错误的视图和模板)。这是一项保护我们的安全功能,将在我们的安全章节中更多地讨论。

现在我们的项目已配置好,让我们运行 Django 开发服务器:

$ cd django
$ python manage.py runserver

运行时,我们可以使用我们的网络浏览器打开localhost:8000/not-a-real-page。我们的结果应该是这样的:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

测试我们的视图和模板

由于我们现在在MoveList模板中有一些逻辑,让我们写一些测试。我们将在第八章 测试 Answerly中更多地讨论测试。但是,基础知识很简单,遵循常见的 XUnit 模式,即TestCase类包含进行断言的测试方法。

对于 Django 的TestRunner来找到一个测试,它必须在已安装应用的tests模块中。现在,这意味着tests.py,但是,最终,您可能希望切换到一个目录 Python 模块(在这种情况下,为了让TestRunner找到它们,为您的测试文件名加上test前缀)。

让我们添加一个执行以下功能的测试:

  • 如果有超过 10 部电影,那么分页控件应该在模板中呈现

  • 如果有超过 10 部电影,而我们没有提供page GET参数,请考虑以下事项:

  • page_is_last上下文变量应该是False

  • page_is_first上下文变量应该是True

  • 分页中的第一项应该被标记为活动状态

以下是我们的tests.py文件:

from django.test import TestCase
from django.test.client import \
    RequestFactory
from django.urls.base import reverse

from core.models import Movie
from core.views import MovieList

class MovieListPaginationTestCase(TestCase):

    ACTIVE_PAGINATION_HTML = """
    <li class="page-item active">
      <a href="{}?page={}" class="page-link">{}</a>
    </li>
    """

    def setUp(self):
        for n in range(15):
            Movie.objects.create(
                title='Title {}'.format(n),
                year=1990 + n,
                runtime=100,
            )

    def testFirstPage(self):
        movie_list_path = reverse('core:MovieList')
        request = RequestFactory().get(path=movie_list_path)
        response = MovieList.as_view()(request)
        self.assertEqual(200, response.status_code)
        self.assertTrue(response.context_data['is_paginated'])
        self.assertInHTML(
            self.ACTIVE_PAGINATION_HTML.format(
                movie_list_path, 1, 1),
            response.rendered_content)

让我们看一些有趣的地方:

  • class MovieListPaginationTestCase(TestCase): TestCase是所有 Django 测试的基类。它内置了许多便利功能,包括许多方便的断言方法。

  • def setUp(self): 像大多数 XUnit 测试框架一样,Django 的TestCase类提供了一个在每个测试之前运行的setUp()钩子。如果需要,还可以使用tearDown()钩子。在每个测试之间清理数据库,因此我们不需要担心删除任何我们添加的模型。

  • def testFirstPage(self):: 如果方法的名称以test开头,那么它就是一个测试方法。

  • movie_list_path = reverse('core:MovieList'): reverse()之前提到过,它是url Django 模板标签的 Python 等价物。它将解析名称为路径。

  • request = RequestFactory().get(path=movie_list_path): RequestFactory是一个方便的工厂,用于创建虚拟的 HTTP 请求。RequestFactory具有创建GETPOSTPUT请求的便利方法,这些方法以动词命名(例如,get()用于GET请求)。在我们的情况下,提供的path对象并不重要,但其他视图可能希望检查请求的路径。

  • self.assertEqual(200, response.status_code): 这断言两个参数是否相等。检查响应的status_code以检查成功或失败(200是成功的状态代码——在浏览网页时从不会看到的代码)。

  • self.assertTrue(response.context_data['is_paginated']):这断言该参数评估为Trueresponse公开了在渲染模板中使用的上下文。这使得查找错误变得更容易,因为您可以快速检查在渲染中使用的实际值。

  • self.assertInHTML(: assertInHTML是 Django 提供的许多便利方法之一,作为其一揽子哲学的一部分。给定一个有效的 HTML 字符串needle和有效的 HTML 字符串haystack,它将断言needle是否在haystack中。这两个字符串需要是有效的 HTML,因为 Django 将解析它们并检查一个是否在另一个中。您不需要担心间距或属性/类的顺序。当您尝试确保模板正常工作时,这是一个非常方便的断言。

要运行测试,我们可以使用manage.py

$ cd django
$ python manage.py test 
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.035s

OK
Destroying test database for alias 'default'...

最后,我们可以确信我们已经正确地实现了分页。

添加人物和模型关系

在本节中,我们将在项目中为模型添加关系。人物与电影的关系可以创建一个复杂的数据模型。同一个人可以是演员、作家和导演(例如,The Apostle(1997)由 Robert Duvall 编写、导演和主演)。即使忽略了工作人员和制作团队并简化了一些,数据模型将涉及使用ForiengKey字段的一对多关系,使用ManyToManyField的多对多关系,以及使用ManyToManyField中的through类添加关于多对多关系的额外信息的类。

在本节中,我们将逐步执行以下操作:

  1. 创建一个Person模型

  2. MoviePerson添加一个ForeignKey字段以跟踪导演

  3. MoviePerson添加一个ManyToManyField来跟踪编剧

  4. 添加一个带有through类(Actor)的ManyToManyField来跟踪谁在电影中扮演了什么角色

  5. 创建迁移

  6. 将导演、编剧和演员添加到电影详情模板

  7. 为列表添加一个PersonDetail视图,指示一个人导演了哪些电影,写了哪些电影,以及在哪些电影中表演了

添加具有关系的模型

首先,我们需要一个Person类来描述和存储参与电影的人:

class Person(models.Model):
    first_name = models.CharField(
        max_length=140)
    last_name = models.CharField(
        max_length=140)
    born = models.DateField()
    died = models.DateField(null=True,
                            blank=True)

    class Meta:
        ordering = (
            'last_name', 'first_name')

    def __str__(self):
        if self.died:
            return '{}, {} ({}-{})'.format(
                self.last_name,
                self.first_name,
                self.born,
                self.died)
        return '{}, {} ({})'.format(
                self.last_name,
                self.first_name,
                self.born)

Person中,我们还看到了一个新字段(DateField)和字段的一个新参数(null)。

DateField用于跟踪基于日期的数据,使用数据库上的适当列类型(Postgres 上的date)和 Python 中的datetime.date。Django 还提供了DateTimeField来存储日期和时间。

所有字段都支持null参数(默认为False),它指示列是否应该接受NULL SQL 值(在 Python 中表示为None)。我们将died标记为支持null,以便我们可以记录人是活着还是死了。然后,在__str__()方法中,如果某人是活着的或死了,我们打印出不同的字符串表示。

现在我们有了Person模型,它可以与Movies有各种关系。

不同类型的关系字段

Django 的 ORM 支持映射模型之间的关系的字段,包括一对多、多对多和带有中间模型的多对多。

当两个模型有一对多的关系时,我们使用ForeignKey字段,它将在两个表之间创建一个带有外键FK)约束的列(假设有数据库支持)。在没有ForeignKey字段的模型中,Django 将自动添加RelatedManager对象作为实例属性。RelatedManager类使得在关系中查询对象更容易。我们将在以下部分看一些例子。

当两个模型有多对多的关系时,它们中的一个(但不是两者)可以得到ManyToManyField();Django 将在另一侧为你创建一个RelatedManager。正如你可能知道的,关系数据库实际上不能在两个表之间有多对多的关系。相反,关系数据库需要一个桥接表,其中包含到每个相关表的外键。假设我们不想添加任何描述关系的属性,Django 将自动为我们创建和管理这个桥接表。

有时,我们想要额外的字段来描述多对多的关系(例如,它何时开始或结束);为此,我们可以提供一个带有through模型的ManyToManyField(有时在 UML/OO 中称为关联类)。这个模型将对关系的每一侧都有一个ForeignKey和我们想要的任何额外字段。

在我们添加导演、编剧和演员到我们的Movie模型时,我们将为每一个创建一个例子。

导演 - 外键

在我们的模型中,我们将说每部电影可以有一个导演,但每个导演可以导演很多电影。让我们使用ForiengKey字段来为我们的电影添加一个导演:

class Movie(models.Model):
   # constants, methods, Meta class and other fields omitted for brevity.
    director = models.ForeignKey(
        to='Person',
        on_delete=models.SET_NULL,
        related_name='directed',
        null=True,
        blank=True)

让我们逐行查看我们的新字段:

  • to='Person':Django 的所有关系字段都可以接受字符串引用以及对相关模型的引用。这个参数是必需的。

  • on_delete=models.SET_NULL: Django 需要指示在引用的模型(实例/行)被删除时该怎么做。SET_NULL将把所有由已删除的Person导演的Movie模型实例的director字段设置为NULL。如果我们想要级联删除,我们将使用models.CASCADE对象。

  • related_name='directed':这是一个可选参数,表示另一个模型上的RelatedManager实例的名称(它让我们查询Person导演的所有Movie模型实例)。如果没有提供related_name,那么Person将得到一个名为movie_set的属性(遵循<具有 FK 的模型>_set模式)。在我们的情况下,我们将在MoviePerson之间有多个不同的关系(编剧,导演和演员),所以movie_set将变得模糊不清,我们必须提供一个related_name

这也是我们第一次向现有模型添加字段。在这样做时,我们必须要么添加null=True要么提供一个default值。如果不这样做,那么迁移将强制我们这样做。这是因为 Django 必须假设在迁移运行时表中存在现有行(即使没有),当数据库添加新列时,它需要知道应该插入现有行的内容。在director字段的情况下,我们可以接受它有时可能是NULL

我们现在已经向Movie添加了一个字段,并向Person实例添加了一个名为directed的新属性(类型为RelatedManager)。RelatedManager是一个非常有用的类,它类似于模型的默认 Manager,但自动管理两个模型之间的关系。

让我们看看person.directed.create()并将其与Movie.objects.create()进行比较。这两种方法都将创建一个新的Movie,但person.directed.create()将确保新的Movieperson作为其directorRelatedManager还提供了addremove方法,以便我们可以通过调用person.directed.add(movie)Movie添加到Persondirected集合中。还有一个类似的remove()方法,但是从关系中删除一个模型。

Writers - ManyToManyField

两个模型也可以有多对多的关系,例如,一个人可以写很多电影,一个电影也可以由很多人写。接下来,我们将在我们的Movie模型中添加一个writers字段:

class Movie(models.Model):
   # constants, methods, Meta class and other fields omitted for brevity.
    writers = models.ManyToManyField(
        to='Person',
        related_name='writing_credits',
        blank=True)

ManyToManyField建立了一个多对多的关系,并且像RelatedManager一样,允许用户查询和创建模型。我们再次使用related_name来避免给Person一个movie_set属性,而是给它一个writing_credits属性,它将是一个RelatedManager

ManyToManyField的情况下,关系的两侧都有RelatedManager,因此person.writing_credits.add(movie)的效果与写movie.writers.add(person)相同。

Role - 通过类的 ManyToManyField

我们将看一个关系字段的最后一个例子,当我们想要使用一个中间模型来描述两个其他模型之间的多对多关系时使用。Django 允许我们通过创建一个模型来描述两个多对多关系模型之间的连接表来实现这一点。

在我们的例子中,我们将通过RoleMoviePerson之间创建一个多对多关系,它将有一个name属性:

class Movie(models.Model):
   # constants, methods, Meta class and other fields omitted for brevity.
    actors = models.ManyToManyField(
        to='Person',
        through='Role',
        related_name='acting_credits',
        blank=True)

class Role(models.Model):
    movie = models.ForeignKey(Movie, on_delete=models.DO_NOTHING)
    person = models.ForeignKey(Person, on_delete=models.DO_NOTHING)
    name = models.CharField(max_length=140)

    def __str__(self):
        return "{} {} {}".format(self.movie_id, self.person_id, self.name)

    class Meta:
        unique_together = ('movie',
                           'person',
                           'name')

这看起来像前面的ManyToManyField,只是我们有一个to(引用Person)参数和一个through(引用Role)参数。

Role模型看起来很像一个连接表的设计;它对多对多关系的每一侧都有一个ForeignKey。它还有一个额外的字段叫做name来描述角色。

Role还对其进行了唯一约束。它要求moviepersonbilling一起是唯一的;在RoleMeta类上设置unique_together属性将防止重复数据。

这种使用ManyToManyField将创建四个新的RelatedManager实例:

  • movie.actors将是Person的相关管理器

  • person.acting_credits将是Movie的相关管理器

  • movie.role_set将是Role的相关管理器

  • person.role_set将是Role的相关管理器

我们可以使用任何管理器来查询模型,但只能使用role_set管理器来创建模型或修改关系,因为存在中间类。如果尝试运行movie.actors.add(person),Django 将抛出IntegrityError异常,因为没有办法填写Role.name的值。但是,您可以编写movie.role_set.add(person=person, name='Hamlet')

添加迁移

现在,我们可以为我们的新模型生成一个迁移:

$ python manage.py makemigrations core
Migrations for 'core':
  core/migrations/0002_auto_20170926_1650.py
    - Create model Person
    - Create model Role
    - Change Meta options on movie
    - Add field movie to role
    - Add field person to role
    - Add field actors to movie
    - Add field director to movie
    - Add field writers to movie
    - Alter unique_together for role (1 constraint(s))

然后,我们可以运行我们的迁移,以应用这些更改:

$ python manage.py migrate core
Operations to perform:
  Apply all migrations: core
Running migrations:
  Applying core.0002_auto_20170926_1651... OK

接下来,让我们让我们的电影页面链接到电影中的人物。

创建一个 PersonView 并更新 MovieList

让我们添加一个PersonDetail视图,我们的movie_detail.html模板可以链接到。为了创建我们的视图,我们将经历一个四步过程:

  1. 创建一个管理器来限制数据库查询的数量

  2. 创建我们的视图

  3. 创建我们的模板

  4. 创建引用我们视图的 URL

创建自定义管理器-PersonManager

我们的PersonDetail视图将列出一个Person在其中扮演、编写或导演的所有电影。在我们的模板中,我们将打印出每个角色中每部电影的名称(以及扮演角色的Role.name)。为了避免向数据库发送大量查询,我们将为我们的模型创建新的管理器,这些管理器将返回更智能的QuerySet

在 Django 中,每当我们跨越关系访问属性时,Django 将查询数据库以获取相关项目(例如在每个相关Role上循环时person.role_set.all(),对于每个相关Role)。对于出演N部电影的Person,这将导致N次数据库查询。我们可以使用prefetch_related()方法避免这种情况(稍后我们将看到select_related()方法)。使用prefetch_related()方法,Django 将在单个附加查询中查询单个关系的所有相关数据。但是,如果我们最终没有使用预取的数据,查询它将浪费时间和内存。

让我们创建一个PersonManager,其中包含一个新的方法all_with_prefetch_movies(),并将其设置为Person的默认管理器:

class PersonManager(models.Manager):
    def all_with_prefetch_movies(self):
        qs = self.get_queryset()
        return qs.prefetch_related(
            'directed',
            'writing_credits',
            'role_set__movie')

class Person(models.Model):
    # fields omitted for brevity

    objects = PersonManager()

    class Meta:
        ordering = (
            'last_name', 'first_name')

    def __str__(self):
        # body omitted for brevity

我们的PersonManager仍将提供与默认相同的所有方法,因为PersonManager继承自models.Manager。我们还定义了一个新方法,该方法使用get_queryset()获取QuerySet,并告诉它预取相关模型。QuerySets是惰性的,因此直到查询集被评估(例如通过迭代、转换为布尔值、切片或通过if语句进行评估)之前,与数据库的通信都不会发生。DetailView在使用get()获取模型时才会评估查询。

prefetch_related()方法接受一个或多个lookups,在初始查询完成后,它会自动查询这些相关模型。当您访问与您的QuerySet中的模型相关的模型时,Django 不必查询它,因为您已经在QuerySet中预取了它。

查询是 Django QuerySet用来表示模型中的字段或RelatedManager的方式。查询甚至可以跨越关系,通过用两个下划线分隔关系字段(或RelatedManager)和相关模型的字段来实现:

Movie.objects.all().filter(actors__last_name='Freeman', actors__first_name='Morgan')

上述调用将返回一个QuerySet,其中摩根·弗里曼曾经是演员的所有Movie模型实例。

在我们的PersonManager中,我们告诉 Django 预取Person执导、编写和扮演的所有电影,以及预取角色本身。使用all_with_prefetch_movies()方法将导致查询数量保持恒定,无论Person的作品有多么丰富。

创建一个 PersonDetail 视图和模板

现在我们可以在django/core/views.py中编写一个非常简单的视图:

class PersonDetail(DetailView):
    queryset = Person.objects.all_with_prefetch_movies()

这个DetailView不同的地方在于我们没有为它提供一个model属性。相反,我们从我们的PersonManager类中给它一个QuerySet对象。当DetailView使用QuerySetfilter()get()方法来检索模型实例时,DetailView将从模型实例的类名中派生模板的名称,就像我们在视图上提供了模型类属性一样。

现在,让我们在django/core/templates/core/person_detail.html中创建我们的模板:

{% extends 'base.html' %}

{% block title %}
  {{ object.first_name }}
  {{ object.last_name }}
{% endblock %}

{% block main %}

  <h1>{{ object }}</h1>
  <h2>Actor</h2>
  <ul >
    {% for role in object.role_set.all %}
      <li >
        <a href="{% url 'core:MovieDetail' role.movie.id %}" >
          {{ role.movie }}
        </a >:
        {{ role.name }}
      </li >
    {% endfor %}
  </ul >
  <h2>Writer</h2>
  <ul >
    {% for movie in object.writing_credits.all %}
      <li >
        <a href="{% url 'core:MovieDetail' movie.id %}" >
          {{ movie }}
        </a >
      </li >
    {% endfor %}
  </ul >
  <h2>Director</h2>
  <ul >
    {% for movie in object.directed.all %}
      <li >
        <a href="{% url 'core:MovieDetail' movie.id %}" >
          {{ movie }}
        </a >
      </li >
    {% endfor %}
  </ul >

{% endblock %}

我们的模板不需要做任何特殊的事情来利用我们的预取。

接下来,我们应该给MovieDetail视图提供与我们的PersonDetail视图相同的好处。

创建 MovieManager

让我们从django/core/models.py中创建一个MovieManager开始:

class MovieManager(models.Manager):

    def all_with_related_persons(self):
        qs = self.get_queryset()
        qs = qs.select_related(
            'director')
        qs = qs.prefetch_related(
            'writers', 'actors')
        return qs

class Movie(models.Model):
    # constants and fields omitted for brevity
    objects = MovieManager()

    class Meta:
        ordering = ('-year', 'title')

    def __str__(self):
         # method body omitted for brevity

MovieManager引入了另一个新方法,称为select_related()select_related()方法与prefetch_related()方法非常相似,但当关系只导致一个相关模型时(例如,使用ForeignKey字段),它会被使用。select_related()方法通过使用JOIN SQL 查询来在一次查询中检索两个模型。当关系可能导致多个模型时(例如,ManyToManyField的任一侧或RelatedManager属性),使用prefetch_related()

现在,我们可以更新我们的MovieDetail视图,以使用查询集而不是直接使用模型:

class MovieDetail(DetailView):
    queryset = (
        Movie.objects
            .all_with_related_persons())

视图渲染完全相同,但在需要相关的Person模型实例时,它不必每次查询数据库,因为它们都已经被预取。

本节的快速回顾

在这一部分,我们创建了Person模型,并在MoviePerson模型之间建立了各种关系。我们使用ForeignKey字段类创建了一对多的关系,使用ManyToManyField类创建了多对多的关系,并使用了一个中介(或关联)类,通过为ManyToManyField提供一个through模型来为多对多关系添加额外的信息。我们还创建了一个PersonDetail视图来显示Person模型实例,并使用自定义模型管理器来控制 Django 发送到数据库的查询数量。

总结

在本章中,我们创建了我们的 Django 项目,并启动了我们的core Django 应用程序。我们看到了如何使用 Django 的模型-视图-模板方法来创建易于理解的代码。我们在模型附近创建了集中的数据库逻辑,视图中的分页,以及遵循 Django 最佳实践的模板中的 HTML,即fat models, thin views,dumb templates

现在我们准备添加用户,他们可以注册并投票给他们最喜欢的电影。

第二章:将用户添加到 MyMDB

在上一章中,我们启动了我们的项目并创建了我们的core应用程序和我们的core模型(MoviePerson)。在本章中,我们将在此基础上做以下事情:

  • 让用户注册、登录和退出

  • 让已登录用户对电影进行投票

  • 根据投票为每部电影评分

  • 使用投票来推荐前 10 部电影。

让我们从管理用户开始这一章。

创建user应用程序

在本节中,您将创建一个名为user的新 Django 应用程序,将其注册到您的项目中,并使其管理用户。

在第一章 构建 MyMDB 的开头,您了解到 Django 项目由许多 Django 应用程序组成(例如我们现有的core应用程序)。Django 应用程序应提供明确定义和紧密范围的行为。将用户管理添加到我们的core应用程序中违反了这一原则。让一个 Django 应用程序承担太多责任会使测试和重用变得更加困难。例如,我们将在本书中的整个过程中重用我们在这个user Django 应用程序中编写的代码。

创建一个新的 Django 应用程序

在我们创建core应用程序时所做的一样,我们将使用manage.py来生成我们的user应用程序:

$ cd django
$ python manage.py startapp user
$ cd user
$ ls
__init__.py     admin.py        apps.py         migrations      models.py       tests.py        views.py

接下来,我们将通过编辑我们的django/config/settings.py文件并更新INSTALLED_APPS属性来将其注册到我们的 Django 项目中:

INSTALLED_APPS = [
    'user',  # must come before admin
    'core',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

出于我们将在登录和退出部分讨论的原因,我们需要将user放在admin应用程序之前。通常,将我们的应用程序放在内置应用程序之上是一个好主意。

我们的user应用程序现在是我们项目的一部分。通常,我们现在会继续为我们的应用程序创建和定义模型。但是,由于 Django 内置的auth应用程序,我们已经有了一个可以使用的用户模型。

如果我们想使用自定义用户模型,那么我们可以通过更新settings.py并将AUTH_USER_MODEL设置为模型的字符串 python 路径来注册它(例如,AUTH_USER_MODEL=myuserapp.models.MyUserModel)。

接下来,我们将创建我们的用户注册视图。

创建用户注册视图

我们的RegisterView类将负责让用户注册我们的网站。如果它收到一个GET请求,那么它将向用户显示UserCreationFrom;如果它收到一个POST请求,它将验证数据并创建用户。UserCreationFormauth应用程序提供,并提供了一种收集和验证注册用户所需数据的方式;此外,如果数据有效,它还能保存一个新的用户模型。

让我们将我们的视图添加到django/user/views.py中:

from django.contrib.auth.forms import (
    UserCreationForm,
)
from django.urls import (
    reverse_lazy,
)
from django.views.generic import (
    CreateView,
)

class RegisterView(CreateView):
    template_name = 'user/register.html'
    form_class = UserCreationForm
    success_url = reverse_lazy(
        'core:MovieList')

让我们逐行查看我们的代码:

  • class RegisterView(CreateView)::我们的视图扩展了CreateView,因此不必定义如何处理GETPOST请求,我们将在接下来的步骤中讨论。

  • template_name = 'user/register.html':这是一个我们将创建的模板。它的上下文将与我们以前看到的有些不同;它不会有objectobject_list变量,但会有一个form变量,它是form_class属性中设置的类的实例。

  • form_class = UserCreationForm:这是这个CreateView应该使用的表单类。更简单的模型可以只说model = MyModel,但是用户稍微复杂一些,因为密码需要输入两次然后进行哈希处理。我们将在第三章 海报、头像和安全 中讨论 Django 如何存储密码。

  • success_url = reverse_lazy('core:MovieList'):当模型创建成功时,这是您需要重定向到的 URL。这实际上是一个可选参数;如果模型有一个名为model.get_absolute_url()的方法,那么将使用该方法,我们就不需要提供success_url

CreateView的行为分布在许多基类和 mixin 中,它们通过方法相互作用,作为我们可以重写以改变行为的挂钩。让我们来看看一些最关键的点。

如果CreateView收到GET请求,它将呈现表单的模板。 CreateView的祖先之一是FormMixin,它重写了get_context_data()来调用get_form()并将表单实例添加到我们模板的上下文中。 渲染的模板作为响应的主体由render_to_response返回。

如果CreateView收到POST请求,它还将使用get_form()来获取表单实例。 表单将被绑定到请求中的POST数据。 绑定的表单可以验证其绑定的数据。 CreateView然后将调用form.is_valid(),并根据需要调用form_valid()form_invalid()form_valid()将调用form.save()(将数据保存到数据库)然后返回一个 302 响应,将浏览器重定向到success_urlform_invalid()方法将使用包含错误消息的表单重新呈现模板,供用户修复并重新提交。

我们还第一次看到了reverse_lazy()。 它是reverse()的延迟版本。 延迟函数是返回值直到使用时才解析的函数。 我们不能使用reverse(),因为视图类在构建完整的 URLConfs 集时进行评估,所以如果我们需要在视图的级别使用reverse(),我们必须使用reverse_lazy()。 值直到视图返回其第一个响应才会解析。

接下来,让我们为我们的视图创建模板。

创建 RegisterView 模板

在编写带有 Django 表单的模板时,我们必须记住 Django 不提供<form><button type='submit>标签,只提供表单主体的内容。 这让我们有可能在同一个<form>中包含多个 Django 表单。 有了这个想法,让我们将我们的模板添加到django/user/templates/user/register.html中:

{% extends "base.html" %}

{% block main %}
  <h1>Register for MyMDB</h1>
  <form method="post">
    {{ form.as_p}}
    {% csrf_token %}
    <button
        type="submit"
        class="btn btn-primary">
      Register
    </button>
  </form>
{% endblock %}

与我们之前的模板一样,我们扩展base.html并将我们的代码放在现有block之一中(在这种情况下是main)。 让我们更仔细地看看表单是如何呈现的。

当表单呈现时,它分为两部分,首先是一个可选的<ul class='errorlist'>标签,用于一般错误消息(如果有的话),然后每个字段分为四个基本部分:

  • 一个带有字段名称的<label>标签

  • 一个<ul class="errorlist">标签,显示用户先前表单提交的错误;只有在该字段有错误时才会呈现

  • 一个<input>(或<select>)标签来接受输入

  • 一个<span class="helptext">标签,用于字段的帮助文本

Form带有以下三个实用方法来呈现表单:

  • as_table(): 每个字段都包裹在一个<tr>标签中,标签中包含一个<th>标签和一个包裹在<td>标签中的小部件。 不提供包含的<table>标签。

  • as_ul: 整个字段(标签和帮助文本小部件)都包裹在一个<li>标签中。 不提供包含的<ul>标签。

  • as_p: 整个字段(标签和帮助文本小部件)都包裹在一个<p>标签中。

对于相同的表单,不提供包含<table><ul>标签,也不提供<form>标签,以便在必要时更容易一起输出多个表单。

如果您想对表单呈现进行精细的控制,Form实例是可迭代的,在每次迭代中产生一个Field,或者可以按名称查找为form["fieldName"]

在我们的示例中,我们使用as_p()方法,因为我们不需要精细的布局控制。

这个模板也是我们第一次看到csrf_token标签。 CSRF 是 Web 应用程序中常见的漏洞,我们将在第三章中更多地讨论它,海报、头像和安全性。 Django 自动检查所有POSTPUT请求是否有有效的csrfmiddlewaretoken和标头。 缺少这个的请求甚至不会到达视图,而是会得到一个403 Forbidden的响应。

现在我们有了模板,让我们在我们的 URLConf 中为我们的视图添加一个path()对象。

添加到 RegisterView 的路径

我们的user应用程序没有urls.py文件,所以我们需要创建django/user/urls.py文件:

from django.urls import path

from user import views

app_name = 'user'
urlpatterns = [
    path('register',
         views.RegisterView.as_view(),
         name='register'),
]

接下来,我们需要在django/config/urls.py的根 URLConf 中include()此 URLConf:

from django.urls import path, include
from django.contrib import admin

import core.urls
import user.urls

urlpatterns = [
    path('admin/', admin.site.urls),
    path('user/', include(
        user.urls, namespace='user')),
    path('', include(
        core.urls, namespace='core')),
]

由于 URLConf 只会搜索直到找到第一个匹配的path,因此我们总是希望将没有前缀或最广泛的 URLConfs 的path放在最后,以免意外阻止其他视图。

登录和登出

Django 的auth应用程序提供了用于登录和注销的视图。将此添加到我们的项目将是一个两步过程:

  1. user URLConf 中注册视图

  2. 为视图添加模板

更新用户 URLConf

Django 的auth应用程序提供了许多视图,以帮助简化用户管理和身份验证,包括登录/注销、更改密码和重置忘记的密码。一个功能齐全的生产应用程序应该为用户提供所有三个功能。在我们的情况下,我们将限制自己只提供登录和注销。

让我们更新django/user/urls.py以使用auth的登录和注销视图:

from django.urls import path
from django.contrib.auth import views as auth_views

from user import views

app_name = 'user'
urlpatterns = [
    path('register',
         views.RegisterView.as_view(),
         name='register'),
    path('login/',
         auth_views.LoginView.as_view(),
         name='login'),
    path('logout/',
         auth_views.LogoutView.as_view(),
         name='logout'),
]

如果您提供了登录/注销、更改密码和重置密码,则可以使用auth的 URLConf,如下面的代码片段所示:

from django.contrib.auth import urls
app_name = 'user'
urlpatterns = [
    path('', include(urls)),
]

现在,让我们添加模板。

创建一个 LoginView 模板

首先,在django/user/templates/registration/login.html中为登录页面添加模板:

{% extends "base.html" %}

{% block title %}
Login - {{ block.super }}
{% endblock %}

{% block main %}
  <form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button
        class="btn btn-primary">
      Log In
    </button>
  </form>
{% endblock %}

前面的代码看起来与user/register.html非常相似。

但是,当用户登录时应该发生什么?

成功的登录重定向

RegisterView中,我们能够指定成功后将用户重定向到何处,因为我们创建了视图。LoginView类将按照以下步骤决定将用户重定向到何处:

  1. 如果POST参数next是一个有效的 URL,并指向托管此应用程序的服务器,则使用POST参数nextpath()名称不可用。

  2. 如果next是一个有效的 URL,并指向托管此应用程序的服务器,则使用GET参数nextpath()名称不可用。

  3. LOGIN_REDIRECT_URL设置默认为'/accounts/profile/'path()名称可用

在我们的情况下,我们希望将所有用户重定向到电影列表,所以让我们更新django/config/settings.py以设置LOGIN_REDIRECT_URL

LOGIN_REDIRECT_URL = 'core:MovieList'

但是,如果有情况需要将用户重定向到特定页面,我们可以使用next参数将其专门重定向到特定页面。例如,如果用户尝试在登录之前执行操作,我们将他们所在的页面传递给LoginView作为next参数,以便在登录后将他们重定向回所在的页面。

现在,当用户登录时,他们将被重定向到我们的电影列表视图。接下来,让我们为注销视图创建一个模板。

创建一个 LogoutView 模板

LogoutView类的行为有些奇怪。如果它收到一个GET请求,它将注销用户,然后尝试呈现registration/logged_out.htmlGET请求修改用户状态是不寻常的,因此值得记住这个视图有点不同。

LogoutView类还有另一个问题。如果您没有提供registration/logged_out.html模板,并且已安装admin应用程序,则 Django 可能会使用admin的模板,因为admin应用程序确实有该模板(退出admin应用程序,您会看到它)。

Django 将模板名称解析为文件的方式是一个三步过程,一旦找到文件,就会停止,如下所示:

  1. Django 遍历settings.TEMPLATESDIRS列表中的目录。

  2. 如果APP_DIRSTrue,则它将遍历INSTALLED_APPS中列出的应用程序,直到找到匹配项。如果adminINSTALLED_APPS列表中出现在user之前,那么它将首先匹配。如果user在前面,user将首先匹配。

  3. 引发TemplateDoesNotExist异常。

这就是为什么我们把user放在已安装应用程序列表的第一位,并添加了一个警告未来开发人员不要改变顺序的注释。

我们现在已经完成了我们的user应用程序。让我们回顾一下我们取得了什么成就。

快速回顾本节

我们创建了一个user应用来封装用户管理。在我们的user应用中,我们利用了 Django 的auth应用提供的许多功能,包括UserCreationFormLoginViewLogoutView类。我们还了解了 Django 提供的一些新的通用视图,并结合UserCreationForm类使用CreateView来创建RegisterView类。

现在我们有了用户,让我们允许他们对我们的电影进行投票。

让用户对电影进行投票

像 IMDB 这样的社区网站的一部分的乐趣就是能够对我们喜欢和讨厌的电影进行投票。在 MyMDB 中,用户将能够为电影投票,要么是外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传,要么是外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传。一部电影将有一个分数,即外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传的数量减去外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传的数量。

让我们从投票的最重要部分开始:Vote模型。

创建 Vote 模型

在 MyMDB 中,每个用户可以对每部电影投一次票。投票可以是正面的—外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传—或者是负面的—外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

让我们更新我们的django/core/models.py文件来拥有我们的Vote模型:

class Vote(models.Model):
    UP = 1
    DOWN = -1
    VALUE_CHOICES = (
        (UP, "",),
        (DOWN, "",),
    )

    value = models.SmallIntegerField(
        choices=VALUE_CHOICES,
    )
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )
    movie = models.ForeignKey(
        Movie,
        on_delete=models.CASCADE,
    )
    voted_on = models.DateTimeField(
        auto_now=True
    )

    class Meta:
        unique_together = ('user', 'movie')

这个模型有以下四个字段:

  • value,必须是1-1

  • user是一个ForeignKey,它通过settings.AUTH_USER_MODEL引用User模型。Django 建议您永远不要直接引用django.contrib.auth.models.User,而是使用settings.AUTH_USER_MODELdjango.contrib.auth.get_user_model()

  • movie是一个引用Movie模型的ForeignKey

  • voted_on是一个带有auto_now启用的DateTimeFieldauto_now参数使模型在每次保存模型时更新字段为当前日期时间。

unique_together属性的Meta在表上创建了一个唯一约束。唯一约束将防止两行具有相同的usermovie值,强制执行我们每个用户每部电影一次投票的规则。

让我们为我们的模型创建一个迁移,使用manage.py

$ python manage.py makemigrations core
Migrations for 'core':
  core/migrations/0003_auto_20171003_1955.py
    - Create model Vote
    - Alter field rating on movie
    - Add field movie to vote
    - Add field user to vote
    - Alter unique_together for vote (1 constraint(s))

然后,让我们运行我们的迁移:

$ python manage.py migrate core
Operations to perform:
  Apply all migrations: core
Running migrations:
  Applying core.0003_auto_20171003_1955... OK

现在我们已经设置好了我们的模型和表,让我们创建一个表单来验证投票。

创建 VoteForm

Django 的表单 API 非常强大,让我们可以创建几乎任何类型的表单。如果我们想创建一个任意的表单,我们可以创建一个扩展django.forms.Form的类,并向其中添加我们想要的字段。然而,如果我们想构建一个代表模型的表单,Django 为我们提供了一个快捷方式,即django.forms.ModelForm

我们想要的表单类型取决于表单将被放置的位置以及它将如何被使用。在我们的情况下,我们想要一个可以放在MovieDetail页面上的表单,并让它给用户以下两个单选按钮:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

让我们来看看可能的最简单的VoteForm

from django import forms

from core.models import Vote

class VoteForm(forms.ModelForm):
    class Meta:
        model = Vote
        fields = (
            'value', 'user', 'movie',)

Django 将使用valueusermovie字段从Vote模型生成一个表单。usermovie将是使用<select>下拉列表选择正确值的ModelChoiceField,而value是一个使用<select>下拉小部件的ChoiceField,这不是我们默认想要的。

VoteForm将需要usermovie。由于我们将使用VoteForm来保存新的投票,我们不能消除这些字段。然而,让用户代表其他用户投票将会创建一个漏洞。让我们自定义我们的表单来防止这种情况发生:

from django import forms
from django.contrib.auth import get_user_model

from core.models import Vote, Movie

class VoteForm(forms.ModelForm):

    user = forms.ModelChoiceField(
        widget=forms.HiddenInput,
        queryset=get_user_model().
            objects.all(),
        disabled=True,
    )
    movie = forms.ModelChoiceField(
        widget=forms.HiddenInput,
        queryset=Movie.objects.all(),
        disabled=True
    )
    value = forms.ChoiceField(
        label='Vote',
        widget=forms.RadioSelect,
        choices=Vote.VALUE_CHOICES,
    )

    class Meta:
        model = Vote
        fields = (
            'value', 'user', 'movie',)

在前面的表单中,我们已经自定义了字段。

让我们仔细看一下user字段:

  • user = forms.ModelChoiceField(: ModelChoiceField接受另一个模型作为该字段的值。通过提供有效选项的QuerySet实例来验证模型的选择。

  • queryset=get_user_model().objects.all(),:定义此字段的有效选择的QuerySet。在我们的情况下,任何用户都可以投票。

  • widget=forms.HiddenInput,: HiddenInput小部件呈现为<input type='hidden'>HTML 元素,这意味着用户不会被任何 UI 分散注意力。

  • disabled=True,: disabled参数告诉表单忽略此字段的任何提供的数据,只使用代码中最初提供的值。这可以防止用户代表其他用户投票。

movie字段与user基本相同,但queryset属性查询Movie模型实例。

值字段以不同的方式进行了定制:

  • value = forms.ChoiceField(: ChoiceField用于表示可以从有限集合中具有单个值的字段。默认情况下,它由下拉列表小部件表示。

  • label='Vote',: label属性让我们自定义此字段使用的标签。虽然value在我们的代码中有意义,但我们希望用户认为他们的投票是![](https://github.com/OpenDocCN/freelearn-python-web-zh/raw/master/docs/bd-dj20-webapp/img/05b14743-dedd-4122-97df-cc15869422be.png)/![](https://github.com/OpenDocCN/freelearn-python-web-zh/raw/master/docs/bd-dj20-webapp/img/73102249-cbaf-442e-a8f8-7ba208bb4348.png)

  • widget=forms.RadioSelect,: 下拉列表隐藏选项,直到用户点击下拉列表。但我们的值是我们希望始终可见的有效行动呼叫。使用RadioSelect小部件,Django 将每个选择呈现为<input type='radio'>标签,并带有适当的<label>标签和name值,以便更容易进行投票。

  • choices=Vote.VALUE_CHOICES,: ChoiceField必须告知有效选择;方便的是,它使用与模型字段的choices参数相同的格式,因此我们可以重用模型中使用的Vote.VALUE_CHOICES元组。

我们新定制的表单将显示为标签vote和两个单选按钮。

现在我们有了表单,让我们将投票添加到MovieDetail视图,并创建知道如何处理投票的视图。

创建投票视图

在这一部分,我们将更新MovieDetail视图,让用户投票并记录投票到数据库中。为了处理用户的投票,我们将创建以下两个视图:

  • CreateVote,这将是一个CreateView,如果用户尚未为电影投票

  • UpdateVote,这将是一个UpdateView,如果用户已经投票但正在更改他们的投票

让我们从更新MovieDetail开始,为电影提供投票的 UI。

将 VoteForm 添加到 MovieDetail

我们的MovieDetail.get_context_data方法现在会更加复杂。它将需要获取用户对电影的投票,实例化表单,并知道将投票提交到哪个 URL(create_voteupdate_vote)。

我们首先需要一种方法来检查用户模型是否对给定的Movie模型实例有相关的Vote模型实例。为此,我们将创建一个带有自定义方法的VoteManager类。我们的方法将具有特殊行为 - 如果没有匹配的Vote模型实例,它将返回一个未保存的空白Vote对象。这将使我们更容易使用正确的movieuser值实例化我们的VoteForm

这是我们的新VoteManager

class VoteManager(models.Manager):

    def get_vote_or_unsaved_blank_vote(self, movie, user):
        try:
            return Vote.objects.get(
                movie=movie,
                user=user)
        except Vote.DoesNotExist:
            return Vote(
                movie=movie,
                user=user)

class Vote(models.Model):
    # constants and field omitted

    objects = VoteManager()

    class Meta:
        unique_together = ('user', 'movie')

VoteManager与我们以前的Manager非常相似。

我们以前没有遇到的一件事是使用构造函数实例化模型(例如,Vote(movie=movie, user=user))而不是使用其管理器的create()方法。使用构造函数在内存中创建一个新模型,但在数据库中创建。未保存的模型本身是完全可用的(通常可用所有方法和管理器方法),但除了依赖关系的任何内容。未保存的模型没有id,因此在调用其save()方法保存之前,无法使用RelatedManagerQuerySet查找它。

现在我们已经拥有了MovieDetail所需的一切,让我们来更新它:

class MovieDetail(DetailView):
    queryset = (
        Movie.objects
           .all_with_related_persons())

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        if self.request.user.is_authenticated:
            vote = Vote.objects.get_vote_or_unsaved_blank_vote(
                movie=self.object,
                user=self.request.user
            )
                    if vote.id:
                vote_form_url = reverse(
                    'core:UpdateVote',
                    kwargs={
                        'movie_id': vote.movie.id,
                        'pk': vote.id})
            else:
                vote_form_url = (
                    reverse(
                        'core:CreateVote',
                        kwargs={
                            'movie_id': self.object.id}
                    )
                )
            vote_form = VoteForm(instance=vote)
            ctx['vote_form'] = vote_form
            ctx['vote_form_url'] = \
                vote_form_url
        return ctx

我们在上述代码中引入了两个新元素,self.request和使用实例化表单。

视图通过它们的request属性访问它们正在处理的请求。此外,Request有一个user属性,它让我们访问发出请求的用户。我们使用这个来检查用户是否已经验证,因为只有已验证的用户才能投票。

ModelForms可以使用它们所代表的模型的实例进行实例化。当我们使用一个实例实例化ModelForm并渲染它时,字段将具有实例的值。一个常见任务的一个很好的快捷方式是在这个表单中显示这个模型的值。

我们还将引用两个我们还没有创建的path;我们马上就会创建。首先,让我们通过更新movie_detail.html模板的侧边栏块来完成我们的MovieDetail更新:

{% block sidebar %}
 {# rating div omitted #}
  <div>
    {% if vote_form %}
      <form
          method="post"
          action="{{ vote_form_url }}" >
        {% csrf_token %}
        {{ vote_form.as_p }}
        <button
            class="btn btn-primary" >
          Vote
        </button >
      </form >
    {% else %}
      <p >Log in to vote for this
        movie</p >
    {% endif %}
  </div >
{% endblock %}

在设计这个过程中,我们再次遵循模板应该具有尽可能少的逻辑的原则。

接下来,让我们添加我们的CreateVote视图。

创建CreateVote视图

CreateVote视图将负责使用VoteForm验证投票数据,然后创建正确的Vote模型实例。然而,我们不会为投票创建一个模板。如果有问题,我们将把用户重定向到MovieDetail视图。

这是我们应该在django/core/views.py文件中拥有的CreateVote视图:

from django.contrib.auth.mixins import (
    LoginRequiredMixin, )
from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic import (
    CreateView, )

from core.forms import VoteForm

class CreateVote(LoginRequiredMixin, CreateView):
    form_class = VoteForm

    def get_initial(self):
        initial = super().get_initial()
        initial['user'] = self.request.user.id
        initial['movie'] = self.kwargs[
            'movie_id']
        return initial

    def get_success_url(self):
        movie_id = self.object.movie.id
        return reverse(
            'core:MovieDetail',
            kwargs={
                'pk': movie_id})

    def render_to_response(self, context, **response_kwargs):
        movie_id = context['object'].id
        movie_detail_url = reverse(
            'core:MovieDetail',
            kwargs={'pk': movie_id})
        return redirect(
            to=movie_detail_url)

在前面的代码中,我们引入了四个与RegisterView类不同的新概念——get_initial()render_to_response()redirect()LoginRequiredMixin。它们如下:

  • get_initial()用于在表单从请求中获取data值之前,使用initial值预填充表单。这对于VoteForm很重要,因为我们已经禁用了movieuserForm会忽略分配给禁用字段的data。即使用户在表单中发送了不同的movie值或user值,它也会被禁用字段忽略,而我们的initial值将被使用。

  • render_to_response()CreateView调用以返回一个包含渲染模板的响应给客户端。在我们的情况下,我们不会返回一个包含模板的响应,而是一个 HTTP 重定向到MovieDetail。这种方法有一个严重的缺点——我们会丢失与表单相关的任何错误。然而,由于我们的用户只有两种输入选择,我们也无法提供太多错误消息。

  • redirect()来自 Django 的django.shortcuts包。它提供了常见操作的快捷方式,包括创建一个 HTTP 重定向响应到给定的 URL。

  • LoginRequiredMixin是一个可以添加到任何View中的 mixin,它将检查请求是否由已验证用户发出。如果用户没有登录,他们将被重定向到登录页面。

Django 的默认登录页面设置为/accounts/profile/,所以让我们通过编辑settings.py文件并添加一个新的设置来改变这一点:

LOGIN_REDIRECT_URL = 'user:login'

现在我们有一个视图,它将创建一个Vote模型实例,并在成功或失败时将用户重定向回相关的MovieDetail视图。

接下来,让我们添加一个视图,让用户更新他们的Vote模型实例。

创建UpdateVote视图

UpdateVote视图要简单得多,因为UpdateView(就像DetailView)负责查找投票,尽管我们仍然必须关注Vote的篡改。

让我们更新我们的django/core/views.py文件:

from django.contrib.auth.mixins import (
    LoginRequiredMixin, )
from django.core.exceptions import (
    PermissionDenied)
from django.shortcuts import redirect
from django.urls import reverse
from django.views.generic import (
    UpdateView, )

from core.forms import VoteForm

class UpdateVote(LoginRequiredMixin, UpdateView):
    form_class = VoteForm
    queryset = Vote.objects.all()

    def get_object(self, queryset=None):
        vote = super().get_object(
            queryset)
        user = self.request.user
        if vote.user != user:
            raise PermissionDenied(
                'cannot change another '
                'users vote')
        return vote

    def get_success_url(self):
        movie_id = self.object.movie.id
        return reverse(
            'core:MovieDetail',
            kwargs={'pk': movie_id})

    def render_to_response(self, context, **response_kwargs):
        movie_id = context['object'].id
        movie_detail_url = reverse(
            'core:MovieDetail',
            kwargs={'pk': movie_id})
        return redirect(
            to=movie_detail_url)

我们的UpdateVote视图在get_object()方法中检查检索到的Vote是否是已登录用户在其中的投票。我们添加了这个检查来防止投票篡改。我们的用户界面不会让用户错误地这样做。如果Vote不是由已登录用户投出的,那么UpdateVote会抛出一个PermissionDenied异常,Django 会处理并返回一个403 Forbidden响应。

最后一步将是在core URLConf 中注册我们的新视图。

core/urls.py中添加视图

我们现在创建了两个新视图,但是,和往常一样,除非它们在 URLConf 中列出,否则用户无法访问它们。让我们编辑core/urls.py

urlpatterns = [
    # previous paths omitted
    path('movie/<int:movie_id>/vote',
         views.CreateVote.as_view(),
         name='CreateVote'),
    path('movie/<int:movie_id>/vote/<int:pk>',
         views.UpdateVote.as_view(),
         name='UpdateVote'),
]

本节的快速回顾

在本节中,我们看到了如何构建基本和高度定制的表单来接受和验证用户输入。我们还讨论了一些简化处理表单常见任务的内置视图。

接下来,我们将展示如何开始使用我们的用户、投票来对每部电影进行排名并提供一个前 10 名的列表。

计算电影得分

在这一部分,我们将使用 Django 的聚合查询 API 来计算每部电影的得分。Django 通过将功能内置到其QuerySet对象中,使编写与数据库无关的聚合查询变得容易。

让我们首先添加一个计算MovieManager得分的方法。

使用 MovieManager 来计算电影得分

我们的MovieManager类负责构建与Movie相关的QuerySet对象。我们现在需要一个新的方法,该方法检索电影(理想情况下仍与相关人员相关)并根据其收到的投票总和标记每部电影的得分(我们可以简单地对所有的1-1求和)。

让我们看看如何使用 Django 的QuerySet.annotate() API 来做到这一点:

from django.db.models.aggregates import (
    Sum
)

class MovieManager(models.Manager):

    def all_with_related_persons(self):
        qs = self.get_queryset()
        qs = qs.select_related(
            'director')
        qs = qs.prefetch_related(
            'writers', 'actors')
        return qs

    def all_with_related_persons_and_score(self):
        qs = self.all_with_related_persons()
        qs = qs.annotate(score=Sum('vote__value'))
        return qs

all_with_related_persons_and_score中,我们调用all_with_related_persons并获得一个我们可以进一步使用annotate()调用修改的QuerySet

annotate将我们的常规 SQL 查询转换为聚合查询,将提供的聚合操作的结果添加到一个名为score的新属性中。Django 将大多数常见的 SQL 聚合函数抽象为类表示,包括SumCountAverage(以及更多)。

新的score属性可用于我们从QuerySetget()出来的任何实例,以及我们想要在我们的新QuerySet上调用的任何方法(例如,qs.filter(score__gt=5)将返回一个具有score属性大于 5 的电影的QuerySet)。

我们的新方法仍然返回一个懒惰的QuerySet,这意味着我们的下一步是更新MovieDetail及其模板。

更新 MovieDetail 和模板

现在我们可以查询带有得分的电影,让我们更改MovieDetail使用的QuerySet

 class MovieDetail(DetailView):
    queryset = Movie.objects.all_with_related_persons_and_score() 
    def get_context_data(self, **kwargs):
        # body omitted for brevity

现在,当MovieDetail在其查询集上使用get()时,该Movie将具有一个得分属性。让我们在我们的movie_detail.html模板中使用它:

{% block sidebar %}
  {# movie rating div omitted #}
  <div >
    <h2 >
      Score: {{ object.score|default_if_none:"TBD" }}
    </h2 >
  </div>
  {# voting form div omitted #}
{% endblock %}

我们可以安全地引用score属性,因为MovieDetailQuerySet。然而,我们不能保证得分不会是None(例如,如果Movie没有投票)。为了防止空白得分,我们使用default_if_none过滤器来提供一个要打印的值。

我们现在有一个可以计算所有电影得分的MovieManager方法,但是当您在MovieDetail中使用它时,这意味着它只会为正在显示的Movie计算得分。

总结

在本章中,我们向我们的系统添加了用户,让他们注册、登录(和退出登录),并对我们的电影进行投票。我们学会了如何使用聚合查询来高效地计算数据库中这些投票的结果。

接下来,我们将让用户上传与我们的MoviePeople模型相关的图片,并讨论安全考虑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值