一、在Django 中构建一个表单
Form 类
我们已经计划好了我们的 HTML 表单应该呈现的样子。在Django 中,我们的起始点是这里:
from django import forms
class NameForm(forms.Form):
your_name = forms.CharField(label='Your name', max_length=100)
它定义一个Form 类,只带有一个字段(your_name)。我们已经对这个字段使用一个友好的标签,当渲染时它将出现在<label> 中(在这个例子中,即使我们省略它,我们指定的label还是会自动生成)。
字段允许的最大长度通过max_length 定义。它完成两件事情。首先,它在HTML 的<input> 上放置一个maxlength="100" (这样浏览器将在第一时间阻止用户输入多于这个数目的字符)。它还意味着当Django 收到浏览器发送过来的表单时,它将验证数据的长度。
Form 的实例具有一个is_valid() 方法,它为所有的字段运行验证的程序。当调用这个方法时,如果所有的字段都包含合法的数据,它将:
- 返回True
- 将表单的数据放到cleaned_data 属性中。
完整的表单,第一次渲染时,看上去将像:
<label for="your_name">Your name: </label>
<input id="your_name" type="text" name="your_name" maxlength="100">
注意它不包含 <form> 标签和提交按钮。我们必须自己在模板中提供它们。
视图
发送给Django 网站的表单数据通过一个视图处理,一般和发布这个表单的是同一个视图。这允许我们重用一些相同的逻辑。
要操作一个通过URL发布的表单,我们要在视图中实例表单。
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'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>
根据{{ form }},所有的表单字段和它们的属性将通过Django 的模板语言拆分成HTML 标记 。
表单和跨站请求伪造的防护
Django 原生支持一个简单易用的跨站请求伪造的防护。当提交一个启用CSRF 防护的POST 表单时,你必须使用上面例子中的csrf_token 模板标签。然而,因为CSRF 防护在模板中不是与表单直接捆绑在一起的,这个标签在这篇文档的以下示例中将省略。
HTML5 输入类型和浏览器验证
如果你的表单包含URLField、EmailField 或其它整数字段类型,Django 将使用url、email和 number 这样的HTML5 输入类型。默认情况下,浏览器可能会对这些字段进行它们自身的验证,这些验证可能比Django 的验证更严格。如果你想禁用这个行为,请设置form 标签的novalidate 属性,或者指定一个不同的字段,如TextInput。
现在我们有了一个可以工作的网页表单,它通过Django Form 描述、通过视图处理并渲染成一个HTML <form>。
这是你入门所需要知道的所有内容,但是表单框架为了便利提供了更多的内容。一旦你理解了上面描述的基本处理过程,你应该可以理解表单系统的其它功能并准备好学习更多的底层机制。
二、Django Form 类详解
所有的表单类都作为django.forms.Form 的子类创建,包括你在Django 管理站点中遇到的ModelForm。
模型和表单
实际上,如果你的表单打算直接用来添加和编辑Django 的模型,ModelForm 可以节省你的许多时间、精力和代码,因为它将根据Model 类构建一个表单以及适当的字段和属性。
绑定的和未绑定的表单实例
绑定的和未绑定的表单 之间的区别非常重要:
- 未绑定的表单没有关联的数据。当渲染给用户时,它将为空或包含默认的值。
- 绑定的表单具有提交的数据,因此可以用来检验数据是否合法。如果渲染一个不合法的绑定的表单,它将包含内联的错误信息,告诉用户如何纠正数据。
表单的is_bound 属性将告诉你一个表单是否具有绑定的数据。
字段详解
考虑一个比上面的迷你示例更有用的一个表单,我们可以用它来在一个个人网站上实现“contact me”功能:
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 中直接访问到未验证的数据,但是访问验证后的数据更好一些。
在上面的联系表单示例中,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)。如何使用表单处理文件上传的更多细节,请参见绑定上传的文件到一个表单。
三、使用表单模板
你需要做的就是将表单实例放进模板的上下文。如果你的表单在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" /></p>
<p><label for="id_message">Message:</label>
<input type="text" name="message" id="id_message" /></p>
<p><label for="id_sender">Sender:</label>
<input type="email" name="sender" id="id_sender" /></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>,它被一起的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>
渲染表单的错误信息
当然,这个便利性的代价是更多的工作。直到现在,我们没有担心如何展示错误信息,因为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 %}
非字段错误(以及使用form.as_p() 时渲染的隐藏字段错误)将渲染成一个额外的CSS 类型nonfield 以助于和字段错误信息区分。例如,{{ form.non_field_errors }} 看上去会像:
<ul class="errorlist nonfield">
<li>Generic validation error</li>
</ul>
添加上面示例中提到的nonfield CSS 类型。
参见Forms API 以获得关于错误、样式以及在模板中使用表单属性的更多内容。
迭代表单的字段
如果你为你的表单使用相同的HTML,你可以使用{% for %} 循环迭代每个字段来减少重复的代码:
{% for field in form %}
<div class="fieldWrapper">
{{ field.errors }}
{{ field.label_tag }} {{ field }}
</div>
{% endfor %}
{{ field }} 中有用的属性包括:
- {{ field.label }}
- 字段的label,例如Email address。
- {{ field.label_tag }}
-
包含在HTML <label> 标签中的字段Label。它包含表单的label_suffix。例如,默认的label_suffix 是一个冒号:
<label for="id_email">Email address:</label>
- {{ field.id_for_label }}
- 用于这个字段的ID(在上面的例子中是id_email)。如果你正在手工构造label,你可能想使用它代替label_tag。如果你有一些内嵌的JavaScript 并且想避免硬编码字段的ID,这也是有用的。
- {{ field.value }}
- 字段的值,例如someone@example.com。
- {{ field.html_name }}
- 输入元素的name 属性中将使用的名称。它将考虑到表单的前缀。
- {{ field.help_text }}
- 与该字段关联的帮助文档。
- {{ field.errors }}
- 输出一个<ul class="errorlist">,包含这个字段的验证错误信息。你可以使用{% for error in field.errors %}自定义错误的显示。 这种情况下,循环中的每个对象只是一个包含错误信息的简单字符串。
- {{ field.is_hidden }}
- 如果字段是隐藏字段,则为True,否则为False。作为模板变量,它不是很有用处,但是可以用于条件测试,例如:
{% if field.is_hidden %}
{% endif %}
- {{ field.field }}
- 表单类中的Field 实例,通过BoundField 封装。你可以使用它来访问Field 属性,例如{% char_field.field.max_length %}。
可重用的表单模板
如果你的网站在多个地方对表单使用相同的渲染逻辑,你可以保存表单的循环到一个单独的模板中来减少重复,然后在其它模板中使用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 %}
其他:
from django.forms import Form from django.forms import fields from django.forms import widgets class UserForm(Form): username = fields.CharField(label='用户名') password = fields.CharField(label='密码', widget=widgets.PasswordInput) confirm_password = fields.CharField(label='确认密码', widget=widgets.PasswordInput) email = fields.EmailField(label='邮箱') gender = fields.ChoiceField(label='性别', choices=((1, '男'), (2, '女')), initial=1, widget=widgets.RadioSelect) age = fields.IntegerField(label='年龄') birthday = fields.DateField(label='生日', widget=widgets.DateInput(attrs={'type': 'date'})) salary = fields.DecimalField(label='收入') hobby = fields.MultipleChoiceField(label='爱好', choices=((1, '唱歌'), (2, '跳舞'), (3, '游泳')), initial=(1, 2), widget=widgets.CheckboxSelectMultiple) introduce = fields.CharField(label='介绍', widget=widgets.Textarea) is_agree = fields.BooleanField(label='是否同意', initial=True)
自定义错误信息:
email = fields.EmailField(label='邮箱', error_messages={'required': '必填', 'invalid': '邮箱格式不正确'})
验证:
from django.forms import Form from django.forms import fields from django.core.validators import RegexValidator from django.core.exceptions import ValidationError class UserForm(Form): username = fields.CharField(label='用户名', max_length=10, validators=[RegexValidator(r'^w+$', '数字或字母或_'), ], error_messages={'required': '必填', 'max_length': '最多10个字符'}) def clean_username(self): username = self.cleaned_data['username'] # 数据库 判断是否唯一 if True: raise ValidationError('用户名已经存在') return username
from django.forms import Form from django.forms import fields from django.forms import widgets from django.core.exceptions import ValidationError class UserForm(Form): password = fields.CharField(label='密码', widget=widgets.PasswordInput, error_messages={'required': '必填'}) confirm_password = fields.CharField(label='确认密码', widget=widgets.PasswordInput, error_messages={'required': '必填'}) def clean(self): password = self.cleaned_data.get('password', '') confirm_password = self.cleaned_data.get('confirm_password', '') if password != confirm_password: self.add_error('confirm_password', ValidationError('两次输入不一致')) return self.cleaned_data
form 数据更新问题:
class UserForm(Form): name = fields.ChoiceField() def __init__(self, *args, **kwargs): super(UserForm, self).__init__(*args, **kwargs) self.fields['name'].choices = models.UserInfo.objects.values_list('id', 'name')
modelform 使用:
from .models import * from django.forms import ModelForm from django.forms import widgets as Fwidgets
from django.forms import fields
class DeptModelForm(ModelForm):
is_rmb = fields.CharField(widget=Fwidgets.CheckboxInput()) # 额外字段
class Meta:
model = Dept
fields = '__all__'
# fields = ['name', ] # exclude = ['name', ]
widgets = {
'dept_desc': Fwidgets.Textarea(attrs={'class': 'form-control', 'rows': '10', 'cols': '20'})
}
def add(request):if request.method == 'GET': model_form = DeptModelForm() else: model_form = DeptModel(data=request.POST) if model_form.is_valid(): model_form.save() return render(request, 'add.html', {'model_form': model_form})
注:模型中定义的verbose_name,blank等 均是为 modelform 提供的设置项