一,什么是表单
HTML——表单可做为一个简单的表单概念的入门。
二,django与表单
(一)使用原生HTML表单
1,创建模板:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form</title>
<style>
.form {
text-align: center;
margin-right: auto;
margin-left: auto;
width: 300px;
padding: 5px;
border: 5px solid #f88080;
border-radius: 5px;
}
input {
background: transparent;
margin: 5px;
border: solid;
border-radius: 5px;
}
input:hover {
box-shadow: 5px 3px 3px 1px rgba(0, 0, 0, 0.15);
cursor: auto;
}
button {
background: #ffe9e9;
box-shadow: 5px 3px 3px 1px rgba(0, 0, 0, 0.15);
border: solid;
border-radius: 5px;
}
button:hover {
background: #f88080;
cursor: pointer;
}
button:active {
-webkit-transform: translate(1px, 1px);
box-shadow: none;
}
</style>
</head>
<body>
<div class="form_div">
<form id="user-info" class="form" action="{% url 'form' %}" method="post">
<div class="input-group">
<label for="user-name">user name:</label>
<input type="text" id="user-name" name="user-name" required/>
</div>
<div class="input-group">
<label for="user-email">user email:</label>
<input type="email" id="user-email" name="user-email" required/>
</div>
<div class="input-group">
<button type="submit" id="submit">Submit</button>
</div>
</form>
</div>
</body>
</html>
input
元素必须带有name
属性。
2,创建视图:
@csrf_exempt
def index(request):
# 处理表单的POST提交
if request.method == 'POST':
# 获取表单字段
name = request.POST.get('user-name', '')
email = request.POST.get('user-email', '')
# 处理表单字段
data = {'user-name': name, 'user-email': email}
data = json.dumps(data)
print(data)
print(type(data))
# 响应表单提交
return HttpResponse(data, content_type="application/json")
return render(request, 'index.html', locals())
- 必须使用
@csrf_exempt
装饰器关闭CSRF防护。 - 使用
request.POST.get('field-name', '')
的方式从input
元素中获取表单字段值。
3,创建路由:
urlpatterns = [
path('form/', index, name="form")
]
运行效果:
在django中使用一般HTML表单,会有这么一些问题:
- 需要自定义所有的表单小部件。
- 尽管浏览器可根据表单中
input
元素的type
属性实现字段验证,但有时需要在后端自定义一些验证。 - 没有CSRF防护,使得表单数据不够安全。
(二)django中的表单与表单类
django中的表单是对原生HTML表单的进一步优化:
- 模板系统能根据表单类自动扩展HTML表单。
- 能配合模板标签实现CSRF防护
- 表单API提供更自带的、安全的数据验证功能。
- 能根据模型自动生成对应的表单以方便操作模型数据。
无论是与原生HTML表单的生成与渲染方便性相比,还是与使用原生JavaScript进行表单数据验证等操作相比,django的表单系统更占优势。
表单系统依赖于表单类实现。
表单类的定义与模型类的定义方式相似。如果说模型类的字段会映射到数据库字段,那么:
- Form类:将类的字段映射到HTML表单的
input
元素。 - ModelForm类:将模型的字段映射到HTML表单的
input
元素。
(三)在 django 中使用表单
1,通常需要将表单数据保存到数据库,所以先创建模型:
models.py:
from django.db import models
# Create your models here.
class userInfo(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
def __str__(self):
return self.first_name
2,定义表单:
forms.py:
from django import forms
class NameForm(forms.Form):
first_name = forms.CharField(label='first name', max_length=50)
last_name = forms.CharField(label='last name', max_length=50)
- 表单的定义与模型的定义类似。
3,编写视图使用表单实例:
from django.http import HttpResponse
from django.shortcuts import render
from .forms import NameForm
from .models import userInfo
def name(request):
# 处理POST请求
if request.method == 'POST':
# 实例化表单并用请求中的数据填充它
form = NameForm(request.POST)
# 验证表单数据的合理性:
if form.is_valid():
# 清洗表单数据
first_name = form.cleaned_data['first_name']
last_name = form.cleaned_data['last_name']
print(first_name + " " + last_name)
# 将表单数据保存到数据库
user_info = userInfo(first_name=first_name, last_name=last_name)
user_info.save()
# 响应
return HttpResponse("Hello, " + first_name + " " + last_name)
# 处理其他请求方式
else:
# 实例化空表单
form = NameForm()
# 将空表单实例传入模板进行渲染
return render(request, 'name.html', {'form': form})
4,编写模板:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action={% url "name" %} method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Submit">
</form>
</body>
</html>
- 使用
csrf_token
标签用于 CSRF 保护。 {{ form.as_p }}
将表单渲染为一系列p
元素,每个元素都包含一个字段。
从上面那个最简使用流程展开说一下表单及其使用中的细节。
三,表单的定义
表单的定义需要继承Form
类或ModelForm
类。
使用Form
类时,最主要的内容就是定义表单字段:
from django import forms
class NameForm(forms.Form):
first_name = forms.CharField(label='first name', max_length=50)
last_name = forms.CharField(label='last name', max_length=50)
使用ModelForm
类时,最主要的内容就是定义表单的元数据:
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.forms import ModelForm
TITLE_CHOICES = [
('MR', 'Mr.'),
('MRS', 'Mrs.'),
('MS', 'Ms.'),
]
class Author(models.Model):
name = models.CharField(max_length=100)
title = models.CharField(max_length=3, choices=TITLE_CHOICES)
birth_date = models.DateField(blank=True, null=True)
def __str__(self):
return self.name
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name', 'title', 'birth_date')
labels = {
'name': _('Writer'),
}
help_texts = {
'name': _('Some useful help text.'),
}
error_messages = {
'name': {
'max_length': _("This writer's name is too long."),
},
}
(一)Form
类中的字段及其参数
对Form
类而言:
- 每一个字段类型能处理对应的数据类型。
- 所有字段类型都是对应字段类的实例,这些类定义在
django/forms/fields.py
文件中。 - 所有字段类型都有可接受的一些公共参数,不同的字段类型可接受不同的参数。
1,核心字段参数
核心字段参数是每个字段类的构造函数都能够接受的参数,用于对表单字段进行控制,比如:
required
指定字段是否是必填的。label
指定 HTML 输入元素的label元素。initial
指定显示时的初始值,并不用于提交。widget
指定表示 HTML 输入元素的部件。help_text
指定描述性文本。error_messages
指定消息以覆盖该字段将引发的默认消息。validators
指定字段使用的验证函数的列表。localize
实现表单数据输入和渲染输出的本地化。disabled
指定是否禁用表单字段。
2,内置的字段
对于每个内置的字段,在没有指定 widget
参数时使用默认部件。
(二)ModelForm
类中的字段及其属性
对ModelForm
类而言:
- 每一个字段类型就是模型中对应的字段类型。
- 可使用元数据属性额外统一指定表单字段上的参数。
1,字段
每个模型字段都有一个对应的默认表单字段,一帮情况下可以不用再单独指定字段的类型,系统会根据对应模型使用的字段剋行使用默认的字段类型,具体参考字段类型。
2,属性
设置元数据属性才是使用ModelForm
类时的重点。
(1)选择要使用的字段
官方强烈建议使用 fields
属性来显式设置所有应在表单中编辑的字段:
TITLE_CHOICES = [
('MR', 'Mr.'),
('MRS', 'Mrs.'),
('MS', 'Ms.'),
]
class Author(models.Model):
name = models.CharField(max_length=100)
title = models.CharField(max_length=3, choices=TITLE_CHOICES)
birth_date = models.DateField(blank=True, null=True)
def __str__(self):
return self.name
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name', 'title', 'birth_date')
另一种方法是自动包含所有字段,或者只删除部分字段:
class AuthorForm(ModelForm):
class Meta:
model = Author
# 自动包含所有字段
fields = '__all__'
# 删除部分字段
# exclude = ['title']
- 但官方说这种基本方法不太安全,并导致了某些网站的严重漏洞,比如Public Key Security Vulnerability and Mitigation。
不管使用哪一种字段选择方法,都要注意以下一些规则:
- 字段会按模型中定义的顺序在表单中出现,
ManyToManyField
会排在最后。 - 如果在模型字段中定义了
editable=False
, 则任何使用ModelForm
给该模型创建的表单都不会包含这个字段。
(2)覆盖默认字段
不同字段类型使用默认渲染方式,也能用下面的方法来覆盖默认字段类型。
一种方法是使用field_classes
属性定义表单实例化时的字段类型:
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name', 'title', 'birth_date')
field_classes = {
'birth_date': DateTimeField,
}
另一种方法是使用 widgets
属性覆盖字段默认的组件:
TITLE_CHOICES = [
('MR', 'Mr.'),
('MRS', 'Mrs.'),
('MS', 'Ms.'),
]
class Author(models.Model):
name = models.CharField(max_length=100) # CharField由对应ModelForm默认使用<input type="text">
title = models.CharField(max_length=3, choices=TITLE_CHOICES)
birth_date = models.DateField(blank=True, null=True)
def __str__(self):
return self.name
class AuthorForm(ModelForm):
class Meta:
model = Author
fields = ('name', 'title', 'birth_date')
widgets = {
'name': Textarea(attrs={'cols': 80, 'rows': 20}), # 用 <textarea> 覆盖字段默认的组件<input type="text">
}
# 更进一步,定义更多细节:
# labels = {
# 'first_name': _('first_name'),
# }
# help_texts = {
# 'first_name': _('Some useful help text.'),
# }
# error_messages = {
# 'first_name': {
# 'max_length': _("This first_name is too long."),
# },
# }
还有一种方法,可以通过声明指定字段类型并显式设置相关参数来完全控制一个字段:
class ArticleForm(ModelForm):
slug = CharField(validators=[validate_slug])
class Meta:
model = Article
fields = ['pub_date', 'headline', 'content', 'reporter', 'slug']
- 因为显式定义的字段不会从对应的模型中获取它们的属性,所以如果要保持模型中指定的行为,就需要显式设置相关参数。
3,ModelForm的工厂函数
还有一种方法来使用ModelForm,如果不需要额外重新指定相关属性,那么ModelForm
工厂函数比定义一个类来得更简单:
>>> from django.forms import modelform_factory
>>> from myapp.models import Book
>>> BookForm = modelform_factory(Book, fields=("author", "title"))
四,表单实例及其方法
在定义好表单之后,就需要将它导入到视图中进行实例化,再进行一些相关的操作,然后将它作为上下文数据传递到模板进行渲染。
(一)GET与POST
表单的提交方式一般会使用POST方法。
通常情况下,我们会在视图对GET请求的处理中不给表单绑定数据来渲染一个空白表单,以准备被用户填充一些数据。再在对POST请求的处理中操作接收到的表单数据。
(二)绑定和未绑定的表单
一个表单实例要么绑定到一组数据,要么未绑定:
- 如果是绑定了一组数据,它才能够验证这些数据,并将表单和数据渲染成 HTML:
>>> data = {'subject': 'hello',
... 'message': 'Hi there',
... 'sender': 'foo@example.com',
... 'cc_myself': True}
>>> f = ContactForm(data) # 实例化时,通过字典的形式绑定数据到表单,键是字段名,值是将要被验证的数据
# 通常使用 form = ContactForm(request.POST) 这种方式将请求数据绑定到表单
- 如果未绑定,它就不能进行验证(因为没有数据可验证!),但它仍然可以将空白表单渲染为 HTML:
>>> f = ContactForm() # 实例化时,未绑定数据到表单
可以通过查看表单类的 is_bound
属性的值来判断一个表单实例是否被绑定:
>>> f = ContactForm()
>>> f.is_bound
False
>>> f = ContactForm({'subject': 'hello'})
>>> f.is_bound
True
(三)表单初始值
在Form
类中的核心字段参数中提到initial
参数指定显示时的初始值,并不用于提交。
如果想要指定一个能被用于提交的初始值或者说默认值,可在实例化表单时使用initial
参数以字典的形式指定要被初始化的字段名及其数据:
>>> f = ContactForm(initial={'subject': 'Hi there!'})
- 实例化
ModelForm
表单时,initial
参数同样适用。 - 实例化时使用
initial
参数(Form.initial
)会覆盖定义字段时使用的initial
参数(Field.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.get_initial_for_field(field, field_name)
方法会返回一个表单字段的初始数据。如果存在的话,它从 Form.initial
检索数据,否则尝试 Field.initial
:
>>> import uuid
>>> class UUIDCommentForm(CommentForm):
... identifier = forms.UUIDField(initial=uuid.uuid4)
>>> f = UUIDCommentForm()
>>> f.get_initial_for_field(f.fields['identifier'], 'identifier')
UUID('972ca9e4-7bfe-4f5b-af7d-07b3aa306334')
>>> f.get_initial_for_field(f.fields['identifier'], 'identifier')
UUID('1b411fab-844e-4dec-bd4f-e9b0495f04d0')
- 官方更推荐使用
BoundField.initial
方式来获取一个表单字段的初始数据,一是因为它的用法更简单,而是这种方法因为缓存获得的值(这在处理返回值是可能改变的可调用对象时尤其有用(例如,datetime.now 或 uuid.uuid4):
>>> # Using BoundField.initial, for comparison
>>> f['identifier'].initial
UUID('28a09c59-5f00-4ed9-9179-a3b074fa9c30')
>>> f['identifier'].initial
UUID('28a09c59-5f00-4ed9-9179-a3b074fa9c30')
Form.has_changed(data, initial)
方法能检查表单数据是否相较初始数据发生变化:
>>> 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()
Form.changed_data
属性返回表单绑定数据(通常是 request.POST)中与初始数据中数据不同的字段名列表;如果没有不同的数据,则返回一个空列表:
>>> f = ContactForm(request.POST, initial=data)
>>> if f.has_changed():
... print("The following fields changed: %s" % ", ".join(f.changed_data))
>>> f.changed_data
['subject', 'message']
(四)访问表单中的字段、按钮与值
1,字段与值
# 定义表单
class NameForm(forms.Form):
first_name = forms.CharField(label='first name', max_length=50)
last_name = forms.CharField(label='last name', max_length=50, )
# 实例化表单
form = NameForm(request.POST)
>>> print(type(form))
<class 'index.forms.NameForm'>
>>> print(form)
<tr><th><label for="id_first_name">first name:</label></th><td><input type="text" name="first_name" value="a" maxlength="50" required id="id_first_name"></td></tr>
<tr><th><label for="id_last_name">last name:</label></th><td><input type="text" name="last_name" value="c" maxlength="50" required id="id_last_name"></td></tr>
直接从表单中获取字段的值:
>>> print(form['first_name'])
<input type="text" name="first_name" value="a" maxlength="50" required id="id_first_name">
>>> print(form['first_name'].value())
a
从 Form
实例的 data
属性中获取原始表单字段数据:
>>> print(type(form.data))
<class 'django.http.request.QueryDict'>
>>> for row in form.data:
print(row)
csrfmiddlewaretoken
first_name
last_name
>>> for row in form.data.values():
print(row)
BttttDt2qOd4SGbmqB9j72PkMvcVz99K8KWblpWLtG1Elz1W3R21hcoCC1BSUzlR
a
c
ModelForm
表单同理。
从 Form
实例的 fields
属性中访问字段:
>>> print(form.fields)
{'first_name': <django.forms.fields.CharField object at 0x000001B82A8AC948>, 'last_name': <django.forms.fields.CharField object at 0x000001B82A8BEEC8>}
>>> print(form.fields['first_name'])
<django.forms.fields.CharField object at 0x0000011A82BAD5C8>
>>> for row in form.fields.values():
print(row)
<django.forms.fields.CharField object at 0x0000011A82BAD5C8>
<django.forms.fields.CharField object at 0x0000011A82BBCA88>
ModelForm
表单同理。
2,按钮与值
有时候,一个表单需要多个按钮,比如,登录页面的表单中需要登录按钮、注册\按钮等。
如果需要为这些不同的按钮实现不同的点击功能,首先就需要在模板的按钮定义中为元素添加有不同值的name
属性以区分不同的按钮,然后在同一个视图中分别判断请求数据中是否有某个name
值,进而实现不同功能:
模板:
<input type="submit" name="signin" value="登录">
<input type="submit" name="signup" value="注册">
视图:
if request.method == 'POST':
myform = MyForm(request.POST)
if myform.is_valid():
if 'signup' in request.POST:
注册
if 'signin' in request.POST:
登录
(五)表单验证与数据清洗
无论是使用Form
类定义表单时使用指定的字段类型,还是使用ModelForm
类根据模型自动生成表单,表单的字段都会根据自己的类型提供一套验证功能。
表单类的主要任务是验证数据,以判断输入的数据是否符合字段类型指定的格式以及我们传入的一些参数的要求。
验证数据的本质就是清洗数据。有多个地方进行了数据清洗的过程,每个地方都有不同的目的,但都为提供干净的数据服务。
1,表单验证与清洗的过程
Form
和ModelForm
都继承BaseForm
,自然就就拥有了其中的is_valid()
,它会在表单被绑定且没有数据错误时返回 True
:
def is_valid(self):
"""Return True if the form has no errors, or False otherwise."""
return self.is_bound and not self.errors
is_valid
中 self.is_bound
是一个存储实例化表单时是否绑定了数据的标志位:
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
...
self.is_bound = data is not None or files is not None
...
is_valid
中 self._errors
开启整表数据清洗,并判断表单数据是否有错误:
@property
def errors(self):
"""Return an ErrorDict for the data provided for the form."""
if self._errors is None:
self.full_clean()
return self._errors
self._errors
中 self._errors
是一个存储表单数据清洗中是否有错误的标识位:
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
...
self.is_bound = data is not None or files is not None
...
self._errors = None # Stores the errors after clean() has been called.
...
self._errors
中 self.full_clean()
启动数据清理工作:
def full_clean(self):
"""
Clean all of self.data and populate self._errors and self.cleaned_data.
"""
self._errors = ErrorDict() # 如何以各种格式显示自身的错误集合。字典键是字段名称,值是错误。
if not self.is_bound: # Stop further processing.
return
self.cleaned_data = {}
# If the form is permitted to be empty, and none of the form data has
# changed from the initial data, short circuit any validation.
if self.empty_permitted and not self.has_changed():
return
self._clean_fields()
self._clean_form()
self._post_clean()
首先,self.full_clean()
中self._clean_fields()
调用field.clean()
清理表单中的单个字段:
def _clean_fields(self):
for name, field in self.fields.items():
# value_from_datadict() gets the data from the data dictionaries.
# Each widget type knows how to retrieve its own data, because some
# widgets split data over several HTML fields.
if field.disabled:
value = self.get_initial_for_field(field, name)
else:
value = field.widget.value_from_datadict(self.data, self.files, self.ad
try:
if isinstance(field, FileField):
initial = self.get_initial_for_field(field, name)
value = field.clean(value, initial)
else:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self.add_error(name, e)
self._clean_fields()
中field.clean()
验证给定值并将已清洗的值作为适当的 Python 对象返回,为任何错误引发 ValidationError
并向上传播:
def clean(self, value):
"""
Validate the given value and return its "cleaned" value as an
appropriate Python object. Raise ValidationError for any errors.
"""
value = self.to_python(value)
self.validate(value)
self.run_validators(value)
return value
然后,self.full_clean()
中self._clean_form()
调用self.clean()
获取已清理的数据:
def _clean_form(self):
try:
cleaned_data = self.clean()
except ValidationError as e:
self.add_error(None, e)
else:
if cleaned_data is not None:
self.cleaned_data = cleaned_data
self._clean_form()
中的self.clean()
是一个钩子:在对每个字段调用 Field.clean()
之后进行任何额外的表单范围的数据清洗的钩子:
def clean(self):
"""
Hook for doing any extra form-wide cleaning after Field.clean() has been
called on every field. Any ValidationError raised by this method will
not be associated with a particular field; it will have a special-case
association with the field named '__all__'.
"""
return self.cleaned_data
最后,self.full_clean()
中self._post_clean
也是一个钩子:用于在表单清洗完成后进行额外的清洗。用于ModelForm
中的模型验证:
def _post_clean(self):
"""
An internal hook for performing additional cleaning after form cleaning
is complete. Used for model validation in model forms.
"""
pass
这些方法通常在调用表单上的 is_valid()
方法时执行。尽管还有其他一些事情也可以触发清洗和验证(访问 errors
属性或直接调用 full_clean()
),但官方建议我们一般不要这么做。
2,验证Form的数据
调用Form
类的is_valid()
方法会触发表单数据的验证过程,它返回一个布尔值来显示所有数据是否有效:
>>> data = {'subject': 'hello',
... 'message': 'Hi there',
... 'sender': 'foo@example.com',
... 'cc_myself': True}
>>> f = ContactForm(data)
>>> f.is_valid()
True
>>> data = {'subject': '',
... 'message': 'Hi there',
... 'sender': 'invalid email address',
... 'cc_myself': True}
>>> f = ContactForm(data)
>>> f.is_valid()
False
在验证失败后,还能使用Form.errors
属性来获取错误信息:
>>> f.errors
{'sender': ['Enter a valid email address.'], 'subject': ['This field is required.']}
Form
类还提供一些其他的方法来获取有关于表单验证的相关信息,详见Using forms to validate data。
2,验证ModelForm的数据
验证ModelForm
的数据与验证Form
的数据有所不同,需要两个步骤:
- 验证表单:在调用表单的
is_valid()
或访问表单的errors
属性时隐式触发,在调用模型的full_clean()
时显式触发。 - 验证模型实例:在调用模型的
full_clean()
时触发。
3,获取干净的数据
Form类还拥有数据清洗功能,将输入的数据规范为一致的格式。
比如 DateField
将输入规范化为 Python 的 datetime.date
对象。无论你传递给它的是格式为 ‘1994-07-15’ 的字符串,还是 datetime.date
对象,或者其他一些格式,只要它是有效的,DateField
都会将它规范化为 datetime.date
对象。
当一个 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'}
# 不在 Form 中定义的字段不会被保存到 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'}
ModelForm
表单同理。
4,自定义表单数据清洗
根据前面讲的验证过程,如果想自定义表单数据验证和清洗过程,有这么几处地方可供自定义或覆盖:
-
Form.clean()
方法 -
Form.clean_<fieldname>()
方法:
class VocationForm(forms.Form):
job = forms.CharField(max_length=20, label='职位',
required=False)
title = forms.CharField(max_length=20,
label='职称',
widget=forms.widgets.TextInput(attrs={'class': 'jobtitle'}),
error_messages={'required': '职称不能为空!'})
payment = forms.IntegerField(label='薪资',
widget=forms.widgets.TextInput(attrs={'class': 'jobpayment',
'placeholder': u'人民币数值(单位:元)'}),
validators=[payment_validate])
def clean_title(self):
data = self.cleaned_data['title']
return '初级' + data
Form._post_clean()
方法Field.to_python()
方法Field.validate()
方法Field.run_validators()
方法Field.clean()
方法
5,自定义字段验证器
Django 的表单(和模型)字段支持使用被称为验证器的实用函数和类,它接收一个值,如果该值有效则不返回任何内容,如果无效则引发一个 ValidationError
。它们可以通过字段的 validators
参数传递给字段的构造函数,或者在 Field
类本身的 default_validators
属性中定义。
# 自定义验证
def payment_validate(value):
if value > 300000000000:
raise ValidationError('请输入合理薪资!')
class VocationForm(forms.Form):
use_required_attribute = False
job = forms.CharField(max_length=20, label='职位',
required=False)
title = forms.CharField(max_length=20,
label='职称',
widget=forms.widgets.TextInput(attrs={'class': 'jobtitle'}),
error_messages={'required': '职称不能为空!'})
payment = forms.IntegerField(label='薪资',
widget=forms.widgets.TextInput(attrs={'class': 'jobpayment',
'placeholder': u'人民币数值(单位:元)'}),
validators=[payment_validate])
def clean_title(self):
data = self.cleaned_data['title']
return '初级' + data
(六)保存表单数据到数据库
从表单获取干净的数据后,就能将它们保存都数据库了。
保存Form
表单数据到数据库的需要使用模型的管理器或模型实例方法:
# 方法一:
MyModel.objects.create(modelfield_name=formfield_value)
# 方法二:
my_model_instance = MyModel(modelfield_name=formfield_value)
my_model_instance.save()
而ModelForm
类自带save()
方法,可根据绑定到表单的数据创建并保存数据库对象:
- 如果有一个传入模型实例的关键字参数
instance
,则save()
会更新这个实例;否则会创建一个对应模型的新实例。 - 如果表单尚未验证 ,调用
save()
将通过检查form.errors
来触发验证。
>>> 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()
ModelForm.save()
方法还有一个非常有用的参数instance
。
如果instance=False
,则 save()
会返回一个尚未保存到数据库的对象。
- 如果,要在保存对象之前对对象执行一些额外操作,或者要使用某一个模型保存选项,或者需要给模型里
null=False
字段添加一些非表单的数据,这就非常有用。在操作完之后,只需要在生成的模型实例上再次调用save()
就能完成数据保存:
def index(request):
if request.method == 'POST':
blog_form = Blog(request.POST)
if blog_form.is_valid():
title = blog_form.cleaned_data['title']
content = blog_form.cleaned_data['content']
blog_info = BlogInfo(title=title, content=content)
blog_info.save(commit=False)
blog_info.author = request.user # 添加表单中没有的数据
blog_info.save()
return HttpResponseRedirect("/blog/")
else:
form = Blog()
return render(request, 'index.html', {'form': form})
- 如果使用到的模型与另一个模型有多对多关系,使用了
commit=False
,那么ManyToManyField
的储存需要等该条目存入数据库之后手动调用ModelForm
的save_m2m()
方法(django每次都会在有多对多关系的表单实例使用commit=False
保存表单时,向ModelForm
子类添加一个save_m2m()
方法):
# 用POST的数据创建一个表单实例
>>> f = ClassForm(request.POST)
# 创建但不保存班级表单实例到数据库
>>> new_class = f.save(commit=False)
# Modify the author in some way.
>>> new_class.some_field = 'some_value'
# Save the new instance.
>>> new_class.save()
# Now, save the many-to-many data for the form.
>>> f.save_m2m()
五,表单模板
表单对象的第二个任务是将其本身渲染为 HTML。
(一)基本使用方法
在模板中使用表单的最简单的方法:
- 在视图中,将表单实例 form 放到模板的上下文中
- 在模板中,使用 {{ form }} 将渲染这个表单实例到
<form>
元素中
view:
def view_function(request):
form = NameForm()
return render(request, 'index.html', {'form': form})
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>form</title>
</head>
<body>
<form action="{% url 'form' %}" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit">
</form>
</body>
</html>
(二)表单渲染
1,默认的渲染效果
默认的渲染效果指的是使用 {{ form }} 时的渲染效果。
先来看看未绑定表单的默认渲染效果:
>>> form = NameForm()
>>> form
<tr><th><label for="id_first_name">first name:</label></th><td><input type="text" name="first_name" maxlength="50" required id="id_first_name"></td></tr>
<tr><th><label for="id_last_name">last name:</label></th><td><input type="text" name="last_name" maxlength="50" required id="id_last_name"></td></tr>
这里的输出比较简单:
- 不 包括
<form>
标签,也不包括<table>
标签。 - 每个字段类型都有一个默认的 HTML 表示。例如
CharField
用<input type="text">
表示,EmailField
用<input type="email">
表示。可以手动指定所使用的部件。 - 每个字段的标签的
name
属性值是表单类中的字段名。 - 每个字段的文本标签是表单类中的字段名(将所有下划线转换为空格)。可以手动指定所使用的标签。
- 每个文本标签都被一个
<label>
标签包围,其id
是在字段名前加上 ‘id_’ 生成的。可以手动指定生成方式。
再来看看绑定表单的默认渲染效果:
>>> form = NameForm(request.POST)
>>> form
<tr><th><label for="id_first_name">first name:</label></th><td><input type="text" name="first_name" value="a" maxlength="50" required id="id_first_name"></td></tr>
<tr><th><label for="id_last_name">last name:</label></th><td><input type="text" name="last_name" value="b" maxlength="50" required id="id_last_name"></td></tr>
- 在标签的
value
属性中保存了输入值。
2,格式化渲染
表单提供了如下方法,方便实现格式化渲染,字段都按照表单类中定义的顺序进行显示。
1,as_p()
方法将表单渲染成一系列的 <p>
标签,每个标签包含一个字段:
{{ form.as_p }}
>>> form = NameForm()
>>> form.as_p()
<p><label for="id_first_name">first name:</label> <input type="text" name="first_name" maxlength="50" required id="id_first_name"></p>
<p><label for="id_last_name">last name:</label> <input type="text" name="last_name" maxlength="50" required id="id_last_name"></p>
2,as_ul()
方法将表单渲染成一系列的 <li>
标签,不包括 <ul>
标签,每个标签包含一个字段:
{{ form.as_ul }}
>>> form = NameForm()
>>> form.as_ul()
<li><label for="id_first_name">first name:</label> <input type="text" name="first_name" maxlength="50" required id="id_first_name"></li>
<li><label for="id_last_name">last name:</label> <input type="text" name="last_name" maxlength="50" required id="id_last_name"></li>
3,as_table()
方法将表单渲染成<table>
标签:
{{ form.as_table }}
>>> form = NameForm()
>>> print(form.as_table())
<tr><th><label for="id_first_name">first name:</label></th><td><input type="text" name="first_name" maxlength="50" required id="id_first_name"></td></tr>
<tr><th><label for="id_last_name">last name:</label></th><td><input type="text" name="last_name" maxlength="50" required id="id_last_name"></td></tr>
3,自定义渲染细节
(1)手动渲染字段
每个字段都可以用 {{ form.name_of_field }}
作为表单的一个属性来渲染表单字段,这可以自定义字段顺序、定制HTML内容:
<form action="{% url 'form' %}" method="post">
{% csrf_token %}
<div class="fieldWrapper">
<label for="{{ form.first_name.id_for_label }}">first name:</label>
{{ form.first_name }}
</div>
<div class="fieldWrapper">
<label for="{{ form.last_name.id_for_label }}">last name:</label>
{{ form.last_name }}
</div>
<input type="submit" value="Submit">
</form>
(2)渲染表单的错误信息
使用 {{ form.name_of_field.errors }}
显示某字段的错误信息列表:
<form action="{% url 'form' %}" method="post">
{% csrf_token %}
{{ form.non_field_errors }}
<div class="fieldWrapper">
<label for="{{ form.first_name.id_for_label }}">first name:</label>
{{ form.first_name }}
{{ form.first_name.errors }}
</div>
<div class="fieldWrapper">
<label for="{{ form.last_name.id_for_label }}">last name:</label>
{{ form.last_name }}
{{ form.last_name.errors }}
</div>
<input type="submit" value="Submit">
</form>
(3)遍历表单字段
可以用 {% 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 %}
使用 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 %}
有关字段属性及方法的完整清单,请参阅 BoundField
。
(4)可复用的表单模板
如果在多个地方对表单使用相同的呈现逻辑,则可以将表单的基本样式保存在一个独立的模板中,并覆盖表单类的template_name
属性来使用自定义模板呈现表单:
# 表单的基本样式 form_snippet.html:
{% for field in form %}
<div class="fieldWrapper">
{{ field.errors }}
{{ field.label_tag }} {{ field }}
</div>
<input type="submit" value="Send message">
{% endfor %}
# 使用模板:
{{ form }}
class MyForm(forms.Form):
# 指定template_name
template_name = 'form_snippet.html'
...
(5)设置字段顺序
使用as_p()
、as_ul()
和 as_table()
渲染表单时,字段按照表单类中定义的顺序进行显示。
自定义字段顺序有两种方法:
- 手动渲染各个字段。
- 定义
field_order
指定字段的顺序,其余字段按默认顺序追加,列表中未知的字段名将被忽略。在实例化时定义的field_order
将覆盖表单定义中的field_order
。
六,使用类视图处理表单
类视图的存在,使得基于配置的操作让视图的定义更加简单,在其中使用表单也仅仅只需要进行简单配置即可。
表单:
from django import forms
class ContactForm(forms.Form):
name = forms.CharField()
message = forms.CharField(widget=forms.Textarea)
def send_email(self):
# send email using the self.cleaned_data dictionary
pass
像使用一般视图一样,只需要使用内置的通用编辑视图FormView
并进行简单配置:
在类视图中使用表单:
from myapp.forms import ContactForm
from django.views.generic.edit import FormView
class ContactFormView(FormView):
template_name = 'contact.html'
form_class = ContactForm
success_url = '/thanks/'
def form_valid(self, form):
# This method is called when valid form data has been POSTed.
# It should return an HttpResponse.
print(form.cleaned_data)
form.send_email()
return super().form_valid(form)
模型:
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Send message">
</form>
FormView
视图的工作流程如下:
- 使用
form_class
指定的表单; - 使用
template_name
指定的模板渲染表单; - POST表单的数据无误后调用
form_valid
方法,这里可获得清洗后的干净数据form.cleaned_data
并保存到数据库,或者调用表单类中自定义的一些方法;编辑成功后重定向到success_url
指定的路径。
FormView
视图的继承关系如下:
七,在同一个页面中使用多个表单
在同一个页面使用多个表单有好几种情况:
- 同一个页面有多个不同的表单。
- 同一个页面有多个相同的表单,且各个表单有各自的提交。
- 同一个页面有多个相同的表单,但所有表单都使用同一个提交。
(一)一般情况
第一种情况还是比较好处理。尽管不同表单有不同的定义内容,但只需要各自实例化就行。
针对第二种情况,django提供了一个表单属性prefix
设置表单前缀,根据这个前缀区别相同定义的表单:
>>> mother = PersonForm(prefix="mother")
>>> father = PersonForm(prefix="father")
>>> print(mother.as_ul())
<li><label for="id_mother-first_name">First name:</label> <input type="text" name="mother-first_name" id="id_mother-first_name" required></li>
<li><label for="id_mother-last_name">Last name:</label> <input type="text" name="mother-last_name" id="id_mother-last_name" required></li>
>>> print(father.as_ul())
<li><label for="id_father-first_name">First name:</label> <input type="text" name="father-first_name" id="id_father-first_name" required></li>
<li><label for="id_father-last_name">Last name:</label> <input type="text" name="father-last_name" id="id_father-last_name" required></li>
>>> mother = PersonForm(request.POST, prefix="mother")
>>> father = PersonForm(request.POST, prefix="father")
(二)批量处理表单
第三种情况就是表单的批量处理,常见于在同一个页面中输入多条同性质的数据后,用过一个按钮统一处理,为此。django提供表单集的概念。