Django 入门指南(三)

原文:Beginning Django

协议:CC BY-NC-SA 4.0

五、Django 应用管理

像所有现代应用开发框架一样,Django 要求您最终管理任务来支持项目的核心操作。这可以从有效地设置 Django 应用以在现实世界中运行,到管理应用的静态资源(例如 CSS、JavaScript、图像文件)。

此外,其他例行应用管理任务可以包括以下内容:建立日志记录策略以实施问题检测,为应用用户和/或管理员设置电子邮件传递,以及调试任务以检查复杂操作的结果。在这一章中,您将了解这些以及其他与 Django 应用管理相关的常见主题。

Django settings.py 用于真实世界

settings. py是所有 Django 项目的中心配置。在前面的章节中,您已经使用了这个文件中的一系列变量来配置 Django 应用、数据库、模板和中间件等等。

尽管settings.py文件对几乎所有变量都使用了合理的默认值,但是当 Django 应用过渡到现实世界时,您需要考虑一系列调整,以便高效地运行 Django 应用,为最终用户提供简化的体验,并控制潜在的恶意攻击者。

将调试切换到 False

在现实世界中启动 Django 应用首先要做的事情之一是将变量DEBUG改为False。我在前面的章节中已经简单地提到了 Django 在从DEBUG=False切换到DEBUG=True时的行为变化。所有这些与DEBUG变量相关的行为变化都是为了增强项目的安全性。表 5-1 说明了使用DEBUG=FalseDEBUG=True运行项目的区别。

表 5-1。

Django behavior differences between DEBUG=True and DEBUG=False

| 功能 | 调试=真实行为 | 调试=错误行为 | | --- | --- | --- | | 错误处理和通知 | 在请求页面上显示完整的错误堆栈,以便快速分析。 | 显示默认的“普通”或自定义错误页面,没有任何堆栈详细信息,以限制安全威胁或尴尬。向项目管理员发送错误电子邮件。(有关电子邮件通知的更多详细信息,请参见本节中的“为管理员和管理者定义管理员”一节。) | | 静态资源 | 为简单起见,默认设置在项目的/static/ URL 上。 | 禁用自动安装以避免安全漏洞,并要求整合到单独的目录中,以便在单独的 web 服务器上运行静态资源。(请参阅下一节中的设置静态网页资源-图像、CSS、JavaScript。)。) | | 主机/站点限定符 | 接受对所有主机/站点的请求进行处理。 | 有必要确定项目可以处理请求的主机/站点。如果站点/主机不合格,所有请求都会被拒绝。(有关更多详细信息,请参见本节中的“定义允许的主机”一节。) |

正如您在表 5-1 中所看到的,通过将DEBUG=True更改为DEBUG=False而实施的更改旨在用于可公开访问的应用(即生产环境)。您可能不喜欢适应这些变化的麻烦,但是它们是为了在现实世界中运行的所有 Django 项目上保持更高的安全性而强制实施的。

定义允许的主机

默认情况下,settings.py中的ALLOWED_HOSTS变量为空。ALLOWED_HOSTS的目的是验证请求的 HTTP Host报头。进行验证是为了防止恶意用户发送伪造的 HTTP Host报头,这些报头可能会毒害缓存和带有恶意主机链接的密码重置电子邮件。由于该问题只能在不受控制的用户环境下出现(即公共/生产服务器),因此仅在DEBUG=False时进行验证。

如果切换到DEBUG=FalseALLOWED_HOSTS为空,Django 拒绝服务请求,而是用 HTTP 400 错误请求页面来响应,因为它不能验证传入的 HTTP Host头。清单 5-1 展示了ALLOWED_HOSTS的示例定义。

ALLOWED_HOSTS = [
    '.coffeehouse.com',
    '.bestcoffeehouse.com',
]
Listing 5-1.Django ALLOWED_HOSTS definition

正如您在清单 5-1 中看到的,ALLOWED_HOSTS值是一个字符串列表。在这种情况下,它定义了两个主机域,允许bestcoffeehouse.com充当coffeehouse.com的别名。领头的。(点)表示子域也是允许的主机域(例如,static.coffeehouse.comshop.coffeehouse.com.coffeehouse.com有效)。

如果您想接受一个完全限定的域(FQDN),您可以定义ALLOWED_HOSTS=[' www.coffeehouse.com '],它只接受带有 HTTP Host www.coffeehouse.com 的请求。类似地,如果您想接受任何 HTTP 主机——有效地绕过验证——您可以定义ALLOWED_HOSTS=['*'],它表示一个通配符。

小心使用 SECRET_KEY 值

settings.py中的SECRET_KEY值是另一个与安全相关的变量,如ALLOWED_HOSTS。然而,与ALLOWED_HOSTS不同的是,SECRET_KEY被赋予一个默认值和一个很长的值(例如'oubrz5ado&%+t(qu^fqo_#uhn7*+q*#9b3gje0-yj7^#g#ronn')。

SECRET_KEY值的目的是对某些易被篡改的数据结构进行数字签名。具体来说,Django 默认使用敏感数据结构上的SECRET_KEY,比如会话标识符、cookies 和密码重置标记。但是您可以依靠SECRET_KEY值来加密保护 Django 项目中的任何敏感数据结构。 1

SECRET_KEY签名的默认数据结构的一个共同点是,它们被发送给更广泛的互联网上的用户,然后被发送回应用以代表用户触发动作。正是在这种情况下,我们进入了信任问题。发回应用的数据可信吗?如果恶意用户试图模拟另一个用户的 cookie 或会话数据来劫持他的访问,该怎么办?这是数字签名数据所阻止的。

在 Django 向互联网上的用户发送任何这些敏感的数据结构之前,它会用项目的SECRET_KEY给它们签名。当数据结构返回来完成一个动作时,Django 再次对照SECRET_KEY检查这些敏感的数据结构。如果对数据结构有任何篡改,签名检查就会失败,Django 会中止这个过程。

流氓用户成功发动这种攻击的唯一可能性是SECRET_KEY被破坏——因为攻击者可能会创建一个与项目的SECRET_KEY相匹配的修改过的数据结构。因此,你应该小心暴露你的项目的SECRET_KEY。如果您怀疑某个项目的SECRET_KEY由于任何原因已经被破坏,您应该立即替换它——只有少数短暂的数据结构(即会话、cookies)会随着这种改变而变得无效,直到用户再次重新登录,新的SECRET_KEY用于重新生成这些数据结构。

为管理员和经理定义管理员

一旦最终用户可以访问 Django 项目,您就会希望通过某种方式接收与安全或其他关键因素相关的重要事件的通知。Django 有两套管理组,分别在settings.py : ADMINSMANAGERS中定义。默认情况下,ADMINSMANAGERS都是空的。分配给这两个变量的值必须是元组,其中元组的第一个值是一个名称,元组的第二部分是一封电子邮件。清单 5-2 显示了ADMINSMANAGERS的示例定义。

ADMINS = (('Webmaster','webmaster@coffeehouse.com'),('Administrator','admin@coffeehouse.com'))

MANAGERS = ADMINS

Listing 5-2.Django ADMINS and MANAGERS definition

正如您在清单 5-2 中看到的,变量ADMINS被赋予了两个具有不同管理员的元组。接下来,你可以看到ADMINS的值被赋给了MANAGERS变量。当然,您可以使用与ADMINS相同的语法为MANAGERS定义不同的值,但是在这种情况下,为了简单起见,我给了两个变量相同的值。

settings.py中有这两个管理组的目的是让 Django 发送项目事件的电子邮件通知。默认情况下,这些事件是有限的,并且在特定情况下发生。毕竟,你不希望每分钟 24/7 向管理员发送 10 封电子邮件通知。

默认情况下,当且仅当DEBUG=False出现与django.requestdjango.security包相关的错误时,会向ADMINS发送电子邮件通知。这是一个非常狭窄的标准,因为它只打算通知最严重的错误——对于请求和安全性——并且只针对生产环境,也就是当DEBUG=False出现时。因为没有其他事件或条件,所以ADMINS通过电子邮件通知。

默认情况下,当且仅当DEBUG=False和 Django 中间件django.middleware.common.BrokenLinkEmailsMiddleware启用时,才会向MANAGERS发送断开链接的电子邮件通知(即 HTTP 404 页面请求)。因为 HTTP 404 页面请求不是一个严重的问题,默认情况下BrokenLinkEmailsMiddleware是禁用的。这是一个比ADMINS更窄的标准,因为不管项目是在开发中(DEBUG=True)还是在生产中(DEBUG=False),都需要将BrokenLinkEmailsMiddleware类添加到settings.py中的MIDDLEWARE变量中,以便MANAGERS获得通知。因为没有其他事件或条件,所以MANAGERS通过电子邮件通知。

既然您已经知道了ADMINSMANAGERS的用途,那么就在您的项目中添加您认为合适的用户和电子邮件。记住,对于 Django 项目中的其他定制逻辑,您总是可以利用ADMINSMANAGERS中的值(例如,通知管理员用户注册)。

Modify Logging to Stop Email Notifications to Admins

默认情况下,一旦您切换到 DEBUG=False,ADMINS 中的用户就开始收到错误电子邮件——这与管理器不同,管理器永远不会收到电子邮件,除非您将 BrokenLinkEmailsMiddleware 添加到 MIDDLEWARE_CLASSES。

要在 DEBUG=False 时停止向管理员发送电子邮件通知,您可以修改 Django 的日志设置,这将在本章的日志一节中介绍。您也可以不定义 ADMINS,这样就不会发送电子邮件,但这会使您的项目没有可能对其他用途有用的 ADMINS 定义。

使用动态绝对路径

settings.py中有一些依赖于目录位置的 Django 变量,例如STATIC_ROOT,它为项目的静态文件定义了一个合并目录,或者TEMPLATES变量的DIRS列表,它定义了项目模板的位置,等等。

依赖于目录位置的变量的问题是,如果您在不同的服务器上运行项目或者与其他用户共享项目,那么很难在一系列环境中跟踪或保留相同的目录。要解决这个问题,您可以定义变量来动态确定项目的绝对路径。清单 5-3 展示了一个 Django 项目目录结构,部署到/www/系统目录中。

+-/www/+
       |
       +--STORE--+
                 |
                 +---manage.py
                 |
                 +---coffeestatic--+
                 |                 |
                 |                 +-(Consolidated static resources)
                 |
                 +---coffeehouse--+
                                  |
                                  +-__init__.py
                                  +-settings.py
                                  +-urls.py
                                  +-wsgi.py
                                  |
                                  +---templates---+
                                                  +-app_base_template.html
                                                  +-app_header_template.html
                                                  +-app_footer_template.html
Listing 5-3.Django project structure deployed to /www/

通常 Django settings.py文件会定义TEMPLATESSTATIC_ROOTDIRS的值,如清单 5-4 所示。

# Other configuration variables omitted for brevity
STATIC_ROOT = '/www/STORE/coffeestatic/'

# Other configuration variables omitted for brevity
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['/www/STORE/coffeehouse/templates/',],
}
]

Listing 5-4.Django settings.py with absolute path values

清单 5-4 中设置的问题是,如果您将 Django 应用部署到一个没有/www/目录的服务器上,它将需要编辑(例如,由于限制或 Windows 操作系统中目录以字母 C:/开头)。

清单 5-5 中展示的一种更简单的方法是定义变量来动态确定项目的绝对路径。

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

# Other configuration variables omitted for brevity
STATIC_ROOT = '%s/coffeestatic/' % (BASE_DIR)

# Other configuration variables omitted for brevity
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['%s/templates/'% (PROJECT_DIR),],
}
]

Listing 5-5.Django settings.py with dynamically determined absolute path

清单 5-5 顶部定义的变量依赖于 Python os模块来动态确定相对于settings.py文件的绝对系统路径。PROJECT_DIR=os.path.dirname(os.path.abspath(__file__))语句被翻译成/www/STORE/coffeehouse/值,这是像settings.py这样的文件的绝对系统目录。要访问/www/STORE/coffeehouse/的父级,只需用另一个对os.path.dirname的调用包装相同的语句,并定义BASE_DIR变量,这样它就被转换成/www/STORE/值。

清单 5-5 中的其余语句使用标准的 Python 字符串替换来使用PROJECT_DIRBASE_DIR来设置STATIC_ROOTTEMPLATE_DIRS变量中的绝对路径。通过这种方式,您不需要为任何 Django 配置变量硬编码绝对路径;不管应用部署目录如何,变量都会自动调整到任何绝对目录。

为 Django 使用多个环境或配置文件

在每个 Django 项目中,你最终会意识到你必须将settings.py分成多个环境或文件。这可能是因为settings.py中的值需要在开发和生产服务器之间进行更改,有许多人在不同的需求下工作于同一个项目(例如,Windows 和 Linux),或者您需要将敏感的settings.py信息(例如,密码)保存在不与他人共享的本地文件中。

在 Django 中,没有最好的或标准的方法将settings.py分割成多个环境或文件。事实上,有许多技术和库可以让 Django 项目用一个分割的settings.py文件运行。接下来,我将展示我在项目中使用的三个最流行的选项。根据您的需求,您可能会觉得使用一种方法比使用另一种方法更合适,或者混合使用两种或所有三种方法来实现最终解决方案。

选项 1)同一个 settings.py 文件中的多个环境,带有一个控制变量

settings.py文件被视为普通的 Python 文件,因此使用 Python 库或条件来获得某些行为没有限制。这意味着您可以基于固定值(例如,服务器主机名)轻松引入控制变量,以有条件地设置某些变量值。

例如,更改DATABASES变量——因为密码和数据库名称在开发和生产之间会发生变化——更改EMAIL_BACKEND变量——因为您不需要像在生产中那样在开发中发送实际的电子邮件——或者更改CACHES变量——因为您不需要像在生产中那样在开发中使用缓存来提高性能。

清单 5-6 展示了基于 Python 的socket模块设置一个名为DJANGO_HOST的控制变量;然后,该变量用于根据服务器的主机名加载不同组的 Django 变量。

# Import socket to read host name
import socket
# If the host name starts with 'live', DJANGO_HOST = "production"
if socket.gethostname().startswith('live'):
    DJANGO_HOST = "production"
# Else if host name starts with 'test', set DJANGO_HOST = "test"
elif socket.gethostname().startswith('test'):
    DJANGO_HOST = "testing"
else:
# If host doesn't match, assume it's a development server, set DJANGO_HOST = "development"
    DJANGO_HOST = "development"

# Define general behavior variables for DJANGO_HOST and all others
if DJANGO_HOST == "production":
    DEBUG = False
    STATIC_URL = 'http://static.coffeehouse.com/'
else:
    DEBUG = True
    STATIC_URL = '/static/'

# Define DATABASES variable for DJANGO_HOST and all others
if DJANGO_HOST == "production":
   # Use mysql for live host
   DATABASES = {
    'default': {
        'NAME': 'housecoffee',
        'ENGINE': 'django.db.backends.mysql',
        'USER': 'coffee',
        'PASSWORD': 'secretpass'
    }
  }
else:
   # Use sqlite for non live host
   DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'coffee.sqlite3'),
    }
  }

# Define EMAIL_BACKEND variable for DJANGO_HOST
if DJANGO_HOST == "production":
    # Output to SMTP server on DJANGO_HOST production
    EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
elif DJANGO_HOST == "testing":
    # Nullify output on DJANGO_HOST test
    EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
else:
    # Output to console for all others
    EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

# Define CACHES variable for DJANGO_HOST production and all other hosts
if DJANGO_HOST == "production":
   # Set cache
   CACHES = {
        'default': {
            'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
            'LOCATION': '127.0.0.1:11211',
            'TIMEOUT':'1800',
            }
        }
   CACHE_MIDDLEWARE_SECONDS = 1800
else:
   # No cache for all other hosts
   pass

Listing 5-6.Django settings.py with control variable with host name to load different sets of variables

清单 5-6 中的第一行导入 Python socket模块来访问主机名。接下来,使用socket.gethostname()声明一系列条件,以确定控制变量DJANGO_HOST的值。如果主机名以字母live开头,则DJANGO_HOST变量被设置为"production",如果主机名以test开头,则DJANGO_HOST被设置为"testing",如果主机名不是以前面的选项开头,则DJANGO_HOST被设置为"development"

在这个场景中,字符串方法startswith用于确定如何根据主机名设置控制变量。然而,您可以轻松地使用任何其他 Python 库甚至标准(例如,IP 地址)来设置控制变量。此外,由于控制变量是基于字符串的,您可以根据需要引入任意多的配置变量。在这种情况下,我们使用三种不同的变量来设置settings.py变量——"production""testing""development"——但是如果你需要如此多的不同设置,你可以很容易地定义五个或十几个变量。

选项 2)使用 configparser 的多个环境文件

split settings.py的另一个变化是依赖 Python 内置的 configparser 模块。configparser 允许 Django 从文件中读取配置变量,这些文件使用的数据结构类似于 Microsoft Windows INI 文件中使用的数据结构。清单 5-7 展示了一个样本 configparser 文件。

[general]
DEBUG: false
STATIC_URL: http://static.coffeehouse.com/

[databases]
NAME: housecoffee
ENGINE: django.db.backends.mysql
USER: coffee
PASSWORD: secretpass

[security]
SECRET_KEY: %%ea)cjy@v9(7!b(20gl+4-6iur28dy=tc4f$-zbm-v=!t

Listing 5-7.Python configparser sample file production.cfg

正如您在清单 5-7 中看到的,configparser 文件的格式由括号中声明的各个部分构成(例如,[general][databases]),每个部分下面是不同的键和值。清单 5-7 中的变量代表了一个放置在名为production.cfg的文件中的生产环境。我为这个文件选择了.cfg扩展名,但是如果你愿意,你也可以使用.config.ini扩展名;扩展名与 Python 无关——唯一重要的是文件本身的数据格式。

类似于production.cfg中的内容,您可以为其他环境创建不同变量的其他文件(如testing.cfgdevelopment.cfg)。一旦有了 configparser 文件,就可以将它们导入到 Django settings.py中。清单 5-8 显示了一个使用 configparser 文件中的值的示例settings.py

import os

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

# Access configparser to load variable values
from django.utils.six.moves import configparser
config = configparser.SafeConfigParser(allow_no_value=True)

# Import socket to read host name
import socket
# If the host name starts with 'live', load configparser from "production.cfg"
if socket.gethostname().startswith('live'):
    config.read('%s/production.cfg' % (PROJECT_DIR))
# Else if host name starts with 'test', load configparser from "testing.cfg"
elif socket.gethostname().startswith('test'):
    config.read('%s/testing.cfg' % (PROJECT_DIR))
else:
# If host doesn't match, assume it's a development server, load configparser from "development.cfg"
    config.read('%s/development.cfg' % (PROJECT_DIR))

DEBUG = config.get('general', 'DEBUG')
STATIC_URL = config.get('general', 'STATIC_URL')

DATABASES = {
    'default': {
        'NAME': config.get('databases', 'NAME'),
        'ENGINE': config.get('databases', 'ENGINE'),
        'USER': config.get('databases', 'USER'),
        'PASSWORD': config.get('databases', 'PASSWORD')
    }
  }

SECRET_KEY = config.get('security', 'SECRET_KEY')

Listing 5-8.Django settings.py with configparser import

Note

清单 5-8 中的配置假设主机名以名称 live 开始,以便加载清单 5-7 中的 configparser production.cfg。调整清单 5-8 开头的条件以匹配主机名并加载适当的 configparser 文件。

正如您在清单 5-8 中看到的,configparser 通过django.utils.six.moves加载到 Django 中,这是一个允许 Python 2 和 Python 3 之间交叉导入的实用程序。在 Python 2 中,configparser 包实际上被命名为ConfigParser,但是这个实用程序允许我们在 Python 2 或 Python 3 中使用相同的 import 语句。导入之后,我们使用带有参数allow_no_value=TrueSafeConfigParser类来允许处理 configparser 键中的空值。

然后,我们依靠相同的现有技术,使用 Python 的socket模块来访问主机名,并确定要加载哪个 configparser 文件。使用SafeConfigParser实例的 read 方法加载 configparser 文件。此时,所有 configparser 变量都已加载并准备好供访问。清单 5-8 的剩余部分显示了一系列标准 Django settings.py变量,这些变量使用SafeConfigParser实例的get方法赋值,其中第一个参数是 configparser 部分,第二个参数是 key 变量。

因此,在如何将settings.py中的变量拆分到多个环境中,您有了另一种选择。正如我在开始时提到的,做这件事没有最好或标准的方法。有些人更喜欢 configparser,因为它将值拆分到单独的文件中,避免了选项 1 的许多条件,但其他人可能讨厌 configparser,因为需要处理特殊的语法和单独的文件。选择最适合你的项目的。

选项 3)每个环境有多个不同名称的 settings.py 文件

最后,将 Django 变量拆分到多个环境的另一个选项是创建多个不同名称的settings.py文件。默认情况下,Django 会在项目基本目录的settings.py文件中查找配置变量。

然而,可以告诉 Django 加载一个不同名称的配置文件。Django 为此使用了操作系统变量DJANGO_SETTINGS_MODULE。默认情况下,Django 将这个 OS 变量设置为位于任何 Django 项目的基目录下的manage.py文件中的<project_name>.settings。由于manage.py文件用于引导 Django 应用,该文件中的DJANGO_SETTINGS_MODULE值保证配置变量总是从<project_name>子目录中的settings.py文件加载。

所以让我们假设您为 Django 应用创建了不同的settings.py文件——与settings.py放在同一个目录中——命名为production.pytesting.pydevelopment.py。您有两种选择来加载这些不同的文件。

一种选择是将项目的manage.py文件中的DJANGO_SETTINGS_MODULE定义更改为具有所需配置的文件(例如,os.environ.setdefault("DJANGO_SETTINGS_MODULE", "coffeehouse.production")加载production.py配置文件)。然而,硬编码这个值是不灵活的,因为您需要根据期望的配置不断地改变manage.py中的值。这里,您可以在manage.py中使用一个控制变量,根据主机名动态确定DJANGO_SETTINGS_MODULE的值——类似于前面选项 1 中针对settings.py描述的过程。

设置DJANGO_SETTINGS_MODULE而不改变manage.py的另一种可能性是在操作系统级别定义DJANGO_SETTINGS_MODULE,这样它会覆盖manage.py中的定义。清单 5-9 展示了如何在 Linux/Unix 操作系统上设置DJANGO_SETTINGS_MODULE变量,以便使用testing.py文件中的应用变量来代替settings.py文件。

$ export DJANGO_SETTINGS_MODULE=coffeehouse.load_testing
$ python manage.py runserver
Validating models...

0 errors found
Django version 1.11, using settings 'coffeehouse.load_testing'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Listing 5-9.Override DJANGO_SETTINGS_MODULE to load application variables from a file called testing.py and not the default settings.py

在清单 5-9 中,我们使用标准的 Linux/Unix 语法export variable_name=variable_value来设置环境变量。一旦完成,注意使用开发服务器的 Django 应用显示启动消息"using settings 'coffeehouse.load_testing'"

如果您计划在操作系统级别覆盖DJANGO_SETTINGS_MODULE来加载不同的 Django 应用变量,请注意默认情况下操作系统变量不是永久的或继承的。这意味着您可能需要为启动 Django 的每个 shell 定义DJANGO_SETTINGS_MODULE,并将其定义为运行时环境(例如 Apache)的一个局部变量。

设置静态网页资源——图像、CSS、JavaScript

如果项目使用DEBUG=TrueDEBUG=False运行,Django 项目中静态资源的设置过程会有很大的不同。这意味着静态资源部署取决于您是在开发环境中工作——通常使用DEBUG=True——还是在生产环境中工作——通常使用DEBUG=False

考虑到您总是在开发环境中启动 Django 项目,然后迁移到生产环境,我将首先描述开发设置过程,然后描述生产设置过程。

在开发环境中设置静态资源(DEBUG=False)

默认情况下,当DEBUG=False时,Django 自动从两个主要位置设置静态资源。第一个位置是所有 Django 应用中的static文件夹,第二个位置是在settings.pySTATICFILES_DIR变量中声明的文件夹。

虽然您需要在 Django apps 中手动创建static文件夹,但是在项目中设置静态资源非常容易。因为 Django 为每个项目应用设置了所有的static文件夹,所以建议在static文件夹中进一步添加一个子目录(例如<app_folder>/static/<app_name>/<static_files_here>),以限定静态资源并避免潜在的命名冲突。清单 5-10 展示了一个静态资源的样本目录结构。

+-<BASE_DIR_project_name>
|
+-manage.py
|
+-bootstrap-3.1.1-dist+
|                     +-bootstrap.min.js
|
+-jquery-1-11-1-dist+
|                   +jquery.min.js
|
+-jquery-ui-1.10.4+
|                 +jquery-ui.min.js
|
+-website-static-default+
|                       +-favicon.ico
|                       +-robots.txt
|
|
+---+-<PROJECT_DIR_project_name>
    |
    +-__init__.py
    +-settings.py
    +-urls.py
    +-wsgi.py
    |
    +-about(app)-+
    |            +-__init__.py
    |            +-models.py
    |            +-tests.py
    |            +-views.py
    |            +-static-+
    |                     |
    |                     +-about-+
    |                             +-img-+
    |                             |     +-logo.png
    |                             |
    |                             +-css-+
    |                                   +-custom.css
    +-stores(app)-+
                 +-__init__.py
                 +-models.py
                 +-tests.py
                 +-views.py
                 +-static-+
                          |
                          +-stores-+
                                   +-img-+
                                   |     +-coffee.gif
                                   |
                                   +-css-+
                                         +-custom.css
Listing 5-10.Django app structure with static directories

如清单 5-10 所示,所有 Django 应用目录都有一个包含静态资源的static子目录。这些静态子目录下的任何内容都是为访问而设置的。

还要注意清单 5-10 中的应用名称子目录在static子目录中作为名称空间的重要性。如果静态资源直接放在所有应用中的static文件夹下,在这种情况下,会导致两个相同的文件路径名为/static/css/custom.css,在这种情况下,调用加载这个静态资源会导致冲突。从技术上讲,Django 总是使用它找到的第一个文件,但是第一个文件是正确的吗?通过使用static内的应用名称子目录,它避免了任何潜在的冲突,一个静态资源设置在/static/about/css/custom.css而另一个设置在/static/stores/css/custom.css

因为可能存在不一定属于特定项目应用的静态资源,所以 Django 还支持设置存储在任何子目录中的静态资源的能力。

如果您再次查看清单 5-10 ,在BASE_DIRPROJECT_DIR之间,您将会看到包含流行静态资源库- jqueryjquery-uibootstrap的各种子文件夹,以及包含网站标准静态资源-robots.txt & favicon.ico的子文件夹website-static-default

为了设置这些额外的静态资源,您在settings.pySTATICFILES_DIR变量中定义这些目录的位置。清单 5-11 展示了一个STATICFILES_DIR定义的例子。

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

STATICFILES_DIRS = ('%s/website-static-default/'% (BASE_DIR),
                    ('bootstrap','%s/bootstrap-3.1.1-dist/'% (BASE_DIR)),
                    ('jquery','%s/jquery-1-11-1-dist/'% (BASE_DIR)),
                    ('jquery-ui','%s/jquery-ui-1.10.4/'% (BASE_DIR)),)

Listing 5-11.Django STATICFILES_DIR definition with namespaces in settings.py

正如您在清单 5-11 中看到的,STATICFILES_DIRS接受一个目录列表。在这种情况下,所有目录都在 Django 项目的BASE_DIR下,所以它使用了动态确定父目录的BASE_DIR变量。清单 5-11 中目录列表的另一个方面是你可以选择声明一个名称空间,类似于应用的static子目录中使用的方法。

清单 5-11 中的第一个目录定义是一个简单的字符串(也就是说,它没有名称空间),这意味着website-static-default中的静态资源是用直接访问模式建立的。其余的目录定义是元组而不是字符串。通过使用元组,它将元组的第一部分定义为名称空间,第二部分定义为包含静态资源的目录。带有名称空间的定义意味着给定目录下的所有静态资源将在其访问模式中使用前缀名称空间(例如,要访问bootstrap-3.1.1-dist上的静态资源,访问模式应该以bootstrap为前缀)。

现在您已经知道了在哪里以及如何设置所有静态资源,让我们快速看一下 Django 如何可视化这些静态资源,以理解静态资源的最终访问模式是什么样子的。清单 5-12 展示了前面清单中呈现的静态资源的可视化。

+-favicon.ico
+-robots.txt
|
+-jquery+
|       +jquery.min.js
|
+-jquery-ui+
|          +jquery-ui.min.js
|
+-bootstrap+
|          +-bootstrap.css
|
+-about-+
|       +-img-+
|       |     +-logo.png
|       |
|       +-css-+
|             +-custom.css
|
+-stores-+
         +-img-+
         |     +-coffee.gif
         |
         +-css-+
               +-custom.css
Listing 5-12.Django visualization of static resources in apps and STATICFILES_DIRS

清单 5-12 中的文件favicon.icorobots.txt位于可视化的顶层,因为它们的源目录website-static-defaultSTATICFILES_DIRS中没有命名空间。

其余的静态资源都分组在子文件夹中,因为我们要么在STATICFILES_DIRS中为它们定义了一个名称空间,要么在应用的static子目录中将子文件夹定义为名称空间。

现在您已经理解了 Django 如何将静态资源可视化为一个组,以及这如何决定静态资源的最终访问模式,让我们将注意力转向settings.py中的STATIC_URL变量。

STATIC_URL用于定义清单 5-12 中显示的 Django 静态资源可视化的 URL 入口点。默认情况下,STATIC_URL被赋予/static/值。这意味着如果STATIC_URL='/static/',静态资源robots.txt在 URL /static/robots.txt变得可访问,就像stores/img/coffee.gif在 URL /static/stores/img/coffee.gif变得可访问一样。

这意味着您在/static/ URL 上访问静态资源,或者如果您更改了STATIC_URL值,则在不同的 URL 上访问静态资源。但是,不要在模板上硬编码这些静态资源路径!(如<img src="/static/stores/img/coffee.gif"/>)。您应该使用一个变量,以便在STATIC_URL改变的情况下动态确定最终路径。下一节将描述如何在 Django 模板中做到这一点。

Caution

对静态资源的自动访问只适用于 Django 的内置 web 服务器,并且 DEBUG=True。

之前静态资源的设置过程有 Django 的“幕后”帮助。它只与 Django 的内置 web 服务器(即python manage.py runserver)一起工作,并且只有在DEBUG=True的情况下。一旦你换到一个不同的 web 服务器或者切换DEBUG=False甚至使用 Django 的内置 web 服务器,就没有静态资源可用,如清单 5-12 所示。

这种行为背后的主要原因是因为从应用的主 web 服务器/URL 结构(例如,/static/)保留和调度静态资源是非常低效的。所以这只是为了方便使用 Django 的内置 web 服务器进行开发。当然,您可以将一个完整的 URL 域分配给STATIC_URL(例如 http://static.coffeehouse.com/ ),但是这假设您已经在一个类似生产的环境中设置了项目的静态资源,在我描述如何访问 Django 和 Jinja 模板中的静态资源时,我将很快讨论这个问题。

访问 Django 模板中的静态资源

推荐在 Django 模板中引用静态资源的方法是通过 staticfiles 应用,通过{% static %}标签。清单 5-13 展示了 staticfiles 应用语法的各种例子。

{% load static %}

# For static resource at about/img/logo.png
<img src="{% static 'about/img/logo.gif' %}">

# For static resource at bootstrap/bootstrap.css
<link href="{% static 'bootstrap/bootstrap.css' %}" rel="stylesheet">

# For static resource at jquery/jquery.min.js
<script src="{% static 'jquery/jquery.min.js' %}"></script>

Listing 5-13.Django {% static %} tag to reference static resources

首先,重要的是要注意清单 5-13 中的{% load static %}标签可以通过 staticfiles 应用获得,该应用默认安装在所有 Django 项目的INSTALLED_APPS变量中。如果出于某种原因,你修改了INSTALLED_APPS中的默认值,确保你在INSTALLED_APPS变量中有django.contrib.staticfiles的值,否则接下来的都不会起作用。

正如您在清单 5-13 中看到的,在模板的顶部,您总是声明{% load static %}语句。一旦完成,模板就可以使用{% static %}标签为静态资源生成动态路径。在大多数情况下,{% static %}标签依赖于settings.py中的STATIC_URL变量来生成静态资源的适当路径。

对于更高级的情况,{% static %}标签使用相同的STATIC_URL变量和后备存储技术(例如,CDN-‘内容交付网络’)配置的组合来生成静态资源的适当路径。

例如,请注意清单 5-13 中的{% static %}标签后面总是跟有一个文件路径,该路径与清单 5-12 中静态资源的 Django 可视化相同。由于STATIC_URL变量的值为/static/,这意味着清单 5-13 中的{% static %}语句被替换为该值(例如{% static 'bootstrap/bootstrap.css' %}变成了/static/bootstrap/bootstrap.css)。

当 Django 项目使用非标准的后端来提供静态资源时,{% static %}标签被替换为不同于STATIC_URL变量的东西——最后一种情况将在下面的侧栏中简要讨论。

Why Use the Staticfiles {% Static %} Tag Vs. Using the Static_Url Variable Directly in Templates?

在 Django 的早期版本中,Django 模板在模板中直接使用了 STATIC_URL 变量(例如外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传)。事实上,您仍然可以通过 Django . template . context _ processors . STATIC 上下文处理器访问所有 Django 模板上的 STATIC_URL 变量。

然而,随着服务静态资源的底层技术变得越来越复杂,STATIC_URL 变量本身被证明是不够的。例如,像 CDNs 或亚马逊 S3 这样的静态服务技术经常使用特殊的令牌来实施认证或缓存策略。这意味着类似于外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传的语句需要转换成类似于外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 http://cdnprovider.com/about/img/logo.gif?token=e354534566 " >或< img src=" http://staticresources.com/about/img/logo32AzTB9r5.gif " >的语句。虽然可以将 STATIC_URL 变量更改为完整的域,但是很难修改静态资源的路径本身。

用像{% static %}这样的标签重写静态资源的路径很容易。因为{% static %}可以接受静态资源的基本字符串(例如,about/img/logo.gif ),并使用 STATIC_URL 变量和底层静态服务技术所需的任何特殊标记动态生成完整路径。这个过程是通过使用一个定制的存储类来实现的,该存储类是为静态服务技术而设计的。

当然,并不是所有的项目都需要使用先进的静态服务技术。但是通过使用 staticfiles 应用的{% static %}标记来声明 Django 模板中的静态资源,您可以确保 Django 项目能够使用任何静态服务技术,从最基本的到最高级的。

访问 Jinja 模板中的静态资源

如前一章所述,Jinja 模板提供了 Django 自己的模板的替代品。但是与 Django 模板不同,你必须遵循不同的设置来使用 Jinja 模板中 staticfiles 应用的 Django 的{% static %}标签。

为了能够在 Jinja 模板中使用相同的 staticfiles app / {% static %}标记行为,您需要设置一个名为 static 的全局变量来挂钩这个功能。在前面关于 Jinja 模板的章节中,“在 Django 中为所有 Jinja 模板(如 Django 上下文处理器)上的访问设置数据”一节描述了如何用这个功能创建一个全局变量。

在生产环境中设置静态资源(DEBUG=True)

当您将 Django 项目的DEBUG变量切换到True或者切换到不同的 web 服务器(例如 Apache、Nginx)时,您会惊讶地发现项目中不再出现任何静态资源。不要惊慌,这是故意的。当DEBUG=True使用 Django 的内置 web 服务器或者如果你切换到第三方 web 服务器时,设置 Django 来服务静态资源并不太困难。

Tip

您可以访问静态资源,使 Django 的内置 web 服务器在实际设置为 DEBUG=True 时提供静态资源,就像 DEBUG=False 一样。使用- insecure 标志运行 web 服务器:python manage . py run server–insecure。

Caution

虽然前面的解决方法是可用的,但我建议您不要使用它,以防标志名本身不安全不足以阻止您使用它。

Django 的内置 web 服务器(即python manage.py runserver)确实是一个快速启动和运行的便利工具,作为这种便利的一部分,它还在DEBUG=False时提供静态资源。

然而,允许同一个 web 服务器进程同时处理动态内容(Django web 页面)和静态资源(图像、CSS、JavaScript)确实是一种浪费。推荐的方法是完全使用一个单独的 web 服务器来服务静态资源,这就是为什么 Django 在切换DEBUG=True时打破了内置 web 服务器的便利性。

DEBUG=True出现时,您需要做的第一件事是创建一个目录来保存 Django 可视化为静态资源的所有静态资源的副本。之前你已经了解到当DEBUG=False时,Django 将来自几个位置和子目录的静态资源可视化在一个单独的树中——如清单 5-12 所示。Django 设想的正是这一棵树,您需要创建一个副本来在生产环境中运行。

你需要在settings.py.中定义STATIC_ROOT变量,你分配给STATIC_ROOT的值应该是一个目录,Django 将在那里复制你项目的所有静态资源——与 Django 在清单 5-12 中展示的DEBUG=True时可视化它们的方式相同。请注意,此目录应该为空,因为每次执行同步过程时,它都会被不断覆盖。根据您的需要,该目录的位置可以是系统中的任何位置。为了简单起见,我将把 Django 项目的BASE_DIR下的STATIC_ROOT目录保留为STATIC_ROOT = '%s/coffeestatic/'% (BASE_DIR

要触发同步过程(即,将所有静态资源复制到STATIC_ROOT,您需要使用manage.py脚本中可用的collectstatic命令。清单 5-14 展示了同步过程的示例输出。

[user@coffeehouse ∼]$ python manage.py collectstatic

You have requested to collect static files at the destination
location as specified in your settings:

    /www/STORE/coffeestatic

This will overwrite existing files!
Are you sure you want to do this?

Type 'yes' to continue, or 'no' to cancel: yes
yes
Copying '/www/STORE/website-static-default/sitemap.xml'
Copying '/www/STORE/website-static-default/robots.txt'
Copying '/www/STORE/website-static-default/favicon.ico'
....
....
....
Copying '/www/STORE/coffeehouse/about/static/css/custom.css'

732 static files copied to '/www/STORE/coffeestatic'.

Listing 5-14.Django collectstatic command to copy all static resources

一旦你将所有项目的静态资源收集到一个单独的文件夹中——在这个例子中是/www/STORE/coffeestatic——它们就可以在一个生产服务器上进行设置了(例如,Apache、Nginx 或 AWS S3)。请记住,collectstatic生成的目录/文件结构与清单 5-12 中展示的上一节中 Django 可视化的目录/文件结构相同。

您需要做的最后一步是更新settings.py中的STATIC_URL值,以反映静态资源的新位置。例如,如果您在 Apache 或 Nginx 上的 http://static.coffeehouse.com/ 域下挂载/www/STORE/coffeestatic/目录,您将设置STATIC_URL=' http://static.coffeehouse.com '。类似地,如果您将/www/STORE/coffeestatic/中的静态资源复制到一个名为 http://coffeehouse.s3.amazonaws.com 的 Amazon AWS S3 存储桶,您将设置STATIC_URL=' http://coffeehouse.s3.amazonaws.com '

一旦进行了最后的修改,Django 模板中所有使用{% static %}标签的语句都将更新为新的全域 URL,在这种情况下,像/www/STORE/coffeestatic/bootstrap/bootstrap.css这样的资源将在http://static.coffeehouse.com/bootstrap/bootstrap.css?? 或 http://coffeehouse.s3.amazonaws.com/bootstrap/bootstrap.css 可用。

Django 伐木公司

日志是最有用的应用管理实践之一,也是使用最少的应用管理实践之一。如果您仍然没有在 Django 项目中使用日志,或者使用 Python print()语句来深入了解应用正在做什么,那么您就错过了很多功能。接下来,您将学习 Python 核心日志概念,如何设置 Django 定制日志,以及如何使用监控服务来跟踪日志消息。

Python 核心日志记录概念

Django 构建在 Python 的日志包之上。Python 日志包提供了一种健壮而灵活的方法来设置应用日志。如果您从未使用过 Python 的日志包,我将简要概述它的核心概念。Python 日志记录中有四个核心概念:

  • 伐木工人。-提供日志消息分组的初始入口点。通常,每个 Python 模块(即,py 文件)有一个单独的日志记录器来分配它的日志消息。然而,也可以在同一个模块中定义多个记录器(例如,一个记录器用于业务逻辑,另一个记录器用于数据库逻辑,等等)。).此外,还可以在多个 Python 模块之间使用同一个日志记录器。py 文件。
  • 经手人。-用于将日志消息(由记录器创建)重定向到目标。目的地可以包括平面文件、服务器控制台、电子邮件或 SMS 消息以及其他目的地。可以在多个记录器中使用同一个处理程序,就像一个记录器可以使用多个处理程序一样。
  • 过滤器。-提供一种对日志消息应用规则的方法。例如,您可以使用过滤器将同一记录器生成的日志消息发送到不同的处理程序。
  • 格式化程序。-用于指定日志消息的最终格式。

简要概述了 Python 日志概念之后,让我们直接开始探索 Django 的默认日志功能。

Django 默认日志记录

Django 项目的日志配置在settings.pyLOGGING变量中定义。目前,甚至不要麻烦打开你的项目的settings.py文件,因为你不会在其中看到LOGGING。当您创建一个项目时,这个变量不是硬编码的,但是如果它没有被声明,它确实有一些有效的日志记录值。清单 5-15 显示默认的LOGGING值,如果它没有在settings.py中声明的话。

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse',
        },
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
        },
        'null': {
            'class': 'logging.NullHandler',
        },
        'mail_admins': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'django.utils.log.AdminEmailHandler'
        }
    },
    'loggers': {
        'django': {
            'handlers': ['console'],
        },
        'django.request': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': False,
        },
        'django.security': {
            'handlers': ['mail_admins'],
            'level': 'ERROR',
            'propagate': False,
        },
        'py.warnings': {
            'handlers': ['console'],
        },
    }
}
Listing 5-15.Default LOGGING in Django projects

总之,清单 5-15 中所示的默认 Django 日志记录设置具有以下日志记录行为:

  • 控制台日志记录或console处理程序仅在DEBUG=True时完成,对于比INFO更差的日志消息(含)且仅针对 Python 包django -及其子包(如django.requestdjango.contrib ) -以及 Python 包py.warnings
  • 管理日志或mail_admins处理程序——向ADMINS发送电子邮件——仅在DEBUG=False时完成,用于比ERROR更糟糕的日志消息(包括 ?? ),并且仅用于 Python 包django.requestdjango.security

我们先来分解一下清单 5-14 中的handlers部分。处理程序定义发送日志消息的位置,清单 5-14 中有三个位置:consolenullmail_admins。处理程序名本身什么也不做——它们只是引用名——相关的操作在相关的属性字典中定义。所有的处理程序都有一个class属性,该属性定义了执行实际工作的支持 Python 类。

console处理程序被分配了logging.StreamHandler类,它是核心 Python 日志包的一部分。这个类将日志输出发送到流,如标准输入和标准错误,正如处理程序名称所暗示的,从技术上讲,这是 Django 运行的系统控制台或屏幕。

null处理程序被分配了 l ogging.NullHandler类,它也是核心 Python 日志包的一部分,不生成任何输出。

mail_admins处理程序被分配了django.utils.log.AdminEmailHandler类,这是一个 Django 定制处理程序实用程序,它将日志输出作为电子邮件发送给在settings.py中被定义为ADMINS的人——有关ADMINS变量的更多信息,请参见上一节关于为真实世界设置settings.py的内容。

处理程序中的另一个属性是level,它定义了处理程序必须接受日志消息的阈值级别。Python 日志记录有五个阈值级别,从最差到最差依次是CRITICALERRORWARNINGINFODEBUGconsole处理程序的INFO级别表示所有差于或等于INFO的日志消息——除了DEBUG之外的每个级别——都应该由处理程序处理,这是一个合理的设置,因为控制台可以处理许多消息。mail_admins处理程序的ERROR级别表示只有比ERROR(也就是CRITICAL)更差或相同的消息才应该由处理程序处理,这是一个合理的设置,因为只有两种最差类型的错误消息才应该触发给管理员的电子邮件。

处理程序中的另一个属性是filters,它定义了一个额外的层来限制处理程序的日志消息。处理程序可以接受多个过滤器,这就是为什么filters属性接受 Python 列表的原因。console处理器有一个过滤器require_debug_true,而mail_admins处理器有一个过滤器require_debug_false

正如您在清单 5-15 中所看到的,过滤器是在它们自己的块中定义的。require_debug_false过滤器由django.utils.log.RequireDebugFalse类支持,该类检查 Django 项目是否有DEBUG=False,而require_debug_true过滤器由django.utils.log.RequireDebugTrue类支持,该类检查项目是否有DEBUG=True。这意味着如果 Django 项目有DEBUG=True,那么console处理程序只接受日志消息,如果 Django 项目有DEBUG=False,那么mail_admins处理程序只接受日志消息。

现在您已经理解了处理程序和过滤器,让我们来看看loggers部分。记录器定义通常直接映射到 Python 包,并且具有父子关系。例如,属于名为coffeehouse的包的 Python 模块(即.py文件)一般有一个名为coffeehouse的记录器,属于coffeehouse.about的包的 Python 模块一般有一个名为coffeehouse.about的记录器。记录器名称中的点符号也代表父子关系,因此coffeehouse.about记录器被认为是coffeehouse记录器的子记录器。

在清单 5-15 中有四个记录器:djangodjango.requestdjango.securitypy.warningsdjango日志记录器指示所有与其相关的日志消息及其子代都由console处理程序处理。

django.request logger 表示所有与其相关的日志消息及其子代都由mail_admins处理程序处理。django.request日志记录器还有'level':'ERROR'属性来提供日志记录器应该接受日志消息的阈值级别——这个属性覆盖了handler级别属性。此外,django.request记录器还具有'propagate':'False'语句,用于指示记录器不应将消息传播给父记录器(例如,djangodjango.request的父记录器)。

接下来,我们有一个与django.request记录器功能相同的django.security记录器。以及py.warnings,表示所有与其相关的日志消息及其子节点都将由console处理程序处理。

最后,清单 5-15 中的前两行通常与 Python 日志记录相关。version键将配置版本标识为1,这是目前唯一的 Python 日志版本。而disable_existing_loggers键用于禁用所有现有的 Python 记录器。如果disable_existing_loggersFalse,则保持先前存在的记录器值,如果设置为True,则禁用所有先前存在的记录器值。请注意,即使您在自己的LOGGING变量中使用了'disable_existing_loggers': False,您也可以重新定义/覆盖一些或所有预先存在的记录器值。

现在您已经对 Django 日志记录在默认状态下的功能有了很好的理解,我将描述如何在 Django 项目中创建日志消息,然后描述如何创建定制的LOGGING配置。

创建日志消息

在任何 Python 模块或.py文件的顶部,你可以通过使用 Python logging包的getLogger方法来创建记录器。getLogger方法接收记录器的名称作为其输入参数。清单 5-16 展示了使用__name__和硬编码的dba名称创建两个记录器实例。

# Python logging package
import logging

# Standard instance of a logger with __name__
stdlogger = logging.getLogger(__name__)

# Custom instance logging with explicit name
dbalogger = logging.getLogger('dba')

Listing 5-16.Define loggers in a Python module

清单 5-16 中用于getLogger的 Python __name__语法自动将包名指定为记录器名。这意味着,如果在应用目录coffeehouse/about/views.py下的模块中定义了记录器,记录器将获得名称coffeehouse.about.views。因此,依靠__name__语法,基于日志消息的来源自动创建记录器。

不要担心 Django 项目中的每个模块或.py文件都有几十或几百个记录器。如前一节所述,Python 日志记录与继承一起工作,因此您可以为父日志记录器(例如,coffeehouse)定义一个处理程序来处理所有子日志记录器(例如,coffeehouse.aboutcoffeehouse.about.viewscoffeehouse.drinkscoffeehouse.drinks.models)。

有时,定义一个带有明确名称的日志记录器来对某些类型的消息进行分类是很方便的。在清单 5-16 中,您可以看到一个名为dba的日志记录器,用于记录与数据库问题相关的消息。这样,数据库管理员可以查阅自己的日志流,而不需要查看来自应用其他部分的日志消息。

一旦模块或.py文件中有了记录器,就可以根据需要报告的消息的严重程度,用几种方法中的一种来定义日志消息。下面的列表说明了这些方法:

  • <logger_name>。关键()。-最严重的日志记录级别。使用它来报告潜在的灾难性应用事件(例如,可能导致应用暂停或崩溃的事件)。</logger_name>
  • <logger_name>。错误()。-第二严重的日志记录级别。使用它来报告重要事件(例如,导致最终用户看到错误的意外行为或情况)。</logger_name>
  • <logger_name>。警告()。-中级日志级别。使用它来报告相对重要的事件(例如,不应该发生的意外行为或情况,但不会导致最终用户注意到该问题)。</logger_name>
  • <logger_name>。信息()。-信息记录级别。使用它来报告应用中的信息性事件(例如,应用里程碑或用户活动)。</logger_name>
  • <logger_name>。调试()。-调试日志记录级别。使用它来报告难以编写的分步逻辑(例如,复杂的业务逻辑或数据库查询)。</logger_name>
  • <logger_name>。日志()。-使用它手动发出特定日志级别的日志消息。</logger_name>
  • <logger_name>。异常()。-使用它来创建错误级别日志记录消息,用当前异常堆栈包装。</logger_name>

您使用什么方法来记录项目中的消息完全取决于您自己。就日志记录级别而言,只要尽量与选择标准保持一致即可。您可以随时调整运行时日志记录级别,以停用特定级别的日志消息。

此外,我还建议您尽可能使用最具描述性的日志消息来最大化日志记录的好处。清单 5-17 展示了使用几种日志记录方法和消息的一系列例子。

# Python logging package
import logging

# Standard instance of a logger with __name__
stdlogger = logging.getLogger(__name__)

# Custom instance logging with explicit name
dbalogger = logging.getLogger('dba')

def index(request):
    stdlogger.debug("Entering index method")

def contactform(request):
    stdlogger.info("Call to contactform method")

    try:
         stdlogger.debug("Entering store_id conditional block")
         # Logic to handle store_id
    except Exception, e:
         stdlogger.exception(e)

    stdlogger.info("Starting search on DB")
    try:
         stdlogger.info("About to search db")
         # Loging to search db
    except Exception, e:
         stdlogger.error("Error in searchdb method")
         dbalogger.error("Error in searchdb method, stack %s" % (e))

Listing 5-17.Define log messages in a Python module

正如您在清单 5-17 中看到的,使用清单 5-16 中描述的两个日志记录器,有不同级别的各种日志消息。日志消息根据它们在方法体或 try/except 块中的级别展开。

如果您将清单 5-17 中的记录器和日志语句放在 Django 项目中,您会发现在日志方面什么也没有发生!事实上,您将在控制台中看到的是形式为'No handlers could be found for logger ...<logger_name>'的消息。

这是因为默认情况下 Django 不知道任何关于你的记录器的事情!它只知道清单 5-15 中描述的默认记录器。在下一节中,我将描述如何创建一个定制的LOGGING配置,这样您就可以看到您的项目日志消息。

自定义日志记录

由于 Django 日志中有四种不同的组件可以混合使用(即日志记录器、处理程序、过滤器和格式化程序),因此创建定制日志配置的变化几乎是无穷无尽的。

在接下来的部分中,我将描述 Django 项目的一些最常见的定制日志配置,包括覆盖默认的 Django 日志行为(例如,不发送电子邮件),定制日志消息的格式,以及将日志输出发送到不同的记录器(例如,文件)。

清单 5-18 展示了一个定制的LOGGING配置,您可以将它放在一个项目的settings.py文件中,涵盖了这些常见的需求。接下来的部分解释了每个配置选项。

LOGGING = {
    'version': 1,
    'disable_existing_loggers': True,
    'filters': {
        'require_debug_false': {
            '()': 'django.utils.log.RequireDebugFalse',
        },
        'require_debug_true': {
            '()': 'django.utils.log.RequireDebugTrue',
        },
    },
    'formatters': {
        'simple': {
            'format': '[%(asctime)s] %(levelname)s %(message)s',
            'datefmt': '%Y-%m-%d %H:%M:%S'
        },
        'verbose': {
            'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s',
            'datefmt': '%Y-%m-%d %H:%M:%S'
        },
    },
    'handlers': {
        'console': {
            'level': 'DEBUG',
            'filters': ['require_debug_true'],
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'development_logfile': {
            'level': 'DEBUG',
            'filters': ['require_debug_true'],
            'class': 'logging.FileHandler',
            'filename': '/tmp/django_dev.log',
            'formatter': 'verbose'
        },
        'production_logfile': {
            'level': 'ERROR',
            'filters': ['require_debug_false'],
            'class': 'logging.handlers.RotatingFileHandler',
            'filename': '/var/log/django/django_production.log',
            'maxBytes' : 1024*1024*100, # 100MB
            'backupCount' : 5,
            'formatter': 'simple'
        },
        'dba_logfile': {
            'level': 'DEBUG',
            'filters': ['require_debug_false','require_debug_true'],
            'class': 'logging.handlers.WatchedFileHandler',
            'filename': '/var/log/dba/django_dba.log',
            'formatter': 'simple'
        },
    },
    'root': {
        'level': 'DEBUG',
        'handlers': ['console'],
    },
    'loggers': {
        'coffeehouse': {
            'handlers': ['development_logfile','production_logfile'],
         },
        'dba': {
            'handlers': ['dba_logfile'],
        },
        'django': {
            'handlers': ['development_logfile','production_logfile'],
        },
        'py.warnings': {
            'handlers': ['development_logfile'],
        },
    }
}
Listing 5-18.Custom LOGGING Django configuration

Caution

使用日志文件时,请确保目标文件夹存在(例如/var/log/dba/),并且 Django 进程的所有者拥有文件访问权限。

禁用默认 Django 日志记录配置

清单 5-18 顶部的' disable_existing_loggers' :True语句禁用清单 5-15 中 Django 的默认日志配置。这保证了没有默认的日志行为被应用到 Django 项目中。

禁用 Django 默认日志记录行为的另一种方法是在单个基础上覆盖默认日志记录定义,因为settings.py中的任何显式LOGGING配置优先于 Django 默认配置,即使在'disable_existing_loggers':False时也是如此。例如,要对console记录器应用不同的行为(例如,输出debug级别的消息,而不是默认的info级别),您可以在settings.py中为具有debug级别的console定义一个处理程序——如清单 5-18 所示。

但是,如果您想确保没有默认的日志配置意外地出现在 Django 项目中,您必须将'disable_existing_loggers'设置为True。因为清单 5-18 设置了'disable_existing_loggers':True,注意清单 5-15 中相同的默认过滤器被重新声明,因为默认过滤器由于'disable_existing_loggers':True.而丢失

日志格式化程序:消息输出

默认情况下,Django 没有定义一个日志记录formatters部分,您可以在清单 5-15 中确认。然而,清单 5-18 声明了一个formatters部分来生成带有更简单或更详细输出的日志消息。

默认情况下,所有 Python 日志消息都遵循格式%(levelname)s:%(name)s:%(message)s,这意味着“输出日志消息级别,后跟日志记录器的名称和日志消息本身。”

然而,通过 Python 日志记录可以获得更多的信息,从而使日志消息更加全面。正如您在清单 5-18 中看到的,simpleverbose格式化程序使用了一种特殊的语法和一系列不同于默认的字段。表 5-2 说明了不同的 Python 格式化程序字段,包括它们的语法和含义。

表 5-2。

Python logging formatter fields

| 字段语法 | 描述 | | --- | --- | | %(名称)s | 记录器的名称(记录通道) | | %(级别)s | 消息的数字日志记录级别(调试、信息、警告、错误、严重) | | %(levelname)s | 消息的文本日志记录级别(“调试”、“信息”、“警告”、“错误”、“严重”) | | %(路径名)s | 发出日志记录调用的源文件的完整路径名(如果可用) | | %(文件名)s | 路径名的文件名部分 | | %(模块)s | 模块(文件名的名称部分) | | %(lineno)d | 发出日志记录调用的源代码行号(如果可用) | | %(funcName)s | 函数名 | | %(已创建)f | 创建日志记录的时间(time.time()返回值) | | %(asctime)s | 创建日志记录的文本时间 | | %(毫秒)d | 创建时间的毫秒部分 | | %(相对创建的)d | 创建日志记录的时间(以毫秒为单位),相对于加载日志记录模块的时间(通常在应用启动时) | | %(线程)d | 线程 ID(如果可用) | | %(线程名称) | 线程名称(如果可用) | | %(流程)d | 流程 ID(如果可用) | | %(消息)s | record.getMessage()的结果,在发出记录时进行计算 |

您可以根据表 5-2 中的字段向每个formatterformat字段添加或删除字段。除了每个formatterformat字段之外,还有一个datefmt字段,允许您定制formatter%(asctime)s格式字段的输出(例如,当datefmt字段设置为%Y-%m-%d %H:%M:%S时,如果在 2018 年午夜出现日志消息,%(asctime)输出 2018-01-01 00:00:00)。

Note

datefmt字段的语法遵循 Python 的 strftime()格式。 2

日志处理程序:位置、类、过滤器和日志阈值

清单 5-18 中的第一个处理程序是console处理程序,它提供了默认console处理程序清单 5-15 的定制行为。清单 5-18 中的console处理程序将日志级别提升到DEBUG级别,以处理所有日志消息,而不考虑它们的级别。此外,console处理程序使用定制的simple格式化程序——在上一节中已经介绍过——并使用相同的默认console过滤器和类,它告诉 Django 在DEBUG=True(即'filters': ['require_debug_true'])时处理日志消息,并将日志输出发送到一个流(即'class': 'logging.StreamHandler')。

在清单 5-18 中,您还可以看到其余每个处理程序都有三个不同的class值:logging.FileHandler,它将日志消息发送到一个标准文件;logging.handlers.RotatingFileHandler,向根据给定阈值大小变化的文件发送日志消息;以及logging.handlers.WatchedFileHandler,它将日志消息发送到由第三方工具管理的文件中(例如,logrotate)。

development_logfile处理程序被配置为处理比DEBUG(包含)更差的日志消息——从技术上讲是所有日志消息——并且由于require_debug_true过滤器,只有当DEBUG=True时才工作。此外,development_logfile处理程序被设置为使用自定义的verbose格式化程序,并将输出发送到/tmp/django_dev.log文件。

production_logfile处理程序被配置为处理比ERROR(含)更差的日志消息——这只是ERRORCRITICAL日志消息——并且由于require_debug_false过滤器,只有当DEBUG=False时才工作。此外,该处理程序使用自定义的simple格式化程序,并被设置为将输出发送到文件/var/log/django_production.log。每当日志文件达到 100 MB(即maxBytes)时,日志文件就会旋转,旧的日志文件会通过附加一个数字(例如django_production.log.1,django_production.log.2)备份到backupCount

由于使用了require_debug_truerequire_debug_false过滤器,dba_logfile被配置为处理比DEBUG(包括 ??)更差的日志消息——从技术上讲,?? 是所有日志消息——以及当DEBUG=TrueDEBUG=False出现时。此外,该处理程序使用自定义的simple格式化程序,并被设置为将输出发送到文件/var/log/django_dba.log

dba_logfile处理程序由WatchedFileHandler类管理,它比development_logfile处理程序使用的基本FileHandler类功能多一点。WatchedFileHandler类被设计用来检查一个文件是否改变,如果它改变了一个文件被重新打开;这反过来允许日志文件由 logrotate 之类的 Linux 日志实用程序管理/更改。像 logrotate 这样的日志实用程序的好处是,它允许 Django 使用更复杂的日志文件特性(例如,压缩、日期旋转)。注意,如果不使用像 logrotate 这样的第三方工具来管理使用WatchedFileHandler的日志文件,日志文件会无限增长。

Caution/Tip

清单 5-18 中的 RotatingFileHandler 日志处理类对于多进程应用来说是不安全的。使用 ConcurrentLogHandler 日志处理程序类 3 在多进程应用上运行。

Tip

核心 Python 日志包包括许多其他日志处理程序类,用于处理 Unix 系统日志、电子邮件(SMTP)和 HTTP 之类的消息。 4

日志记录器:使用日志记录的 Python 包

清单 5-18 中的loggers部分定义了附加到 Python 包的处理程序——从技术上来说,附件是附加到记录器名称上的,但是我使用这个术语是因为记录器通常以 Python 包命名。我将很快提供这个“Python 包=记录器名称”的一个例外,这样您可以更好地理解这个概念。

第一个日志记录器coffeehouse告诉 Django 将自己及其子节点(例如coffeehouse.aboutcoffeehouse.about.viewscoffeehouse.drinks)的所有日志消息附加到development_logfileproduction_logfile处理程序。通过分配两个处理程序,来自coffeehouse记录器(及其子进程)的日志消息被发送到两个地方。

回想一下,通过使用 Python 的__name__语法来定义记录器——参见清单 5-16 和 5-17——记录器的名称最终基于 Python 包结构。

接下来,您可以看到dba记录器将其所有日志消息链接到dba_logfile处理程序。在这种情况下,记录器以 Python 包命名的规则是一个例外。正如你在清单 5-17 中看到的,一个日志记录器可以被特意命名为dba,而放弃使用__name__或其他与 Python 包相关的约定。

接下来,djangopy.warnings记录器被重新声明以获得 Django 的一些默认行为,假设清单 5-18 使用了'disable_existing_loggers': Truedjango日志记录器将其所有日志消息链接到development_logfileproduction_logfile处理程序,因为我们希望与django包/日志记录器及其子项(例如django.requestdjango.security)相关的日志消息进入两个日志文件。

注意清单 5-18 没有为django.requestdjango.security声明显式的记录器,不像清单 5-15 中的 Django 默认值。因为django记录器自动处理其子代,我们不需要每个记录器有不同的处理程序——就像默认的日志行为——清单 5-18 只声明了django记录器。

在清单 5-18 的末尾,py.warnings记录器将其所有日志消息链接到development_logfile处理器,以避免py.warnings日志消息在生产日志中留下任何痕迹。

最后,清单 5-18 中有一个root键,虽然它是在loggers部分之外声明的,但实际上是所有记录器的根记录器。root键告诉 Django 处理来自所有记录器的消息——无论在配置中是否声明——并以给定的方式处理它们。在这种情况下,root告诉 Django 所有日志消息——因为DEBUG级别包括所有消息——由任何日志记录器(coffeehouse, dba, django, py.warnings或任何其他)生成,由console处理程序处理。

出错时禁止向管理员发送电子邮件

您可能会感到惊讶,清单 5-18 没有使用清单 5-15 中默认定义的mail_admins处理程序。正如我在上一节关于 Django 默认日志记录中提到的,对于由django.requestdjango.security包/记录器生成的日志消息,mail_admins处理程序发送一个电子邮件错误通知。

虽然这看起来是一个令人惊奇的特性——避免了查看日志文件的麻烦——但是一旦项目开始增长,它就会变得非常不方便。默认日志电子邮件错误通知机制或mail_admins处理程序的问题是,每当触发与django.requestdjango.security软件包/日志程序相关的错误时,它都会发送一封电子邮件。

如果一个 Django 站点每小时有 100 个访问者,并且他们都遇到了同样的错误,这意味着在同一小时内发送了 100 封电子邮件通知。如果你在ADMINS有 3 个人,那么就意味着每小时至少有 300 封邮件通知。所有这些很快就会累积起来,所以几个不同的错误和一天几千个访问者会导致电子邮件超载。因此,尽管获取电子邮件日志错误通知看起来很方便,但您应该关闭此功能。

我建议您坚持使用老方法来持续检查日志文件,或者如果您需要通过电子邮件提供相同的实时日志错误通知,您可以使用专用的报告系统,如 Sentry,这将在下一节中介绍。

哨兵伐木

尽管日志记录是一种强大的发现机制,但检查和理解日志消息可能是一项艰巨的任务。Django 和 Python 项目在这方面没有什么不同。如前一节所述,依赖核心日志记录包仍然会导致日志消息被发送到应用控制台或文件,其中理解日志消息(例如,最相关或最常见的日志消息)可能会导致数小时的分析。

进入 Sentry,一个报告和聚合应用。Sentry 通过一个基于 web 的界面方便了日志消息的检查,在这里您可以快速确定最相关和最常见的日志消息。

要使用 Sentry,您需要遵循两个步骤:设置 Sentry 接收您的项目日志消息,并设置您的 Django 项目向 Sentry 发送日志消息。

Why Sentry?

尽管有替代产品提供与 Sentry 类似的日志监控功能(例如 OverOps、Airbrake、Raygun),但让 Sentry 与众不同的是它是作为 Django 应用构建的!

尽管 Sentry 已经发展到了非常复杂的 Django 应用的地步,但 Sentry 是基于 Django 的开源项目这一事实使得它几乎成为监控 Django 项目的自然选择,因为您可以使用您的 Django 知识来安装和扩展它——尽管有软件即服务 Sentry 的替代方案。

设置岗哨的申请

您可以通过两种方式设置 Sentry:自己安装或使用软件即服务 Sentry 提供商。

Sentry 是一个 Django 开源项目,因此所有人都可以免费获得完整的源代码。 5 但是在你直接下载 Sentry 并继续安装之前, 6 当心 Sentry 已经从它的 Django 根成长了相当大。Sentry 现在需要一个 Docker 环境、关系 Postgres 数据库和 NoSQL Redis 数据库。不幸的是,Sentry 发展到支持各种各样的编程语言和平台,它变得越来越复杂,不再是一个简单的 Django 应用安装。因此,如果你从头开始安装 Sentry,预计要花几个小时来设置它。

Tip

早期的 Sentry 版本(例如,v. 5.0 7 )没有这样严格的依赖关系,可以像基本的 Django 应用一样运行(例如,任何 Django 关系数据库,没有 Docker,没有 NoSQL 数据库)。对于简单的哨兵安装来说,它们是一个很好的选择,尽管它们需要过时的 Django 版本(例如 v. Django 1.4)。

哨兵创建者和维护者运行软件即服务: https://sentry.io 。Sentry 软件即服务提供三种不同的计划:爱好者计划,每月免费参加多达 10,000 次活动,专为一个用户设计;每月 12 美元的专业计划,从每月 50,000 次活动开始,为无限用户设计;以及针对数百万事件和无限用户的定制定价的企业计划。

由于你可以在几分钟内免费设置 Sentry,只需你的电子邮件——无需信用卡——来自 https://sentry.io 的 Sentry 软件即服务是试用 Sentry 的一个好选择。即使在试用后,每月 50,000 次活动的成本为 12 美元,每次额外活动的成本为 0.00034 美元,这也是一个很好的价值-考虑到一个每月生成 50,000 次活动的应用应该有相当多的受众来证明这个价格是合理的。

一旦你创建了一个 sentry.io 帐户,你将进入主仪表板。创建新的 Django 项目。注意客户端密钥或 DSN,它是一个包含@sentry.io 片段的长 url 这是配置 Django 项目向这个特定的 Sentry Django 项目发送日志消息所必需的。如果您错过了项目客户端密钥或 DSN,请点击图 5-1 中所示的右上角“项目设置”按钮,并选择左下角选项“客户端密钥(DSN)”以查看该值。

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

图 5-1。

Sentry SaaS project dashboard

正如你在图 5-1 中所看到的,哨兵 SaaS 项目仪表板作为一个中央存储库来查阅所有的项目日志记录活动。在图 5-1 中,您还可以看到各种操作按钮,这些按钮允许不同用户对日志消息进行分类、搜索、绘制图表以及管理。所有这些创建了一个非常高效的环境,可以在其中实时分析任何 Django 日志记录活动。

一旦设置了 Sentry,就可以配置 Django 项目向 Django 项目发送日志消息。

设置 Django 应用来使用 Sentry

要在 Django 项目中使用 Sentry,您需要一个名为 Raven 的包来建立双方之间的通信。简单地做pip install raven来安装最新的 Raven 版本。

一旦安装了 Raven,就必须在项目的settings.py文件的INSTALLED_APPS列表中声明它,如清单 5-19 所示。此外,还需要通过清单 5-19 中显示的RAVEN_CONFIG变量,配置 Raven 通过 DSN 值与特定的 Sentry 项目通信。

INSTALLED_APPS = [
    ...
    'raven.contrib.django.raven_compat',
    ...
]

RAVEN_CONFIG = {
    'dsn': '<your_dsn_value>@sentry.io/<your_dsn_value>',
}

Listing 5-19.Django project configuration to communicate

with Sentry via Raven

正如您在清单 5-19 中所看到的,RAVEN_CONFIG变量应该声明一个dsn键,其值对应于 Sentry 项目中接收日志消息的 DSN 值。

在设置了这个最低限度的 Raven 配置之后,您可以从 Django 项目的命令行发送运行python manage.py raven test命令的测试消息。如果测试成功,您将在如图 5-1 所示的 Sentry 仪表盘中看到一条测试消息。

一旦您确认 Django 项目和 Sentry 之间的通信成功,您就可以设置 Django 日志来向 Sentry 发送日志消息。对于 Django 的日志记录机制,Sentry 被视为任何其他处理程序(例如文件、流),因此您必须首先使用raven.contrib.django.handlers.SentryHandler类将 Sentry 声明为日志记录处理程序,如清单 5-20 所示。

LOGGING = {
...
'handlers': {
       ....
        'sentry': {
            'level': 'ERROR',
            'class': 'raven.contrib.django.handlers.SentryHandler',
        },
       ...
  }
Listing 5-20.Django logging handler

for Sentry/Raven

清单 5-20 中的sentry处理程序告诉 Django 通过 Sentry 处理具有ERROR级别的日志消息。一旦有了 Sentry 处理程序,最后一步是使用 loggers 上的sentry处理程序来分配哪些包/loggers 通过 Sentry 处理(例如,django.requestroot logger,如前面的“日志记录器”一节所述)。

Django 电子邮件服务

电子邮件已经成为生活在网络上的几乎所有应用的主要部分。无论应用是否需要发送电子邮件来注册、通知或确认购买,很难想象 web 应用不需要某种电子邮件功能。

对于 Django 项目,设置电子邮件有两个主要方面。第一步是建立与电子邮件服务器的连接,第二步是撰写电子邮件。

设置到电子邮件服务器的默认连接

Django 支持连接到任何电子邮件服务器,还提供了各种选项来模拟电子邮件服务器连接。电子邮件模拟在开发和测试期间尤其强大,因为在这期间发送真实的电子邮件是不必要的。Django 电子邮件服务器的设置在settings.py中完成。根据电子邮件服务器的连接,您可能需要在settings.py中设置几个变量。表 5-3 显示了 Django 的各种电子邮件服务器选项。

表 5-3。

Django email server configurations

| Django 电子邮件后端 | 配置 | 描述/注释 | | --- | --- | --- | | 用于开发(调试=真) | | 控制台电子邮件 | EMAIL _ back end = ' django . core . mail . back ends . console . EMAIL back end ' | 将所有电子邮件输出发送到运行 Django 的控制台。 | | 文件电子邮件 | ' EMAIL _ back end = ' django . core . mail . backends . FILE based . EMAIL back end ' EMAIL _ FILE _ PATH = '/tmp/django-EMAIL-dev ' | 将所有电子邮件输出发送到电子邮件文件路径中指定的平面文件。 | | 内存电子邮件 | EMAIL _ back end = ' django . core . mail . backends . locmem . EMAIL back end ' | 将所有电子邮件输出发送到 django.core.mail.outbox 中的内存属性。 | | 取消电子邮件 | EMAIL _ back end = ' django . core . mail . backends . dummy . EMAIL back end ' | 不处理所有电子邮件输出。 | | Python 电子邮件服务器模拟器 | EMAIL _ back end = ' django . core . mail . backends . SMTP . EMAIL back end ' EMAIL _ HOST = 127 . 0 . 0 . 1 EMAIL _ PORT = 2525 还需要 Python 命令行电子邮件服务器:Python-m smtpd-n-c debuggings server localhost:2525 | 将所有电子邮件输出发送到通过命令行设置的 Python 电子邮件服务器。这类似于控制台电子邮件选项,因为 Python 电子邮件服务器将内容输出到控制台。 | | 用于生产(调试=假) | | SMTP 电子邮件服务器(标准) | EMAIL _ back end = ' django . core . mail . backends . SMTP . EMAIL back ends ' 1EMAIL _ HOST = 127 . 0 . 0 . 1 1EMAIL _ PORT = 25 2EMAIL _ HOST _ USER = 2EMAIL _ HOST _ PASSWORD = | 将所有电子邮件输出发送到 SMTP 电子邮件服务器。 | | SMTP 电子邮件服务器( * Secure-TLS) | EMAIL _ back end = ' django . core . mail . backends . SMTP . EMAIL back ends ' 1EMAIL _ HOST = 127 . 0 . 0 . 1 1EMAIL _ PORT = 587 2EMAIL _ HOST _ USER = 2EMAIL _ HOST _ PASSWORD =EMAIL _ USE _ TLS = True | 将所有电子邮件输出发送到安全的 SMTP (TLS)电子邮件服务器。 | | SMTP 电子邮件服务器( * 安全-SSL) | EMAIL _ back end = ' django . core . mail . backends . SMTP . EMAIL back ends ' 1EMAIL _ HOST = 127 . 0 . 0 . 1 1EMAIL _ PORT = 465 2EMAIL _ HOST _ USER = 2EMAIL _ HOST _ PASSWORD =EMAIL _ USE _ SSL = True | 将所有电子邮件输出发送到安全的 SMTP (SSL)电子邮件服务器。 |

1 If the SMTP email server is running on a network or a different port than the default, adjust EMAIL_HOST and EMAIL_PORT accordingly. 2 In today’s email, spam-infested Internet, nearly all SMTP email servers require authentication to send email. If your SMTP server doesn’t require authentication you can omit EMAIL_HOST_USER and EMAIL_HOST_PASSWORD. * The terms SSL and TLS are often used interchangeably or in conjunction with each other (TLS/SSL). There are differences, though, in terms of their underlying protocol. From a Django setup prescriptive, you only need to ensure what type of secure email server you connect to, as they operate differently and on different ports.

您在settings.py中的表格 5-3 中设置的任何电子邮件连接都被视为 Django 项目的默认连接,并在执行任何与电子邮件相关的任务时使用——除非您在执行电子邮件任务时另外指定。

设置到第三方电子邮件提供商的默认连接

上一节提供了在 Django 中设置到电子邮件服务器的默认连接的最通用的方法。然而,由于当今世界运行电子邮件服务器的复杂性——即垃圾邮件过滤和安全问题——使用第三方服务将来自 Django 项目的电子邮件转发给外界可能会更容易、更实用。

尽管您可以使用上一节的配置连接到任何第三方电子邮件服务,但是设置第三方电子邮件服务的配置还是有一些微妙之处。在这一节中,我将提供我认为最流行的三种第三方电子邮件服务的 Django 配置细节。

Django with Exim, Postfix, or Sendmail

虽然您可以设置 Django 将电子邮件发送到本地电子邮件应用(即运行在 127.0.0.1 上),如 Exim、Postfix 或 Sendmail,然后这些应用将电子邮件发送到第三方提供商。我个人不推荐这种替代方案,因为它增加了另一个组件来设置、维护和担心。更不用说这超出了 Django 的范围,因为它涉及到用第三方电子邮件服务设置不同的电子邮件应用。

以下部分描述了如何设置 Django 直接与第三方电子邮件提供商连接。

使用 Google Gmail/Google Apps 发送电子邮件

谷歌提供通过 Gmail 或谷歌应用发送电子邮件的能力,最后一个是用于定制域(如 coffeehouse.com)的 Gmail 版本。一旦你有了 Gmail 或 Google Apps 帐户,你需要在settings.py中设置帐户的用户名/密码凭证。

如果不在你的应用中的某个地方硬编码你的账户凭证,你将无法使用谷歌的电子邮件服务。如果您厌倦了对settings.py中的用户名/密码凭证进行硬编码,我建议您为此创建一个单独的帐户来限制漏洞,考虑使用 Django 的多个环境或配置文件来将用户名/密码保存在不同的文件中,或者使用前一个侧栏中描述的凭证设置一个本地电子邮件服务器。

清单 5-21 展示了设置 Django 通过 Gmail 或 Google Apps 账户发送电子邮件所需的配置。

EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST='smtp.gmail.com'
EMAIL_PORT=587
EMAIL_HOST_USER='username@gmail.com/OR/username@coffeehouse.com'
EMAIL_HOST_PASSWORD='password'
EMAIL_USE_TLS=True
Listing 5-21.Django email configuration for Gmail or Google Apps account

正如你在清单 5-21 中看到的,配置参数与表 5-3 中描述的非常相似。这就是在 Django 中设置 Gmail 或 Google Apps 的默认电子邮件连接所需的全部内容。

Caution

当心用谷歌发太多邮件。由于谷歌的电子邮件服务是免费的,它不是为转发太多电子邮件而设计的。如果你的 Django 应用每小时发送几封电子邮件,你可能不会有问题,但如果你的应用每秒发送一封电子邮件,或者在几分钟内发送数百封电子邮件,该帐户可能会被封。如果帐户被阻止,您将需要等待几个小时或手动登录帐户(例如,通过浏览器)以解除阻止。如果该帐户由于您发送的电子邮件数量而不断被阻止,您应该尝试另一个电子邮件服务提供商。

Note

Google 会用 Google 帐户值覆盖 From: email 字段,除非它是作为别名添加的。Django 允许您将电子邮件的 From:字段设置为您想要的任何值,并默认为 settings.py 中的 EMAIL_HOST_USER 值。但是,为了避免欺骗,如果 From: email 值不是 Gmail 或 Google App 帐户中的别名,Google 会将该字段覆盖到 Google 帐户电子邮件中。这意味着,如果你在 Django 中发送一封发件人:support@coffeehouse.com 的电子邮件,并且该电子邮件没有在 Gmail 或谷歌应用帐户中设置为别名,则最终的电子邮件会显示为谷歌帐户的主电子邮件。

使用亚马逊简单电子邮件服务(SES)发送电子邮件

SES 是由 Amazon.com 运营的 AWS 提供的另一项电子邮件服务。与谷歌的电子邮件服务不同,SES 是一项付费服务,每封电子邮件的平均成本为 0.0001 美分(每 1000 封电子邮件 10 美分)。用 SES 设置 Django 最简单的方法是通过 Python 库 boto 和一个名为 django-ses 的自定义 Django 电子邮件后端。

清单 5-22 说明了安装 boto 的 pip 要求,boto 是一个使用 Python 和 django-ses 集成多个 AWS 服务的库,django-ses 是一个开源项目,专门设计用于使用 Django 运行 SES。

pip install boto
pip install django-ses
Listing 5-22.Python pip requirements for Amazon.com SES with Django

一旦您使用 pip 安装了清单 5-22 中的 Python 包,您就可以继续在settings.py中配置 SES。清单 5-23 展示了设置 Django 使用 SES 的必要变量。

EMAIL_BACKEND = 'django_ses.SESBackend'
AWS_ACCESS_KEY_ID = 'FZINISSZ3542DPIO32CQ'
AWS_SECRET_ACCESS_KEY = '3Nto4vknl+xeZR+1tF3L645EUyOS+zZy/uPJ1rN'
Listing 5-23.Django email configuration for Amazon.com SES

正如您在清单 5-23 中看到的,变量EMAIL_BACKEND被设置为自定义类django_ses.SESBackend,它提供了连接 SES 所需的所有钩子。

要连接到 SES,您还需要提供变量AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY,这是与您的 AWS 帐户相关的访问凭证。这些最后的值在您的 AWS 帐户中提供… 8

这就是你需要设置一个默认的电子邮件连接到亚马逊简单电子邮件服务(SES)的全部内容。不需要在settings.py中设置任何其他变量,例如EMAIL_HOSTEMAIL_HOST_USER——一切都由自定义电子邮件后端处理。

带有 SparkPost 的电子邮件

SparkPost 是 Twitter、Oracle 和 PayPal 等大公司使用的另一种第三方电子邮件服务。Pricing wise SparkPost 是前两种服务的混合体;每月前 100,000 封电子邮件是免费服务,但在此之后,这是一项付费服务,平均每封电子邮件的费用为 0.0002 美分(每月接下来的 1000 封电子邮件为 20 美分),一旦每月发送 100 万封电子邮件,每封电子邮件的费用会更低。

用 SparkPost 设置 Django 最简单的方法是直接在清单settings.py.中 5-24 展示了设置 Django 使用 SparkPost 的必要变量。

EMAIL_BACKEND='django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.sparkpostmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = 'SMTP_Injection'
EMAIL_HOST_PASSWORD = '<sparkpost_api_key>'
EMAIL_USE_TLS = True
Listing 5-24.Django email configuration for SparkPost

正如您在清单 5-24 中看到的,配置参数与表 5-3 中描述的标准电子邮件连接非常相似。请注意,除了EMAIL_HOST_USER值为SMTP_Injection——SparkPost 的要求——您还需要为EMAIL_HOST_PASSWORD分配一个 SparkPost API 键。SparkPost API 密钥是在您的 SparkPost 帐户中创建的。 9

既然您已经了解了在 Django 项目中建立电子邮件连接的各种方法,那么让我们来研究一下电子邮件的实际组成。

内置助手发送电子邮件

发送电子邮件可能涉及许多选项和步骤。为了简化这个过程,Django 提供了四种快捷方法,您可以在应用的任何地方使用(例如,注册时、购买时、出现严重错误时)。表 5-4 说明了各种电子邮件快捷方式。

表 5-4。

Django email shortcut methods

| 快捷方式和描述 | 带所有参数的快捷方式* | 参数描述和注释 | | --- | --- | --- | | send_mail 是发送电子邮件最常见的选项。 | send_mail(主题,消息,from _ email =设置。DEFAULT_FROM_EMAIL,recipient_list,fail _ silently = False,auth_user=None,auth_password=None,connection=None,html_message=None) | 主题。-电子邮件主题字符串。消息。-电子邮件消息字符串。from_email。-电子邮件发件人:字段。如果没有提供,它会从 settings.py 设置为 DEFAULT_FROM_EMAIL,默认情况下是 webmaster@localhost。收件人 _ 列表。-字符串列表形式的电子邮件收件人。无声地失败。-能够在无法发送电子邮件时绕过错误。默认情况下设置为 False,这意味着任何试图发送电子邮件的错误都会引发一个 smtplib。SMTPException 异常。auth_user。SMTP 服务器的身份验证用户。如果提供,它将覆盖 settings.py. auth_password 中的变量 EMAIL_HOST_USER。SMTP 服务器的身份验证密码。如果提供,它将覆盖 settings.py. connection 中的变量 EMAIL_HOST_PASSWORD。- Django 电子邮件后端发送邮件。如果提供,它会覆盖 settings.py 中的变量 EMAIL_BACKEND。有关选项,请参见表 5-3 。html_message。-发送 HTML 和文本电子邮件消息的 HTML 字符串。如果提供,生成的电子邮件是多部分/替代电子邮件,其中 message 为文本/纯文本内容类型,html_message 为文本/html 内容类型。 | | send_mass_mail 比 send_mail 方法效率更高。这是发送多封电子邮件时的首选,因为它会打开到电子邮件服务器的单个连接,并发送元组中包含的所有消息。请注意,send_mass_mail 不支持 send_mail 这样的 HTML 消息。 | send_mass_mail(datatuple,fail _ silently = False,auth_user=None,auth_password=None,connection=None) | 数据元组。-是一个元组,包含以(subject,message,from_email=settings)形式表示电子邮件结构的元组。DEFAULT_FROM_EMAIL,recipient_list)。 | | mail_admins 向 settings.py 的 admins 变量中定义的所有用户发送电子邮件。 | mail_admins(subject,message,fail _ silently = False,connection=None,html_message=None) | 发送电子邮件时,会将 From:字段设置为 settings.py 中的变量 SERVER_EMAIL,默认情况下是 root@localhost。电子邮件主题的前缀是 settings.py 中的变量 EMAIL_SUBJECT_PREFIX,默认情况下是'[Django]'。 | | mail_managers 向 settings.py 的 managers 变量中定义的所有用户发送电子邮件。 | mail_managers(subject,message,fail _ silently = False,connection=None,html_message=None) | 发送电子邮件时,会将 From:字段设置为 settings.py 中的变量 SERVER_EMAIL,默认情况下是 root@localhost。电子邮件主题的前缀是 settings.py 中的变量 EMAIL_SUBJECT_PREFIX,默认情况下是'[Django]'。 |
  • Method arguments without a default value (e.g. subject,message) must always be provided. Method arguments with a default value (e.g. fail_silently=False, connection=None) are optional. Note

如果一旦项目进入生产阶段,你就开始收到带有错误信息的电子邮件(例如,DEBUG=False),这是因为 mail_admins 快捷方式是自动连接的。这是由于 Django 的默认日志工作方式造成的。要禁用此行为,您需要清除 settings.py 中 ADMINS 的所有值,或者覆盖默认的日志记录行为,如上一节日志记录中所述。

自定义电子邮件:电子邮件的附件、标题、抄送、密件抄送等

虽然以前的电子邮件快捷方式可以在大多数情况下使用,但它们不支持附件、抄送、密件抄送或其他电子邮件标题。如果你想完全控制在 Django 中发送电子邮件,前面的快捷方式是行不通的。

Django EmailMessage类被以前的 Django 快捷方法“秘密”使用,并为在 Django 中发送电子邮件提供了最大的灵活性。表 5-5 中描述了EmailMessage类支持的各种参数和方法。

表 5-5。

Django EmailMessage class parameters and methods

| 参数和/或方法 | 描述 | | --- | --- | | 科目 | 电子邮件的主题行。 | | 身体 | 作为纯文本消息的正文文本。 | | 发件人电子邮件 | 发件人的地址。普通电子邮件(如`webmaster@coffeehouse.com`)和全名加电子邮件(如`Webmaster

对表 5-5 中的EmailMessage类提供的功能有了一个清晰的概念,让我们来看一些你会使用 EmailMessage 类发送电子邮件的典型案例。

清单 5-25 提供了一个基本的电子邮件示例,它使用了像 CC、BCC 这样的选项。和回复头,上一节中的 Django 电子邮件快捷方式不支持它们。

from django.core.mail.message import EmailMessage

# Build message
email = EmailMessage(subject='Coffeehouse specials', body='We would like to let you know about this week\'s specials....', from_email='stores@coffeehouse.com',
            to=['ilovecoffee@hotmail.com', 'officemgr@startups.com'], bcc=['marketing@coffeehouse.com'], cc=['ceo@coffeehouse.com']
            headers = {'Reply-To': 'support@coffeehouse.com'})

# Send message with built-in send() method
email.send()

Listing 5-25.Send basic email with EmailMessage class

正如您在清单 5-25 中看到的,EmailMessage 实例是通过指定它的各种类参数创建的。一旦完成,您只需调用send()方法来发送电子邮件。就这么简单。因为在清单 5-25 中的EmailMessage实例中没有提供连接值,Django 使用在settings.py中定义的默认后端连接。

EmailMessage send()方法的一个缺点是,每次调用它时,它都会打开一个到电子邮件服务器的连接。如果你一次发送数百或数千封电子邮件,效率会很低。根据上一节的send_mass_mail()快捷方式的精神,也可以用EmailMessage打开一个到邮件服务器的连接,发送多封邮件。清单 5-26 展示了如何通过EmailMessage使用单个连接发送多个电子邮件。

from django.core import mail
connection = mail.get_connection()

# Manually open the connection
connection.open()

# Build message
email = EmailMessage(subject='Coffeehouse specials', body='We would like to let you know about this week\'s specials....', from_email='stores@coffeehouse.com',
            to=['ilovecoffee@hotmail.com', 'officemgr@startups.com'], bcc=['marketing@coffeehouse.com'], cc=['ceo@coffeehouse.com']
            headers = {'Reply-To': 'support@coffeehouse.com'})
# Build message
email2 = EmailMessage(subject='Coffeehouse coupons', body='New coupons for our best customers....', from_email='stores@coffeehouse.com',
            to=['officemgr@startups.com','food@momandpopshop.com'], bcc=['marketing@coffeehouse.com'], cc=['ceo@coffeehouse.com']
            headers = {'Reply-To': 'support@coffeehouse.com'})

# Send the two emails in a single call
connection.send_messages([email, email2])
# The connection was already open so send_messages() doesn't close it.
# We need to manually close the connection.
connection.close()

Listing 5-26.Send multiple emails in a single connection with EmailMessage class

在清单 5-26 中,第一步是使用mail.get_connection()创建到电子邮件服务器的连接,然后使用open()方法打开连接。接下来,创建各种EmailMessage实例。一旦准备好了电子邮件实例,您就可以调用连接的send_messages()方法,该方法带有一个对应于每个EmailMessage实例的参数列表。最后,一旦发送了电子邮件,您调用连接的close()方法来断开到电子邮件服务器的连接。

另一个常见的电子邮件场景是发送 HTML 电子邮件。Django 为此提供了EmailMultiAlternatives类,它是EmailMessage类的子类。作为一个子类,这意味着你可以利用与EmailMessage相同的功能(例如,抄送、密件抄送),但你不需要做很多工作,因为子类EmailMultiAlternatives是专门为处理多种类型的消息而设计的。清单 5-27 展示了如何使用EmailMultiAlternatives类。

from django.core.mail import EmailMultiAlternatives

subject, from_email, to = 'Important support message', 'support@coffeehouse.com', 'ceo@coffeehouse.com'
text_content = 'This is an important message.'
html_content = '
This is an important message.

'
msg = EmailMultiAlternatives(subject=subject, body=text_content, from_email=from_email, to=[to])
msg.attach_alternative(html_content, "text/html")
msg.send()

Listing 5-27.Send HTML (w/text) emails with EmailMultiAlternatives, a subclass of the EmailMessage class

清单 5-27 首先定义了所有的电子邮件字段,包括电子邮件的文本和 HTML 版本。请注意,拥有文本和 HTML 版本的电子邮件内容是常见的做法,因为不能保证最终用户会允许或能够阅读 HTML 电子邮件,所以提供文本版本作为备份。接下来,定义一个EmailMultiAlternatives类的实例;注意这些参数与那些EmailMessage类的参数是内联的。

接下来,在清单 5-27 中,您可以看到对attach_alternative方法的调用,它特定于EmailMultiAlternatives类。这个方法的第一个参数是 HTML 内容,第二个参数是对应于text/html的内容类型。最后,清单 5-27 调用send()方法——是EmailMessage类的一部分,但也是 to EmailMultiAlternatives的一部分,因为它是一个子类——来发送实际的电子邮件。

在可以保证所有终端用户都能够查看 HTML 电子邮件的受控环境(例如,公司电子邮件)中,只发送电子邮件的 HTML 版本并完全绕过文本版本可能是实用的。在这些情况下,您实际上可以直接使用EmailMesssage类,只需稍加修改。清单 5-28 展示了如何用EmailMessage类发送 HTML 电子邮件。

subject, from_email, to = 'Important support message', 'support@coffeehouse.com', 'ceo@coffeehouse.com'
html_content = '
This is an important message.

'
msg = EmailMessage(subject=subject, body=html_content, from_email=from_email, to=[to])
msg.content_subtype = "html"  # Main content is now text/html
msg.send()

Listing 5-28.Send HTML emails

with EmailMessage class

清单 5-28 看起来像一个标准的EmailMessage过程定义;然而,第四行——msg.content_subtype——是清单 5-28 与众不同的地方。如果发送的 HTML 内容没有行设置msg.content_subtype,最终用户将收到 HTML 内容的一字不差的版本(即,没有呈现 HTML 标签)。这是因为默认情况下,EmailMessage类将内容类型指定为文本。为了切换一个EmailMessage实例的默认内容类型,在第四行调用将content_subtype设置为html。通过这一更改,电子邮件内容类型被设置为 HTML,最终用户能够查看呈现为 HTML 的内容。

Beware of Just Sending Html Email Versions to the Public

虽然发送 HTML 电子邮件版本比发送文本和 HTML 电子邮件版本更快,但如果您不能确定最终用户在哪里阅读他们的电子邮件,这可能会有问题。出于安全原因,某些用户会禁用查看 HTML 电子邮件的功能,某些电子邮件产品也不能或不太擅长呈现 HTML 电子邮件。因此,如果你只是发送一个 HTML 版本的电子邮件,可能会有一部分最终用户无法看到电子邮件的内容。

由于这个原因,如果你发送电子邮件给你无法控制其环境的最终用户(即电子邮件阅读器),最好发送文本和 HTML 电子邮件版本——如清单 5-27 所示——而不是发送清单 5-28 所示的 HTML 电子邮件版本。

发送电子邮件时的另一个常见做法是附加文件。清单 5-29 展示了如何将 PDF 附加到电子邮件中。

from django.core.mail.message import EmailMessage

# Build message
email = EmailMessage(subject='Coffeehouse sales report', body='Attached is sales report....', from_email='stores@coffeehouse.com',
            to=['ceo@coffeehouse.com', 'marketing@coffeehouse.com']
            headers = {'Reply-To': 'sales@coffeehouse.com'})
# Open PDF file
attachment = open('SalesReport.pdf', 'rb')
# Attach PDF file
email.attach('SalesReport.pdf',attachment.read(),'application/pdf')

# Send message with built-in send() method
email.send()

Listing 5-29.Send email with PDF attachment

with EmailMessage class

正如您在清单 5-29 中看到的,在创建了一个EmailMessage实例之后,您只需使用 Python 的标准open()方法打开 PDF 文件。接下来,使用来自EmailMessageattach()方法,该方法有三个参数:文件名、文件内容和文件内容类型或 MIME 类型。最后,调用send()方法来发送邮件。

调试 Django 应用

纠正应用中意外行为的第一步通常是检查您认为有问题的源代码部分和相应的日志。有时,虽然这些审查是没有结果的,要么是因为应用变得越来越复杂,要么是因为不可预料的行为源自不太明显的位置。

在这种情况下,下一步是在工具的帮助下开始调试过程,以便更容易检测和修复问题。在接下来的章节中,我将描述一些调试 Django 应用的最流行的工具。

django Shell:Python manage . py Shell

就像 Python 的 CLI(‘命令行界面’)shell 可以评估表达式(例如,1+3,mystring = 'django '),django 通过python manage.py shell命令提供了自己的 shell 版本——其中manage.py是每个 Django 项目中的顶级文件。

Django 的 shell 非常有用,因为它可以自动加载项目的依赖项和应用,因此您可以评估与 Django 项目相关的表达式(例如查询、方法),而不必经历繁琐的设置过程。清单 5-30 展示了从 Django 的 shell 运行的一系列示例表达式。

[user@coffeehouse ∼]$ python manage.py shell
Python 2.7.3
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from coffeehouse.items.models import *
>>> Drink.objects.filter(item__price__lt=2).filter(caffeine__lt=100).count()
2
>>> from django.test import Client
>>> c = Client()
>>> response = c.get('/stores/1/')
>>> response.content
'<!DOCTYPE html>\n<html....
....
....
<\html>
>>> c.get('/stores/5/')
Not Found: /stores/5/
<HttpResponseNotFound status_code=404, "text/html">
Listing 5-30.Django shell sample expressions

清单 5-30 中的第一个代码片段使用from import语法来访问 Django 项目的模型类,之后对模型进行查询以验证结果。注意不需要导入额外的库或定义数据库连接;所有的依赖项和配置都是从 Django 项目本身加载的。

清单 5-30 中的第二个片段使用 Django 的测试库来模拟客户端/浏览器对/stores/1//stores/5/URL 的请求,之后您可以检查内容响应或 HTTP 状态代码(例如,404 Not Found)。这里再次注意,不需要启动 web 服务器或打开浏览器;您可以从 Django shell 中快速验证 Django 项目的 URL 及其响应。

Django 调试工具栏

与 Django shell 相比,Django 调试工具栏为调试 Django 应用提供了更直观的体验。Django 调试工具栏通过滑动侧边栏提供每页信息,这些信息涉及资源使用(即时间)、Django 设置、HTTP 头、SQL 查询、缓存和日志记录等。图 5-2 和 5-3 显示了 Django 调试工具栏折叠和非折叠的屏幕截图。

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

图 5-3。

Django debug toolbar collapsed

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

图 5-2。

Django debug toolbar hidden

正如您在图 5-2 中所看到的,Django 调试工具栏可以通过每个 Django 项目页面右上角的小标签来访问。图 5-3 展示了 Django 调试工具栏的折叠版本,在这里你可以看到它的各个部分;单击任何部分都会弹出一个窗口,显示每个部分的详细信息。

您可以用pip install django-debug-toolbar命令安装 Django 调试工具栏。一旦安装了django-debug-toolbar,您还需要将debug_toolbar行添加到settings.py中的INSTALLED_APPS变量中,这样 Django 就可以启用工具栏。

Note

Django 调试工具栏只有在项目使用 DEBUG=True 时才起作用。

除了 UI 工具栏,Django 调试工具栏还提供了debugsqlshell实用程序。这个实用程序的工作方式类似于 Django 的标准 shell,但是它输出与任何 Django 模型操作相关联的支持 SQL,如清单 5-31 所示。

[user@coffeehouse ∼]$ python manage.py debugsqlshell
Python 2.7.3
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from coffeehouse.items.models import *
>>> Drink.objects.filter(item__price__lt=2).filter(caffeine__lt=100).count()
SELECT COUNT(*) AS "__count"
FROM "items_drink"
INNER JOIN "items_item" ON ("items_drink"."item_id" = "items_item"."id")
WHERE ("items_item"."price" < 2.0
       AND "items_drink"."caffeine" < 100) [0.54ms]
Listing 5-31.Django debugsqlshell

sample expressions

Note

debugsqlshell 是 Django 调试工具栏的一部分;因此,必须按照前面段落中的描述进行安装(例如,pip install django-debug-toolbar并作为debug_toolbar添加到settings.py中的INSTALLED_APPS变量)。

正如您在清单 5-31 中看到的,debugsqlshell实用程序可以通过manage.py命令获得——就像 Django 的内置 shell 一样——并且在您运行 Django 模型操作之后,它还会输出操作的 SQL 查询。

关于定制 Django 调试工具栏的详细信息,请参阅其官方文档。10

Django pdb

pdb 是“Python Debugger”的缩写,是一个 Python 核心包,用于交互式调试源代码。使用 Python pdb,您可以逐行检查任何 Python 应用的执行情况。为了在 Django 应用的上下文中简化 Python pdb 的过程(例如,调试请求方法),您可以使用 Django pdb 包。

要安装 Python pdb,运行pip install django-pdb,然后在第一个位置将django_pdb添加到settings.py中的INSTALLED_APPS变量——这个位置很重要,这样其他 Django 应用就不会覆盖 Django pdb 的行为(例如,覆盖 runserver 和 test 命令)。请注意 Django pdb 包仅在DEBUG=True时有效。

用 Django 运行 pdb 有多种方法;最简单的方法是将?pdb参数附加到您想用 pdb 分析的任何 Django URL 上。例如,清单 5-32 显示了http://localhost:8000/drinks/mocha/?pdb URL 的调试序列。

[user@coffeehouse ∼]$ python manage.py runserver
INFO "GET /drinks/mocha/ HTTP/1.1" 200 11716
GET /drinks/mocha/?pdb
function "detail" in drinks/views.py:8
args: ()
kwargs: {'drink_type': u'mocha'}
()
> /python/djangodev/local/lib/python2.7/site-packages/django/core/handlers/base.py(79)make_view_atomic()
-> non_atomic_requests = getattr(view, '_non_atomic_requests', set())
(Pdb) n
> /python/djangodev/local/lib/python2.7/site-packages/django/core/handlers/base.py(80)make_view_atomic()
-> for db in connections.all():
...
...
...
--Call--
> /www/code/djangorecipes/5_django_settings/coffeehouse/drinks/views.py(8)detail()
-> def detail(request,drink_type):
(Pdb)
> /www/code/djangorecipes/5_django_settings/coffeehouse/drinks/views.py(9)detail()
(Pdb) c
Listing 5-32.Django pdb sequence

您可以看到清单 5-32 从 Django 的内置 web 服务器开始,并立即接收和发送对常规 URL /drinks/mocha/的响应。到目前为止,一切都是标准的;但是,请注意对 URL /drinks/mocha/?pdb的下一个请求以及随后的详细输出。

详细输出告诉您请求进入应用的位置,包括参数,以及在django.core.handlers.base.py包中进入 Django 核心框架的初始入口点。

在最初的详细输出之后,执行在第一个(Pdb)实例处停止。此时,您遇到了一个断点,因此运行runserver的控制台和请求客户端(即浏览器)会冻结,直到您在控制台上提供额外的输入。在清单 5-32 中,您可以看到字母n代表下一个被引入,执行向前移动到另一行,之后您将看到另一个(Pdb)提示或断点。此时,您只需按下Enter键即可重新调用之前的命令(即n)并向前移动。

如果您想前进而不碰到另一个断点,您可以键入c表示 continue,这样执行会正常继续,不会再次暂停。

正如您所看到的,使用 Django 的 pdb 的强大之处在于,除了能够交互式地分析和设置变量之外,您还可以以非常精细的方式遍历任何部分的执行周期。表 5-6 描述了与 pdb 相关的最基本命令。

表 5-6。

Python pdb commands used at (Pdb) prompt

| Pdb 命令 | 描述 | | --- | --- | | (回车)(按键) | 重新执行上一个命令。 | | n | 将执行移动到下一个断点。 | | c | 继续执行,不再有断点。 | | q | 立即退出执行。 | | p | 打印变量。 | | l (L 小写) | 显示当前断点处的源代码列表,共 11 行:断点行、前 5 行和后 5 行。有助于提供背景。 | | s | 进入子程序。在非方法相关断点中,s 和 n 都移动到下一个断点。在与方法相关的断点中,s 进入方法或子例程。 | | r | 中断子程序。用在 s 之后,返回主例程。 |

除了在 Django 应用中向 URL 附加?pdb参数以输入 pdb 之外,还有两种选择。您可以将--pdb标志附加到runserver上,以便在对应用的每个请求上输入 pdb(例如,python manage.py runserver --pdb)。您还可以使用--pm标志,仅当视图中出现异常时才进入 pdb(例如python manage.py runserver –pm)。

有关 pdb 本身的更多信息,请参考位于 https://docs.python.org/3/library/pdb.html 的官方 Python 文档。有关 Django pdb 的更多信息,请在 https://github.com/tomchristie/django-pdb 查阅项目文档。

Django 扩展

Django 扩展是为 Django 项目设计的工具集合。顾名思义,它为 Django 的标准工具在功能上趋于平衡的许多领域提供了扩展。出于调试的目的,Django extensions 提供了两个工具,我认为这是最值得研究的:runserver_plus 和 runprofileserver。

要使用 Django 扩展,你首先需要安装pip install django-extensions,然后将django_extensions添加到settings.py中的INSTALLED_APPS。一旦设置了 Django 扩展,就可以通过python manage.py命令使用它的各种工具,就像 Django 的标准工具一样。

Django extensions runserver_plus命令为 Django 项目提供了交互式和增强的调试。要使用runserver_plus,你首先需要安装 Werkzeug 工具- pip install Werkzeug。一旦安装了 Werkzeug,只需用python manage.py runserver_plus而不是 Django 的标准python manage.py runserver启动一个 Django 应用。乍一看,runserver_plus命令的工作方式就像 Django 的runserver;然而,如果您碰巧遇到一个异常,您将会看到如图 5-4 和 5-5 所示的错误页面。

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

图 5-5。

Django extensions runserver_plus with interactive console

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

图 5-4。

Django extensions runserver_plus

在图 5-4 中,你可以看到 Django 异常页面与 Django 默认异常页面略有不同。这种不同的布局是由 Werkzeug 生成的,但布局本身并不是这种方法的有趣之处:如果你将鼠标悬停在堆栈跟踪的任何部分,你就可以启动一个交互式调试会话,如图 5-5 所示。这是一种更加简单和强大的调试方法,因为它是直接在浏览器中完成的!

另一个强大的 Django 扩展工具是runprofileserver,它可以为 Django 应用页面创建一个 Python cProfile。Python cProfile 提供了一组统计数据,描述了程序的各个部分执行的频率和时间,这有助于确定加载缓慢和资源密集型 Django 应用页面的解决方案。

使用runprofileserver的第一件事是创建一个文件夹来保存概要文件(例如mkdir /tmp/django-coffeehouse-profiles/)。接下来,简单地用python manage.py runprofileserver --use-cprofile --prof-path=/tmp/django-coffeehouse-profiles/而不是 Django 的标准python manage.py server启动 Django 应用——注意--prof-path标志值指向保存概要文件的目录。

打开浏览器,进入 Django 应用。并在其中导航。如果您打开保存配置文件的文件夹,您会看到类似于root.000037ms.1459139463.profstores.000061ms.1459139465.profstores.2.000050ms.1459139470.prof的文件,其中每个文件代表一个页面点击的 cProfile。

尽管深入研究 cProfile 分析超出了本书的范围,更不用说有许多工具可以用于此目的,但是如果您想要一个快速简单的工具来打开 Python cProfile 文件,我会推荐 SnakeViz。只需做pip install snakeviz然后运行snakeviz <file_name>即可。一旦你在一个文件上运行snakeviz,你会看到 Python cProfile 的细节,如图 5-6 和 5-7 所示。

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

图 5-7。

SnakeViz cProfile listing sorted by run time

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

图 5-6。

SnakeViz cProfile image

正如我在开始时提到的,除了runserver_plusrunprofileserver之外,Django 扩展还提供了许多工具,我认为这些工具最适合调试任务。尽管如此,我还是建议您查看位于 https://django-extensions.readthedocs.org/en/latest/ 的 Django 扩展文档,探索其他可能在您自己的项目中有用的工具(例如,show_urls 工具显示 Django 项目的 url 路由,graph_models 工具为 Django 项目的模型生成图形)。

Django 管理命令

在前面的章节中——包括这一章——您依赖于通过所有 Django 项目中包含的manage.py脚本调用的管理命令。例如,为了启动 Django 项目的开发服务器,您使用了runserver命令(例如python manage.py runserver),为了合并项目的静态资源,您使用了collectstatic命令(例如python manage.py collectstatic)。

Django 管理命令是 Django 应用的一部分,旨在通过一个关键字命令行指令完成重复或复杂的任务。每个 Django 管理命令都有一个脚本支持,该脚本包含一步一步的 Python 逻辑来完成其职责。因此,当您键入python manage.py runserver时,Django 会在幕后触发一个更加复杂的 Python 例程。

如果你在 Django 项目上输入python manage.py(即没有命令),你会看到一个按 app 分类的 Django 管理命令列表(例如authdjangostaticfiles)。从这个列表中,您可以深入了解所有 Django 应用中可用的各种管理命令。

我将在书中描述与核心或第三方 Django 应用相关的 Django 管理命令的用途,就像我到目前为止所做的那样(例如,静态文件主题中的静态文件管理命令,模型主题中的模型管理命令)。

接下来我将描述如何在 Django 应用中创建定制的管理命令,这样您就可以通过一条指令来简化常规或复杂任务的执行。

定制管理命令结构

定制管理命令被构造为 Python 类,这些类从 Django django.core.management.base.BaseCommand类继承它们的行为。最后一个类提供了必要的结构来执行任何 Python 逻辑(例如,文件系统、数据库或特定于 Django 的),同时处理通常与 Django 管理命令一起使用的参数。清单 5-33 展示了最可能的 Django 管理命令之一。

from django.core.management.base import BaseCommand, CommandError
from django.conf import settings

class Command(BaseCommand):
    help = 'Send test emails'

    def handle(self, *args, **options):
        for admin_name,email in settings.ADMINS:
            try:
                self.stdout.write(self.style.WARNING("About to send email to %s" % (email)))
                # Logic to send email here
                # Any other Python logic can also go here
                self.stdout.write(self.style.SUCCESS('Successfully sent email to "%s"' % email))
                raise Exception
            except Exception:
                raise CommandError('Failed to send test email')

Listing 5-33.Django management command class with no arguments

注意在清单 5-33 中,管理命令类必须被命名为Command,并从 Django BaseCommand类继承其行为。接下来,有一个help属性来描述管理命令的目的。如果您键入python manage.py help <task_file_name>python manage.py <task_file_name> --help,Django 将输出help属性的值。

handle方法包含核心命令逻辑,在调用命令时自动运行。注意 handle 方法声明了三个输入参数:self来引用类实例;*args引用方法本身的论据;和**options来引用作为管理命令的一部分传递的参数。清单 5-33 中的任务逻辑只使用了self引用。另一个任务管理的例子——在清单 5-34 中——展示了如何使用参数。

清单 5-33 中的任务逻辑仅限于循环settings.py中的ADMINS值并输出任务结果。然而,在 handle 方法中可以执行的逻辑没有限制,只要它是有效的 Python。

尽管标准的 Python try/except 块在 Django 管理任务中可以正常工作,但是在创建 Django 管理任务时,有两个语法细节需要注意:输出消息和错误处理。

要在执行任务逻辑时发送输出消息——成功或信息——您可以看到清单 5-33 使用了self.stdout.write引用,它代表管理任务运行的标准输出通道。此外,您可以看到self.stdout.write同时使用了self.style.WARNINGself.style.SUCCESS来声明要输出的实际消息。在self.style.*中包装消息是可选的,但是根据 Django 语法着色角色输出彩色格式化的消息(例如,绿色字体的成功,黄色字体的警告)。 11

要在执行任务逻辑时发送错误消息,可以使用self.stderr.write引用,它代表管理任务运行的标准错误通道。为了由于错误而终止管理任务的执行,您可以raise这个django.core.management.base.CommandError异常——如清单 5-33 中所做的那样——它接受一个错误消息,该消息被发送到self.stderr.write通道。

在大多数情况下,很少有像清单 5-33 中这样的固定 Django 管理命令,它不使用任何参数来改变其逻辑工作流。例如,Django runserver命令接受像addrport--nothreading这样的参数来影响 web 服务器的启动方式。

Django 管理命令可以使用两种类型的参数:位置参数——声明它们的顺序赋予它们意义;或者命名参数——在名称前加两个破折号(也叫旗帜)来给出它们的含义。

尽管handle()方法的**options参数——如清单 5-33 所示——提供了对管理命令参数的访问,以改变逻辑工作流,为了在自定义 Django 管理命令中使用参数,您还必须声明add_arguments()方法。

add_arguments()方法必须定义管理任务的参数,包括它们的类型——位置的或命名的——默认值、选择值和帮助消息,等等。本质上,add_arguments()方法作为命令参数的预处理程序,然后在handle()方法的**options参数中提供这些参数。

add_arguments(self,parser)签名的parser引用是一个基于标准 Python argparse 包 12 的参数解析器,旨在轻松处理 Python 脚本的命令行参数。

要在add_arguments()方法中添加命令参数,可以通过parser.add_argument()方法,如清单 5-34 所示。

from django.core.management.base import BaseCommand, CommandError
from django.conf import settings

class Command(BaseCommand):
    help = 'Clean up stores'

    def add_arguments(self, parser):
        # Positional arguments are standalone name
        parser.add_argument('store_id')

        # Named (optional) arguments start with --
        parser.add_argument(
            '--delete',
            default=False,
            help='Delete store instead of cleaning it up',
        )

    def handle(self, *args, **options):
        # Access arguments inside **options dictionary
        #options={'store_id': '1', 'settings': None, 'pythonpath': None,
        #         'verbosity': 1, 'traceback': False, 'no_color': False, 'delete': False}

Listing 5-34.Django management task class with arguments

清单 5-34 中的管理命令声明了位置参数和命名参数。注意,这两个参数都是用parser.add_argument()方法添加的。不同之处在于,命名参数使用前导破折号——如果省略,参数被认为是位置性的。

根据定义,位置参数是必需的。所以在清单 5-34 的情况下,需要store_id参数(例如python manage.py cleanupstores 1,其中1store_id),否则 Django 抛出“参数太少”错误。

命名参数总是可选的。因为命名参数是可选的,你可以在清单 5-34 中看到--delete参数声明了一个default=False值,确保参数总是接收一个默认值来运行handle()方法中的逻辑。

清单 5-34 中的--delete参数也使用help属性来定义关于参数用途的描述性文本。除了defaulthelp之外,parser.add_argument()方法还基于 Python argparse 包支持各种各样的属性——参见前面的脚注,了解该方法支持的一些参数。

最后,您可以在清单 5-34 中看到,handle()方法通过**options字典访问命令参数,其中的值可以用于构建命令逻辑。请注意**optionssettingspythonpath等中的附加论点。–由于BaseCommand类,默认情况下被继承。

自定义管理命令安装

所有 Django 管理任务都放在单独的 Python 文件中(即每个文件一个命令),并存储在/management/commands/文件夹下的 app 目录结构中。清单 5-35 显示了几个带有自定义管理任务的应用的文件夹结构。

+-<BASE_DIR_project_name>
|
+-manage.py
|
|
+---+-<PROJECT_DIR_project_name>
    |
    +-__init__.py
    +-settings.py
    +-urls.py
    +-wsgi.py
    |
    +-about(app)-+
    |            +-__init__.py
    |            +-models.py
    |            +-tests.py
    |            +-views.py
    |            +-management-+
    |                         +-__init__.py
    |                         +-commands-+
    |                                    +-__init__.py
    |                                    |
    |                                    |
    |                                    +-sendtestemails.py
    |
    +-stores(app)-+
                 +-__init__.py
                 +-models.py
                 +-tests.py
                 +-views.py
                 +-management-+
                              +-__init__.py
                              +-commands-+
                                         +-__init__.py
                                         |
                                         |
                                         +-cleanupstores.py
                                         +-updatemenus.py
Listing 5-35.Django management task folder structure and location

正如你在清单 5-35 中看到的,about应用在/management/commands/文件夹中有一个管理命令,stores应用在自己的/management/commands/文件夹中嵌套了两个管理命令。

Caution

为了确保应用管理命令的可见性,不要忘记添加空的 init。py 文件到/management/和/commands/文件夹中,如清单 5-35 所示,并在项目的 settings.py 文件中将应用声明为 INSTALLED_APPS 的一部分。

管理指挥自动化

Django 管理命令通常从命令行运行,需要人工干预。然而,有时从其他位置(例如,Django 视图方法或 shell)自动执行管理命令是有帮助或必要的。

例如,如果一个用户在 Django 应用中上传了一个图像,并且您希望该图像可以公开访问,那么您需要运行collectstatic命令,这样该图像就可以到达公开和合并位置(STATIC_ROOT)。类似地,您可能想在用户每次登录时运行一个cleanuprofile命令。

为了自动化管理命令的执行,Django 提供了django.core.management.call_command()方法。清单 5-36 展示了使用call_command()方法的各种方式。

from django.core import management

# Option 1, no arguments
management.call_command('sendtestemails')

# Option 2, no pause to wait for input
management.call_command('collectstatic', interactive=False)

# Option 3, command input with Command()
from django.core.management.commands import loaddata
management.call_command(loaddata.Command(), 'stores', verbosity=0)

# Option 4, positional and named command arguments
management.call_command('cleanupdatastores', 1, delete=True)

Listing 5-36.Django management automation with call_command()

清单 5-35 中的第一个选项执行一个没有任何参数的管理。清单 5-35 中的第二个选项使用 i nteractive=False参数来表示命令不能因为用户输入而暂停(例如,collectstatic总是询问你是否确定要覆盖先前存在的文件,interactive=False参数避免了这种暂停和输入的需要)。

清单 5-35 中的第三个选项调用管理命令,首先导入它,然后直接调用它的Command()类,而不是使用命令字符串值。最后,第四个选项——就像清单 5-35 中的第三个选项一样,使用了一个位置参数——声明为独立值(例如'stores'1)和一个命名参数——声明为key=value(例如verbosity=0delete=True)。

Footnotes 1

https://docs.djangoproject.com/en/1.11/topics/signing/

2

https://docs.python.org/3/library/time.html#time.strftime

3

https://pypi.python.org/pypi/ConcurrentLogHandler/0.9.1

4

https://docs.python.org/3/library/logging.handlers.html

5

https://github.com/getsentry/sentry

6

https://docs.sentry.io/server/installation/

7

https://github.com/getsentry/sentry/releases/tag/5.0.21

8

http://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html#access-keys-and-secret-access-keys

9

https://support.sparkpost.com/customer/portal/articles/1933377-create-api-keys

10

http://django-debug-toolbar.readthedocs.org/

11

https://docs.djangoproject.com/en/1.11/ref/django-admin/#syntax-coloring

12

https://docs.python.org/3/library/argparse.html

六、Django 表单

表单是用户在 web 应用中输入或编辑数据的标准方式。在最底层,表单由具有特殊含义的 HTML 标签组成。虽然您可以直接向 Django 或 Jinja 模板添加 HTML 表单标记,但是您确实希望避免这种情况,并使用 Django 的内置表单支持来简化表单处理。

在这一章中,你将学习如何构建 Django 表单以及表单所经历的工作流程。您还将了解 Django 表单支持的各种字段类型和小部件,如何验证表单数据并管理其错误,以及如何在模板中布置表单及其错误。

一旦您对 Django 表单背后的基础有了牢固的理解,您将学习如何创建定制的表单字段和窗口小部件。最后,您将学习更复杂的 Django 表单处理技术,例如部分表单处理、使用 AJAX 的表单处理、如何处理通过 Django 表单发送的文件,以及如何使用 Django 表单集处理同一页面上的多个表单。

Django 表单结构和工作流程

Django 有一个特殊的表单包,它提供了一种处理表单的综合方法。这个包的特性包括在单一位置定义表单功能的能力、数据验证、与 Django 模型的紧密集成等等。让我们先来看看清单 6-1 中的一个独立 Django 表单类,它用于支持一个联系人表单。

# forms.py in app named 'contact'
from django import forms

class ContactForm(forms.Form):
      name = forms.CharField(required=False)
      email = forms.EmailField(label='Your email')
      comment = forms.CharField(widget=forms.Textarea)

Listing 6-1.Django form class definition

Note

Django 并不希望表单出现在特定的位置。您同样可以将 Django 表单类放在应用中它们自己的文件中(例如 forms.py),或者放在其他应用文件中(例如 models.py、views.py)。您可以稍后将 Django 表单类导入到需要它们的地方,就像 Django 视图或 Python 包一样。

清单 6-1 中要注意的第一个重要方面是 Django 表单定义是forms.Form类的子类,所以它自动拥有这个父类的所有基本功能。接下来,您可以看到 form 类有三个属性,两个类型为forms.CharField,一个类型为forms.EmailField。这些表单域定义将输入限制为某些特征。

例如,forms.CharField表示输入应该是一组字符,而forms.EmailField表示输入应该是电子邮件。此外,你可以看到每个表单字段都包括属性(如required)来进一步限制输入的类型。目前,关于 Django 表单字段类型,这已经足够详细了;,关于 Django 表单字段类型的下一节将更详细地讨论这个主题。

接下来,让我们将清单 6-1 中的 Django 表单集成到 Django 视图方法中,这样就可以在 Django 模板中传递和呈现它。清单 6-2 展示了这个视图方法的初始迭代。

# views.py in app named 'contact'
from django.shortcuts import render
from .forms import ContactForm

def contact(request):
    form = ContactForm()
    return render(request,'about/contact.html',{'form':form})

Listing 6-2.Django view method that uses a Django form

清单 6-2 中的视图方法首先实例化ContactForm表单类,并将其分配给form引用。然后这个form引用作为一个参数被传递到about/contact.html模板中。

接下来,在 Django 模板中,您可以将 Django 表单作为常规变量输出。清单 6-3 展示了使用标准模板语法{{form.as_table}}时 Django 表单是如何呈现的。

<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" name="name" type="text" /></td></tr>
<tr><th><label for="id_email">Your email:</label></th><td><input id="id_email" required name="email" type="email" /></td></tr>
<tr><th><label for="id_comment">Comment:</label></th><td><textarea cols="40" id="id_comment" required name="comment" rows="10">
</textarea></td></tr>
Listing 6-3.Django form instance rendered in template as HTML

在清单 6-3 中,您可以看到 Django 表单是如何被翻译成 HTML 标签的!注意 Django 表单如何为每个表单字段生成适当的 HTML <input>标签(例如,forms.EmailField(label='Your email')创建指定的<label>和一个 HTML 5 type="email"来执行电子邮件的客户端验证)。此外,请注意name字段缺少 HTML 5 required属性,这是因为清单 6-1 中的表单字段使用了required=False语句。

如果仔细观察清单 6-3 ,Django 表单实例的 HTML 输出只是内部 HTML 表格标签(即<tr><th><td>)。输出缺少一个 HTML <table>包装器标签和支持 HTML 表单标签(即,<form>标签和action属性,用于指示将表单发送到哪里,以及一个submit按钮)。这意味着您需要将缺少的 HTML 表单标签添加到模板中来创建一个工作的 web 表单——这个过程将在清单 6-4 中简要描述。

此外,如果您不想像清单 6-3 那样输出被 HTML 表格元素包围的整个表单域,有许多其他的语法变体可以输出精细的表单域并去掉 HTML 标签。在这种情况下,模板中的{{form.as_table}}引用是用来简化事情的,但是本章的下一节“在模板中设置 Django 表单的布局”将详细阐述在模板中输出 Django 表单的不同语法变化。

接下来,让我们看一下图 6-1 ,它显示了 Django 表单的工作流程,以更好地说明 Django 表单是如何从头到尾工作的。

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

图 6-1。

Django forms workflow

图 6-1 中工作流程的前两步是我到目前为止所描述的。它包括用户点击一个 URL,该 URL 由一个 view 方法处理,该方法根据 Django 表单类定义返回一个空表单。清单 6-3 显示了 Django 表单的原始 HTML 输出,但是正如我已经提到的,该表单缺少一些元素以成为一个功能性的 web 表单;接下来,我将描述获得一个功能性 web 表单需要添加的元素。

Django 表单的功能性 Web 表单语法

到目前为止,您已经了解了如何将 Django 表单类定义快速转换成 HTML 表单。但是这种自动 HTML 生成只是使用 Django 表单类定义的好处的一部分;您还可以更快地验证表单值并向最终用户显示错误。

要执行最后这些操作,首先需要有一个功能性的 web 表单。清单 6-4 展示了从 Django 表单创建功能性 web 表单的模板语法。

<form method="POST">
  {% csrf_token %}

<table>
{{form.as_table}}
</table>
<input type="submit" value="Submit form">
</form>

Listing 6-4.Django form template declaration for functional web form

关于清单 6-4 要注意的第一件事是表单被包装在 HTML <form>标签中,这是所有 web 表单的标准。Django 强迫您显式设置<form>标签的原因是因为它的属性决定了 web 表单的大部分行为,并且会根据表单的用途而变化。

在清单 6-4 中,method属性告诉 web 浏览器,当表单被提交时,它将数据发送到服务器。POST 方法值是处理用户数据的 web 表单中的标准做法——另一个方法选项值是 GET,但它不是传输用户提供的数据的典型选择。POST 方法的使用应该很快就会变得更加清晰,但是要了解更多关于表单method属性的背景知识,您可以参考许多关于 HTTP 请求方法的互联网参考资料。?? 1

另一个重要的<form>属性——在清单 6-4 中实际上是没有的——是action,它告诉 web 浏览器将表单提交到哪里(即哪个 URL 负责处理表单)。在这种情况下,因为清单 6-4 没有action属性,浏览器的行为是将数据发送到当前所在的同一个 URL,这意味着如果浏览器从/contact/ URL 获得表单,它会将数据发送到同一个/contact/ URL。如果您想将表单发布到一个单独的 URL,那么您可以将 action 属性(例如,action="/urltoprocessform/")添加到<form>标签中。

请记住,因为同一个 URL 通过 GET 请求交付初始表单,还必须通过 POST 请求处理表单数据,所以 URL 的 backing view 方法必须设计为处理这两种情况。在下一节中,我将描述清单 6-2 的修改版本——它只处理 GET 请求情况——也处理 POST 情况。

清单 6-4 中的{% csrf_token %}语句是一个 Django 标签。{% csrf_token %}是一个特殊的标记,用于通过 POST 提交 web 表单并由 Django 处理的情况。csrf 首字母的意思是跨站点请求伪造,这是 Django 实施的默认安全机制。虽然可以禁用 CSRF 而不在表单中包含{% csrf_token %} Django 标签,但我不建议这样做,并建议您继续将{% csrf_token %} Django 标签添加到 POST 的所有表单中,因为 CSRF 是一种保护措施,并且主要在幕后工作。第一部分的最后一小节更详细地描述了 CSRF 背后的推理以及它在 Django 是如何运作的。

清单 6-4 中的下一个是包装在<table>标签中的{{form.as_table}}片段,它表示 Django 表单实例并输出清单 6-3 中所示的 HTML 输出。最后,还有<input type="submit">标记,它生成表单的提交按钮——当用户点击时提交表单——和结束的</form>标记。

Django 查看流程表单的方法(后期处理)

一旦在 Django 中有了一个功能性的 web 表单,就有必要创建一个视图方法来处理它。在上一节中,我提到了同一个 URL(通过扩展,view 方法)如何处理空白 HTML 表单的生成,以及带有数据的 HTML 表单的处理。清单 6-5 展示了清单 6-2 中视图方法的一个修改版本,它就是这样做的。

from django.shortcuts import render
from django.http import HttpResponseRedirect
from .forms import ContactForm

def contact(request):
    if request.method == 'POST':
        # POST, generate form with data from the request
        form = ContactForm(request.POST)
        # check if it's valid:
        if form.is_valid():
            # process data, insert into DB, generate email,etc
            # redirect to a new URL:
            return HttpResponseRedirect('/about/contact/thankyou')
    else:
        # GET, generate blank form
        form = ContactForm()
    return render(request,'about/contact.html',{'form':form})

Listing 6-5.Django view method that sends and processes Django form

清单 6-5 的视图方法中最重要的构造是检查request方法类型的 if/else 条件。如果请求方法类型是 POST(即数据提交或图 6-1 中的步骤 3),则处理表单的数据,但如果request方法类型是其他类型(即初始请求或图 6-1 中的步骤 1),则生成一个空表单,产生与清单 6-2 相同的行为。注意清单 6-5 中的最后一行是一个return语句,该语句分配表单实例——无论是空的(也称为未绑定的)还是填充的(也称为绑定的)——并将其返回给模板进行呈现。

现在让我们仔细看看清单 6-5 中的 POST 逻辑。如果request方法类型是 POST,这意味着有传入的用户数据,所以我们用request.POST引用访问传入的数据,并用它初始化 Django 表单。但是请注意,没有必要访问单个表单字段或进行逐段赋值——尽管如果您想的话,您可以这样做,这一过程将在本章后面的章节中介绍——使用request.POST作为 Django 表单类的参数就足以用用户数据填充 Django 表单实例;就这么简单!值得一提的是,以这种方式(即使用用户提供的数据)创建的 Django 表单实例被称为绑定表单实例。

此时,我们仍然不知道用户提供的关于 Django 表单的字段定义的数据是否有效(例如,值是文本还是有效的电子邮件)。要验证表单的数据,必须在绑定的表单实例上使用is_valid() helper 方法。如果form.is_valid()True,则处理数据并采取后续行动;在清单 6-5 中,这个额外的动作包括将控制重定向到/about/contact/thankyou URL。如果form.is_valid()False,这意味着表单数据有错误,在这之后,控制落到最后一个return语句,该语句现在传递一个绑定的表单实例来呈现模板。在最后一种情况下,通过使用绑定表单实例,用户得到一个填充了初始数据提交和错误的呈现表单,这样他就能够纠正数据,而无需从头开始重新引入值。

我故意不提关于is_valid() helper 方法或错误消息显示的更多细节,因为 Django 表单处理可能会有点复杂。下一节“Django 表单处理:初始化、字段访问、验证和错误处理”涵盖了您需要了解的关于 Django 表单处理的所有内容,因此不会影响到本介绍性章节。

CSRF:这是什么?它是如何与 Django 一起工作的?

CSRF 或跨站请求伪造是网络犯罪分子使用的一种技术,目的是迫使用户在 web 应用上执行不想要的操作。当用户与 web 表单交互时,他们会执行各种状态更改任务,从下订单(例如,产品、汇款)到更改数据(例如,姓名、电子邮件、地址)。大多数用户在与 web 表单交互时会有一种增强的安全感,因为他们看到了一个 HTTPS/SSL 安全符号,或者他们在与 web 表单交互之前使用了用户名/密码,所有这些都会让人觉得网络罪犯无法窃听、猜测或干扰他们的行为。

CSRF 攻击在很大程度上依赖于社交工程和 web 应用松散的应用安全性,因此攻击媒介是开放的,与其他安全措施无关(例如,HTTPS/SSL、强密码)。图 6-2 展示了一个 web 应用的 CSRF 漏洞场景。

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

图 6-2。

Web application with no CSRF protection

在用户“X”与网络应用“A”交互之后(例如,下订单、更新他的电子邮件),他简单地导航离开并转到其他站点。像大多数 web 应用一样,web 应用“A”将有效的用户会话保持几个小时或几天,以防用户回来决定做其他事情而不必再次登录。同时,网络罪犯也使用了站点“A ”,并且确切地知道其所有网络表单在哪里以及如何工作(例如,URL、诸如电子邮件、信用卡之类的输入参数)。

接下来,网络犯罪分子创建链接或页面,模仿在 web 应用“a”上提交 web 表单。例如,这可能是一个更改用户电子邮件地址以控制帐户或从用户帐户转移资金以窃取资金的表单。然后,网络罪犯通过电子邮件、社交媒体或其他带有诱人或可怕标题的网站,在互联网上植入这些链接或页面:“从网站‘A’获得 100 美元优惠券”、“紧急:由于安全风险,请更改您在网站‘A’的密码”。实际上,这些链接或页面并不像它们宣传的那样,而是在一次点击中模仿来自站点“A”的 web 表单提交(例如,更改用户的电子邮件或转移资金)。

现在让我们把注意力转向几小时或几天前访问过站点 A 的不知情用户 X。他瞥了一眼这些最后的广告,心想:“哇,我不能错过这个机会。”考虑到点击会造成什么危害,他点击了虚假广告,然后用户被发送到一个合法网站的“A”页面,或者点击“看起来”什么也没做。用户“X”对此毫不在意,继续执行其他任务。如果站点“A”没有带 CSRF 保护的 web 窗体,那么用户“X”只是无意中——在一次单击中——在站点“A”上执行了一个他没有意识到的操作。

如您所见,为了实施 CSRF 攻击,用户只需在给定的网站上进行一次活动会话,并且网络犯罪分子足够狡猾,可以诱骗用户点击在所述网站上执行操作的链接或页面。因此这个术语的名字是:“跨站点”,因为请求不是来自原始站点,而“请求伪造”是因为这是一个网络罪犯伪造的请求。

为了防止 web form CSRF 攻击,web 应用信任经过身份验证的用户是不够的,因为正如我刚才描述的,经过身份验证的用户可能无意中触发了他们没有意识到的操作。Web 表单必须配备一个惟一的标识符——通常称为 CSRF 令牌——它对用户来说是惟一的,并且有一个过期时间,类似于会话标识符。通过这种方式,如果一个经过身份验证的用户向一个站点发出请求,只有与他的 CSRF 令牌相匹配的请求才被认为是有效的,所有其他请求都被丢弃,如图 6-3 所示。

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

图 6-3。

Web application with CSRF protection

如图 6-3 所示,在 web 表单中包含 CSRF 令牌使得伪造用户请求变得非常困难。

在 Django 中,在 web 表单中生成一个 CSRF 令牌,该令牌带有一个以<input type="hidden" name="csrfmiddlewaretoken" value="32_character_string">形式生成 HTML 标签的{% csrf token %}标签,其中 32 个字符的字符串值因用户而异。以这种方式,如果 Django 应用发出 POST 请求——就像 web 表单发出的请求一样——它只接受 CSRF 令牌存在且对给定用户有效的请求;否则将产生“403 禁止”页面错误。

请注意,CSRF 在所有 Django 应用上是默认启用的,这要归功于其开箱即用的中间件设置,包括负责实施 CSRF 功能的django.middleware.csrf.CsrfViewMiddleware类。如果您希望完全禁用 Django 应用中的 CSRF 支持,您可以从settings.py中的MIDDLEWARE变量中删除django.middleware.csrf.CsrfViewMiddleware类。

如果希望在某些 web 表单上启用或禁用 CSRF,可以有选择地在处理 web 表单 POST 请求的视图方法上使用@csrf_exempt()@csrf_protect装饰器。

要在所有 web 表单上启用 CSRF,并在某些 web 表单上禁用 CSRF 行为,请在MIDDLEWARE中保留django.middleware.csrf.CsrfViewMiddleware类,并使用@csrf_exempt()装饰器将不需要的视图方法装饰为 CSRF 验证,如清单 6-6 所示。

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def contact(request):
    # Any POST-processing inside view method
    # ignores if there is or isn't a CSRF token

Listing 6-6.Django view method decorated with @csrf_exempt() to bypass CSRF enforcement

要在所有 web 表单上禁用 CSRF,并在某些 web 表单上启用 CSRF 行为,请从MIDDLEWARE中移除django.middleware.csrf.CsrfViewMiddleware类,并用@csrf_protect()装饰器装饰您想要 CSRF 验证的视图方法,如清单 6-7 所示。

from django.views.decorators.csrf import csrf_protect

@csrf_protect
def contact(request):
    # Any POST processing inside view method
    # checks for the presence of a CSRF token
    # even when CsrfViewMiddleware is removed

Listing 6-7.Django view method decorated with @csrf_protect() to enforce CSRF when CSRF is disabled at the project level

Django 表单处理:初始化、字段访问、验证和错误处理

因为 Django 表单处理可以有很多变化,所以我将从上一节中解释的相同的表单和视图方法开始讨论,现在合并到清单 6-8 中。

from django import forms
from django.shortcuts import render

class ContactForm(forms.Form):
      name = forms.CharField(required=False)
      email = forms.EmailField(label='Your email')
      comment = forms.CharField(widget=forms.Textarea)

def contact(request):
    if request.method == 'POST':
        # POST, generate form with data from the request
        form = ContactForm(request.POST)
        # Reference is now a bound instance with user data sent in POST
        # process data, insert into DB, generate email, redirect to a new URL,etc
    else:
        # GET, generate blank form
        form = ContactForm()
        # Reference is now an unbound (empty) form
    # Reference form instance (bound/unbound) is sent to template for rendering
    return render(request,'about/contact.html',{'form':form})

Listing 6-8.Django form class with backing processing view method

初始化表单:字段和表单的 Initial、init 方法、label_suffix、auto_id、field_order 和 use_required_attribute

当用户请求由 Django 表单支持的页面时,他们会收到一个空表单(也称为未绑定表单),由清单 6-8 中的 GET 部分和ContactForm()实例表示。虽然从用户数据的角度来看,表单总是空的,但是表单可以包含来自的数据,作为其初始化序列的一部分(例如,带有用户电子邮件或姓名的预填充表单)。

要执行这种 Django 表单初始化,您有三种选择。第一种技术是通过 view 方法中声明的表单实例上的initial参数,用值字典初始化表单。清单 6-9 展示了这种技术。

def contact(request):
        ....
        ....
    else:
        # GET, generate blank form
        form = ContactForm(initial={'email':'johndoe@coffeehouse.com','name':'John Doe'})
        # Form is now initialized for first presentation to display these values
    # Reference form instance (bound/unbound) is sent to template for rendering
    return render(request,'about/contact.html',{'form':form})
Listing 6-9.Django form instance with initial argument declared in view method

ContactForm()现在使用initial={'email':'johndoe@coffeehouse.com','name':'John Doe'}生成一个预填充的表单实例,其中包含电子邮件和姓名字段的值,这样最终用户就不必麻烦地自己从头开始键入这些值。

清单 6-9 中的技术是针对一次性的或者特定于实例的初始化值。如果你想不断地为一个给定的表单域提供相同的值,一个更合适的技术是直接在表单域上使用相同的initial参数,如清单 6-10 所示。

from django import forms

class ContactForm(forms.Form):
      name = forms.CharField(required=False,initial='Please provide your name')
      email = forms.EmailField(label='Your email', initial='We need your email')
      comment = forms.CharField(widget=forms.Textarea)

def contact(request):
        ....
        ....
    else:
        # GET, generate blank form
        form = ContactForm()
        # Form is now initialized for first presentation and is filled with initial values in form definition
    # Reference form instance (bound/unbound) is sent to template for rendering
    return render(request,'about/contact.html',{'form':form})

Listing 6-10.Django form fields with initial argument

请注意清单 6-10 中的表单字段是如何配备了initial参数的。值得一提的是,如果在表单实例和表单字段上都使用了initial参数,则表单实例值优先于任何表单字段(例如,如果将清单 6-9 和清单 6-10 中的语句组合在一起,表单将总是用清单 6-9 中的email='johndoe@coffeehouse.com' and name='John Doe'预先填充)。

在像清单 6-10 这样的表单字段中使用initial参数的一个缺点是,值不能在运行时动态改变(也就是说,你不能像清单 6-9 那样根据谁调用表单来个性化initial值)。为了解决这个问题并为最复杂的表单初始化场景提供最大的灵活性,第三种技术使用了清单 6-11 中所示的表单类的__init__方法。

from django import forms

class ContactForm(forms.Form):
      name = forms.CharField(required=False)
      email = forms.EmailField(label='Your email')
      comment = forms.CharField(widget=forms.Textarea)

      def __init__(self, *args, **kwargs):
            # Get 'initial' argument if any
            initial_arguments = kwargs.get('initial', None)
            updated_initial = {}
            if initial_arguments:
                  # We have initial arguments, fetch 'user' placeholder variable if any
                  user = initial_arguments.get('user',None)
                  # Now update the form's initial values if user
                  if user:
                        updated_initial['name'] = getattr(user, 'first_name', None)
                        updated_initial['email'] = getattr(user, 'email', None)
            # You can also initialize form fields with hardcoded values
            # or perform complex DB logic here to then perform initialization
            updated_initial['comment'] = 'Please provide a comment'
            # Finally update the kwargs initial reference
            kwargs.update(initial=updated_initial)
            super(ContactForm, self).__init__(*args, **kwargs)

def contact(request):
        ....
        ....
    else:
        # GET, generate blank form
        form = ContactForm(initial={'user':request.user,'otherstuff':'otherstuff'})
        # Form is now initialized via the form's __init__ method
    # Reference form instance (bound/unbound) is sent to template for rendering
    return render(request,'about/contact.html',{'form':form})

Listing 6-11.Django form initialized with __init__ method

清单 6-11 中第一个重要的方面是如何在视图方法中初始化表单;注意,它使用了相同的initial参数,但是字典值现在是{'user':request.user,'otherstuff':'otherstuff'}。这些值看起来奇怪吗?该表单甚至没有名为userotherstuff的字段,这是怎么回事呢?

这些最后的值完全有效,在其他情况下会被忽略,因为 Django 表单确实没有这些名称的字段,但是因为我们将在__init__方法中操作表单初始化过程的内部,我们可以访问这些占位符值用于间接初始化目的。更重要的是,使用这些占位符值说明了如何使用上下文数据或不相关的表单数据来初始化 Django 表单字段。

接下来,让我们将注意力转向 Django 表单的__init__方法,当您创建一个表单实例时会调用该方法。__init__方法的参数*args**kwargs是标准的 Python 语法——如果您从未见过最后一种语法,请查看 Python 基础的附录:方法:默认、可选、*参数& **kwargs 参数。

__init__中的第一步是检查初始值,并创建一个引用来保存新值以初始化表单。如果有一个initial值,检查一个user值,将这些值用于表单的实际nameemail字段。接下来,不考虑任何传入的值,对表单的comment字段进行直接赋值。

最后,用一组反映表单实际字段的新值更新表单的initial引用,导致使用表单上下文之外的数据(例如,请求数据、数据库查询等)初始化表单。).作为__init__方法的最后一步,调用super()方法,这样基类/父类初始化过程就开始了。

Always Use the Initial Argument or Init Method to Populate Forms with Initialization Data and Keep Them Unbound.

需要注意的是,前面所有的初始化技术都保持表单未绑定,这个术语用于描述没有填充用户数据的表单实例。术语“绑定”是为用用户数据填充表单的字段值而保留的。当您进入下一节中描述的 Django 表单的验证阶段时,绑定和未绑定之间的细微差别非常重要。

这也意味着语法 contact form(initial = { ’ email ‘:’ John Doe @ coffee house . com ‘,’ name’:‘John Doe’})不等同于 contact form({ ’ email ‘:’ John Doe @ coffee house . com ‘,’ name’:‘John Doe’})。第一种变体使用初始参数创建一个未绑定表单实例,而第二种变体通过直接传递值而不使用任何参数来创建一个绑定表单实例。

除了初始化 Django 表单上加载的第一组数据之外,Django 表单还有另外四个初始化选项,它们会影响模板中表单的布局:label_suffixauto_idfield_orderuse_required_attribute

当您在模板中为 Django 表单生成输出时——这个主题将在本章后面的“在模板中设置 Django 表单的布局”中详细描述——表单字段通常伴随着所谓的字段标签,这是对人类友好的描述符的另一个名称。例如,如果您有名为nameemail的字段,它们的默认标签分别是Your nameYour email。为了将字段标签从字段的 HTML 标记中分离出来(例如,<input type="text">),Django 定义了一个标签后缀(默认为: (冒号符号)来产生带有模式<field_label>:<input type="<field_type>">的输出。通过label_suffix你可以定义一个自定义的标签后缀符号来分隔每个表单域和它的标签。因此,例如,ContactForm(label_suffix='...')语法输出由...分隔的每个表单字段标签(例如,Your email...<input type="text">)。

Tip

单个表单域也可以使用 label_suffix 属性;参见 Django 表单字段类型部分。如果在表单域和表单初始化中都声明了 label_suffix,则前者优先。

Django 表单的另一个初始化选项是auto_id,它为每个表单字段自动生成一个idlabel。默认情况下,Django 表单总是被设置为auto_id=True,所以当输出带有form.as_table()的表单时,您总是会得到自动生成的 HTML ids 和标签,如清单 6-12 的第一部分所示。

<!-- Option 1, default auto_id=True -->
<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" name="name" type="text" /></td></tr>
<tr><th><label for="id_email">Your email:</label></th><td><input id="id_email" name="email" type="email" /></td></tr>
<tr><th><label for="id_comment">Comment:</label></th><td><textarea cols="40" id="id_comment" name="comment" rows="10">
</textarea></td></tr>

<!-- Option 2 auto_id=False -->
<tr><th>Name:</th><td><input name="name" type="text" /></td></tr>
<tr><th>Your email:</th><td><input name="email" type="email" /></td></tr>
<tr><th>Comment:</th><td><textarea cols="40" name="comment" rows="10">\r\n</textarea></td></tr>

Listing 6-12.Django form with automatic ids

(default auto_id=True option) and no automatic ids auto_id=False option

注意清单 6-12 顶部输出中的field标签是如何围绕<label for="id_field_name"> <label>的,并且字段 HTML 标签包含了id="id_field_name"属性。在大多数情况下,这是一个理想的输出,因为它允许容易地引用字段来附加 JavaScript 事件或 CSS 类。然而,在其他情况下auto_id=True会产生非常冗长的输出和包含冲突的 HTML 标签(例如,如果在同一个页面上有两个相同类型的表单实例,就会有两个相同的 id)。

要关闭 id 和标签的自动生成,您可以用auto_id=False选项初始化一个表单。例如,ContactForm(auto_id=False)语法生成清单 6-12 的后半部分中的输出。

另一个影响表单布局的未绑定表单实例的初始化选项是field_order选项。默认情况下,表单字段按照声明的顺序输出,所以清单 6-10 中的表单定义遵循输出顺序:nameemailcomment。您可以通过使用field_order选项来覆盖这个默认的字段输出,该选项接受一个带有期望输出顺序的字段名列表。field_order选项可以声明为初始化过程的一部分,也可以包含在内,就像它是一个表单字段一样。

例如,ContactForm(field_order=['email','comment','name'])语法确保首先输出email字段,然后是commentname。值得一提的是,field_order选项可以接受一个不完整的字段列表,比如ContactForm(field_order=['email']),输出email字段,然后是剩余的表单字段,按照它们声明的顺序,在本例中是name,然后是comment。如果您经常设置field_order来初始化表单的字段顺序,一个更快的解决方案是设置默认的field_order作为表单本身的一部分:

class ContactForm(forms.Form):
      name = forms.CharField(required=False)
      email = forms.EmailField(label='Your email')
      comment = forms.CharField(widget=forms.Textarea)
      field_order = ['email','comment','name'] # Sets order email,comment,name

如果将field_order声明为表单本身的一部分,在初始化时,初始化field_order优先。下一节“在模板中设置 Django 表单的布局”包含了更多关于在模板中实际使用field_order的细节。

最后,use_required_attribute选项允许您设置 HTML 5 required属性的总体用途。默认情况下use_required_attribute=True,这意味着所有必需的表单域都用 HTML 5 required属性输出,确保浏览器总是提供这些表单域。通过用use_required_attribute=False初始化表单,可以禁用这个 HTML 5 客户端验证required属性。请注意,设置use_required_attribute=False不会影响 Django 服务器端对表单字段的验证(例如,如果表单字段是必需的,Django 服务器端验证仍然会捕获未提供的字段,而不管use_required_attribute选项如何)。

访问表单值:请求。发布和清理 _ 数据

一旦用户填写了 Django 表单,该表单将被发送回服务器进行处理,并对用户数据执行一个操作(例如,创建订单、发送电子邮件、将数据保存到数据库)——这一步在清单 6-8 的 POST 部分中有所描述。

Django 表单处理的一个主要优点是可以使用request.POST变量创建绑定表单实例。尽管request.POST变量是用用户数据填充 Django 表单的初始访问点,但是除了初始化表单实例之外,不应该使用这些数据,因为request.POST中的数据对于直接访问来说太原始了。

例如,在request.POST中,你仍然不知道用户提供的数据是否有效。此外,request.POST中的数据仍然被视为字符串,因此如果您的 Django 表单碰巧有一个IntegerField()DateField(),它仍然需要手动转换为预期的数据类型(例如,“6”到 6 整数,“2017-01-01”到 2017-01-01 日期时间),这只是 Django 表单的另一部分处理的不必要的工作。

一旦用request.POST变量生成了绑定表单,就可以通过cleaned_data字典访问表单的每个字段值。例如,如果绑定表单有一个名为name的表单字段,您可以使用语法form.cleaned_data['name']来访问用户提供的name值。更重要的是,如果表单字段是名为ageIntegerField(),语法form.cleaned_data['age']会产生一个整数值,这种格式化行为也适用于其他非字符串数据类型的表单字段(例如DateField())。

Caution

在对表单调用 is_valid()之前,您不能访问 cleaned_data。

按照设计,除非首先调用is_valid()方法,否则不可能访问表单实例的cleaned_data字典。如果你试图在调用is_valid()之前访问cleaned_data,你会得到错误AttributeError: 'form_reference' object has no attribute 'cleaned_data'

如果你考虑一下,这是一个好的实践,毕竟,你为什么要访问未经验证的数据呢?下一节描述了is_valid()方法。

验证表单值:is_valid()、validators、clean_ ()和 clean()

is_valid()方法是 Django 表单处理的重要部分之一。一旦用request.POST创建了绑定表单,就调用实例上的is_valid()方法来确定包含的值是否符合表单的字段定义(例如,EmailField()值是否是有效的电子邮件)。尽管is_valid()方法返回布尔值TrueFalse,但它有两个重要的副作用:

调用is_valid()还会在表单实例上创建cleaned_data字典来保存通过验证规则的表单字段值。

  • 调用is_valid()还会在表单实例上创建errors字典,为每个没有通过验证规则的字段保存表单错误。

清单 6-13 展示了清单 6-8 使用is_valid()方法的修改版本。

from django.http import HttpResponseRedirect

def contact(request):
    if request.method == 'POST':
        # POST, generate form with data from the request
        form = ContactForm(request.POST)
        # Reference is now a bound instance with user data sent in POST
        # Call is_valid() to validate data and create cleaned_data and errors dict
        if form.is_valid():
           # Form data is valid, you can now access validated values in the cleaned_data dict
           # e.g. form.cleaned_data['email']
           # process data, insert into DB, generate email
           # Redirect to a new URL
           return HttpResponseRedirect('/about/contact/thankyou')
        else:
           pass # Not needed
           # is_valid() method created errors dict, so form reference now contains errors
           # this form reference drops to the last return statement where errors
           # can then be presented accessing form.errors in a template
    else:
        # GET, generate blank form
        form = ContactForm()
        # Reference is now an unbound (empty) form
    # Reference form instance (bound/unbound) is sent to template for rendering
    return render(request,'about/contact.html',{'form':form})

Listing 6-13.Django form is_valid() method

for form processing

注意在清单 6-13 中,在绑定表单实例创建之后,调用了is_valid()方法。如果所有的表单字段值都符合表单字段数据类型,我们输入一个条件,在这里可以通过cleaned_data字典访问表单值,执行任何必要的业务逻辑,并将控制权交给另一个页面,在清单 6-13 中是执行重定向。

如果任何表单字段值未能通过规则,那么is_valid()返回False,并在此过程中创建一个errors字典,其中包含未能通过规则的值的详细信息。因为最后自动创建了errors,所以在is_valid()返回False之后,所有需要做的就是返回相同的表单实例,以便向最终用户显示errors字典,这样他就可以纠正错误。

但是对于 Django 表单处理来说,与is_valid()方法同样重要的是,它的验证只是针对表单字段的数据类型进行的。例如,is_valid()可以验证一个值是否为空,一个值是否匹配给定的数字范围,或者一个值是否是有效的日期:本质上是 Django 表单字段类型支持的任何东西。

但是如果您想在is_valid()之后执行更复杂的验证呢?比如在认为一个值有效之前对照数据库检查该值,或者对照两个值进行检查(例如,对照提供的城市值检查提供的邮政编码值)。虽然您可以在调用is_valid()之后直接添加这些验证检查,但是 Django 提供了三种更有效的方法来执行高级规则,方法是将它们添加到表单字段或表单类定义中。

如果您想要一个可以跨多个 Django 表单域使用的可重用验证机制,最好的选择是通过表单域的validators选项分配一个验证器。validators表单域选项需要一个方法列表,用于在值不符合预期规则的情况下引发forms.ValidationError错误。清单 6-14 展示了一个 Django 表单,它的一个字段通过validators选项使用一个定制的验证器方法。

from django import forms
import re

def validate_comment_word_count(value):
      count = len(value.split())
      if count < 30:
            raise forms.ValidationError(('Please provide at least a 30 word message, %(count)s words is not descriptive enough'), params={'count': count},)

class ContactForm(forms.Form):
      name = forms.CharField(required=False)
      email = forms.EmailField(label='Your email')
      comment = forms.CharField(widget=forms.Textarea,validators=[validate_comment_word_count])

Listing 6-14.Django form field validators option with custom validator method for form processing

清单 6-14 中的第一部分展示了自定义的validate_command_word_count()方法,它(初步地)检查message是否至少有 30 个单词。如果方法的输入不是至少 30 个单词,Django 的forms.ValidationError错误就会被抛出,以表明违反了规则。

在清单 6-14 的下半部分,您可以看到一个修改过的ContactForm,其中comment字段使用了validators=[validate_csv]选项。这告诉 Django,在运行了is_valid()并且所有的表单字段都根据它们的数据类型检查了错误之后,它还应该根据为comment字段提供的值运行validate_comment_word_count验证器方法。如果注释值不符合这条规则,那么就会产生一个ValidatioError错误,这个错误会被添加到表单errors字典中——这个字典与上一节中描述的字典相同,用于根据字段值的数据类型检查字段值。

从清单 6-14 中的例子可以看出,通过validators选项,你同样可以在同一个表单或另一个 Django 表单中的任何其他表单字段上重用自定义的validate_comment_word_count()方法。此外,您还可以对一个字段应用多个验证器,因为validators选项接受一个验证器列表。最后,值得一提的是django.core.validators包包含了一系列验证器,你也可以重用 2 ,这些验证器由某些表单域数据类型在后台使用。

除了表单字段validators选项,还可以通过clean_<field>()clean()方法添加验证表单规则,它们是作为 Django 表单类的一部分创建的——就像前面描述的__init__()。就像在表单字段validators选项中指定的方法一样,clean_<field>()clean()方法在is_valid()方法运行时被自动调用。清单 6-15 展示了两种clean_<field>()方法的使用。

from django import forms

class ContactForm(forms.Form):
      name = forms.CharField(required=False)
      email = forms.EmailField(label='Your email')
      comment = forms.CharField(widget=forms.Textarea)

      def clean_name(self):
          # Get the field value from cleaned_data dict
          value = self.cleaned_data['name']
          # Check if the value is all upper case
          if value.isupper():
             # Value is all upper case, raise an error
             raise forms.ValidationError("Please don't use all upper case for your name, use lower case",code='uppercase')
          # Always return value
          return value

      def clean_email(self):
          # Get the field value from cleaned_data dict
          value = self.cleaned_data['email']
          # Check if the value end in @hotmail.com
          if value.endswith('@hotmail.com'):
             # Value ends in @hotmail.com, raise an error
             raise forms.ValidationError("Please don't use a hotmail email, we simply don't like it",code='hotmail')
          # Always return value
          return value

Listing 6-15.Django form field validation with clean_<field>() methods

在清单 6-15 中,有两个clean_<field>()方法来为nameemail字段添加验证规则。Django 会自动搜索以clean_为前缀的表单方法,并尝试将表单的字段名与剩余的名称进行匹配,以对有问题的字段进行验证。这意味着你可以拥有和表单域一样多的clean_<field>()方法。

每个clean_<field>()方法内部的逻辑遵循与validators方法相似的模式。首先,通过代表表单实例的self引用从表单的cleaned_data字典中提取一个字段的值。接下来,对字段值运行您想要的任何规则或逻辑,如果您认为该值不符合,您将引发一个forms.ValidationError,它将错误添加到表单实例中。最后,这与validators方法唯一不同的是,您必须返回字段值,而不管是否引发错误或更改其值。

有时有必要应用一个不一定属于特定领域的规则,在这种情况下,通用的clean()方法是优于clean_<field>()方法的方法。清单 6-16 展示了clean()方法的使用。

from django import forms

class ContactForm(forms.Form):
      name = forms.CharField(required=False)
      email = forms.EmailField(label='Your email')
      comment = forms.CharField(widget=forms.Textarea)

      def clean(self):
          # Call clean() method to ensure base class validation
          super(ContactForm, self).clean()

          # Get the field values from cleaned_data dict
          name = self.cleaned_data.get('name','')
          email = self.cleaned_data.get('email','')

          # Check if the name is part of the email
          if name.lower() not in email:
             # Name is not in email, raise an error
             raise forms.ValidationError("Please provide an email that contains your name, or viceversa")

Listing 6-16.Django form field validation with clean() method

在清单 6-16 中,你可以看到与清单 6-15 中之前的clean_<field>()方法相似的方法。但是因为clean()方法是一个类范围的方法,并且你要自己覆盖它,它与clean_<field>()方法的不同之处在于,你必须首先显式调用基类/父窗体类的clean()方法(即super(...).clean())来确保基类验证被应用。表单字段值提取也是通过cleaned_data数据字典完成的,验证逻辑也是如此,并引发forms.ValidationError来指示违反规则。最后,clean()方法与clean_<field>()方法的不同之处在于它不返回值。

从功能上讲,clean()方法是不同的,因为它是在validators选项和clean_<field>()方法中的所有方法之后被调用的,这一行为很重要,因为所有这些方法都依赖于cleaned_data字典中的数据。这意味着如果一个validatorsclean_<field>()方法引发了一个ValidationError错误,它将不会返回任何值,并且cleaned_data字典也不会包含这个字段的值。因此,当运行clean()方法时,如果在validatorsclean_<field>()方法中有一个被短路,那么cleaned_data字典可能不一定具有所有的表单字段值,这就是为什么在cleaned_data字典没有给定字段的情况下,clean()方法使用更安全的字典访问语法cleaned_data.get('<field>','')来分配默认值。

clean()方法、clean_<field>()validators方法之间另一个重要的行为差异是它们如何对待forms.ValidationError。当一个forms.ValidationError在一个validatorsclean_<field>()方法中被引发时,错误被分配给表单的errors字典中有问题的<field>——这对显示很重要。但是当在clean()方法中引发一个forms.ValidationError时,错误被分配给一个名为__all__的特殊占位符字段——也称为“非字段错误”——它也被放在表单的errors字典中。

如果你想将clean()方法中的错误分配给特定的表单字段,你可以使用清单 6-17 中所示的add_error()方法。

def clean(self):
    # Call clean() method to ensure base class validation
    super(ContactForm, self).clean()

    # Get the field values from cleaned_data dict
    name = self.cleaned_data.get('name','')

    # Check if the name is part of the email
    if name.lower() not in email:
       # Name is not in email, raise an error
       message = "Please provide an email that contains your name, or viceversa"
       self.add_error('name', message)
       self.add_error('email', forms.ValidationError(message))
       self.add_error(None, message)

Listing 6-17.Django form field error assignment with add_error()

in clean() method

注意清单 6-17 是如何在表单实例上使用add_error()方法而不是raise forms.ValidationError()语法的。add_error()方法接受两个参数:第一个是要分配错误的表单字段名称,第二个可以是错误消息字符串或ValidationError类的实例。

清单 6-17 中的前两个add_error()方法分别将错误分配给nameemail字段。带有None键的第三个add_error()方法将错误分配给__all__占位符,使其等同于清单 6-16 中的raise forms.ValidationError()

错误表单值:错误

在调用is_valid()方法后,前面章节中描述的表单错误会自动添加到errors字典中的表单实例中。包容地说,不像cleaned_data字典,不需要调用is_valid()方法就可以直接访问errors字典。

字典很重要,因为所有的表单错误都在上面。无论错误是因为值不符合表单字段数据类型还是不符合clean()方法、clean_<field>()方法、validators方法或clean()方法中的add_error()方法中的 raise forms.ValidationError()而引发的,所有错误最终都将出现在表单的errors字典中。

errors字典遵循{'<field_name>':'<error_message>'}模式,这使得识别视图方法或模板中的表单错误变得容易。最后一种模式的唯一例外是表单错误不是特定于字段的(例如,在clean()方法中创建的那些),在这种情况下,表单错误被分配给一个名为__all__的特殊键,这种错误也称为非字段错误。

虽然您可以像访问任何其他 Python 字典一样访问errors字典,但是 Django 提供了表 6-1 中描述的一系列方法,使得处理错误更加容易。

表 6-1。

Django form errors methods

| 方法 | 描述 | | --- | --- | | 表单.错误 | 允许您访问原始错误字典。 | | form.errors.as_data() | 输出包含原始 ValidationError 实例的字典。例如,如果 errors 输出{'email':['此字段是必需的']},则 errors.as_data()输出{'email':[ValidationError(['此字段是必需的'])]}。 | | form . errors . as _ JSON(escape _ html = False) | 输出包含错误字典内容的 JSON 结构。例如,如果 errors 输出{'email':['此字段是必需的']},则 errors.as_json()输出{'email':[{'message ':'此字段是必需的',' code ':'必需的' }]}。请注意,默认情况下,as_json()不会对其输出进行转义,如果您希望对错误进行转义,请使用 escape_html 标志(例如,as_json(escape_html=True))。 | | form.add_errorfield,message) | 将错误信息与给定的表单域相关联。尽管通常在 clean()方法中使用,但如果需要,也可以在视图方法中使用。请注意,如果未指定 field,则错误消息将成为被视为非字段错误的错误中 __all__ 占位符键的一部分。 | | form.has_error(字段,代码=无) | 如果给定字段有错误,则返回 True 或 False 值。请注意,默认情况下,如果任何错误类型与某个字段相关联,has_error 将返回 True。要针对特定的错误类型执行评估,可以使用 code 关键字(例如,form.has_error('email ',code='required '))。要检查表单是否有非字段错误,可以使用 NON_FIELD_ERRORS 作为字段值。 | | form.non_field_errors() | 返回与表单相关联的非表单错误列表(即 __all__ 占位符键)。这些错误通常通过 ValidationError 或 add_error(None,' message ')在 clean() clean 方法中创建。 |

您可能已经注意到,在所有前面的例子中创建的ValidationError类实例使用不同的参数,这意味着有多种方法来创建ValidationError实例。例如,一些ValidationError实例使用一个简单的字符串,但是也可以用一系列ValidationError实例创建一个ValidationError实例,并指定一个code属性来进一步分类错误类型。清单 6-18 展示了一系列使用这些变体的ValidationError类实例。

from django import forms

# Placed inside def clean_email(self):
raise forms.ValidationError("Please don't use a hotmail email, we simply don't like it",code='hotmail')

# Placed inside def clean(self):
raise forms.ValidationError(
     forms.ValidationError("Please provide an email that matches your name, or viceversa",code='custom'),
     forms.ValidationError("Please provide your professional email, %(value)s doesn't look professional ",code='required',params={'value':self.cleaned_data.get('email') })

Listing 6-18.Django form ValidationError

instance creation

清单 [6-18 中的ValidationError实例变体都是可选的,但是当需要在模板上显示或过滤错误消息时,这个过程将在本章后面的“在模板中设置 Django 表单的布局”一节中详细描述。

Django 表单字段类型:小部件、选项和验证

由于 Internet 不受控制的特性——它有许多类型的设备、浏览器和用户体验级别——它对 web 表单必须处理的数据类型提出了各种各样的要求,以及如何净化数据以符合 web 表单的原始目的的各种各样的要求。

当您为 Django 应用创建 web 表单时,您依赖于由表单字段组成的 Django 表单类。每个 Django 表单字段都很重要,因为它规定了最终构成 web 表单整体行为的一小部分功能。

Django 表单域定义了两种类型的功能,表单域的 HTML 标记和它的服务器端验证工具。例如,Django 表单字段转换成实际的 HTML 表单标记(例如,<input><select><textarea>标签)、HTML 标记属性(例如,长度、字段是否可以留空,或者字段是否必须禁用),以及对表单字段的数据轻松执行服务器端验证的必要挂钩。

Django 表单字段出于需要定义了这两种类型的 web 表单功能。尽管随着 HTML5 等技术的发展,浏览器已经取得了巨大的进步,通过 HTML 标记本身(无需 JavaScript)提供了开箱即用的表单字段验证,但浏览器仍然完全控制着最终用户,只要有足够的知识,最终用户就可以绕过表单,将他想要的任何数据输入表单。因此,一旦用户提交了表单域数据,标准的做法是进一步检查它是否符合表单的规则,由于使用了 Django 表单域,这个过程变得非常容易。

表 6-2 展示了各种 Django 表单字段,包括它们的类型、它们生成的 HTML、它们的默认小部件以及它们的验证行为。

表 6-2。

Django form field types, generated HTML, default widget, and validation behavior

| 字段类型 | Django 表单字段类型 | HTML 输出 | 默认 Django 小部件 | 验证行为 | | --- | --- | --- | --- | --- | | 布尔代数学体系的 | 表格。布尔字段() |

正如你在表 6-2 中看到的,Django 表单域为几乎所有现存的 HTML 表单输入的生成提供了现成的支持,并为各种数据类型提供了必要的服务器端验证。例如,您可以使用CharField()表单字段类型来捕获标准文本,或者使用更专业的EmailField()表单字段类型来确保捕获的值是有效的电子邮件。正如您可以使用ChoiceField()生成一个带有预定义值的表单列表,或者使用DateField()强制表单值是一个有效日期。

小部件和表单域之间的关系

在表 6-2 中,你可以看到除了实际的 Django 表单域语法(例如forms.CharField()forms.ImageField())之外,每个表单域都与一个默认的小部件相关联。Django 小部件在很大程度上没有被注意到,并且经常与表单字段本身的功能混合在一起(例如,如果您想要一个 HTML 文本输入<input type="text"..>,您可以使用forms.CharField())。但是,当您需要更改表单域生成的 HTML 或者表单域数据最初的处理方式时,您需要使用小部件。

更令人困惑的是,您在表单字段上指定的许多选项最终都被用作小部件的一部分。例如,表单字段forms.CharField(max_length=25)告诉 Django 在处理时将一个值限制为最多 25 个字符,但是这个相同的max_length选项被传递给forms.widgets.TextInput()小部件以生成 HTML <input type="text" maxlength="25"...>,从而通过 HTML maxlength="25"属性在浏览器上执行相同的规则。所以在这种情况下,您实际上可以通过一个表单域选项来更改 HTML 输出,甚至不需要了解小部件!

那么,您真的需要使用小部件来改变表单字段产生的 HTML 吗?答案是视情况而定。许多表单域选项被自动传递给幕后的小部件,实际上改变了生成的 HTML,但是不要搞错,它是一个负责生成 HTML 而不是表单域的小部件。如果表单域的选项不能实现期望的 HTML 输出,那么就有必要改变表单域的小部件来实现自定义的 HTML 输出。

在本章接下来的章节中,我将详述 Django 窗口小部件的主题,并描述如何覆盖和定制 Django 表单字段的默认窗口小部件。现在您已经知道了 Django 小部件的存在以及它们与 Django 表单字段的关系,我将继续 Django 表单字段的主题,并解释各种 Django 表单字段选项及其验证行为。

Django ‘Hidden’ Built-in Widgets

表 6-2 显示了 Django 中所有内置的表单字段,这可能会误导您认为同一个表也显示了所有 Django 内置的表单小部件。事实并非如此。表 6-2 中的 widget 列仅显示分配给所有内置表单域的默认 widget。forms.widgets. package 中还包含一些 Django 内置的小部件:

  • 密码输入。-密码字段的小部件(例如,当用户键入文本时显示****)。还支持在验证错误后重新显示字段值。
  • HiddenInput。-隐藏字段的小部件(如)。
  • MultipleHiddenInput。-类似 HiddenInput,但用于多个值(即一个列表)。
  • 文本区域。-文本区域字段的小部件(如 )。

无线电选择。-类似于 Select 小部件,但生成一个单选按钮列表(如

)。

复选框选择多个。-类似于 SelectMultiple 小部件,但生成一个复选框列表(如

  • [ ]

)。

时间输入。-类似于 DateTimeInput 小部件,但仅用于时间输入(例如,13:54,13:54:59)。

选择日期小工具。- widget 生成三个选择日期的 widget(例如,选择日期的 widget、选择月份的 Widget、选择年份的 Widget)。

SplitHiddenDateTimeWidget。-类似 SplitDateTimeWidget 小部件,但是使用隐藏的日期和时间输入。

文件输入。-类似于 ClearableFileInput 小部件,但是没有复选框输入来清除字段的值。

本章后面的章节描述了表单域的小部件参数以及如何定制 Django 小部件,提供了更多关于如何以及何时使用这些额外的内置小部件的上下文。

空值、默认值和预定值:必需值、初始值和选项

默认情况下,所有 Django 表单字段都被标记为 required,这意味着每个字段都必须包含一个值才能通过验证。required参数对表 6-2 中描述的所有 Django 表单字段都有效,除了在服务器端强制值不为空,HTML 5 required属性也被分配给表单字段,因此用户的浏览器也强制验证。

Tip

可以用 use_required_field=False 初始化表单,以放弃使用 HTML 5 required 属性。请参阅上一小节“初始化表单”

如果你想让一个字段值为空- None或空字符串'' -那么一个字段必须被赋予required=False参数。

您还可以通过initial参数为字段分配默认值。初始参数对表 6-2 中描述的所有 Django 表单字段同样有效,并在前面的小节“初始化表单”中有详细描述

如果您不想让用户为字段引入开放式值,您可以通过choices参数将字段值限制为一组预先确定的值。如果您想使用choices属性,您必须使用一个表单域数据类型,该数据类型被设计用来生成一个 HTML <select>列表,如forms.ChoiceField()forms.MultipleChoiceFieldforms.FielPathField()choices参数不能用于像forms.CharField()这样为开放式输入设计的数据类型。

限制文本值:max_length、min_length、strip 和验证器

接受文本(如CharField()EmailField()以及表 6-2 中描述的其他文本)的表单字段数据类型可以接受max_lengthmin_length参数,以将字段值分别限制为最大和最小字符长度。

strip参数用于将 Python 的strip()方法应用于字段值——该方法去除了所有尾随和前导空格。strip参数只能用于两种 Django 字段数据类型,默认为strip=True,CharField()和默认为strip=FalseRegexField()

要对接受文本值的字段应用更高级的限制规则,请参阅上一小节“验证表单值”,该小节描述了限制字段值的validators和其他技术。

限制数值:最大值、最小值、最大位数、小数位数和验证器

接受数字如IntegerField()DecimalField()FloatField()的表单字段数据类型可以接受max_valuemin_value参数,分别限制字段数值的上限和下限。

此外,DecimalField()数据类型接受更精细的数字类型,可以使用max_digits参数来限制值中的最大位数,或者使用decimal_places参数来指定值中的最大小数位数。

要对接受数字值的字段应用更高级的限制规则,请参阅上一小节“验证表单值”,该小节描述了限制字段值的validators和其他技术。

错误消息:错误消息

每个 Django 字段数据类型都有内置的错误消息。例如,当一个字段的数据类型是required并且用户没有添加任何值时,Django 会将错误消息This field is required分配给该字段,作为表单的errors字典的一部分。类似地,如果一个字段数据类型使用了max_length参数,并且用户提供的值超过了这个阈值,Django 就会创建错误消息Ensure this value has at most X characters (it has X)

如清单 6-18 中所述,除了错误消息本身,Django 错误消息通常还会给出一个消息错误代码。正是这些消息错误代码用于通过error_messages参数分配定制消息。

error_messages参数需要一个字典,其中每个键是消息错误代码,其值是自定义错误消息。例如,为了给required代码提供一个定制的消息,你可以使用语法forms.CharField(error_messages={"required":"Please, pretty please provide a comment"})。类似地,如果您希望一个表单字段违反它的max_length值,您可以通过max_length代码(例如error_messages={"max_length":"This value exceeds its max length value"})指定一个定制的错误消息。

错误代码通常直接映射到它们违反的规则(例如,如果一个forms.IntegerField违反了它的max_value,Django 使用max_value代码分配一个默认的错误消息,您可以使用这个代码覆盖它)。然而,有二十多个内置错误消息代码(例如'missing''contradiction'),其中一些并不太明显。例如,forms.ImageField可以生成错误消息'Upload a valid image. The file you uploaded was either not an image or a corrupted image.',它使用'invalid_image'错误代码,这意味着要覆盖这个默认的错误消息,您需要事先知道错误代码,以将其声明为error_messages的一部分。

在大多数情况下,很少需要为一些更深奥的错误代码定制错误消息。但是如果您在为给定字段定制错误消息时遇到了麻烦,因为您无法确定它的错误代码,那么在表单错误字典上进行一点日志调试(例如,form.errors.as_json()或表 6-1 中的一些其他方法)可以快速地为您找到表单的错误代码,以覆盖带有error_messages参数的消息。

字段布局值:标签、标签后缀、帮助文本

当你在一个模板中输出一个表单域时,除了基本的 HTML 表单域标记(例如<input type="text">)之外,它几乎总是伴随着一个人类友好的描述符来指示一个域是做什么用的(例如Email: <input type="text">)。这个对人友好的描述符被称为标签,在 Django 中默认情况下,它被赋予与字段名相同的值。

要定制一个表单域的标签,你可以使用label参数。例如,要为名为 email 的字段提供更具描述性的标签,可以使用语法email = EmailField(label="Please provide your email")。默认情况下,表单上的所有标签都带有一个:符号,它的作用相当于一个后缀。您可以使用单个表单字段或表单实例本身的label_suffix参数进一步定制字段标签的输出。

例如,语法email = EmailField(label_suffix='-->')覆盖了电子邮件字段中用于-->符号的默认后缀标签。

Tip

label_suffix 也可以用来初始化一个表单(例如,form = ContactForm(label_suffix='-->')),这样所有的字段都会收到一个标签后缀,而不是一个字段一个字段地初始化。有关更多细节,请参见前面的“初始化表单”一节。

在某些情况下,向表单域添加更明确的说明会很有帮助;对于这种情况,您可以使用help_text参数。根据您使用的模板布局,help_text值被添加到 HTML 表单字段标记的右侧。

例如,语法comment = CharField(help_text="Please be as specific as possible to receive a quick response")生成紧挨着comment输入字段的给定的html_text值(例如Please be as specific as possible to receive a quick response <input type="text">)。下一节“在模板中设置 Django 表单的布局”将更详细地介绍help_text和其他表单布局属性的使用。

在模板中设置 Django 表单的布局

当您将 Django 表单(未绑定的或绑定的)传递给模板时,有许多选项可以生成它的布局。您可以使用 Django 的一个预构建的 HTML 助手来快速生成表单的输出,或者粒度化地输出每个字段来创建一个高级的表单布局(例如,响应式设计 3 )。此外,还可以有许多方式来输出表单错误(例如,除了字段本身之外或者在表单的顶部)。接下来,我将描述在模板中输出 Django 表单的各种选项。

清单 6-19 显示了我将在其余布局部分使用的 Django 表单——这与本章中使用的表单相同。

from django import forms

class ContactForm(forms.Form):
      name = forms.CharField(required=False)
      email = forms.EmailField(label='Your email')
      comment = forms.CharField(widget=forms.Textarea)

Listing 6-19.Django form class definition

输出表单字段:form.as_table、form.as_p、form.as_ul 和按字段粒度

Django 表单提供了三个助手方法来简化所有表单字段的输出。语法form.as_table o 输出一个表单的字段来容纳一个 HTML <table>,如清单 6-20 所示。语法form.as_p输出带有 HTML <p>标签的表单字段,如清单 6-21 所示。Where as 语法form.as_ul输出一个表单的字段来容纳一个 HTML <ul>列表标签,如清单 6-22 所示。

Caution

如果您使用 form.as_table、form.as_p、form.as_ul,您必须声明开始/结束 HTML 标签、换行

标签、Django {% csrf_token %}标签和按钮,如本章开头部分“Django 表单的功能性 web 表单语法”所述。
<tr>
    <th><label for="id_name">Name:</label></th>
    <td><input id="id_name" name="name" type="text" /></td>
</tr>\n
<tr>
    <th><label for="id_email">Your email:</label></th>
    <td><input id="id_email" name="email" type="email" required/></td>
</tr>\n
<tr>
    <th><label for="id_comment">Comment:</label></th>
    <td><textarea cols="40" id="id_comment" name="comment" rows="10" required>\r\n</textarea></td>
</tr>
Listing 6-20.Django form output with form.as_table

<p>
    <label for="id_name">Name:</label> <input id="id_name" name="name" type="text" />
</p>\n
<p>
    <label for="id_email">Your email:</label> <input id="id_email" name="email" type="email" required/>
</p>\n
<p>
    <label for="id_comment">Comment:</label> <textarea cols="40" id="id_comment" name="comment" rows="10" required>\r\n</textarea>
</p>'
Listing 6-21.Django form output with form.as_p

<li>
    <label for="id_name">Name:</label> <input id="id_name" name="name" type="text" />
</li>\n
<li>
    <label for="id_email">Your email:</label> <input id="id_email" name="email" type="email" required/>
</li>\n
    <li><label for="id_comment">Comment:</label> <textarea cols="40" id="id_comment" name="comment" rows="10" required>\r\n</textarea>
</li>
Listing 6-22.Django form output with form.as_ul

Tip

通过用auto_id=False初始化表单,可以使form.as_tableform.as_p & form.as_ul输出不那么冗长——省略标签和 id 属性。此外,您还可以通过使用label_suffix变量初始化表单,来更改分隔标签名称的符号(默认为:)和另一个符号。也可以使用field_order选项改变输出场顺序。

在某些情况下,前面的帮助器方法都不足以实现特定的表单布局。例如,要创建响应式设计,您需要手动输出每个字段,以适应特定的布局要求(例如,引导 CSS 网格列)。为了实现字段的自定义输出,每个表单实例都允许使用表 6-3 中的属性通过form.<field_name>语法访问其字段。

表 6-3。

Django form field attributes accessible in templates

| 属性名 | 描述 | | --- | --- | | { {表单。 }}(即没有属性,只有字段名本身) | 输出与字段相关联的 HTML 表单标记——技术上称为 Django 小部件(例如,)。) | | { {表单。。姓名}} | 输出在 form 类中定义的字段名称。 | | { {表单。。值}} | 输出分配了初始数据或用户提供的数据的字段的值。如果您需要单独输出 HTML 表单标记的 value 属性(例如,对于,{{form.name.value}}输出 John Doe),这很有用。 | | { {表单。。标签}} | 输出字段的标签,默认情况下使用语法“您的”(例如,对于电子邮件字段,{{form.email.label}}输出您的电子邮件)。 | | { {表单。。id_for_label}} | 输出字段的标签 id,默认情况下使用语法 id_ (例如,对于电子邮件字段,{ { form . email . id _ for _ label } } outputs id _ email)。 | | { {表单。。auto_id}} | 输出字段的自动 id,默认情况下使用语法 id_ (例如,对于电子邮件字段,{{form.email.auto_id}}输出 id_email)。 | | { {形式.。label_tag} | Helper 方法输出 HTML | | { {表单。。help_text}} | 输出与字段相关的帮助文本。 | | { {表单。。错误}} | 输出与字段相关的错误。 | | { {表单。。css_classes}} | 输出与字段关联的 CSS 类。 | | { {表单。。as_hidden}} | 将字段的 HTML 输出为隐藏的 HTML 字段(如)。 | | { {表单。。is_hidden}} | 字段隐藏状态的布尔结果。 | | { {表单。。as_text}} | 将字段的 HTML 输出为文本 HTML 字段(如)。 | | { {表单。。as_textarea}} | 将字段的 HTML 输出为 textarea HTML 字段(如 )。 | | { {表单。。as_widget}} | 输出与字段相关联的 Django 小部件;从技术上讲,生成的输出与使用语法{ { form }调用独立字段的输出相同。} }–显示在此表格的顶部。 |

Tip

通过使用表单字段上的labellabel_suffixhelp_text选项,您可以覆盖表格 6-4 中{{form.<field_name>.label}}的默认输出、{{form.<field_name>.label_tag}}的后缀和{{form.<field_name>.help_text}}的默认输出。这个过程在上一节“Django 表单域类型:小部件、选项和验证”中有描述

正如您在表 6-4 中看到的,有许多字段属性可用于定制表单的布局。只是要小心,如果你输出的表单字段很细,你不会错过一个字段,因为如果你错过了一个字段,最有可能的结果是 Django 将无法处理表单,因为它不会从丢失的字段接收值。

表 6-4。

FORM_RENDERER values that influence finding and loading custom widget templates

| 表单渲染器类 | 描述 | | --- | --- | | django . forms . renderers . django templates(默认) | 从内置的 django/forms/templates/目录(即发行版)中搜索并加载小部件模板。从 INSTALLED_APPS 中声明的应用内的所有模板目录中搜索并加载小部件模板。 | | django . forms . renderers . jinja templates | 从内置的 django/forms/jinja2/目录(即发行版)中搜索并加载小部件模板。从 INSTALLED_APPS 中声明的应用内的所有 jinja2 目录中搜索并加载小部件模板。 | | django . forms . renderers . templates 设置 | 基于项目的模板配置(例如,其 DIRS 值)搜索和加载微件模板。注意:这个渲染器要求您将 django.forms 包声明为 INSTALLED_APPS 的一部分。 |

清单 6-23 展示了一个标准的{% for %}循环,它确保你不会错过任何字段,并提供了比之前的form.as_tableform.as_p & form.as_ul方法更大的灵活性。

{% for field in form %}
    <div class="row">
       <div class="col-md-2">
        {{ field.label_tag }}
        {% if field.help_text %}
          <sup>{{ field.help_text }}</sup>
        {% endif %}
        {{ field.errors }}
       </div><div class="col-md-10 pull-left">
         {{ field }}
       </div>
    </div>
 {% endfor %}
Listing 6-23.Django form {% for %} loop over all fields

在清单 6-23 中,在form引用上创建了一个循环,以确保没有字段被遗漏。如果您想避免在某些表单布局中显示某个字段,那么我建议您使用{{field.as_hidden}} vs. {{field}},因为这样可以确保该字段仍然是表单的一部分,以便进行验证,并且只是对用户隐藏——关于这种情况的更多细节将在下一节高级表单处理和部分表单中提供。

输出字段顺序:field_order 和 order_fields

如果您使用清单 6-20 、 6-21 、 6-22 或 6-23 中介绍的任何技术,表单字段的输出顺序与它们在清单 6-19 的表单类中声明的顺序相同(即姓名、电子邮件、评论)。但是,您可以使用几种技术来改变表单域的输出顺序。

第一种也是最明显的方法是直接在表单类定义中改变表单字段的顺序。因为最后一种技术需要修改表单的源代码,Django 还提供了field_order选项。field_order选项按照您希望它们输出的顺序接受表单字段名列表(例如,field_order=['email','name','comment']首先输出email字段,然后是namecomment)。field_order选项非常灵活,您可以提供表单域的部分列表(例如,field_order=['email']首先输出电子邮件域,其余的表单域按照它们声明的顺序输出),还可以声明不存在的域名,这些域名会被忽略,在使用表单继承时非常有用。

可以在两个位置声明field_order选项。首先,它可以被声明为表单类定义的一部分,如清单 6-24 所示。

from django import forms

class ContactForm(forms.Form):
      name = forms.CharField(required=False)
      email = forms.EmailField(label='Your email')
      comment = forms.CharField(widget=forms.Textarea)
      field_order = ['email','comment','name']

Listing 6-24.Django form field_order option to enforce field order

正如您在清单 6-24 中看到的,field_order被声明为任何其他表单字段,并被分配了一个字段名列表,以确保这些字段按顺序输出:电子邮件、评论和名称。也可以使用field_order选项作为表单初始化过程的一部分——在表单处理部分有详细描述。值得一提的是,如果在类定义(如清单 6-24 所示)和表单实例初始化上都使用了field_order选项,那么后者的值优先于前者。

除了field_order选项,Django 还提供了order_fields,它也希望字段名列表能够改变表单的输出字段顺序。但是与必须在表单类中声明或作为表单实例初始化的一部分的field_order选项不同,order_fields可以直接在表单实例上调用,这使得它成为在视图方法或模板中使用的一个好选项(例如,form.order_fields(['email']))。

输出 CSS 类、样式和字段属性:error_css_class、required_css_class、小部件、定制和各种表单字段选项

默认情况下,当您输出表单字段和标签时,没有与它们相关联的 CSS 类或样式。Django 提供了几种将 CSS 类与表单字段关联起来的机制。前两种方法是error_css_classrequired_css_class字段,它们以 Django 形式直接声明,如清单 6-25 所示。

from django import forms

class ContactForm(forms.Form):
      name = forms.CharField(required=False)
      email = forms.EmailField(label='Your email')
      comment = forms.CharField(widget=forms.Textarea)
      error_css_class = 'error'
      required_css_class = 'bold'

Listing 6-25.Django form error_css_class and required_css_class

fields to apply CSS formatting

正如您在清单 6-25 中看到的,error_css_classrequired_css_class字段被添加,就像常规的表单字段一样。当与这种表单实例相关联的字段呈现在模板上时,Django 将error CSS 类添加到所有标记有错误的字段中,并将bold CSS 类添加到所有标记为必需的字段中。

例如,除了明确使用required=False时,所有表单域都被视为必需的。这意味着如果您使用form.as_p输出清单 6-25 中的一个未绑定表单实例,那么注释字段将输出为<p class="bold">Comment: <textarea cols="40" name="comment" rows="10" required>\r\n</textarea></p>——注意<p>标签中的class。类似地,如果与清单 6-25 中的绑定表单实例相关联的字段出现错误,Django 会将error CSS 类添加到该字段中(例如,如果 email 字段值无效,email 字段会输出为<p class="bold error">Your email: <input name="email" type="email" value="aninvalidemail" required /></p>,注意bold CSS 类会保留,因为表单字段也是必需的)。

尽管error_css_classrequired_css_class字段很有帮助,但它们仍然提供有限的 CSS 格式化功能。为了获得对 CSS 类输出的完全控制,你需要对表 6-4 中的字段使用一些更细粒度的输出选项,或者定制一个表单字段的小部件,如清单 6-26 所示。

from django import forms

class ContactForm(forms.Form):
      name = forms.CharField(required=False)
      email = forms.EmailField(label='Your email', widget=forms.TextInput(attrs={'class' : 'myemailclass'}))
      comment = forms.CharField(widget=forms.Textarea)

Listing 6-26.Django form with inline widget definition to add custom CSS class

注意清单 6-26 中的 email 字段是如何用widget=forms.TextInput(attrs={'class' : 'myemailclass'})参数声明的。最后一条语句告诉 Django,当它输出email字段时,它使用定制的forms.TextInput小部件,该小部件用myemailclass值声明 CSS class属性。

通过使用清单 6-26 中的表单定义,email字段被输出为<input class="myemailclass" type="text"...>。如果您不知道什么是 Django 小部件,请参阅前面题为“小部件和表单字段之间的关系”的部分

清单 6-26 中介绍的方法是一种强大的技术,因为正如您可以声明 CSS class属性一样,您也可以声明任何其他表单域 HTML 属性。例如,如果你想声明定制的 HTML 属性——比如那些被像 jQuery 或 Bootstrap 这样的框架使用的属性——你可以很容易地使用同样的技术(例如,widget=forms.TextInput(attrs={'role' : 'dialog'})将输出<input role="dialog" type="text"...>)。

但是,现在需要注意的是,您已经知道在 Django 表单字段旁边输出任何 HTML 属性是多么容易。请注意,几乎所有 Django 表单字段数据类型都带有内置选项,可以转换成 HTML 属性。例如,forms.CharField(max_length=25)语句输出到<input type="text" maxlength="25"...>,这意味着表单字段max_length选项自动生成 HTML maxlength="25"属性。所以在开始使用清单 6-26 中的方法添加 HTML 属性时要小心,因为它们可能已经被内置的数据类型选项所支持。有关这些内置数据类型选项的更多细节,请参见上一节“Django 表单字段类型:小部件、选项和验证”。

输出表单域错误:表单。<field_name>。错误,表单.错误,表单. _ field _ 错误</field_name>

正如表单域可以用不同的方式输出一样,表单域错误也可以用不同的方式输出。在清单 6-23 第一部分的末尾,您可以看到我们如何使用{{field.errors}}语法来输出与特定字段相关的错误。然而,当以这种方式输出字段的errors值时,需要记住的一件重要事情是,输出是作为 HTML 格式的列表生成的:

<ul class="errorlist">
    <li>Name is required.</li>
</ul>

正如你在清单 6-27 中看到的,{{fields.errors}}值是带有errorlist CSS 类的列表——这允许你提供 CSS 行为,如背景颜色或边框——并且这些值被预先包装成列表元素。

如果你想去掉这些包装的 HTML 列表标签以获得对错误布局的更多控制(例如,创建一个响应式设计或 CSV 列表),你可以在每个field.errors上创建一个循环,如清单 6-27 所示。

 {% for field in form %}
    <div class="row">
      <div class="col-md-2">
        {{ field.label_tag }}
        {% if field.help_text %}
          <sup>{{ field.help_text }}</sup>
          {% endif %}
          {% for error in field.errors %}

           <div class="row">

             <div class="alert alert-danger">{{error}}</div>

           </div>

          {% endfor %}

       </div><div class="col-md-10 pull-left">
         {{ field }}
       </div>
    </div>
  {% endfor %}
Listing 6-27.Django loop over form.<field_name>.errors

您可以在清单 6-27 中看到,在每个字段的循环中,对field.errors引用进行了另一个循环,以粒度输出并为每个字段错误分配自定义标记。

正如清单 6-27 中的错误输出一样,这种布局假设除了需要在表单的字段上循环之外,你还想在每个字段旁边显示表单的错误信息。但是,如果您想在顶部或主表单旁边显示表单的错误,该怎么办呢?或者想继续使用 Django 的快捷方式(即form.as_tableform.as_p & form.as_ul)仍然显示错误?

除了清单 6-27 中的form.<field_name>.errors语法来访问字段错误之外,Django 还可以使用清单 6-28 中所示的errorsnon_field_errors字典来输出表单的错误。

<!-- Field errors -->
 {% if form.errors %}
  <div class="row">
    {% for field_with_error,error_messages in form.errors.items %}
        <div class="alert alert-danger">{{field_with_error}}  {{error_messages}}</div>
    {% endfor %}
  </div>
  {% endif %}

<!-- Non-field errors -->
 {% if form.non_field_errors %}
  <div class="row">
    {% for error in form.non_field_errors %}
    <div class="alert alert-danger">{{error}}</div>
    {% endfor %}
  </div>
  {% endif %}

Listing 6-28.Django form.errors and form.non_field_errors

with custom HTML output

正如您在清单 6-28 中看到的,form.errors字典提供了所有form.<field_name>.errors的聚合版本,其中每个字典键代表表单字段名称,值是一个带有错误代码的错误消息列表(例如required),以进一步过滤错误列表。如果你想在一个表单/页面的顶部输出每个表单错误,要求按照代码类型过滤错误,或者想继续使用 Django 的快捷输出表单方法(例如form.as_table)并获得表单错误,那么在模板上使用form.errors是一个不错的选择。

此外,注意清单顶部的 6-28 在form.non_field_errors字典上的循环。form.non_field_errors包含不属于特定表单域的错误——如前面“错误表单值:错误”一节所讨论的,以及名为__all__的特殊错误占位符域。因为非字段错误不适用于特定的表单字段,所以在访问non_field_errors字典的表单顶部输出这种类型的错误是很常见的。

请注意,如果您使用form.errorsform.non_field_errors来输出错误,默认情况下,错误引用——清单 6-28 中的{{error_messages}}{{error}}——被包装为 HTML 格式的列表(例如<ul class="errorlist"><li>...</li></ul>),但是您可以向错误列表添加一个额外的 for 循环——如清单 6-27 所示——来创建一个定制的 HTML 错误布局。

最后,值得一提的是有一系列的辅助方法被设计来促进错误输出(例如,以 JSON 格式);Django 表单处理部分的表 6-1 描述了这些方法。

Django 定制表单字段和小部件

在表 6-2 中,您看到了各种各样的内置 Django 表单字段,从基本的文本和数字类型到更专业的文本类型(如 CSV、预定义选项),包括文件和目录类型。但是,尽管这些内置表单域非常广泛,在某些情况下,还是有必要构建自定义表单域。

类似地,在表 6-2 中,您了解了所有 Django 表单域是如何链接到 Django 窗口小部件的,这些窗口小部件定义了表单域产生的 HTML。在前面的章节中(例如,清单 6-25 和 6-26 ),您了解了如何使用表单字段的widget属性用自定义属性(例如,CSS class属性)覆盖它的默认小部件,或者给它分配一个完全不同的小部件(例如,forms.Textarea小部件分配给forms.CharField字段),在某些情况下,使用 Django 的内置小部件(例如,添加属性或切换一个内置小部件

接下来,您将学习如何定制 Django 表单字段和小部件。

Customize Django Form Fields, Widgets, or Both?

正如您在本章中学到的,表单域和表单小部件之间存在模糊关系,当内置选项不足时,很难决定定制哪一个。

根据经验,如果您需要经常更改表单域的数据或验证逻辑,您应该使用自定义表单域。如果您需要不断地改变表单域的 HTML 输出(例如,表单标签、CSS 类、JavaScript 事件),您应该使用定制的小部件。

创建自定义表单域

与其他定制 Django 构造(例如,定制模板过滤器或上下文处理器)相比,创建定制表单字段的好消息是您不必从头开始编写所有内容。由于 Django 的内置表单字段是从forms.Form类继承其行为的子类,所以您可以将内置表单字段进一步子类化为更专业的表单字段。因此,自定义表单域可以从内置表单域中的一组基本功能开始,然后您可以根据需要对其进行自定义。

例如,如果您发现自己在使用内置的forms.FileField创建表单,并不断地以某种方式对其进行自定义(例如,针对某些文件大小或类型),您可以创建一个自定义表单字段(例如,PdfFileFieldMySpecialFileField),它继承了forms.FileField的行为,对其进行自定义,并直接在您的表单中使用该自定义表单字段。

清单 6-29 展示了一个定制表单域,它从内置的forms.ChoiceField表单域继承了它的行为。

class GenderField(forms.ChoiceField):
      def __init__(self, *args, **kwargs):
            super(GenderField, self).__init__(*args, **kwargs)
            self.error_messages = {"required":"Please select a gender, it's required"}
            self.choices = ((None,'Select gender'),('M','Male'),('F','Female'))
Listing 6-29.Django custom form field inherits behavior from forms.ChoiceField

正如您在清单 6-29 中看到的,GenderField类从内置的forms.ChoiceField字段类中继承了它的行为,使它能够自动访问与后一个类相同的行为和特性。接下来,__init__方法——用于初始化类的实例——调用super(),以确保父类(即forms.ChoiceField)的初始化过程完成,并在值被分配给error_messageschoices字段后立即执行。

清单 6-29 中的error_messageschoices字段可能看起来很熟悉,因为它们是内置forms.ChoiceField中通常使用的参数,也是本章前面描述的 Django 标准表单字段参数的一部分。您可以类似地添加表单域支持的任何其他参数(例如,self.requiredself.widget),以便使用表单域参数设置的行为创建自定义表单域。

一旦你有了一个定制的表单域,如清单 6-29 所示,你就可以用它在 Django 表单类中声明一个表单域(例如,gender = <pkg_location>.GenderField() vs. gender = forms.ChoiceField(error_messages={...},choices=...))。如您所见,如果您要对内置表单域进行重复的自定义,自定义表单域是一个很好的选择。

自定义内置小部件

在清单 6-25 和 6-26 中,您已经探索了一些与 Django 内置部件相关的定制。例如,在清单 6-25 中,您看到了如何定制分配给表单字段的默认内置小部件,而在清单 6-26 中,您看到了如何向内置小部件添加定制属性。在本节中,您将学习如何全局定制内置小部件。

回头看看表 6-2 ,你可以看到,例如,forms.widget.TextInput()小部件产生一个类似于<input type="text" ...>的 HTML 输出,类似地,表 6-2 中的所有其他小部件产生它们自己特定的 HTML 输出。

考虑到许多前端设计提出的高期望,对于许多 Django 项目来说,生成这种类型的基本样板 HTML 输出是不可能的。例如,如果您想将 JavaScript jQuery 或 ReactJS 逻辑紧密集成到 HTML 表单中,那么定制由 Django 的内置小部件生成的默认 HTML 可能是必要的。在这种情况下,理想的方法是为 Django 的内置小部件生成定制的 HTML,放弃使用 Django 在开箱即用状态下定义的默认标记。

关于定制 Django 的内置小部件,您需要知道的第一件事是 Django 在哪里保存它的默认内置小部件。Django 从 Django 主发行版中的django/forms/templates/django/forms/widgets/目录中的 Django 模板为其内置小部件构建 HTML 输出(例如,如果您的 Python 安装位于/python/coffeehouse/lib/python3.5/site-packages/,则附加此目录路径以定位小部件模板)。

如果查看最后一个目录,您会发现每个内置 Django 小部件的模板(例如,input.htmlradio.html)。请注意,所有这些小部件模板都不使用普通的 HTML,而是使用 Django 模板语法(例如,{% include %}标签,{% if %}条件)来支持代码重用。如果你对 Django 模板语法不熟悉,请查看第三章。

现在,虽然您可以在这个位置直接修改模板来改变每个小部件产生的 HTML 输出,但是不要这样做。为每个内置小部件定制输出的推荐方法是基于项目包含定制的内置小部件。因此,定制 Django 内置小部件的第一步是构建定制的内置小部件,并使它们成为 Django 项目的一部分。

Tip

将 Django 发行版中所有内置的小部件(即django/forms/templates/django/forms/widgets/中的模板)复制到您的项目中,并根据需要进行修改。

因为内置的小部件是 Django 模板,它们需要放在一个可以被发现的项目目录中。这意味着定制的内置小部件必须放在项目目录中,该目录是在settings.pyTEMPLATES变量中声明的DIRS列表的一部分。在大多数情况下,一个项目声明一个名为templates的目录——作为DIRS的一部分——它也包含一个项目的模板——但是你可以使用任何目录,只要它被声明为DIRS的一部分——参见第三章中的“模板搜索路径”一节以获得关于DIRS的更多细节。

除了使用属于DIRS列表的一部分的目录来定位小部件模板,Django 还期望在用于其默认内置小部件的相同路径中定位定制内置小部件(即,包括在 Django 发行版中的那些)。

因此,如果您有一个名为templates的项目目录作为DIRS列表的一部分,那么在这个templates目录中,您将需要创建相同的目录路径django/forms/widgets/,并且在这个最后的widgets子目录中放置定制的内置小部件(例如,要在django/forms/templates/django/forms/widgets/input.html定制位于 Django 发行版中的内置input.html小部件,您将在<project_dir>/templates/django/forms/widgets/input.html创建一个项目版本,这样后面的input.html模板优先于默认的内置发行版模板)。

一旦在项目中设置了定制的内置小部件,就需要对项目的settings.py进行两项配置更改。第一个配置要求您添加FORM_RENDERER变量,如下所示:

FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'

默认情况下,Django 内置的小部件是通过独立的 Django 模板渲染器django.forms.renderers.DjangoTemplates加载的,该渲染器与包含项目模板的DIRS列表的项目主TEMPLATES配置无关。因为您现在将定制的内置小部件放在声明为DIRS列表的一部分的目录中,所以您需要配置 Django 来使用项目的主TEMPLATES配置,作为表单呈现过程的一部分。通过将FORM_RENDERER变量设置为django.forms.renderers.TemplatesSetting,Django 还会检查主TEMPLATES配置的DIRS列表中的路径,以查找内置的小部件。关于小部件的最后一小节提供了关于FORM_RENDERER变量的更多细节。

最后,因为您正在覆盖项目中的django.forms包以获得定制的内置部件,所以您还必须将django.forms包声明为settings.py中的INSTALLED_APPS列表的一部分。

创建自定义表单小部件

定制表单小部件用于需要保持 Django 内置小部件功能不变,但仍需要定制小部件产生的 HTML 输出的情况。

Tip

在创建定制表单小部件之前,仔细查看表 6-2 中的内置小部件和侧栏“Django 隐藏内置小部件”中的小部件您也许能够找到您正在寻找的东西,而不需要创建自定义的小部件。

与定制表单域类似,定制表单小部件具有基于类的优势,因此可以从内置小部件继承它们的行为。例如,如果您发现自己经常以某种方式定制内置的forms.widgets.TextInput小部件(例如,HTML 属性或 JavaScript 事件),您可以创建一个从forms.widgets.TextInput继承其行为的定制小部件(例如,CustomerWidgetEmployeeWidget),对其进行定制,并直接在表单字段中使用定制小部件。

例如,假设您一直在修改 Django 文本小部件,使其包含 HTML 5 placeholder属性——该属性用于向最终用户提示输入表单字段的用途,当用户关注某个字段时,该属性就会消失。清单 6-30 展示了一个定制表单小部件,它基于分配给小部件的字段name属性生成 HTML 5 placeholder属性。

class PlaceholderInput(forms.widgets.Input):
      template_name = 'about/placeholder.html'
      input_type = 'text'
      def get_context(self, name, value, attrs):
            context = super(PlaceholderInput, self).get_context(name, value, attrs)
            context['widget']['attrs']['maxlength'] = 50
            context['widget']['attrs']['placeholder'] = name.title()
            return context
Listing 6-30.Django custom form widget inherits behavior from forms.widgets.Input

Note

forms.widgets.Input窗口小部件是比forms.widgets.TextInput更通用的窗口小部件,不包括文本输入行为;因此,由于它的基本特性集,它通常是构建定制小部件的首选。值得一提的是forms.widgets.Inputforms.widgets.TextInputforms.widgets.NumberInputforms.widgets.EmailInput以及表 6-2 中描述的其他输入控件的父控件。

正如您在清单 6-30 中看到的,PlaceholderInput类从内置的forms.widgets.Input小部件类继承了它的行为,使它能够访问与后者相同的行为和特性。

接下来是两个类字段。template_name字段定义了自定义小部件的支持模板,该模板指向'about/placeholder.html'–注意,如果您省略了template_name字段,则使用父类模板(即对于forms.widgets.Input小部件,模板是django/forms/widgets/input.html)。input_type字段是forms.widgets.Input子类所必需的,用于分配 HTML 输入type属性,值可以包括:textnumberemailurlpassword或任何其他有效的 HTML 输入type值。

自定义小部件类内部是get_context()方法,用于设置支持小部件模板的上下文——就像标准 Django 模板一样。在这种情况下,调用super()以确保设置了父窗口小部件类模板(即forms.widgets.Input)的上下文,并且紧接着在['widget']['attrs']字典中的窗口小部件上下文上设置了一对属性——maxlengthplaceholder——以便在窗口小部件模板内使用。注意maxlength的值是固定的,而placeholder的值取自字段name属性,并使用 Python 的标准title方法转换为标题。最后,get_context()方法返回更新后的context引用,将其传递给小部件模板。

现在让我们看看清单 6-31 中的小部件模板'about/placeholder.html'

# about/placeholder.html
<input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value }}"{% endif %}{% include "django/forms/widgets/attrs.html" %} />

# django/forms/widgets/attrs.html
{% for name, value in widget.attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value }}"{% endif %}{% endif %}{% endfor %}

Listing 6-31.Django custom form widget inherits behavior from forms.widgets.Input

您可以在清单 6-31 中看到,HTML input标签是用通过get_context方法设置的widget字典中的值生成的。widget.typewidget.namewidget.value的值都是在父类(即forms.widgets.Input)中后台设置的。此外,注意 HTML input标签使用了清单 6-31 底部所示的内置小部件模板django/forms/widgets/attrs.html。最后一个小部件模板循环遍历widgets.attrs上下文字典中的所有元素,并生成input属性。由于清单 6-30 中的widgets.attrs上下文字典被修改为包含maxlengthplaceholder属性,所以input标签是用这些附加属性生成的(例如<input type="text" name="email" maxlength="50" placeholder="Email">)。

现在让我们后退一步来讨论一下'about/placeholder.html'小部件模板的位置。默认情况下,Django 定制小部件只在INSTALLED_APPS中定义的所有 Django 应用的templates文件夹中搜索。这意味着,为了让 Django 找到定制的'about/placeholder.html'小部件模板,它必须放在任何项目应用的templates文件夹中(例如,给定一个名为about的项目应用,定制小部件应该位于templates/about/placeholder.html下,其中templates文件夹与应用的models.pyviews.py文件在同一级别)。在全局目录上定义定制的小部件是可能的,但是我将在关于小部件的最后一小节中讨论这一点。

最后,一旦在正确的位置定义了定制小部件,就可以将它作为 Django 表单字段的一部分(例如,email=forms.EmailField(widget=<pkg_location>.PlaceholderInput))来生成一个带有默认placeholder属性的 HTML input标签。

自定义表单小部件配置选项

在前两节关于定制表单小部件的内容中,为了避免偏离手头的主要任务,我没有提到各种配置选项。但是现在您已经知道了如何定制 Django 的内置小部件以及如何创建定制表单小部件,我可以为您提供这些额外的细节。

第一个主题与查找和加载定制小部件模板有关。Django 通过settings.py中的FORM_RENDERER变量定义了一个表单渲染器。表 6-4 描述了FORM_RENDERER变量支持的三个不同值。

正如你在表 6-4 中看到的,除了前面章节中使用的其他渲染器之外,甚至还有一个 Jinja 模板渲染器允许你定制由 Jinja 模板支持的部件。

在清单 6-30 中,您了解了定制小部件类如何使用字段(例如template_nameinput_type)来指定某些行为。小部件类中的字段高度依赖于父小部件类。例如,尽管template_name对所有内置小部件都有效,但是更特殊的内置小部件可以接受额外的字段。如果有疑问,请查阅用作小部件父类的内置小部件 4 支持的字段。

在清单 6-30 中,您还学习了如何通过get_context()方法访问和修改小部件的模板上下文。虽然在清单 6-30 中,您只向context['widget']['attrs']字典添加了几个小部件属性,但是父context['widget']字典是一个大型数据结构,存储了与小部件相关的所有数据,您可以在其中包含更新小部件的字段值。下面的代码片段展示了使用清单 6-30 中的PlaceholderInput小部件类的表单字段的context['widget']的内容。

{'widget': {'attrs': {'placeholder': 'Email', 'maxlength': 50}, 'name': 'email', 'is_hidden': False, 'type': 'text', 'value': None, 'template_name': 'about/placeholder.html', 'required': True}}

正如您所看到的,除了存储在attrs键下的 HTML 输入属性之外,还有其他的widget字段键(例如nameis_hidden)可以在模板中使用,以呈现最终的输出。这也意味着您可以在get_context()方法中向context['widget']字典添加自定义数据键,将它们集成为最终模板布局的一部分(例如'react':{<react_data>}'jquery':{<jquery_data>}).

Django 高级表单处理:部分表单、AJAX 和文件

在大多数情况下,Django 表单处理遵循本章第一节概述的步骤和代码序列,如图 6-1 所示。然而,Django 表单处理可能需要针对某些场景进行小的调整,比如涉及部分表单处理、AJAX 表单和文件上传的场景。

部分表单

有时,您可能会发现自己需要使用基于预先存在的 Django 表单的部分表单。例如,您收到一个请求,请求将一个类似的表单添加到目前使用的ContactForm中——在清单 6-25 中——但是不需要nameemail字段。由于ContactForm经过了测试,包括必要的后处理逻辑(例如,将表单数据添加到 CRM(客户关系管理)系统),重用ContactForm类是一个值得考虑的想法。但是如何部分地使用 Django 表单呢?

实现部分表单的第一种方法是对用户隐藏不需要的表单字段,允许原始表单类保持不变,同时不需要新的子类。这是侵入性最小的方法,但是您需要注意两个方面。第一个方面是表单模板布局,如清单 6-32 所示。

<form method="POST">
  {% csrf_token %}
    <div class="row">
      <div class="col-md-2">
        {{ form.comment.label_tag }}
          {% if form.comment.help_text %}
          <sup>{{ form.comment.help_text }}</sup>
          {% endif %}
          {% for error in form.comment.errors %}
           <div class="row">
             <div class="alert alert-danger">{{error}}</div>
           </div>
          {% endfor %}
       </div><div class="col-md-10 pull-left">
         {{ form.comment }}
       </div>
    </div>
    {{form.name.as_hiddden}}

    {{form.email.as_hidden}}

<input type="submit" value="Submit form" class="btn btn-primary">
</form>
Listing 6-32.Django form with fields marked as hidden

首先注意清单 6-32 中的表单字段是单独输出的,不使用快捷方式(如form.as_table)或像前面的一些表单布局例子那样循环。原因是,最后的form字段通过as_hidden方法被显式输出为隐藏——如表 6-4 所述。as_hidden方法生成一个表单域作为隐藏的 HTML 输入(如<input type="hidden"...>),不管它的表单域数据类型。这种机制有效地对最终用户隐藏了字段,但是将字段作为表单的一部分保留下来。

为什么将字段保留为表单的一部分而不仅仅是删除它们很重要?验证。记住表单实例没有变,只有需求变了;您可能不需要这个模板页面上的nameemail数据,但是表单实例和验证逻辑不知道这一点。所有这些把我们带到了你需要注意的第二个方面:必需的值。

如果您发布清单 6-32 中的模板而没有考虑第二个方面,当您意识到表单从未通过验证时,您可能会感到惊讶!为什么呢?底层的ContactForm类根据需要处理email字段,因此必须给它一个值,但是由于该字段现在对最终用户是隐藏的,您必须自己给email字段赋值——注意name字段不存在这个问题,因为它是用required=False配置的。因此,为了让清单 6-32 中的模板工作,表单必须用一个值初始化,如下所示:

form = ContactForm(initial={'email':'anonymous@gmail.com'})

通过以这种方式初始化表单,电子邮件表单字段被赋予这个初始值作为隐藏输入。因此,一旦用户提交表单,email 值就作为表单的一部分被传输回 Django,在那里通过验证,因为后处理逻辑得到了所有预期的值。当然,如果表单的字段都是可选的(也就是说,它们标有required=False,这个初始化过程是不必要的,因为验证无论如何都不会期望任何值。

实现部分表单的第二种方法是从表单类创建一个子类,并删除不需要的字段。这是一种更简单的方法,但是它需要创建一个 form 子类,这对于某些场景来说可能是多余的。清单 6-33 展示了如何子类化 Django 表单类并移除其父类的一些字段。

from coffeehouse.about.forms import ContactForm

class ContactCommentOnlyForm(ContactForm):
    def __init__(self, *args, **kwargs):
        super(ContactCommentOnlyForm, self).__init__(*args, **kwargs)
        del self.fields['name']
        del self.fields['email']

Listing 6-33.Django form subclass with removed parent fields

正如您在清单 6-33 中看到的,ContactCommentOnlyForm表单从ContactForm类继承了它的行为。接下来,在表单类__init__方法中,通过super()调用父类的__init__,并使用 Python 的delself.fields进行两次调用,从表单子类中删除nameemail字段。

一旦有了清单 6-33 中的表单子类,就可以在视图方法中生成一个未绑定的实例表单,并将其传递给模板进行呈现。由于最后一个子类删除了nameemail字段,所以在验证时,您不会遇到前面描述的隐藏字段缺少值的问题,因为ContactCommentOnlyForm表单现在只有一个字段。

AJAX 表单提交

AJAX 是一种技术,其中网页上的 JavaScript 与服务器端应用通信,并根据通信结果重新呈现网页,所有这些都不需要网页转换。Django 表单通常被设计成通过 AJAX 提交数据,以创建更流畅的工作流(即,不需要用 web 表单改变原始的 web 页面)

就初始表单交付而言,使用 AJAX 的表单的工作流程(图 6-1 中的步骤 1 和 2)与常规 Django 非 AJAX 表单的工作流程相同:在 view 方法中创建一个未绑定的表单实例,该实例被传递给一个模板,以呈现给最终用户的 web 页面。

通过 AJAX 提交数据的 Django 表单的第一个不同之处是,交付未绑定表单的网页必须包含必要的 JavaScript 逻辑,以便将表单数据发送到服务器端应用,并处理服务器端响应。清单 6-34 展示了使用 jQuery 库通过 AJAX 提交 Django 表单的 JavaScript 逻辑。

# NOTE: The following is only the Django (HTML) template with AJAX logic
# See Listing 6-35 for AJAX processing views.py and urls.py
<h4>Feedback</h4>
<div class="row">
  <div id="feedbackmessage"</div>
</div>
<form method="POST" id="feedbackform" action="{% url 'stores:feedback' %}">
  {% csrf_token %}
    <div class="row">
      <div class="col-md-12 pull-left">
         {{ form.comment }}
       </div>
    </div>
    {{form.name.as_hiddden}}
    {{form.email.as_hidden}}
<input type="submit" value="Submit feedback" class="btn btn-primary">
</form>

<script>
$(document).ready(function() {
    $("#feedbackform").submit(function(event) {
       event.preventDefault();
       $.ajax({ data: $(this).serialize(),
                type: $(this).attr('method'),
                url: $(this).attr('action'),
                success: function(response) {
                     console.log(response);
                     if(response['success']) {
                         $("#feedbackmessage").html("<div class='alert alert-success'>Succesfully sent feedback, thank you!</div>");
                         $("#feedbackform").addClass("hidden");
                     }
                     if(response['error']) {
                         $("#feedbackmessage").html("<div class='alert alert-danger'>" + response['error']['comment'] +"</div>");
                     }
                },
                error: function (request, status, error) {
                     console.log(request.responseText);
                }
       });
   });
})
</script>

Listing 6-34.JavaScript jQuery logic to submit Django form via AJAX

清单 6-34 的第一部分是标准的 Django 表单布局。然而,请注意<form>标签包含了action属性。在以前的 Django 表单中,没有使用action属性,因为服务 URL——GET 请求——和处理 URL——POST 请求——是同一个属性。在清单 6-34 中,action属性明确地告诉浏览器应该将 POST 请求发送到哪个 url——在本例中,{% url 'stores:feedback' %}是一个 Django 模板标签,它被翻译成一个 URL(例如/stores/feedback/)。在这种情况下,不同的 URL 是必要的,因为处理 AJAX 请求/响应的 Django 视图方法不同于处理标准 web 请求/响应的 Django 视图方法。

现在让我们把注意力转向清单 6-34 的下半部分和包含在<script>标签中的 JavaScript 逻辑。简而言之——因为描述 jQuery 语法超出了我们的讨论范围——网页等待点击#feedbackform表单的提交按钮。一旦检测到这个点击,使用表单数据的 AJAX POST 请求将被发送到表单的action属性中定义的 url。AJAX 请求完成后(即服务器端发回响应时),将分析 JSON 格式的响应。如果 AJAX 响应包含一条success消息,则该消息输出在表单的顶部,并且表单被隐藏以避免多次提交。如果 AJAX 响应包含一条error消息,该消息将输出到表单的顶部。

现在您已经知道 Django 表单数据是如何通过 AJAX 提交到服务器端的,清单 6-35 展示了处理这个 AJAX 请求有效负载所必需的 Django 视图方法。

# urls.py (Main)
urlpatterns = [
    url(r'^stores/',include('coffeehouse.stores.urls',namespace="stores"))
]

# urls.py (App stores)
urlpatterns = [
    url(r'^$',views.index,name="index"),
    url(r'^feedback/$',views.feedback,name="feedback"),
]

# views.py (App stores)
from django.http import HttpResponse, JsonResponse
from coffeehouse.about.forms import ContactForm

def feedback(request):
    if request.POST:
        form = ContactForm(request.POST)
        if form.is_valid():
            return JsonResponse({'success':True})
        else:
            return JsonResponse({'error':form.errors})
    return HttpResponse("Hello from feedback!")

Listing 6-35.Django view method to process Django form via AJAX

关于清单 6-35 中的视图方法,最重要的一点是它只处理 POST 请求。注意,如果if request.POST:语句不为真,view 方法总是用Hello from feedback!字符串来响应。

接下来,在request.POST段中,流程开始时非常类似于标准 Django 表单处理序列(即,创建一个绑定表单实例,并调用表单的is_valid()方法来验证用户提供的数据)。但是,请注意使用JsonResponse()的有效和无效表单数据的响应。因为 AJAX 表单提交是在 JavaScript 上下文中操作的,所以 JSON 格式响应是一种常见的格式,用于将响应发送回浏览器。

如果您将注意力转回到清单 6-34 ,您可以看到 JavaScript 逻辑被设计为处理和输出清单 6-35 中的 view 方法返回的 JSON 响应(successerror)。

表单中的文件

Django 提供了几个表单域来通过表单捕获文件——如表 6-2 中描述的forms.ImageField()forms.ImageField()。然而,尽管这些表单域生成必要的 HTML 和验证逻辑来强制文件通过表单提交,但是在表单中处理文件还是有一些微妙之处。

第一个问题是用于传输文件的表单的 HTML <form>标签必须显式地将编码类型设置为enctype="multipart/form-data",此外还要使用method=POST。第二个问题是文件的内容被放在 Django request对象的特殊FILES字典键下。清单 6-36 展示了一个带有文件字段的表单,它对应的视图方法,以及它的模板布局。

# forms.py
from django import forms

class SharingForm(forms.Form):
    # NOTE: forms.PhotoField requires Python PIL & other operating system libraries,
    #       so generic FileField is used instead
    video = forms.FileField()
    photo = forms.FileField(widget=forms.ClearableFileInput(attrs={'multiple': True}))

# views.py
def index(request):
    if request.method == 'POST':
        # POST, generate form with data from the request
        form = SharingForm(request.POST,request.FILES)
        # check if it's valid:
        if form.is_valid():
            # Process file data in request.FILES
            # Process data, insert into DB, generate email,etc
            # redirect to a new URL:
            return HttpResponseRedirect('/about/contact/thankyou')
    else:
        # GET, generate blank form
        form = SharingForm()
    return render(request,'social/index.html',{'form':form})

# social/index.html
<form method="post" enctype="multipart/form-data">
  {% csrf_token %}
  <ul>
    {{form.as_ul}}
  </ul>
    <input type="submit" value="Submit photo" class="btn btn-primary">
</form>

Listing 6-36.Django form with file fields, corresponding view method, and template layout

请注意 HTML <form>标签是如何声明所需属性的,另外请注意request.FILES引用是如何用于创建绑定表单的——以及标准的request.POST。清单 6-36 中剩余的表单结构与常规的非文件表单相同(例如,创建未绑定表单,调用is_valid())。

但是类似于常规的 Django 表单处理,一旦表单有效——如果is_valid()返回True——您将希望对表单数据的内容做一些事情;因此在这种情况下,你需要访问request.FILES来处理用户上传的文件。

但是在描述如何处理request.FILES中的文件内容之前,理解request.FILES字典的可能内容是很重要的:

Option 1) request.FILES = {'photo': [<InMemoryUploadedFile>,<TemporaryUploadedFile>], 'video': [<InMemoryUploadedFile>]}
Option 2) request.FILES = {'photo': [<TemporaryUploadedFile>],'video': [<InMemoryUploadedFile>]}

request.FILES字典包含对应于文件字段名的键。在这种情况下,您可以看到photovideo键对应于清单 6-36 中声明的表示文件的表单字段名称。接下来,注意每个键的值都是一个列表,不管文件表单域是否可以接受多个文件——比如photo,因为被覆盖的小部件具有'multiple': True属性——或者在video的情况下接受单个文件。

现在让我们分析分配给每个文件表单字段键的列表内容。每个列表元素代表一个上传的文件。但是请注意这些文件是如何由InMemoryUploadedFileTemporaryUploadedFile类表示的——这两个类都是django.core.files.uploadedfile.UploadedFile类的子类。上传文件由InMemoryUploadedFileTemporaryUploadedFile实例类表示的原因取决于上传文件的大小。

事实证明,在您决定如何处理上传的文件之前,Django 必须决定做什么以及将上传的文件放在哪里。为此,Django 依赖于上传处理程序。默认情况下,除非在settings.py,中的FILE_UPLOAD_HANDLERS变量中被显式覆盖,否则 Django 使用以下两个文件上传处理程序:

FILE_UPLOAD_HANDLERS= [
    'django.core.files.uploadhandler.MemoryFileUploadHandler',
    'django.core.files.uploadhandler.TemporaryFileUploadHandler',
]

默认情况下,如果文件小于 2.5 MB,Django 使用MemoryFileUploadHandler将上传文件的内容放入内存;如果文件大于 2.5 MB,Django 使用TemporaryFileUploadHandler将上传文件的内容放在一个临时文件中(例如/tmp/Af534.upload)。

你可以在settings.py中定义FILE_UPLOAD_HANDLERS,只包含你想要的文件上传处理器(例如,如果你想节省内存资源,删除MemoryFileUploadHandler)。但是,请注意,文件上传处理程序必须始终处于活动状态,文件上传才能正常进行。如果您不喜欢 Django 内置文件上传处理程序的行为,那么您需要编写自己的文件上传处理程序。 5

除了在settings.py中定义FILE_UPLOAD_HANDLERS之外,您还可以在settings.py中声明另外两个参数来影响文件上传处理程序的行为。FILE_UPLOAD_MAX_MEMORY_SIZE变量设置文件保存在内存中与作为临时文件处理的阈值大小,默认为2621440字节(或 2.5 MB)。并且FILE_UPLOAD_TEMP_DIR变量设置必须保存临时上传文件的目录,默认为None,意味着使用操作系统的临时文件目录(例如,在 Linux OS 上/tmp/)。

现在您已经知道了 Django 在哪里以及如何存储上传的文件,甚至在您决定如何处理它们之前,让我们看看如何处理request.FILES的内容。清单 6-37 显示了清单 6-36 的延续,其中包含处理上传文件所需的相关代码片段。

# views.py

from django.conf import settings

def save_uploaded_file_to_media_root(f):

    with open('%s%s' % (settings.MEDIA_ROOT,f.name), 'wb+') as destination:

        for chunk in f.chunks():

            destination.write(chunk)

def index(request):
    if request.method == 'POST':
        # POST, generate form with data from the request
        form = SharingForm(request.POST,request.FILES)
        # check if it's valid:
        if form.is_valid():
            for field in request.FILES.keys():

                for formfile in request.FILES.getlist(field):

                    save_uploaded_file_to_media_root(formfile)

            return HttpResponseRedirect('/about/contact/thankyou')
    else:
        # GET, generate blank form
        form = SharingForm()
    return render(request,'social/index.html',{'form':form})

Listing 6-37.Django form file processing with save procedure to MEDIA_ROOT

在 form is_valid()方法之后,您知道request.FILES字典中的每个键都是文件表单字段,因此创建了一个循环来获取每个文件字段(即videophoto)。接下来,使用request.FILES字典的getlist()方法,获得每个文件字段的文件实例列表(即InMemoryUploadedFile,TemporaryUploadedFile),并遍历每个元素以获得文件实例。最后,在每个文件实例上执行save_uploaded_file_to_media_root()方法。

每个文件实例——在清单 6-37 中表示为formfile引用——要么是InMemoryUploadedFile要么是TemporaryUploadedFile实例类型。但是正如我前面提到的,这些类是从父类django.core.files.uploadedfile.UploadedFile派生的,这意味着文件实例也是UploadedFile实例。

按照设计,Django django.core.files.uploadedfile.UploadedFile类有一系列方法和字段,专门用于轻松处理上传文件的内容,其中一些包括:

<file_instance>.name。-输出文件的名称,就像在用户的计算机上一样。

  • <file_instance>.size。-输出文件的大小,以字节为单位。
  • <file_instance>.content_type。-输出分配给文件的 HTTP 内容类型头(例如,text/html、application/zip)。
  • <file_instance>.chunks()。-一种生成器方法,可以高效地输出文件的块内容。

通过使用这些UploadedFile字段和方法,您可以在清单的顶部看到 6-37save_uploaded_file_to_media_root()方法逻辑包括使用其原始名称保存上传的文件——使用 Python 的标准with...open语法——然后使用chunks()方法有效地将上传文件的所有片段写入文件系统。

Note

列表 6-37 中上传的文件保存在设置下。媒体根文件夹。虽然您可以将文件保存到任何想要的位置,但是 Django 中用户上传文件的标准做法是将它们保存在 settings.py 中定义的 MEDIA_ROOT 文件夹下。

Django formsets

因为 Django 表单代表了用户将数据引入 Django 项目的主要方式,所以 Django 表单被用作数据捕获机制并不罕见。然而,这可能导致效率问题,因为每个 web 页面都依赖一个 Django 表单。Django 表单集允许您将多个相同类型的表单集成到一个模板中——带有所有必要的验证和布局工具——通过多个表单简化数据捕获。

好消息是,到目前为止,您对 Django 表单的所有了解——表单字段、验证工作流、模板布局、小部件和所有其他主题——同样适用于 Django 表单集。这意味着表单集的学习曲线相当简单,尽管您必须学习一些新概念。

表单集初始化。尽管表单集被初始化为一组常规的 Django 表单,但是表单集初始化需要特定于表单集的参数。

  • 表单集管理表单。-为了跟踪同一页面上的多个 Django 表单,表单集还使用了一种特殊的表单,称为“管理表单”。

假设您正在向 coffehouse 应用添加在线订购功能。您已经有了一个饮料表单,用户可以订购一种饮料,但是希望添加用户订购多种饮料的功能,因此您需要在同一页面上有多个饮料表单或一个饮料表单集。

清单 6-38 展示了独立的DrinkForm类,生成空表单集的相应视图方法,以及用于显示表单集的模板布局。

# forms.py
from django import forms

DRINKS = ((None,'Please select a drink type'),(1,'Mocha'),(2,'Espresso'),(3,'Latte'))
SIZES = ((None,'Please select a drink size'),('s','Small'),('m','Medium'),('l','Large'))

class DrinkForm(forms.Form):
    name = forms.ChoiceField(choices=DRINKS,initial=0)
    size = forms.ChoiceField(choices=SIZES,initial=0)
    amount = forms.ChoiceField(choices=[(None,'Amount of drinks')]+[(i, i) for i in range(1,10)])

# views.py

from django.forms import formset_factory

def index(request):
    DrinkFormSet = formset_factory(DrinkForm, extra=2, max_num=20)

    if request.method == 'POST':
        # TODO
    else:
        formset = DrinkFormSet(initial=[{'name': 1,'size': 'm','amount':1}])

    return render(request,'online/index.html',{'formset':formset})

# online/index.html
<form method="post">
          {% csrf_token %}
    {{ formset.management_form }}

    <table>
        {% for form in formset %}

        <tr><td><ul class="list-inline">{{ form.as_ul }}</ul></td></tr>
        {% endfor %}

    </table>
    <input type="submit" value="Submit order" class="btn btn-primary">
</form>

Listing 6-38.Django formset factory initialization and template layout

清单 6-38 中的DrinkForm类使用标准的 Django 表单语法,所以没有什么新东西。然而,index视图方法是从使用django.forms.formset_factory方法开始的。formset_factory用于从给定的表单类生成一个FormSet类。在这种情况下,注意formset_factory使用DrinkForm参数——代表表单类——来生成DrinkForm表单集。我将很快提供关于额外的formset_factory参数的细节。

接下来,在清单 6-38 中,您可以看到一个未绑定的Drink FormSet()实例是用一个initial值创建的——类似于如何创建独立的未绑定表单并使用initial参数。然而,注意initial值是一个列表,不像标准表单中使用的独立字典。因为表单集是一组表单,所以表单集的initial值是一组字典,其中每个字典代表每个表单的initial值。在清单 6-38 的情况下,formset 的一个表单被设置为用{'name': 1,'size': 'm','amount':1}值初始化。

在清单 6-38 的底部,你可以看到formset的模板。独立表单模板的第一个区别,是输出管理表单的{{ formset.management_form }}语句。第二个区别是formset参考输出的不同形式。在这种情况下,每个 formset 表单都以完整的形式输出,而{{form.as_ul}}作为内嵌表单,但是您同样可以使用任何 Django 表单模板布局技术以定制的方式输出每个表单实例(例如,删除字段 id 值)。

表单集工厂

清单 6-38 中使用的formset_factory()方法是使用表单集的核心方法之一。尽管清单 6-38 中的例子只使用了三个参数,但是formset_factory()方法可以接受多达九个参数。下面的代码片段说明了formset_factory()方法中每个参数的名称和默认值。

formset_factory(form, formset=BaseFormSet, extra=1, can_order=False, can_delete=False,
                  max_num=None, min_num=None, validate_max=False, validate_min=False)

正如您可以在这个代码片段中确认的那样,formset_factory()方法唯一需要的参数(即没有默认值)是form。每个参数的含义如下:

form。-定义要在其上创建窗体集的窗体类。

formset。-定义表单集基类,默认为django.forms.formsets.BaseFormSet。当您需要自定义窗体集进行自定义窗体集验证时会发生变化。

extra。-定义添加到窗体集中的空窗体的数量。如果一个表单集的表单都是空的(也就是说,它们没有被初始化),那么一个表单集包含了extra个表单。如果一个窗体集的窗体包含数据(即它们被初始化),一个窗体集包含初始化窗体的数量+ extra(空窗体)。

can_order。-将 ORDER 字段(作为一个forms.IntegerField)添加到表单集中的每个表单,目的是改变表单集中的表单顺序。默认为False

can_delete。-向表单集中的每个表单添加删除字段(作为一个forms.BooleanField),目的是将表单集中的一个表单标记为删除。默认为False

max_num。-定义要在窗体集中显示的最大窗体数。默认为None,表示最多显示 1000 个表单实例。

min_num。-定义要在窗体集中显示的最小窗体数。默认为None,表示至少显示 0 个表单实例。

validate_max。-与max_num一起使用,以确保验证得到执行,并且表单实例不会超过max_num。默认为False

validate_min。-与min_num一起使用,以确保实施验证,并且表单实例永远不会少于min_num。默认为False

现在你已经知道了各种formset_factory()方法选项,让我们把注意力转回到清单 6-38 中使用的方法:

formset_factory(DrinkForm, extra=2, max_num=20)

这个formset_factory方法用DrinkForm类创建一个表单集;extra=2表示总是包含 2 个空的DrinkForm实例;这意味着,因为表单集是用清单 6-38 中的一个DrinkForm实例初始化的,所以表单集中的表单总数将是 1,加上两个空表单,因为extra=2;``max_num=20参数表明表单集应该包含最多 20 个DrinkForm实例。

表单集管理表单和表单集处理

要理解表单集的管理表单的目的,最简单的方法是查看该表单包含的内容。清单 6-39 基于清单 6-38 中的例子展示了表单集管理表单的内容。

<input type="hidden" name="form-TOTAL_FORMS" value="3" id="id_form-TOTAL_FORMS" />
<input type="hidden" name="form-INITIAL_FORMS" value="1" id="id_form-INITIAL_FORMS" />
<input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS" />
<input type="hidden" name="form-MAX_NUM_FORMS" value="20" id="id_form-MAX_NUM_FORMS" />
Listing 6-39.Django formset management form contents and fields

正如您在清单 6-39 中看到的,表单集管理表单的内容(即清单 6-38 中的{{formset.management_form}}语句)是四个隐藏的输入变量。form-TOTAL_FORMS字段表示表单集中的表单总数;form-INITIAL_FORMS字段表示表单集中已初始化表单的总数;form-MIN_NUM_FORMS字段表示表格集中表格的最小数量;而form-MAX_NUM_FORMS字段表示表单集中表单的最大数量。

乍一看,清单 6-39 中的变量似乎并不重要,但是一旦表单集中的各种表单进入处理和呈现阶段,它们就在表单集中扮演了重要的角色。为了更好地说明这些表单集管理字段的相关性,让我们修改清单 6-38 中的表单集,以允许用户添加更多的饮料表单,这样他们就可以根据需要增加订单。

#views.py
def index(request):
    extra_forms = 2
    DrinkFormSet = formset_factory(DrinkForm, extra=extra_forms, max_num=20)
    if request.method == 'POST':
        if 'additems' in request.POST and request.POST['additems'] == 'true':

            formset_dictionary_copy = request.POST.copy()

            formset_dictionary_copy['form-TOTAL_FORMS'] = int(formset_dictionary_copy['form-TOTAL_FORMS']) + extra_forms

            formset = DrinkFormSet(formset_dictionary_copy)

        else:

            formset = DrinkFormSet(request.POST)

            if formset.is_valid():

                return HttpResponseRedirect('/about/contact/thankyou')

    else:
        formset = DrinkFormSet(initial=[{'name': 1,'size': 'm','amount':1}])
    return render(request,'online/index.html',{'formset':formset})

# online/index.html
<form method="post">
          {% csrf_token %}
    {{ formset.management_form }}
    <table>
        {% for form in formset %}
        <tr><td>{{ form }}</td></tr>
        {% endfor %}
    </table>
    <input type="hidden" value="false" name="additems" id="additems">

    <button class="btn btn-primary" id="additemsbutton">Add items to order</button>

    <input type="submit" value="Submit order" class="btn btn-primary">
</form>

<script>

$(document).ready(function() {

        $("#additemsbutton").on('click',function(event) {

         $("#additems").val("true");

       });

});

</script>

Listing 6-40.Django formset designed to add extra forms by user

清单 6-40 中的第一个重要修改来自表单集模板。请注意,表单声明了一个名为additems的隐藏输入字段,设置为false,以及一个按钮,单击该按钮会将隐藏输入字段的值更改为true。这种机制是一个简单的控制变量,用于跟踪用户何时想要向订单添加更多的项目(例如,向表单集添加更多的饮料表单)。

现在让我们转向清单 6-40 中处理表单集的视图方法。首先,注意表单集使用设置为 2 的extra_forms变量来定义表单集的extra值,所以初始表单集工厂包含两个额外的表单,就像清单 6-38 中一样。

接下来是request.POST处理表单集部分,它有两种可能的结果。如果request.POST包含了additems字段,并且它被设置为true,这表明用户点击了按钮,向表单集中添加了更多的表单。如果request.POST不包含additems字段或者它被设置为 false,则表明用户点击了标准的提交按钮,因此创建了一个绑定的表单集,并在表单集上调用了is_valid()。现在,让我们来分析一下每种可能结果背后的逻辑。

当用户向表单集中添加更多表单时,会生成一个request.POST的副本——因为request.POST是不可变的,所以copy()方法是必需的。接下来,通过添加extra_forms变量,修改表单集的管理表单字段form-TOTAL_FORMS以反映附加的表单。最后,DrinkFormSet用这个新修改的request.POST字典重新绑定——用一个改变的form-TOTAL_FORMS。当重新绑定的表单集被发送回用户时,由于简单地修改了form-TOTAL_FORMS管理表单集字段,该表单集现在将包含两个额外的空表单。

如果表单集属于清单 6-40 中的标准后处理和验证部分,会发生以下情况。首先,使用request.POST创建一个绑定表单集——就像使用常规绑定表单一样——接下来,在表单集上调用is_valid()方法——也像在常规绑定表单中一样。如果is_valid()返回 true(即表单集或单个表单中没有错误),则重定向到成功页面。如果is_valid()返回 false(即在表单集或单个表单中有错误),一个errors字典被附加到表单集引用,控制落到最后一行—return render(request,'online/index.html',{'formset':formset})—它发送带有错误的formset实例以显示在模板上——这个过程也几乎与标准表单验证和错误管理相同。

正如您所看到的,通过对 Django 表单集的管理表单中的一个字段进行微小的修改,就可以动态地改变表单集中的表单数量。注意,在大多数情况下,没有必要直接操作表单集的管理表单字段,Django 更多时候在幕后使用这些值来跟踪处理和呈现任务。尽管一旦您创建了更高级的表单集行为(例如,订购表单、删除表单),剩余的管理表单集字段承担了同样重要的角色,可能需要直接操作。

表单集自定义验证和表单集错误

表单集中的所有表单都根据上一节中解释的表单验证规则进行验证(例如,validatorsclean_<field>()方法)。然而,有时有必要在一个表单集中实施表单间规则,在这种情况下,你需要构建一个自定义的表单集类,如清单 6-41 所示。

from django.forms import BaseFormSet

class BaseDrinkFormSet(BaseFormSet):
    def clean(self):
        # Check errors dictionary first, if there are any error, no point in validating further
        if any(self.errors):
            return
        name_size_tuples = []
        for form in self.forms:
            name_size = (form.cleaned_data['name'],form.cleaned_data['size'])
            if name_size in name_size_tuples:
                raise forms.ValidationError("Ups! You have multiple %s %s items in your order, keep one and increase the amount" % (dict(SIZES)[name_size[1]],dict(DRINKS)[int(name_size[0])]))
            name_size_tuples.append(name_size)

Listing 6-41.Django custom formset with custom validation

首先,注意清单 6-41 中的类继承了django.forms.BaseFormSet类的行为,赋予它 Django 表单集的所有基本功能。接下来,自定义 formset 类定义了一个clean()方法,该方法与标准 Django 表单中的clean()方法具有相同的用途:在整体(即 formset)而不是单个部分(即表单)上实施验证规则。clean()方法中的验证逻辑强制规定,如果表单集中的两个表单具有相同的namesize,则会引发错误。

请注意清单 6-41 中验证错误创建也使用了标准表单中使用的相同的forms.ValidationError()类。如果clean()方法引发了一个验证错误,这个错误将被分配给一个特殊的字段,这个字段在表单集的errors字典中被称为non_form_errors。清单 6-42 显示了清单 6-38 中表单集模板的更新版本,说明了如何消除表单集的非表单错误。

<form method="post">
          {% csrf_token %}
    {{ formset.management_form }}
    {% if formset.non_form_errors %}

      <div class="alert alertdanger">{{formset.non_form_errors}}</div>

    {% endif %}

     {{ formset.management_form }}
    <table>
        {% for form in formset %}
        <tr><td><ul class="list-inline">{{ form.as_ul }}</ul></td></tr>
        {% endfor %}
    </table>
</form>
Listing 6-42.Django custom formset to display non_form_errors

您可以在清单 6-42 中的{{formset.management_form}}语句下面看到一个条件循环,它输出放置在表单集的errors字典的non_forms_errors键中的任何错误。虽然名称不同,但formset.non_form_errors的目的与常规 Django 表单的form.non_field_errors相同,显示与特定零件无关的错误。因为表单集使用表单构造,所以错误变量称为non_forms_errors,因为表单使用字段构造,所以变量称为non_field_errors

请注意,如果表单集的表单中出现任何错误,它们会像在常规表单中一样被放置在表单的error字典中,因此您可以使用清单 6-28 中概述的技术来自定义表单集中各个表单错误的输出。

Django Form Tools and Django Crispy Forms

除了你在本章中学到的 Django 内置表单功能,还有几个第三方 Django 应用值得一提,它们旨在解决更高级的 Django 表单问题。

Django 表单工具包 6 支持表单审核流程和表单向导的创建。在 Django 表单的数据被验证之后,表单检查过程会强制进行预览,当您希望最终用户在表单生命周期结束之前仔细检查他们的表单数据(例如,预订或采购订单)时,这个过程非常有用。表单向导由不同页面中的分组表单组成,作为序列的一部分(例如,注册或调查问卷)。Django 表单工具包的好处是,它实现了支持这些类型的 Django 表单工作流所需的所有“低级”逻辑。

Django crisp forms7是另一款专注于高级表单布局的第三方 Django 应用。Django crispy forms 是 Django 表单与 Bootstrap 库集成以及需要复杂小部件和模板布局的表单(例如,内联和水平表单)的流行选择。

Footnotes 1

https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods

2

https://docs.djangoproject.com/en/1.11/ref/validators/#built-in-validators

3

https://en.wikipedia.org/wiki/Responsive_web_design

4

https://docs.djangoproject.com/en/1.11/ref/forms/widgets/#built-in-widgets

5

https://docs.djangoproject.com/en/1.11/ref/files/uploads/#custom-upload-handlers

6

https://django-formtools.readthedocs.io/

7

http://django-crispy-forms.readthedocs.io/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值