除非您计划搭建的网站和应用只发布内容且不接收访问者的输入,否则您就需要理解和使用表单。
Django提供了一系列的工具和库来帮助您构建表单来接收网站访客的输入,然后处理以及响应这些输入。
在HTML中,表单是在 <form>...</form>
中的一些元素,它允许访客做一些类似输入文本、选择选项、操作对象或空间等动作,然后发送这些信息到服务端。
一些表单界面元素(文本框或复选框)内置在HTML中。其他会更复杂些;像弹出日期选择或者允许您移动滑块或者操作控件,一般通过使用JavaScript,CSS以及HTML表单中的 <input>
元素来实现这些效果。
和它的元素 <input>
一样,表单必须指定两样东西:
例如,Django admin的登录表单包含了一些 <input>
元素:用户名用 type="text"
,密码用 type="password"
,登录按钮用 type="submit"
。它还包含一些用户看不到的隐藏文本字段,Django用它们来决定下一步行为。
它还告诉浏览器表单数据应该发往 <form>
的 action
属性指定的URL—— /admin/
,并且应该使用它的 method
属性指定的HTTP方法—— post
。
当 <input type="submit" value="Log in">
元素被触发的时候,数据会发送到 /admin/
。
GET
和 POST
¶处理表单时只会用到 GET
和 POST
两种HTTP方法。
Django的登录表单使用 POST
方法传输数据,在这个方法中浏览器会封装表单数据,为了传输会进行编码,然后发送到服务端并接收它的响应。
相比之下,GET
方法将提交的数据捆绑到一个字符串中,并用它来组成一个URL。该URL包含了数据要发送的地址以及一些键值对应的数据。如果您在Django文档中进行一次搜索,就会看到这点,它会生成一个形似 https://docs.djangoproject.com/search/?q=forms&release=1
的URL。
GET
和 POST
通常用于不同的目的。
任何可能用于更改系统状态的请求应该使用 POST
—— 比如一个更改数据库的请求。GET
应该只被用于不会影响系统状态的请求。
GET
也不适合密码表格,因为密码会出现在 URL 中,因此也会出现在浏览器历史和服务器日志中,都是纯文本。它也不适合于大量的数据,或二进制数据,如图像。一个使用 GET
请求管理表单的网络应用程序是一个安全风险:攻击者很容易模仿表单的请求来获得对系统敏感部分的访问。POST
,加上其他保护措施,如 Django 的 CSRF 保护,可以对访问进行更多控制。
另一方面, GET
方法适用于诸如网页搜索表单这样的内容,因为这类呈现为一个 GET
请求的URL很容易被存为书签、分享或重新提交。
处理表单是一件挺复杂的事情。想想看Django的admin,许多不同类型的数据可能需要在一张表单中准备显示,渲染成HTML,使用方便的界面进行编辑,传到服务器,验证和清理数据,然后保存或跳过进行下一步处理。
Django的表单功能可以简化和自动化上述工作的大部分内容,并且也能比大多数程序员自己编写代码去实现来的更安全些。
Django会处理涉及表单的三个不同部分:
您 可以 手动编写代码来实现,但Django 可以帮你完成所有这些工作。
我们已经简单的描述过了HTML 表单,但是一个HTML <form>
只是其所需的一部分。
在网络应用的上下文中,“表单”可能指的是那个HTML <form>
,或者指产生它的 Django Form
,或者指它提交时返回的结构化数据,或者指这些部分的端到端工作集合。
Form
类¶Django表单系统的核心组件是 Form
类。它与Django模型描述对象的逻辑结构、行为以及它呈现给我们内容的形式的方式大致相同, Form
类描述一张表单并决定它如何工作及呈现。
类似于模型类的字段映射到数据库字段的方式,表单类的字段会映射到HTML表单的 <input>
元素。 ModelForm
通过 Form
映射模型类的字段到HTML表单的 <input>
元素,Django admin就基于此。
表单字段本身也是类;他们管理表单数据并在提交表单时执行验证。 DateField
和 FileField
处理的数据类型差别很大,所以必须用来处理不同的字段。
在浏览器中,表单字段以HTML“控件”(用户界面的一个片段)的形式展现给我们。每个字段类型都有与之相匹配的 控件类 ,但必要时可以覆盖。
在Django中渲染一个对象的时候,我们通常:
在模板中渲染表单几乎与渲染任何其他类型的对象的一样,但是存在一些关键性的差异。
如果模型实例不包含数据,在模板中对它做任何处理几乎没什么用。但完全有理由用来渲染一张空表单——当我们希望用户来填充的时候就会这么做。
所以当我们在视图中处理模型实例时,我们一般从数据库中获取它。当我们处理表单时,我们一般在视图中实例化它。
When we instantiate a form, we can opt to leave it empty or prepopulate it, for example with:
最后一种情况最有趣,因为这使得用户不仅可以阅读网站,而且还可以将信息发回给它。
假设您希望在您的网站上创建一张简易的表单,用来获取用户的名字。您需要在模板中使用类似代码:
<form action="/your-name/" method="post">
<label for="your_name">Your name: </label>
<input id="your_name" type="text" name="your_name" value="{{ current_name }}">
<input type="submit" value="OK">
</form>
这告诉浏览器将表单数据返回给URL /your-name/
,并使用 POST
方法。它将显示一个标签为 "Your name:" 的文本字段,以及一个 "OK" 按钮。如果模板上下文包含一个 current_name
变量,它会被预填充到 your_name
字段。
您需要一个视图来渲染这个包含HTML表单的模板,并能适当提供 current_name
字段。
提交表单时,发送给服务器的 POST
请求将包含表单数据。
现在,您还需要一个与该 /your-name/
URL相对应的视图,该视图将在请求中找到相应的键/值对,然后对其进行处理。
This is a very simple form. In practice, a form might contain dozens or hundreds of fields, many of which might need to be prepopulated, and we might expect the user to work through the edit-submit cycle several times before concluding the operation.
我们可能需要在浏览器中进行一些验证,甚至在表单提交之前;我们可能希望使用更复杂的字段 ,以允许用户做类似日期选择等操作。
此刻,我们很容易通过使用Django来完成以上大部分工作。
Form
类¶我们已经很清楚想要的HTML表单看起来会是什么样子。首先,在Django中这样做:
from django import forms
class NameForm(forms.Form):
your_name = forms.CharField(label="Your name", max_length=100)
它定义了一个只包含一个字段( your_name
)的 Form
类。我们已经为这个字段提供了友好的标签,当它渲染后会显示在 <label>
中(在这种情况下,如果我们省略之前指定的 label
,它还是会自动生成一个一样的标签)。
字段的最大长度由 max_length
来定义。它做了两件事情。首先它在HTML的 <input>
上增加了 maxlength="100"
(这样浏览器会在第一时间阻止用户输入超过这个数量的字符串)。其次它还会在Django收到浏览器传过来的表单时,对数据长度进行验证(也就是服务器端验证)。
A Form
instance has an is_valid()
method, which runs
validation routines for all its fields. When this method is called, if all
fields contain valid data, it will:
True
cleaned_data
中。这样整个表单在第一次渲染时,会显示如下:
<label for="your_name">Your name: </label>
<input id="your_name" type="text" name="your_name" maxlength="100" required>
注意它 没有 包含 <form>
标签和提交按钮。我们必须自己在模板中提供。
发回Django网站的表单数据由视图来处理,一般和发布这个表单用的是同一个视图。这允许我们重用一些相同的逻辑。
为了处理表单,我们需要将它实例化到我们希望发布的URL的对应的视图中:
from django.http import HttpResponseRedirect
from django.shortcuts import render
from .forms import NameForm
def get_name(request):
# if this is a POST request we need to process the form data
if request.method == "POST":
# create a form instance and populate it with data from the request:
form = NameForm(request.POST)
# check whether it's valid:
if form.is_valid():
# process the data in form.cleaned_data as required
# ...
# redirect to a new URL:
return HttpResponseRedirect("/thanks/")
# if a GET (or any other method) we'll create a blank form
else:
form = NameForm()
return render(request, "name.html", {"form": form})
如果我们访问这个视图用的是 GET
请求,它会创建一个空的表单实例并将其放置在模板上下文中进行渲染。这是我们在首次访问这个URL时能预料到会发生的情况。
如果表单提交用的是 POST
请求,那么该视图将再次创建一个表单实例并使用请求中的数据填充它: form = NameForm(request.POST)
这叫“绑定数据到表单” (现在它是一张 绑定的 表单)。
我们调用表单的 is_valid()
方法;如果不为 True
,我们带着表单返回到模板。这次表单不再为空( 未绑定 ),所以HTML表单将用之前提交的数据进行填充,放到可以根据需要进行编辑和修正的位置。
如果 is_valid()
为 True
,我们就能在其 cleaned_data
属性中找到所有通过验证的表单数据。我们可以在发送一个HTTP重定向告诉浏览器下一步去向之前用这些数据更新数据库或者做其他处理。
我们没有必要在模板 name.html
中做过多的操作:
<form action="/your-name/" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit">
</form>
所有的表单字段及其属性都将通过Django模板语言从 {{ form }}
中被解包成HTML标记。
表格和跨站请求伪造保护
Django自带一个简单易用的 跨站请求伪造防护 。当通过 POST
方法提交一张启用了CSRF防护的表单时,您必须使用上例中这样的模板标签 csrf_token
。但是,由于CSRF防护在模板中没有与表单直接绑定,因此这个标签在本页文档之后的示例中都将被忽略。
HTML5输入类型和浏览器验证
如果您的表单包含 URLField
, EmailField
或者其他整数字段类型,Django将使用 url
, email
和 number
HTML5输入类型。默认情况下,浏览器可能会在这些字段上应用他们自己的验证,这也许比Django的验证更加严格。如果您想禁用这个行为,请在 form
标签上设置 novalidate 属性,或者在字段上指定一个不同的控件,比如 TextInput
。
现在我们有了一个可以工作的web表单,它通过一张Django Form
描述,由一个视图来处理并渲染成一个HTML <form>
。
以上是您入门需要了解的所有内容,但是表单框架提供了更多垂手可得的内容。一旦您理解了上述过程的基础知识,您应该再了解下表单系统的其他功能,然后学习更多的底层机制。
Form
类¶所有表单类都作为 django.forms.Form
或者 django.forms.ModelForm
的子类来创建。您可以把 ModelForm
想象成 Form
的子类。实际上 Form
和 ModelForm
从(私有) BaseForm
类继承了通用功能,但是这个实现细节不怎么重要。
模型和表单
实际上,如果您的表单是要直接用来添加或编辑Django模型,用 ModelForm ,可以省时、省力、省代码,因为它会根据 Model
类构建一张对应字段及其属性的表单。
The distinction between 绑定和非绑定表单 is important:
表单的 is_bound
属性将告诉您一张表单是否具有绑定的数据。
考虑一下比我们上面的小示例更有用的一张表单,我们可以用它在个人网站上实现“联系我”的功能:
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
message = forms.CharField(widget=forms.Textarea)
sender = forms.EmailField()
cc_myself = forms.BooleanField(required=False)
我们之前的表单只用了一个 CharField
类型的字段 your_name
。在这个例子中,我们的表单有四个字段: subject
、 message
、 sender
和 cc_myself
。只用到三种可用的字段类型: CharField
、 EmailField
和 BooleanField
;完整的字段类型清单请参看 表单字段 。
每个表单字段都有一个相对应的 控件类 ,这个控件类又有对应的HTML表单控件,比如 <input type="text">
。
多数情况下,字段都有合适的默认控件。比如,默认情况下, CharField
有个 TextInput
控件,它会在HTML中生成一个 <input type="text">
。如果您想要的是 <textarea> `` ,您要在定义表单字段的时候指定控件,就像我们对 ``message
字段那样处理。
无论用表单提交了什么数据,一旦通过调用 is_valid()
验证成功( is_valid()
返回 True
),已验证的表单数据将被放到 form.cleaned_data
字典中。这里的数据已经很好的为你转化为Python类型。
备注
此时您依然能够直接从 request.POST
中访问到未验证的数据,但最好还是使用经验证的数据。
在上面的联系表单示例中, cc_myself
会被转化成一个布尔值。同样的,字段 IntegerField
和 FloatField
的值分别会被转化为Python的 int
和 float
类型。
下面例举了如何在视图中处理表单数据:
from django.core.mail import send_mail
if form.is_valid():
subject = form.cleaned_data["subject"]
message = form.cleaned_data["message"]
sender = form.cleaned_data["sender"]
cc_myself = form.cleaned_data["cc_myself"]
recipients = ["info@example.com"]
if cc_myself:
recipients.append(sender)
send_mail(subject, message, sender, recipients)
return HttpResponseRedirect("/thanks/")
小技巧
更多关于从Django中发送电子邮件的内容,请参见 发送邮件 。
有些字段类型需要一些额外的处理。例如,使用表单上传文件就要以不同的方式处理(它们可以从 request.FILES
获取,而不是 request.POST
中)。有关如何使用表单处理文件上传的详细信息,请参见 将上传的文件绑定到表单中 。
您只需将表单实例放到模板的上下文中即可。因此,如果您的表单在上下文中叫 form
,那么 {{ form }}
将渲染它相应的 <label>
和 <input>
元素。
额外表单模板标签
不要忘记,一张表单的输出 不 包含外层 <form>
标签以及 submit
控件。这些必须由你自己提供。
The HTML output when rendering a form is itself generated via a template. You
can control this by creating an appropriate template file and setting a custom
FORM_RENDERER
to use that
form_template_name
site-wide. You
can also customize per-form by overriding the form's
template_name
attribute to render the form using the
custom template, or by passing the template name directly to
Form.render()
.
The example below will result in {{ form }}
being rendered as the output of
the form_snippet.html
template.
在你的模板中:
# In your template:
{{ form }}
# In form_snippet.html:
{% for field in form %}
<div class="fieldWrapper">
{{ field.errors }}
{{ field.label_tag }} {{ field }}
</div>
{% endfor %}
Then you can configure the FORM_RENDERER
setting:
from django.forms.renderers import TemplatesSetting
class CustomFormRenderer(TemplatesSetting):
form_template_name = "form_snippet.html"
FORM_RENDERER = "project.settings.CustomFormRenderer"
… or for a single form:
class MyForm(forms.Form):
template_name = "form_snippet.html"
...
… or for a single render of a form instance, passing in the template name to
the Form.render()
. Here's an example of this being used in a view:
def index(request):
form = MyForm()
rendered_form = form.render("form_snippet.html")
context = {"form": rendered_form}
return render(request, "index.html", context)
See 将表单输出为 HTML for more details.
The ability to set the default form_template_name
on the form renderer
was added.
对于 <label>
/ <input>
对,还有其他输出选项:
{{ form.as_div }}
will render them wrapped in <div>
tags.{{ form.as_table }}
will render them as table cells wrapped in <tr>
tags.{{ form.as_p }}
will render them wrapped in <p>
tags.{{ form.as_ul }}
will render them wrapped in <li>
tags.注意,您必须自己提供外层的 <table>
或 <ul>
元素。
下面是我们 ContactForm
实例用 {{ form.as_p }}
的输出:
<p><label for="id_subject">Subject:</label>
<input id="id_subject" type="text" name="subject" maxlength="100" required></p>
<p><label for="id_message">Message:</label>
<textarea name="message" id="id_message" required></textarea></p>
<p><label for="id_sender">Sender:</label>
<input type="email" name="sender" id="id_sender" required></p>
<p><label for="id_cc_myself">Cc myself:</label>
<input type="checkbox" name="cc_myself" id="id_cc_myself"></p>
请注意,每个表单字段都有一个 id_<field-name>
这样的ID属性,它被附带的label标签引用。这对于确保表单可供屏幕阅读软件这样的辅助技术访问非常重要。您还可以 自定义Label和ID的生成方式 。
更多相关信息,请参阅 将表单输出为 HTML 。
我们没有必要非要让Django来解包表单字段;如果我们喜欢,可以手动来处理(比如,让我们对字段重新排序)。每个字段都可以用 {{ form.name_of_field }}
作为表单的一个属性,并被相应的渲染在Django模板中。例如:
{{ form.non_field_errors }}
<div class="fieldWrapper">
{{ form.subject.errors }}
<label for="{{ form.subject.id_for_label }}">Email subject:</label>
{{ form.subject }}
</div>
<div class="fieldWrapper">
{{ form.message.errors }}
<label for="{{ form.message.id_for_label }}">Your message:</label>
{{ form.message }}
</div>
<div class="fieldWrapper">
{{ form.sender.errors }}
<label for="{{ form.sender.id_for_label }}">Your email address:</label>
{{ form.sender }}
</div>
<div class="fieldWrapper">
{{ form.cc_myself.errors }}
<label for="{{ form.cc_myself.id_for_label }}">CC yourself?</label>
{{ form.cc_myself }}
</div>
完整的 <label>
元素还可以使用 label_tag()
来生成。例如:
<div class="fieldWrapper">
{{ form.subject.errors }}
{{ form.subject.label_tag }}
{{ form.subject }}
</div>
这种灵活性的代价需要多做一点工作。到目前为止,我们不必担心如何显示表单的错误信息,因为它们已经帮我们处理好了。下面的例子中,我们需要自己处理每个字段的错误信息以及表单整体的所有错误信息。注意表单顶部的 {{ form.non_field_errors }}
以及模板中对每个字段查找错误信息。
使用 {{ form.name_of_field.errors }}
显示该字段的错误信息列表,它被渲染成无序列表。看起来如下:
<ul class="errorlist">
<li>Sender is required.</li>
</ul>
该列表有一个CSS class errorlist
,允许您自定义其样式。如果你想进一步自定义错误信息的显示,您可以通过遍历它们来实现:
{% if form.subject.errors %}
<ol>
{% for error in form.subject.errors %}
<li><strong>{{ error|escape }}</strong></li>
{% endfor %}
</ol>
{% endif %}
非字段验证错误信息(或者通过使用像 form.as_p()
这样的辅助方法渲染产生在表单顶部的隐藏错误信息)渲染后会额外带上一个class nonfield
以便与字段验证错误信息区分。例如, {{ form.non_field_errors }}
渲染后会像这样:
<ul class="errorlist nonfield">
<li>Generic validation error</li>
</ul>
更多错误、样式以及在模板中使用表单属性的内容,请参阅 表单 API 。
如果您要给每个表单字段使用相同的HTML,您可以用 {% for %}
依次循环遍历每个字段来减少重复代码:
{% for field in form %}
<div class="fieldWrapper">
{{ field.errors }}
{{ field.label_tag }} {{ field }}
{% if field.help_text %}
<p class="help">{{ field.help_text|safe }}</p>
{% endif %}
</div>
{% endfor %}
Useful attributes on {{ field }}
include:
{{ field.errors }}
<ul class="errorlist">
,其中包含这个字段的所有验证错误信息。你可以使用 {% for error in field.errors %}
循环来自定义错误信息的显示。在这种情况下,循环中的每个对象是包含错误信息的字符串。{{ field.field }}
Field
实例由 BoundField
封装。您可以用它来访问 Field
的属性,比如 {{ char_field.field.max_length }}
。{{ field.help_text }}
{{ field.html_name }}
{{ field.id_for_label }}
id_email
)。如果您要手动构建label,您可能要用这个来替换 label_tag
。例如,如果你有一些内嵌的JavaScript并且想要避免硬编码字段的ID,这也很有用。{{ field.is_hidden }}
True
,否则为 False
。它作为模板变量没多大作用,但可用于条件测试,例如:{% if field.is_hidden %}
{# Do something special #}
{% endif %}
{{ field.label }}
Email address
。{{ field.label_tag }}
The field's label wrapped in the appropriate HTML <label>
tag. This
includes the form's label_suffix
. For example,
the default label_suffix
is a colon:
<label for="id_email">Email address:</label>
{{ field.legend_tag }}
New in Django 4.1.Similar to
field.label_tag
but uses a<legend>
tag in place of<label>
, for widgets with multiple inputs wrapped in a<fieldset>
.
{{ field.use_fieldset }}
New in Django 4.1.This attribute is
True
if the form field's widget contains multiple inputs that should be semantically grouped in a<fieldset>
with a<legend>
to improve accessibility. An example use in a template:
{% if field.use_fieldset %}
<fieldset>
{% if field.label %}{{ field.legend_tag }}{% endif %}
{% else %}
{% if field.label %}{{ field.label_tag }}{% endif %}
{% endif %}
{{ field }}
{% if field.use_fieldset %}</fieldset>{% endif %}
{{ field.value }}
someone@example.com
。参见
有关字段属性及方法的完整清单,请参阅 BoundField
。
5月 12, 2023