表单
# 表单
在之前的案例中,每次我们需要提交表单数据的时候。我们都需要去手动编辑html表单,根据不同的字段,字段名,进行编码。做了很多重复的部分,所以django提供了一个专门用来处理表单的类,django.forms.Form
。
通过它,我们不仅能够自动生成前端页面,也可以用来验证数据的合法性。我们通过改写添加修改学生的表单来学习它。
# 创建表单
在app根目录下,创建一个forms.py
的模块,代码如下:
from django import forms
from .models import Channel
class StudentForm(forms.Form):
name = forms.CharField(label='姓名', max_length=20)
age = forms.IntegerField(label='年龄', required=False)
sex = forms.ChoiceField(label='性别', choices=((1, '男'), (0, '女')))
phone = forms.CharField(label='手机号码', required=False, max_length=20)
channel = forms.ModelChoiceField(label='渠道', required=False, queryset=Channel.objects.all())
2
3
4
5
6
7
8
9
10
每一个模型表单,都是forms.Form
的一个子类,类属性与模型的类属性类似,都表示不同类型的字段。不同的字段,将会渲染成不同的input类型。字段名与每一个input标签的name属性对应。
每一个字段都是一个字段类的实例,其中label
参数渲染成label
标签的内容。max_length
用来限制用户输入字符长度。required
参数表示该字段是否必填,默认为True
,要指定一个字段是不必填的,设置required=False
。
# 在模板中使用表单
只需要将表单实例放到模板上下文就可以通过模板变量使用表单。
# 渲染表单对象
修改学生添加页面视图如下:
from .forms import StudentForm
class StudentCreateView(View):
"""
学生添加视图
"""
def get(self, request):
"""学生添加页面"""
# 1. 获取渠道对象
channels = Channel.objects.all()
form = StudentForm()
return render(request, 'crm/student_detail.html', context={'channels': channels, 'form': form})
2
3
4
5
6
7
8
9
10
11
12
13
在视图中,实例化了一个表单对象,然后传递变量form
给了模板。那么在模板中通过{{ form }}
将会渲染对应的<label>
和<input>
元素,下面是StduentForm
实例用{{ form }}
的输出:
<tr><th><label for="id_name">姓名:</label></th><td><input type="text" name="name" maxlength="20" required id="id_name"></td></tr>
<tr><th><label for="id_age">年龄:</label></th><td><input type="number" name="age" id="id_age"></td></tr>
<tr><th><label for="id_sex">性别:</label></th><td><select name="sex" id="id_sex">
<option value="1">男</option>
<option value="0">女</option>
<option value="1">百度</option>
<option value="2">抖音</option>
<option value="3">b站</option>
</select></td></tr>
2
3
4
5
6
7
8
9
我们看到表单对象默认渲染了表格格式的字段,所以需要在模板中提供外层<form>
标签和submit
控件。
那么在模板中可以按照如下方式渲染:
<form >
<talbe>
{{ form }}
</talbe>
<input type="submit" value="添加" />
</form>
2
3
4
5
6
对于表单字段的渲染,还有如下格式:
{{ form.as_table }}
字段会渲染成表格元素<tr>
{{ form.as_p }}
字段会渲染成<p>
标签{{ form.as_ul }}
字段会渲染成<li>
标签
注意
记得提供外层的<table>
或<ul>
元素
# 手动渲染字段
直接渲染表单对象,不是太灵活,我们可以手动处理。每个字段都可以用{{ form.name_of_field }}
作为表单的一个属性,并被相应的渲染在模板中。例如:
{{ 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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
完整的<label>
元素还可以使用label_tag()
来生成。例如:
<div class="fieldWrapper">
{{ form.subject.errors }}
{{ form.subject.label_tag }}
{{ form.subject }}
</div>
2
3
4
5
# 渲染表单错误信息
表单的错误信息分两种,一种是{{ form.name_of_field.errors }}
显示对应字段的错误信息列表,它默认被渲染成为无序列表,看起来如下:
<ul class="errorlist">
<li>Sender is required.</li>
</ul>
2
3
该列表有一个CSS class errorlist
,允许自定义样式。如果想要进一步定义错误信息的显示,可以通过遍历来实现:
{% if form.subject.errors %}
<ol>
{% for error in form.subject.errors %}
<li><strong>{{ error|escape }}</strong></li>
{% endfor %}
</ol>
{% endif %}
2
3
4
5
6
7
第二种是{{ form.non_field_errors }}
显示非字段验证错误信息,它渲染后看起来如下:
<ul class="errorlist nonfield">
<li>Generic validation error</li>
</ul>
2
3
该列表会额外带上一个classnonfield
以便与字段验证错误信息区分。
# 遍历表单字段
如果表单字段使用相同的结构,可以对表单对象进行迭代:
{% 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 %}
2
3
4
5
6
7
8
9
有用的字段属性:
{{ field.lable }}
字段的label,比如Email address
{{ field.label_tag }}
该字段的label标签,它包含表单的label_suffix
,默认是个冒号,例如:
<label for="id_email">Email address:</label>
{{ field.id_for_label }}
该字段的id,用于手动构建label
{{ field.value }}
该字段的值
{{ field.html_name }}
字段名称,用于输入元素的name属性中。如果设置了表单前置,它也会被加进去。
{{ field.help_text }}
与该字段关联的帮助文本
{{ field.errors }}
输出错误信息列表
{{ field.is_hidden }}
如果该字段是隐藏字段,这个属性是True
,否则为False
# 部件
每一个表单字段,都会有一个对应的HTML元素与之对应。部件用来处理HTML渲染,以及从对应的GET/POST
字典中提取数据。
# 指定部件
每一个表单字段,django都会使用一个默认的部件来显示数据类型。要想知道哪个字段使用哪个部件,请查看内置Field类 (opens new window)。
有时候我们可能需要修改默认的部件,通过字段参数widget
来处理。例如:
from django import forms
class CommentForm(forms.Form):
name = forms.CharField()
url = forms.URLField()
comment = forms.CharField(widget=forms.Textarea)
2
3
4
5
6
字段comment
将会使用Textarea
部件,而不是默认的TextInput
部件。
# 样式化部件实例
默认情况下,部件渲染的表单标签没有css类,没有额外属性。可以通过attrs
参数进行设置:
class CommentForm(forms.Form):
name = forms.CharField(widget=forms.TextInput(attrs={'class': 'special'}))
url = forms.URLField()
comment = forms.CharField(widget=forms.TextInput(attrs={'size': '40'}))
2
3
4
也可以在表单定义中修改部件:
class CommentForm(forms.Form):
name = forms.CharField()
url = forms.URLField()
comment = forms.CharField()
name.widget.attrs.update({'class': 'special'})
comment.widget.attrs.update(size='40')
2
3
4
5
6
7
或者如果该字段没有直接在表单上声明(比如模型表单字段),可以使用Form.fields
属性:
class CommentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['name'].widget.attrs.update({'class': 'special'})
self.fields['comment'].widget.attrs.update(size='40')
2
3
4
5
Django会将这些属性包含在渲染的输出中:
>>> f = CommentForm(auto_id=False)
>>> f.as_table()
<tr><th>Name:</th><td><input type="text" name="name" class="special" 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" size="40" required></td></tr>
2
3
4
5
# 表单的校验
django中的表单除了渲染html外,还有一个很重要的作用就是校验数据。
看添加学生的视图案例:
class StudentCreateView(View):
"""
学生添加视图
"""
def post(self, request):
"""添加学生"""
form = StudentForm(request.POST)
if form.is_valid():
obj = Student.objects.create(**form.cleaned_data)
return redirect(reverse('student-list'))
return render(request, 'crm/student_detail.html', context={'form': form})
2
3
4
5
6
7
8
9
10
11
12
实例化表单时,可以将GET/POST
参数传入,然后调用表单对象的is_valid()
方法进行校验。如果校验通过,这个方法会返回True
,否则返回False
。
校验通过后通过cleaned_data
属性访问干净的数据。
# 指定字段校验
定义表单时,可以定义方法clean_<fieldname>()
方法对指定的字段进行校验,该方法不接受参数。在方法中通过self.cleaned_data
获取该字段的值。
如果校验不通过需要触发一个ValidationError
的异常,校验通过请return
该值。
在学生创建的逻辑中,我们没有验证电话号码的格式,在表单中编写一个校验方法如下:
import re
from django import forms
from django.core.exceptions import ValidationError
from .models import Channel, Student
class StudentForm(forms.ModelForm):
class Meta:
model = Student # 指定要生成表单的模型
exclude = ['c_time'] # 指定不需要生成的字段
def clean_phone(self):
phone = self.cleaned_data.get('phone')
if phone is not None:
if not re.match(r'1[3-9]\d{9}$', phone):
raise ValidationError('手机号码格式不正确!')
return phone
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 验证相互依赖的字段
有时候需要同时校验多个字段,比如注册时,校验密码和重复密码。这时复写clean()
方法是一个很好的办法:
from django import forms
from django.core.exceptions import ValidationError
class RegistorForm(forms.Form):
# Everything as before.
def clean(self):
cleaned_data = super().clean()
password = cleaned_data.get('password')
password_confirm = cleaned_data.get('password_confirm')
if not password == password_confirm:
raise ValidationError('输入的密码不一致!')
return cleaned_data
2
3
4
5
6
7
8
9
10
11
12
13
14
在表单的clean()
方法被调用前,上一节中的单字段校验方法都会先被运行。clean()
方法中如果出现验证错误,在模板中使用{{form.non_field_errors}}
显示。
# 模型表单
django提供了一个辅助类,可以从一个模型创建一个Form
类,而不需要重复定义字段。
修改学生表单如下:
from django import forms
from .models import Channel, Student
class StudentForm(forms.ModelForm):
class Meta:
model = Student # 指定要生成表单的模型
exclude = ['c_time'] # 指定不需要生成的字段
2
3
4
5
6
7
8
每一个模型表单都是forms.ModelForm
的一个子类,和普通表单不同。由于模型已经定义了字段,在模型表单中,只需要在Meta
类中指定模型和字段。
字段可以通过属性fields=['field1', 'field2', ..]
指定需要的字段,fields='all'
表示生成所有的字段, 也可以通过exclude = ['field1', 'field2', ..]
排除字段。
# save()
模型表单与普通的表单还有一个不同就是save()
方法。在校验过的表单实例上调用save()
方法,会自动调用对应的模型在数据库中创建数据或修改数据。
学生添加案例:
class StudentCreateView(View):
"""
学生添加视图
"""
def post(self, request):
"""添加学生"""
# 实例化表单
form = StudentForm(request.POST)
# 校验
if form.is_valid():
# 保存数据
form.save()
return redirect(reverse('student-list'))
return render(request, 'crm/student_detail.html', context={'form': form})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上面的代码中,如果表单校验通过,执行form.save()
会创建返回Student
实例并保存到数据库。
学生更新案例:
class StudentUpdateView(View):
"""
学生更新视图
"""
def get_obj(self, pk):
obj = get_object_or_404(Student, pk=pk)
return obj
def get(self, request, pk):
# 1. 获取修改对象
obj = self.get_obj(pk)
# 2. 实例化表单对象,并填充模型对象
form = StudentForm(instance=obj)
# 2. 渲染并返回修改页面
return render(request, 'crm/student_detail.html', context={'form': form})
def post(self, request, pk):
# 1. 获取修改对象
obj = self.get_obj(pk)
# 2. 实例化表单对象,填充前端传递的数据和模型对象
form = StudentForm(request.POST, instance=obj)
# 3. 校验
if form.is_valid():
form.save() # 保存更新
return redirect(reverse('student-list'))
return render(request, 'crm/student_detail.html', context={'form': form})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
上面的代码中实例化表单时传递POST参数,同时把要更新的模型对象传给instance
参数,在校验通过后,执行form.save()
会使用校验后的参数更新模型对象。
# 学生创建,更新视图案例
# 表单
# crm/froms.py
import re
from django import forms
from django.core.exceptions import ValidationError
from .models import Student
class StudentForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for name in self.fields:
self.fields[name].widget.attrs.update({'class': 'form-control'})
class Meta:
model = Student # 指定要生成表单的模型
exclude = ['c_time'] # 指定不需要生成的字段
def clean_phone(self):
phone = self.cleaned_data.get('phone')
if not re.match(r'1[3-9]\d{9}$', phone):
raise ValidationError('手机号码格式不正确!')
return phone
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 视图
# crm/views.py
class StudentCreateView(View):
"""
学生添加视图
"""
def get(self, request):
"""学生添加页面"""
# 1. 获取渠道对象
channels = Channel.objects.all()
form = StudentForm()
return render(request, 'crm/student_detail.html', context={'channels': channels, 'form': form})
def post(self, request):
"""添加学生"""
form = StudentForm(request.POST)
if form.is_valid():
form.save()
return redirect(reverse('student-list'))
return render(request, 'crm/student_detail.html', context={'form': form})
class StudentUpdateView(View):
"""
学生更新视图
"""
def get_obj(self, pk):
obj = get_object_or_404(Student, pk=pk)
return obj
def get(self, request, pk):
# 1. 获取修改对象
obj = self.get_obj(pk)
# 2. 实例化表单对象,并填充模型对象
form = StudentForm(instance=obj)
# 2. 渲染并返回修改页面
return render(request, 'crm/student_detail.html', context={'form': form})
def post(self, request, pk):
# 1. 获取修改对象
obj = self.get_obj(pk)
# 2. 实例化表单对象,填充前端传递的数据和模型对象
form = StudentForm(request.POST, instance=obj)
# 3. 校验
if form.is_valid():
form.save() # 保存更新
return redirect(reverse('student-list'))
return render(request, 'crm/student_detail.html', context={'form': form})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 模板
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<title>{% if obj %}修改{% else %}添加{% endif %}学生</title>
<!-- Bootstrap -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"
integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<!-- HTML5 shim 和 Respond.js 是为了让 IE8 支持 HTML5 元素和媒体查询(media queries)功能 -->
<!-- 警告:通过 file:// 协议(就是直接将 html 页面拖拽到浏览器中)访问页面时 Respond.js 不起作用 -->
<!--[if lt IE 9]>
<![endif]-->
</head>
<body>
<div class="container">
<div style="width: 800px">
<h1>学生{% if obj %}修改{% else %}添加{% endif %}页面</h1>
<form class="form-horizontal" method="post">
{% for field in form %}
<div class="form-group {% if field.errors %}has-error{% endif %}">
<label for="{{ field.id_for_label }}" class="col-sm-2 control-label">{{ field.label }}</label>
<div class="col-sm-10">
{{ field }}
{% for error in field.errors %}
<span class="help-block">{{ error }}</span>
{% endfor %}
</div>
</div>
{% endfor %}
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">{% if form.instance %}修改{% else %}
添加{% endif %}</button>
</div>
</div>
</form>
</div>
</div>
<!-- jQuery (Bootstrap 的所有 JavaScript 插件都依赖 jQuery,所以必须放在前边) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"
integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ"
crossorigin="anonymous"></script>
<!-- 加载 Bootstrap 的所有 JavaScript 插件。你也可以根据需要只加载单个插件。 -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"
integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd"
crossorigin="anonymous"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
本文完,感谢你的耐心阅读,如有需要可加我微信,备注「博客」并说明原因,我们一起进步,下次见。
