HTML表单
在HTML
中,表单是<form>...</form>
之间元素的集合,它们允许访问者输入文本、选择选项、操作对象等等,然后将信息发送回服务器。
某些表单的元素---文本框和复选框---非常简单而且内建于HTML
本身。其它的表单会复杂些:例如弹出一个日期选择对话框的界面、允许你移动滚动条的界面、使用JavaScript
和CSS
以及HTML
表单元素<input>
来实现操作控制的界面。
与<input>
元素一样,一个表单必须指定两样东西:
- 目的地:响应用户输入数据的
URL
; - 方式: 发送数据所使用的
HTTP
方法;
例如,Django Admin
站点的登录表单包含几个<input>
元素:type='text'
用于用户名,type='password'
用于密码,type='submit'
用于Log in
按钮。它还包含一些用户看不到的隐藏的文本字段,Django
使用它们来决定下一步的行为。
它还告诉浏览器表单数据应该发往<form>
的action
属性指定的URL---/admin/
,而且应该使用method
属性指定的HTTP
方法---post
。
当触发<input type='submit' value='Log in'>
元素时,数据将发送给/admin/
。
GET和POST
处理表单时只会用到GET
和POST
方法。
Django
的登录表单使用POST
方法,在这个方法中浏览器组合表单数据,对它们进行编码以用于传输,将它们发送到服务器然后接收它的响应。
相反,GET
组合提交的数据为一个字符串,然后使用它来生成一个URL
。这个URL
将包含数据发送的地址以及数据的键和值。
GET
和POST
用于不同的目的。
用于改变系统状态的请求---例如,给数据库带来变化的请求---应该使用POST
。GET
只应该用于不会影响系统状态的请求。
Django在表单中的角色
处理表单是一件很复杂的事情。考虑一下Django
的Admin
站点,不同类型的大量数据项需要在一个表单中准备好、渲染成HTML
、使用一个方便的界面编辑、返回给服务器、验证并清除,然后保存或者向后继续处理。
Django
的表单功能可以简化并自动化大部分这些工作,并且还可以比大部分程序自己所编写的代码更安全。
Django
会处理表单工作中的三个显著不同的部分:
- 准备数据、重构数据,以便下一步渲染;
- 为数据创建
HTML
表单; - 接收并处理客户端提交的表单和数据;
可以手工编写代码来实现,但是Django
可以帮你完成所有这些工作。
Django中的表单
HTML
的<form>
只是其机制的一部分。
在一个Web
应用中,‘表单’可能是指HTML <form>
、或者生成它的Django
的Form
、或者提交时发送的结构化数据、或者这些部分的总和。
Django的Form类
表单系统的核心部分是Django
的Form
类。Django
的模型描述一个对象的逻辑结构、行为以及展现给我们的方式,与此类似, Form
类描述一个表单并决定它如何工作和展现。
就像模型类的属性映射到数据库的字段一样,表单类的字段会映射到HTML
的<input>
表单的元素(ModelForm
通过一个Form
映射模型类的字段到HTML
表单的<input>
元素。Django
的Admin
站点就是基于这个)。
表单的字段本身也是类,它们管理表单的数据并在表单提交时进行验证。DateField
和FileField
处理的数据类型差别很大,必须完成不同的事情。
表单字段在浏览器中呈现给用户的是一个HTML
的'widget'
---用户界面的一个片段。每个字段类型都有一个合适的默认Widget
类,需要时可以覆盖。
实例化、处理和渲染表单
在Django
中渲染一个对象时,我们通常:
- 在视图中获得它(例如,从数据库中获取);
- 将它传递给模板上下文;
- 使用模板变量将它扩展为
HTML
标记语言
当我们在视图中处理模型实例时,我们一般从数据库中获取它。当我们处理表单时,一般在视图中实例化它。
构建一个表单
在Django中构建一个表单
Form类
我们已经计划好了我们的HTML
表单应该呈现的样子。在Django
中,我们的起始点是这里:
forms.py
from django import forms
class NameForm(forms.Form):
your_name = forms.CharField(label='Your name', max_length=100)
它定义一个Form
类,只带有一个字段(your_name)
。我们已经对这个字段使用一个友好的标签,当渲染时它将出现在<label>
中。
Form
的实例具有一个is_valid()
方法,它为所有的字段运行验证程序。当调用这个方法时,如果所有的字段都包含合法的数据,它将:
返回
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
对应的视图中实例化我们将要发布的表单。
views.py
from django.shortcuts import render
from django.http import HttpResponseRedirect
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 will 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()
为Ture
,我们将能够在cleaned_data
属性中找到所有合法的表单数据。在发送HTTP
重定向给浏览器告诉它下一步的去向之前,我们可以用这个数据来更新数据库或者做其它处理。
模板
我们不需要在name.html
模板中做很多工作,最简单的例子是:
<form action="/your-name/" method="post">
{%csrf_token%}
{{form}}
<input type='submit' value="Submit" />
</form>
根据{{form}}
,所有的表单字段和它们的属性将通过Django
的模板语言拆分成HTML标记语言。
现在我们有一个可以工作的网页表单,它通过Django Form
描述、通过视图处理并渲染成一个HTML <form>
。
Django Form类详解
所有的表单类都作为django.forms.Form
的子类创建,包括你在Django
管理站点中遇到的ModelForm
。
模型和表单
实际上,如果你的表单打算直接用来添加和编辑Django
的模型,ModelForm
可以节省你的许多时间、精力和代码,因为它将根据Model
类构建一个表单以及适当的字段和属性。
绑定的和未绑定的表单实例
绑定的和未绑定的表单之间的区别非常重要:
- 未绑定的表单没有关联的数据。当渲染给用户时,它将为空或者包含默认的值。
- 绑定的表单具有提交地数据,因此可以用来检验数据是否合法。如果渲染一个不合法的绑定的表单,它将包含内联的错误信息,告诉用户如何纠正数据。
表单的is_bound
属性将告诉你一个表单是否具有绑定的数据。
字段详解
考虑一个比上面的迷你示例更有用的一个表单,在一个网站上实现“contact me”功能:
forms.py
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)
我们前面的表单只使用一个字段your_name
,它是一个CharField
。在这个例子中,我们的表单具有四个字段:subject、message、sender和cc_myself。共用到三种字段类型:CharField、EmailField和BooleanField。
窗口小部件
每个表单字段都有一个对应的Widget
类,它对应一个HTML
表单Widget
,例如<input type='text'>
。
在大部分情况下,字段都具有一个合理的默认Widget
。例如,默认情况下,CharField
具有一个TextInput Widget
,它在HTML
中生成一个<input type='text'>
。如果你需要<textarea>
,在定义表单字段时你应该指定一个合适的Widget
,例如我们定义的message
字段。
字段的数据
不管表单提交的是什么数据,一旦通过调用is_valid()
成功验证(is_valid()
返回True
),验证后的表单数据将位于form.cleaned_data
字典中。这些数据已经为Python
的类型。
注意,此时,依然可以从request.POST
中直接访问到未验证的数据,但是访问验证后的数据更好一些。
下面是在视图中如何处理表单数据:
views.py
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, recipient)
return HttpResponseRedirect('/thanks/')
有些字段类型需要一些额外的处理。例如,使用表单上传的文件需要不同地处理(它们可以从request.FILES
获取,而不是request.POST
)。
使用表单模板
你需要做的就是将表单实例放进模板的上下文。如果你的表单在Context中叫做form,那么{{form}}将正确的渲染它的<label>和<input>元素。
表单渲染的选项
不要忘记,表单的输出不包含<form>标签和表单的submit按钮,必须自己提供它们。
对于<label>/<input>对,还有几个输出选项:
- {{form.as_table}}以表格的形式将它们渲染在<tr>标签中;
- {{form.as_p}}将它们渲染在<p>标签中;
- {{form.as_ul}} 将它们渲染在<li>标签中;
注意,你必须自己提供<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属性并设置为id_<field-name>。
手工渲染字段
我们没有必要非要让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>
渲染表单的错误信息
当然,这个便利性的代价是更多的工作。直到现在,我们没有担心如何展示错误信息,因为Django已经帮我们处理好。在下面的例子中,我们将自己处理每个字段的错误和表单整体的各种错误。注意,表单和模板顶部的{{form.non_field_errors}}
查找每个字段的错误。
使用{{form.name_of_field.errors}}显示表单错误的一个清单,并渲染成一个ul。看上去可能像:
<ul class="errorlist">
<li>Sender is required.</li>
</ul>
这个ul有一个errorlist的CSS样式表,你可以用它来定义外观。如果你希望进一步自定义错误信息的显示,你可以迭代它们来实现:
{% if form.subject.errors %}
<ol>
{% for error in form.subject.errors %}
<li><strong>{{ error|escape }}</strong></li>
{% endfor %}
</ol>
{% endif %}
迭代表单字段
如果你为你的每个表单字段使用相同的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 %}
迭代隐藏和可见的字段
如果你正在手工布局模板中的一个表单,而不是依赖Django 默认的表单布局,你可能希望将<input type="hidden"> 字段与非隐藏的字段区别对待。例如,因为隐藏的字段不会显示,在该字段旁边放置错误信息可能让你的用户感到困惑 —— 所以这些字段的错误应该有区别地来处理。
Django 提供两个表单方法,它们允许你独立地在隐藏的和可见的字段上迭代:hidden_fields() 和visible_fields()。下面是使用这两个方法对前面一个例子的修改:
{# Include the hidden fields #}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{# Include the visible fields #}
{% for field in form.visible_fields %}
<div class="fieldWrapper">
{{ field.errors }}
{{ field.label_tag }} {{ field }}
</div>
{% endfor %}
这个示例没有处理隐藏字段中的任何错误信息。通常,隐藏字段中的错误意味着表单被篡改,因为正常的表单填写不会改变它们。然而,你也可以很容易地为这些表单错误插入一些错误信息显示出来。
可重用的表单模板
如果你的网站在多个地方对表单使用相同的渲染逻辑,你可以保存表单的循环到一个单独的模板中来减少重复,然后在其它模板中使用include标签来重用它:
# In your form template:
{% include "form_snippet.html" %}
# In form_snippet.html:
{% for field in form %}
<div class="fieldWrapper">
{{ field.errors }}
{{ field.label_tag }} {{ field }}
</div>
{% endfor %}
如果传递到模板上下文中的表单对象具有一个不同的名称,你可以使用include标签的with参数来对它起个别名:
{% include "form_snippet.html" with form=comment_form %}
绑定的表单和未绑定的表单
表单要么是绑定的,要么是未绑定的。
- 如果是绑定的,那么能够验证数据,并渲染表单及其数据成HTML。
- 如果是未绑定的,那么它不能够完成验证(因为没有可验证的数据),但是仍然能渲染成空白的表单成HTML。
class Form
若要创建一个未绑定的表单实例,只需要简单地实例化该类:
>>>f = ContactForm()
若要绑定数据到表单,可以将数据以字典的形式传递给表单类的构造函数的第一个参数:
>>> data = {'subject': 'hello',
... 'message': 'Hi there',
... 'sender': 'foo@example.com',
... 'cc_myself': True}
>>> f = ContactForm(data)
在这个字典中,键为字段的名称,它们对应于表单类中的属性。值为需要验证的数据。它们通常为字符串,但是没有强制要求必须是字符串。传递的数据类型取决于字段。
Form.is_bound
如果在运行时你要区分绑定的表单和未绑定的表单,可以检查下表单is_bound属性的值。
>>> f = ContactForm()
>>> f.is_bound
False
>>> f = ContactForm({'subject': 'hello'})
>>> f.is_bound
True
注意,传递一个空的字典将创建一个带有空数据的绑定的表单:
>>> f = ContactForm({})
>>> f.is_bound
True
如果你有一个绑定的表单实例但是想修改下数据,或者你想绑定一个未绑定的表单到某些数据,你需要创建另外一个表单列表。Form实例的数据没有办法修改。表单实例一旦创建,你应该将它的数据视为不可变的,无论它有没有数据。
使用表单来验证数据
Form.clean()
当你需要为相互依赖的字段添加自定义的验证时,你可以实现表单的clean()方法。
Form.is_valid()
表单对象的首要任务就是验证数据。对于绑定的表单实例,可以调用is_valid()方法来执行验证并返回一个表示数据是否合法的布尔值。
>>> data = {'subject': 'hello',
... 'message': 'Hi there',
... 'sender': 'foo@example.com',
... 'cc_myself': True}
>>> f = ContactForm(data)
>>> f.is_valid()
True
让我们试下非法的数据。下面的情形中,subject 为空(默认所有字段都是必需的)且sender 是一个不合法的邮件地址:
>>> data = {'subject': '',
... 'message': 'Hi there',
... 'sender': 'invalid email address',
... 'cc_myself': True}
>>> f = ContactForm(data)
>>> f.is_valid()
False
Form.errors
访问errors属性可以获得错误信息的一个字典。
>>> f.errors
{'sender': ['Enter a valid email address.'], 'subject': ['This field is required.']}
在这个字典中,键为字段的名称,值为表示错误信息的Unicode字符串组成的列表。错误信息保存在列表中是因为字段可能有多个错误信息。
你可以访问errors而不必须先调用is_valid()。表单的数据将在第一次调用is_valid()或者访问errors时验证。
验证只会调用一次,无论你访问errors或者调用is_valid()多少次。这意味着,如果验证过程有副作用,这些副作用将只触发一次。
Form.non_field_errors()
这个方法返回Form.errors中不是与特定字段相关联的错误。它包含在Form.clean()中引发的ValidationError和使用Form.add_error(None,'...')添加的错误。
动态的初始值
Form.initial
表单字段的初始值使用initial声明。例如,你可能希望使用当前会话的用户名填充username字段。
使用Form的initial参数可以实现。该参数是字段名到初始值的一个字典。只需要包含你期望给出初始值的字段,不需要包含表单中的所有字段。例如:
>>> f = ContactForm(initial={'subject': 'Hi there!'})
这些值只显示在没有绑定的表单中,即使没有提供特定值它们也不会作为后备的值。
注意,如果字段有定义initial,而实例化表单时也提供initial,那么后面的initial将优先。在下面的例子中,initial在字段和表单实例化中都有定义,此时后者具有优先权。
>>> from django import forms
>>> class CommentForm(forms.Form):
... name = forms.CharField(initial='class')
... url = forms.URLField()
... comment = forms.CharField()
>>> f = CommentForm(initial={'name': 'instance'}, auto_id=False)
>>> print(f)
<tr><th>Name:</th><td><input type="text" name="name" value="instance" required /></td></tr>
<tr><th>Url:</th><td><input type="url" name="url" required /></td> </tr>
<tr><th>Comment:</th><td><input type="text" name="comment" required /></td></tr>
检查表单数据是否改变
Form.has_changed()
当你需要检查表单的数据是否从初始数据发生改变时,可以使用表单的has_changed()方法。
>>> data = {'subject': 'hello',
... 'message': 'Hi there',
... 'sender': 'foo@example.com',
... 'cc_myself': True}
>>> f = ContactForm(data, initial=data)
>>> f.has_changed()
False
当提交表单时,我们可以重新构建表单并提供初始值,这样可以实现比较:
>>> f = ContactForm(request.POST, initial=data)
>>> f.has_changed()
如果request.POST中的数据与inital中的不同,has_changed()将为True,否则为False。计算的结果是通过调用表单每个字段的Field.has_changed()得到的。
访问'clean'的数据
Form.cleaned_data
表单类中的每个字段不仅负责验证数据,还负责‘清洁’它们---将它们转换为正确的格式。这是个非常好用的功能,因为它允许字段以多种方式输入数据,并总能得到一致的输出。
一旦你创建一个表单实例并通过验证后,你就可以通过它的cleaned_data属性访问清洁的数据:
>>> data = {'subject': 'hello',
... 'message': 'Hi there',
... 'sender': 'foo@example.com',
... 'cc_myself': True}
>>> f = ContactForm(data)
>>> f.is_valid()
True
>>> f.cleaned_data
{'cc_myself': True, 'message': 'Hi there', 'sender': 'foo@example.com', 'subject': 'hello'}
如果你的数据没有通过验证,cleaned_data字典中只包含合法的字段:
>>> data = {'subject': '',
... 'message': 'Hi there',
... 'sender': 'invalid email address',
... 'cc_myself': True}
>>> f = ContactForm(data)
>>> f.is_valid()
False
>>> f.cleaned_data
{'cc_myself': True, 'message': 'Hi there'}
cleaned_data 始终只 包含表单中定义的字段,即使你在构建表单 时传递了额外的数据。在下面的例子中,我们传递一组额外的字段给ContactForm 构造函数,但是cleaned_data 将只包含表单的字段:
>>> data = {'subject': 'hello',
... 'message': 'Hi there',
... 'sender': 'foo@example.com',
... 'cc_myself': True,
... 'extra_field_1': 'foo',
... 'extra_field_2': 'bar',
... 'extra_field_3': 'baz'}
>>> f = ContactForm(data)
>>> f.is_valid()
True
>>> f.cleaned_data # Doesn't contain extra_field_1, etc.
{'cc_myself': True, 'message': 'Hi there', 'sender': 'foo@example.com', 'subject': 'hello'}
当表单合法时,cleaned_data 将包含所有字段的键和值,即使传递的数据不包含某些可选字段的值。在下面的例子中,传递的数据字典不包含nick_name 字段的值,但是cleaned_data 任然包含它,只是值为空:
>>> from django import forms
>>> class OptionalPersonForm(forms.Form):
... first_name = forms.CharField()
... last_name = forms.CharField()
... nick_name = forms.CharField(required=False)
>>> data = {'first_name': 'John', 'last_name': 'Lennon'}
>>> f = OptionalPersonForm(data)
>>> f.is_valid()
True
>>> f.cleaned_data
{'nick_name': '', 'first_name': 'John', 'last_name': 'Lennon'}
在上面的例子中,cleaned_data 中nick_name 设置为一个空字符串,这是因为nick_name 是CharField而 CharField 将空值作为一个空字符串。每个字段都知道自己的“空”值 —— 例如,DateField 的空值是None 而不是一个空字符串。关于每个字段空值的完整细节,参见“内建的Field 类”一节中每个字段的“空值”提示。
表单必填行和错误行的样式表
Form.error_css_class
Form.required_css_class
将必填的表单行和有错误的表单行定义不同的样式表特别常见。例如,你想将必填的表单行以粗体显示,将错误以红色显示。
表单类具有一对钩子,可以使用它们来添加class属性给必填的行或者有错误的行:只需要简单地设置Form.error_css_class或 Form.required_css_class属性:
from django import forms
class ContactForm(forms.Form):
error_css_class = 'error'
required_css_class = 'required'
# ... and the rest of your fields here
一旦你设置好,将根据需要,设置行的"error" 或"required" CSS类型。 其HTML 看上去将类似:
>>> f = ContactForm(data)
>>> print(f.as_table())
<tr class="required"><th><label class="required" for="id_subject">Subject:</label> ...
<tr class="required"><th><label class="required" for="id_message">Message:</label> ...
<tr class="required error"><th><label class="required" for="id_sender">Sender:</label> ...
<tr><th><label for="id_cc_myself">Cc myself:<label> ...
>>> f['subject'].label_tag()
<label class="required" for="id_subject">Subject:</label>
>>> f['subject'].label_tag(attrs={'class': 'foo'})
<label for="id_subject" class="foo required">Subject:</label>
从模型创建表单
ModelForm
如果你正在构建一个数据库驱动的应用,那么你应该会有与Django的模型紧密映射的表单。举个例子,你也许会有个BlogComment模型,并且你还想创建一个表单让大家提交评论到这个模型中。在这种情况下,在表单中定义定义字段是冗余的,因为你已经在模型中定义了字段。
基于这个原因,Django提供一个辅助类来让你从Django的模型创建表单。例如,
>>> from django.forms import ModelForm
>>> from myapp.models import Article
# Create the form class.
>>> class ArticleForm(ModelForm):
... class Meta:
... model = Article
... fields = ['pub_date', 'headline', 'content', 'reporter']
# Creating a form to add an article.
>>> form = ArticleForm()
# Creating a form to change an existing article.
>>> article = Article.objects.get(pk=1)
>>> form = ArticleForm(instance=article)
字段类型
生成的表单类中将具有和指定的模型字段对应的表单字段。字段顺序为fields属性中指定的顺序。
每个模型字段有一个对应的默认表单字段。比如,模型中的CharField对应表单中的CharField。模型中的ManyToManyField字段会表现成MultipleChoiceField字段。
模型表单的验证
验证模型表单主要有两步:
- 验证表单;
- 验证模型实例;
与普通的表单验证类似,模型表单的验证在调用is_valid()或者访问errors属性时隐式调用,或者通过full_clean()显式调用,尽管在实际应用中很少使用后一种方法。
模型的验证Model.full_clean()在表单验证这一步的内部触发,紧跟在表单的clean()方法调用之后。
save()方法
每个模型表单还具有一个save()方法。这个方法根据表单绑定的数据创建并保存数据库对象。模型表单的子类可以用关键字参数instance接收一个已经存在的模型实例。如果提供,save()将更新这个实例。如果没有提供,save()将创建模型的一个新实例。
>>> from myapp.models import Article
>>> from myapp.forms import ArticleForm
# Create a form instance from POST data.
>>> f = ArticleForm(request.POST)
# Save a new Article object from the form's data.
>>> new_article = f.save()
# Create a form to edit an existing Article, but use
# POST data to populate the form.
>>> a = Article.objects.get(pk=1)
>>> f = ArticleForm(request.POST, instance=a)
>>> f.save()
注意,如果表单没有验证,save()调用将通过检查form.errors来进行验证。如果表单中的数据不合法,将引发ValuelError。
save() 接受一个可选的commit 关键字参数,其值为True 或False。如果save() 时commit=False,那么它将返回一个还没有保存到数据库的对象。这种情况下,你需要调用返回的模型实例的save()。这是很有用的,如果你想在保存之前自定义一些处理了,或者你想使用特定的模型保存选项。
使用commit=False 的另外一个副作用是在模型具有多对多关系的时候。如果模型具有多对多关系而且当你保存表单时指定commit=False,Django 不会立即为多对多关系保存表单数据。这是因为只有实例在数据库中存在时才可以保存实例的多对多数据。
为了解决这个问题,每当你使用commit=False 保存表单时,Django 将添加一个save_m2m() 方法到你的模型表单子类。在你手工保存有表单生成的实例之后,你可以调用save_m2m() 来保存多对多的表单数据。例如:
# Create a form instance with POST data.
>>> f = AuthorForm(request.POST)
# Create, but don't save the new author instance.
>>> new_author = f.save(commit=False)
# Modify the author in some way.
>>> new_author.some_field = 'some_value'
# Save the new instance.
>>> new_author.save()
# Now, save the many-to-many data for the form.
>>> f.save_m2m()
save_m2m() 只在你使用save(commit=False) 时才需要。当你直接使用save(),所有的数据 —— 包括多对多数据 —— 都将保存而不需要任何额外的方法调用。例如:
# Create a form instance with POST data.
>>> a = Author()
>>> f = AuthorForm(request.POST, instance=a)
# Create and save the new author instance. There's no need to do anything else.
>>> new_author = f.save()
处理关系的字段Fields which handle relationships
两个字段可用于表示模型之间的关系:ModelChoiceField和ModelMultipleChoiceField
。这两个字段都需要单个queryset
参数,用于创建字段的选择。在表单验证时,这些字段将把一个模型对象(在ModelChoiceField
的情况下)或多个模型对象(在ModelMultipleChoiceField
的情况下)放置到表单的cleaned_data
字典。
对于更复杂的用法,可以在声明表单字段时指定queryset=None
,然后在表单的__init__()
方法中填充queryset
。
class FooMultipleChoiceForm(forms.Form):
foo_select = forms.ModelMultipleChoiceField(queryset=None)
def __init__(self, *args, **kwargs):
super(FooMultipleChoiceForm, self).__init__(*args, **kwargs)
self.fields['foo_select'].queryset = ...
ModelChoiceField
class ModelChoiceField(**kwargs)
- 默认的Widget: Select
- 空值: None
- 规范化为: 一个模型实例
- 验证给定的id是否存在于查询集中
- 错误信息的键: required, invalid_choice
可以选择一个单独的模型对象,适用于表示一个外键字段。ModelChoiceField默认widget不适用选择数量很大的情况,在大于100项时应该避免使用它。
需要一个单独参数:
queryset: 将导出字段选择的模型对象的QuerySet,将用于验证用户的选择。
ModelChoiceField有两个可选参数:
-
empty_label: 默认情况下,ModelChoiceField使用的<select>小部件将在列表顶部有一个空选项。您可以使用empty_label属性更改此标签的文本(默认为"________"),也可以完全禁用空白标签通过将empty_label设置为None:
#A custom empty label field1 = forms.ModelChoiceField(queryset=..., empty_label="(Nothing)") #No empty label field2 = forms.ModelChoiceField(queryset=..., empty_label=None)
请注意如果需要用到ModelChoiceField有一个默认的初始值,则不会创建空选项(不管empty_label的值)。
-
to_field_name: 这个参数用于指定要用作字段小部件选项的值的字段。确保它是模型的唯一字段,否则选定的值可以匹配多个对象。默认情况下,它设置为None,在这种情况下,将使用每个对象的主键。
#No custom to_field_name field1 = forms.ModelChoiceField(queryset=...)
会渲染成:
<select id='id_field1' name='field1'>
<option value='obj1.pk'>Object1</option>
<option value='obj2.pk'>Object2</option>
...
</select>
和:
#to_field_name provided
field2 = forms.ModelChoiceField(queryset=... to_field_name='name')
会生成:
<select id='id_field2' name='field2'>
<option value='obj1.name'>Object1</option>
<option value='obj2.name'>Object2</option>
...
</select>
ModelMultipleChoiceField
- 默认的Widget: SelectMultiple
- 控制: QuerySet(self.queryset.none())
- 规范化为: 模型实例的一个QuerySet。
- 错误信息的键: required, list, invalid_choice, invalid_pk_value
允许选择适合于表示多对多关系的一个或多个模型对象。queryset是必需的参数。
queryset:将导出字段选择的模型对象的QuerySet,将用于验证用户的选择。
表单验证和字段验证
表单验证发生在数据清洗时。如果需要自定义这个过程,有几个不同的地方可以修改,每个地方的目的不一样。表单处理过程中要运行三种类别的验证方法:它们通常在你调用表单的is_valid()
方法时执行。还有其它方法可以触发验证过程(访问errors属性
或直接调用full_clean()
),但是通常情况下不需要。
一般情况下,如果处理的数据有问题,每个验证方法都会引发ValidationError
,并将相关信息传递给ValidationError
构造器。如果没有引发ValidationError
,这些方法应该返回验证后的(规整化的,清洗后的)数据的Python
对象。
大部分验证应该可以使用validators
完成,它们可以很容易地重用。Validators
是简单的函数(或可调用对象),它们接收一个参数并对非法的输入抛出ValidationError
。Validators
在字段的to_python
和validate
方法调用之后运行。
表单的验证分成几个步骤,它们可以定制或覆盖:
- 字段的
to_python()
方法是验证的第一步。它将值强制转换为正确的数据类型,如果不能转换则引发ValidationError
。这个方法从Widget
接收原始的值并返回转换后的值。例如,FloatField
将数据转换为Python
的float
或引发ValidationError
。 - 字段的
validate()
方法处理字段的特别的验证,这种验证不适合validator
。它接收一个已经转换成正确数据类型的值,并在发现错误时引发ValidationError
。这个方法不返回任何东西且不应该改变任何值。当您遇到不能或不想放在validator
中的验证逻辑时,应该覆盖它来处理验证。 - 字段的
run_validators()
方法运行字段所有的Validator
,并将所有的错误信息聚合成一个单一的ValidationError
。你应该不需要覆盖这个方法。 - Filed子类的
clean()
方法。它负责以正确的顺序运行to_python
、validate
和run_validators
并传播它们的错误。如果任何时刻、任何方法引发ValidationError
,验证将停止并引发这个错误。这个方法返回验证后的数据,这个数据在后面将插入到表单的cleaned_data
字典中。 - 表单子类中的
clean_<fieldname>
方法---<fieldname>
通过表单中的字段名称替换。这个方法完成于特定属性相关的验证,这个验证与字段的类型无关。这个方法没有任何输入的参数。你需要查找self.cleaned_data
中该字段的值,记住此时它已经是一个Python对象而不是表单中提交的原始字符串(它位于cleaned_data
中是因为字段的clean()
方法已经验证过一次数据)。
例如,如果你想验证名为serialnumber
的CharField
的内容是否唯一,clean_serialnumber()
将是实现这个功能的理想之处。你需要的不是一个特别的字段(它只是一个CharField
),而是一个特定于表单字段的特定验证,并规整化数据。
这个方法返回的值将代替cleaned_data
中已经存在的值,因此它必须是cleaned_data
中字段的值或一个新的清洗后的值。 - 表单子类的
clean()
方法。这个方法可以实现需要同时访问表单多个字段的验证。这里你可以验证如果提供字段A,那么字段B必须包含一个合法的邮件地址以及类似的功能。这个方法可以返回一个完全不同的字典,该字典将用作cleaned_data
。
因为字段的验证方法在调用clean()
时会运行,你还可以访问表单的errors
属性,它包含验证每个字段时的所有错误。
注意,你覆盖的Form.clean()
引发的任何错误将不会与任何特定的字段关联。它们位于一个特定的‘字段’(叫做__all__
)中,如果需要可以通过non_field_errors()
方法访问。如果你想添加一个特定字段的错误到表单中,需要调用add_error()
。
还要注意,覆盖ModelForm
子类的clean()
方法需要特殊的考虑。
这些方法按照以上给出的顺序执行,一次验证一个字段。也就是说,对于表单中的每个字段(按照它们在表单定义中出现的顺序),先运行Field.clean()
,然后运行clean_<fieldname>()
。每个字段的这两个方法都执行完之后,最后运行Form.clean()
方法,无论前面的方法是否抛出过异常。
下面由上面每个方法的示例。
我们已经提到过,所有这些方法都可以抛出ValidationError
。对于每个字段,如果Field.clean()
方法抛出ValidationError,那么将不会调用该字段对应的clean_<fieldname>()
方法。但是,剩余的字段的验证方法仍然会执行。
在实践中应用验证
使用Validator
Django的表单(以及模型)字段支持使用简单的函数和类用于验证,它们叫做Validator。Validator是可调用对象或函数,它接收一个值,如果该值合法则什么也不返回,否则抛出ValidationError。它们可以通过字段的validators参数传递给字段的构造函数,或者定义在Field类的default_validators属性中。
简单的Validator可以用于在字段内部验证值,让我们看下Django的SlugField:
from django.forms import CharField
form django.core import validators
class SlugField(CharField):
default_validators = [validators.validate_slug]
正如你所看到的,SlugField只是一个带有自定义Validator的CharField,它们验证提交的文本符合某些字符规则。这也可以在字段定义时实现,所以:
slug = forms.SlugField()
等同于:
slug = forms.CharField(validators=[validtors.validate_slug])
常见的情形,例如验证邮件地址和正则表达式,可以使用Django中已经存在的Validator类处理。
表单字段的默认验证
让我们首先创建一个自定义的表单字段,它验证其输入是一个由逗号分隔的邮件地址组成的字符串。完整的类像这样:
from django import forms
from django.core.validators import validate_mail
class MultiEmailField(forms.Field):
def to_python(self, value):
"""Nomalize data to a list of strings."""
#Return an empty list if no input was given.
if not value:
return []
return value.split(',')
def validate(self, value):
"""Check if value consists only of valid emails."""
#Use the parent's handling of required fields, etc.
super(MultiEmailField, self).validate(value)
for email in value:
validate_email(email)
使用这个字段的每个表单都将在处理该字段数据之前运行这些方法。这个验证特定于该类型的字段,与后面如何使用它无关。
让我们来创建一个简单的ContactForm来向你演示如何使用这个字段:
class ContactForm(forms.Form):
subject = forms.CharField(max_lenght=100)
message = forms.CharField()
sender = forms.EmailField()
recipients = MultiEmailField()
cc_myself = forms.BooleanField(required=False)
只需要简单地使用MultiEmailField,就和其它表达字段一样。当调用表单的is_valid()方法时,MultiEmailField.clean()方法将作为验证过程的一部分运行,它将调用自定义的to_python()和validate()方法。
验证特定字段属性
继续上面的例子,假设在ContactForm中,我们想确保recipients字段始终包含"fred@example.com"。这是特定于我们这个表达的验证,所以我们不打算将它放在通用的MultiEmailField类中,我们将编写一个运行在recipients字段上的验证方法,像这样:
from django import forms
class ContactForm(forms.Form):
#Everything as before.
...
def clean_recipients(self):
data = self.cleaned_data['recipients']
if 'fred@example.com' not in data:
raise forms.ValidationError("You have forgotten about Fred!")
#Always return a value to use as the new cleaned data, even if this method didn't change it.
return data
验证相互依赖的字段
假设我们添加另外一个需求到我们的ContactForm表单中:如果cc_myself
字段为True
,那么subject
必须包含单词"help"。我们的这个验证包含多个字段,所以表单的clean()
方法是个不错的地方。注意,我们这里讨论的是表单的clean()
方法,之前我们编写过字段的clean()
方法。区别字段和表单之间的差别非常重要。字段是单个数据,表单是字段的集合。
在调用表单clean()
方法的时候,所有字段的验证方法已经执行完,所以self.cleaned_data
填充的是目前为止已经合法的数据。所以你需要记住这个事实,你需要验证的字段可能没有通过初始的字段检查。
在这一步,有两种方法报告错误。最简单的方法是在表单的顶端显示错误。你可以在clean()
方法中抛出ValidationError
来创建错误。例如,
from django import forms
class ContactForm(forms.Form):
#Everythign as before.
...
def clean(self):
cleaned_data = super(ContactForm, self).clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject:
# Only do something if both fields are valid so far.
if "help" not in subject:
raise forms.ValidationError(
"Did not send for 'help' in the subject despite "
"CC'ing yourself."
)
在这段代码中,如果抛出验证错误,表单将在表单的顶部显示(通常是)描述该问题的一个错误信息。
注意,示例代码中super(ContactForm, self).clean()的调用是为了维持父类中的验证逻辑。如果你的表单继承自另外一个在clean()方法中没有返回一个cleaned_data字典的表单,那么不要把cleand_data联系到super()的结果而要使用self.cleaned_data。
def clean(self):
super(ContactForm, self).clean()
cc_myself = self.cleaned_data.get('cc_myself')
...
第二种方法涉及将错误消息关联到某个字段。在这种情况下,让我们在表单的显示中分别关联一个错误信息到“subject” 和“cc_myself” 行。在实际应用中要小心,因为它可能导致表单的输出变得令人困惑。我们只是向你展示这里可以怎么做,在特定的情况下,需要你和你的设计人员确定什么是好的方法。我们的新代码(代替前面的示例)像这样:
from django import forms
class ContactForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super(ContactForm, self).clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject and "help" not in subject:
msg = "Must put 'help' in subject when cc'ing yourself."
self.add_error('cc_myself', msg)
self.add_error('subject', msg)
add_error()
的第二个参数可以是一个简单的字符串,但更倾向是ValidationError
的一个实例
编写Validator
验证器是一个可调用的对象,它接受一个值,并在不符合一些规则时候抛出ValidationError异常。验证器有助于在不同类型的字段之间重复使用验证逻辑。
例如,这个验证器只允许偶数:
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
def validate_even(value):
if value % s != 0:
raise ValidationError(
_('%(value)s is not an even number'),
params={'value': value},
)
你可以通过字段的validators参数将它添加到模型字段中:
from django.db import models
class MyModel(models.Model):
even_field = models.IntegerField(validators=[validate_even])
由于值在验证器运行之前会转化为Python,你可以在表单上使用相同的验证器:
from django import forms
class MyForm(forms.Form):
even_field = forms.IntegerField(validators=[validate_even])
你可以可以使用带有__call__()
方法的类,实现更复杂或可配置的验证器。
Validator如何运行
参考:
- http://python.usyiyi.cn/translate/django_182/topics/forms/index.html
- http://python.usyiyi.cn/translate/django_182/ref/forms/api.html
- http://python.usyiyi.cn/translate/django_182/topics/forms/modelforms.html
- http://python.usyiyi.cn/translate/django_182/ref/forms/fields.html
- http://python.usyiyi.cn/translate/django_182/ref/forms/validation.html