表单
在之前的案例中,每次我们需要提交表单数据的时候。我们都需要去手动编辑html表单,根据不同的字段,字段名,进行编码。做了很多重复的部分,所以django提供了一个专门用来处理表单的类,django.forms.Form
。
通过它,我们不仅能够自动生成前端页面,也可以用来验证数据的合法性。我们通过改写添加修改学生的表单来学习它。
创建表单
在app根目录下,创建一个forms.py
的模块,代码如下:
from django import forms
from .models import Channelclass 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())
每一个模型表单,都是forms.Form
的一个子类,类属性与模型的类属性类似,都表示不同类型的字段。不同的字段,将会渲染成不同的input类型。字段名与每一个input标签的name属性对应。
每一个字段都是一个字段类的实例,其中label
参数渲染成label
标签的内容。max_length
用来限制用户输入字符长度。required
参数表示该字段是否必填,默认为True
,要指定一个字段是不必填的,设置required=False
。
在模板中使用表单
只需要讲表单实例放到模板上下文就可以通过模板变量使用表单。
渲染表单对象
修改学生添加页面视图如下:
from .forms import StudentFormclass 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})
在视图中,实例化了一个表单对象,然后传递变量form
给了模板。那么在模板中通过{{ form }}
将会渲染对应的<label>
和<input>
元素,下面是StduentForm
实例用{{from}}
的输出:
<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>
我们看到表单对象默认渲染了表格格式的字段,所以记住需要在模板中提供外层<form>
标签和submit
控件。
那么在模板中可以安装如下方式渲染:
<form ><talbe>{{ form }}</talbe><input type="submit" value="添加" />
</form>
对于表单字段的渲染,还有如下格式:
- {{ 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>
完整的<label>
元素还可以使用label_tag()
来生成。例如:
<div class="fieldWrapper">{{ form.subject.errors }}{{ form.subject.label_tag }}{{ form.subject }}
</div>
渲染表单错误信息
表单的错误信息分两种,一种是{{ 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.non_field_errors }}
显示非字段验证错误信息,它渲染后看起来如下:
<ul class="errorlist nonfield"><li>Generic validation error</li>
</ul>
该列表会额外带上一个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 %}
有用的字段属性:
- {{ 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类。
有时候我们可能需要修改默认的部件,通过字段参数widget
来处理。例如:
from django import formsclass CommentForm(forms.Form):name = forms.CharField()url = forms.URLField()comment = forms.CharField(widget=forms.Textarea)
字段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'}))
也可以在表单定义中修改部件:
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')
或者如果该字段没有直接在表单上声明(比如模型表单字段),可以使用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')
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>
表单的校验
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})
实例化表单时,可以将GET/POST
参数传入,然后调用表单对象的is_valid()
方法进行校验。如果校验通过,这个方法会返回True
,否则返回False
。
校验通过后通过cleaned_data
属性访问干净的数据。
指定字段校验
定义表单时,可以定义方法clean_<fieldname>()
方法对指定的字段进行校验,该方法不接受参数。在方法中通过self.cleaned_data
获取该字段的值。
如果校验不通过需要触发一个ValidationError
的异常,校验通过请return该值。
在学生创建的逻辑中,我们没有验证电话号码的格式,在表单中编写一个校验方法如下:
import refrom django import forms
from django.core.exceptions import ValidationErrorfrom .models import Channel, Studentclass 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
验证相互依赖的字段
有时候需要同时校验多个字段,比如注册时,校验密码和重复密码。这时复写clean()
方法是一个很好的办法:
from django import forms
from django.core.exceptions import ValidationErrorclass 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
在表单的clean()
方法被调用前,上一节中的单字段校验方法都会先被运行。clean()
方法中如果出现验证错误,在模板中使用{{form.non_field_errors}}
显示。
模型表单
django提供了一个辅助类,可以从一个模型创建一个Form
类,而不需要重复定义字段。
修改学生表单如下:
from django import forms
from .models import Channel, Studentclass StudentForm(forms.ModelForm):class Meta:model = Student # 指定要生成表单的模型exclude = ['c_time'] # 指定不需要生成的字段
每一个模型表单都是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})
上面的代码中,如果表单校验通过,执行form.save()
会创建返回Student
实例并保存到数据库。
学生更新案例:
class StudentUpdateView(View):"""学生更新视图"""def get_obj(self, pk):obj = get_object_or_404(Student, pk=pk)return objdef 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})
上面的代码中实例化表单时传递POST参数,同时把要更新的模型对象传给instance
参数,在校验通过后,执行form.save()
会使用校验后的参数更新模型对象。
学生创建,更新视图案例
表单
# crm/froms.py
import refrom django import forms
from django.core.exceptions import ValidationErrorfrom .models import Studentclass 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
视图
# 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 objdef 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})
模板
<!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>