forms 组件
内容概要
- forms 组件简介
- froms 组件类的书写
- forms 组件校验数据
- forms 组件渲染标签
- forms 组件展示错误信息
- forms 组件钩子函数
- forms 类的常用重要参数
- forms 字段类型
- forms 组件的源码分析
内容详细
forms 组件简介
我们之前都是在HTML页面中利用form表单向后端提交数据,但我们仍然需要对数据进行一定的处理步骤,比如
对用户的输入做校验,比如校验用户是否输入,输入的长度和格式等正不正确。如果用户输入的内容有错误就需要在页面上相应的位置显示对应的错误信息.。
Django forms组件就为我们提供了上面的那些功能,提高我们开发效率
- 渲染HTML代码
- 校验数据(主要)
- 展示提示信息
forms组件进行数据校验都是在后端进行的,前端的校验比较弱不禁风,需要使用forms组2件提供给我们的功能在后端进行再一次校验
froms 组件类的书写
因为forms组件提供众多校验方法,如果都书写在views.py文件中会显得代码冗余
所以我们在Django的功能文件夹 app01 下创建一个 myform 文件夹,专门存放各类form组件类
以下展示一个注册功能的form组件类的书写
from django import forms
from app01 import models
class MyRegForms(forms.Form): # forms类需要继承django.forms.Form类
username = forms.CharField(label='用户名', max_length=8, min_length=3,
error_messages={
'required': '用户名不能为空',
'max_length': '用户名最大8位',
'min_length': '用户名最小3位',
},
# 添加 bootstrap 样式
widget=forms.TextInput(attrs={'class': 'form-control'})
)
password = forms.CharField(label='密码', max_length=8, min_length=3,
error_messages={
'required': '密码不能为空',
'max_length': '密码最大8位',
'min_length': '密码最小3位',
},
# 添加 bootstrap 样式
widget=forms.PasswordInput(attrs={'class': 'form-control'})
)
confirm_password = forms.CharField(label='确认密码', max_length=8, min_length=3,
error_messages={
'required': '密码不能为空',
'max_length': '密码最大8位',
'min_length': '密码最小3位',
},
# 添加 bootstrap 样式
widget=forms.PasswordInput(attrs={'class': 'form-control'})
)
email = forms.EmailField(label='邮件',
error_messages={
'required': '邮件不能为空',
'invalid': '邮件格式不正确'
},
# 添加 bootstrap 样式
widget=forms.PasswordInput(attrs={'class': 'form-control'})
)
# 钩子函数
# 局部钩子,判断用户名是否正确
def clean_username(self):
username = self.cleaned_data.get('username')
id_exit = models.UserInfo.objects.filter(username=username)
if id_exit:
self.add_error('username', '用户名已存在')
return username
# 全局钩子
def clean(self):
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
if not password == confirm_password:
self.add_error('confirm_password', '两次密码不一致')
return self.cleaned_data
forms 组件校验数据
校验函数在 views.py 视图模块中书写
步骤:
1、用户的注册信息包含在 post 请求数据中传到视图函数,因为 request.POST得到的本就是一个 QueryDict 字典,可以直接作为参数传给注册 forms 组件类 MyRegForms,生成的对象赋值给 form_obj 对象。
注意: request.POST 中所带的键值对不能少于 forms 组件类中定义的字段对象个数,否则某些字段对象会报非空错误(多则无妨)
2、form_obj.is_valid() , form_obj 对象调用 is_valid() 对数据进行具体校验(长度,自定义钩子校验等)
- 校验成功无问题,则会把用户信息数据都存在 cleaned_data 属性中,form_obj.cleaned_data 即可获取用户信息,从而进行处理
- 检验失败,则会把失败原因信息保存到 errors 属性中,form_obj.errors 即可获取错误信息,并传给前端
from app01.myforms.regforms import MyRegForms # 导入写好的注册 forms 组件类
def register(request):
form_obj = MyRegForms()
if request.method == 'POST':
back_dic = {'code': 1000, 'msg': ''}
form_obj = MyRegForms(request.POST)
if form_obj.is_valid(): # 记得加上括号 is_valid()
# print(form_obj.cleaned_data) # {'username': 'elijah', 'password': '123', 'confirm_password': '123', 'email': '123@qq.com'}
cleaned_data = form_obj.cleaned_data
cleaned_data.pop('confirm_password')
file_obj = request.FILES.get('avatar')
if file_obj:
cleaned_data['avatar'] = file_obj
models.UserInfo.objects.create_user(**cleaned_data)
back_dic['url'] = '/app01/login/'
else:
back_dic['code'] = 2000
back_dic['msg'] = form_obj.errors
return JsonResponse(back_dic)
return render(request, 'register.html', locals())
forms 组件渲染标签
在视图函数中生成一个 forms 对象 form_obj 返回给前端
前端拿到对象之后就可以使用 form_obj 里面的属性渲染标签
<h1 class="text-center">注册</h1>
<form id="MyForm">
{% csrf_token %}
{% for form in form_obj %}
<div class="form-group">
<label for="{{ form.auto_id }}">{{ form.label }}</label>
{{ form }} <!--生成 form类中定义好的 form 标签-->
<span style="color: red" class="pull-right"></span>
</div>
{% endfor %}
<div class="form-group">
{% load static %}
<label for="myfile">头像: <br><img id="myimg" src="{% static 'img/7_m.jpg' %}" alt="" width="80px"></label>
<input type="file" id="myfile" name="avatar" style="display: none">
</div>
<input type="button" class="btn btn-primary pull-right" id="id_commit" value="提交">
</form>
forms 组件展示错误信息
添加错误信息是进行后端的数据校验之后的事情,后端数据校验完成后,会把检验结果(字段错误信息)以字典格式返回给前端,
前端在ajax 的回调函数中获取到字典(包含状态码和错误信息),把错误信息渲染到相应的form标签下的span标签
$('#id_commit').click(function () {
// 因为有文件数据,所以要使用formdata数据,使用到FormData对象
let formDataObj = new FormData()
// 用序列组件循环出表单中所有的普通数据
{#console.log($('#MyForm').serializeArray())#}
$.each($('#MyForm').serializeArray(), function (index, obj) {
formDataObj.append(obj.name, obj.value)
})
// 添加文件数据
formDataObj.append('avatar', $('#myfile')[0].files[0])
$.ajax({
url: "",
type: 'post',
data: formDataObj,
contentType: false,
processData: false,
success: function (args) {
{#alert(args)#}
if(args.code==1000){
window.location.href = args.url
}
else {
$.each(args.msg, function (index, obj){
{#console.log(index, obj)#}
// 拼接前端input框的id名称
let TagId = '#id_' + index
$(TagId).next().text(obj[0]).parent().addClass('has-error')
})
}
}
})
})
forms 组件钩子函数
在 forms 类中自带的校验条件毕竟有限,一般只有限制最长和最短字符,非空或者email邮件格式
很多时候我们需要自定义一些需要的校验条件,比如检验用户名是否已经存在,存在则添加错误信息,
这时我们就需要使用到钩子函数了
钩子函数分为局部钩子和全局钩子
局部钩子
注意:
- 局部钩子函数书写: clean_字段名(self)
- 从 cleaned_data中取出什么key,函数结束时就要返回那个 key
# 钩子函数
# 局部钩子,判断用户名是否正确
def clean_username(self):
username = self.cleaned_data.get('username')
id_exit = models.UserInfo.objects.filter(username=username)
if id_exit:
self.add_error('username', '用户名已存在') # 给 form_obj 对象添加错误信息
return username
全局钩子
注意:
- 全局钩子函数书写: clean(self)
- 函数结束时需要把整个 cleaned_data 返回出去
# 全局钩子
def clean(self):
password = self.cleaned_data.get('password')
confirm_password = self.cleaned_data.get('confirm_password')
if not password == confirm_password:
self.add_error('confirm_password', '两次密码不一致')
return self.cleaned_data
forms 类的常用重要参数
参数:
- label 字段名
- error_messages 自定义报错信息
- initial 初始默认值
- required 表示该字段是否必填
username = forms.CharField(label='用户名', max_length=8, min_length=3, initial='elijah'
error_messages={
'required': '用户名不能为空',
'max_length': '用户名最大8位',
'min_length': '用户名最小3位',
},
支持正则校验
RegexValidator验证器
from django.forms import Form
from django.forms import widgets
from django.forms import fields
from django.core.validators import RegexValidator
class MyForm(Form):
user = fields.CharField(
validators=[RegexValidator(r'^[0-9]+$', '请输入数字'), RegexValidator(r'^159[0-9]+$', '数字必须以159开头')],
)
字段类型
在 forms.CharField() 内还有一个 widget 参数,可以给当前form标签对象定义 input标签类型
TextInput
username = forms.CharField(label='用户名', max_length=8, min_length=3,
error_messages={
'required': '用户名不能为空',
'max_length': '用户名最大8位',
'min_length': '用户名最小3位',
},
# 添加 bootstrap 样式
widget=forms.TextInput(attrs={'class': 'form-control'}) # 添加 form-control 类
)
PasswordInput
password = forms.CharField(label='密码', max_length=8, min_length=3,
error_messages={
'required': '密码不能为空',
'max_length': '密码最大8位',
'min_length': '密码最小3位',
},
# 添加 bootstrap 样式
widget=forms.PasswordInput(attrs={'class': 'form-control'})
)
RadioSelect
单radio值为字符串
gender = forms.fields.ChoiceField(
choices=((1, "男"), (2, "女"), (3, "保密")),
label="性别",
initial=3,
widget=forms.widgets.RadioSelect()
)
单选 Select
class LoginForm(forms.Form):
...
hobby = forms.ChoiceField(
choices=((1, "篮球"), (2, "足球"), (3, "双色球"), ),
label="爱好",
initial=3,
widget=forms.widgets.Select()
)
多选 Select
class LoginForm(forms.Form):
...
hobby = forms.MultipleChoiceField(
choices=((1, "篮球"), (2, "足球"), (3, "双色球"), ),
label="爱好",
initial=[1, 3],
widget=forms.widgets.SelectMultiple()
)
单选 Checkbox
class LoginForm(forms.Form):
...
keep = forms.ChoiceField(
label="是否记住密码",
initial="checked",
widget=forms.widgets.CheckboxInput()
)
多选 Checkbox
class LoginForm(forms.Form):
...
hobby = forms.MultipleChoiceField(
choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
label="爱好",
initial=[1, 3],
widget=forms.widgets.CheckboxSelectMultiple()
)
forms 组件的源码分析
forms 组件类进行数据校验,是从 form_obj.is_valid() 开始的,我们从 is_valid() 方法开始分析 forms 组件的校验源码
以下 is_valid 函数可知:
如果用户信息对象中无空(self.is_bound)并且无错误信息(self.errors 为 False),is_valid 函数的返回值是 True,反之则为 False
def is_valid(self):
"""Return True if the form has no errors, or False otherwise."""
return self.is_bound and not self.errors
self.is_bound 是校验是否传入的数据
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=None,
empty_permitted=False, field_order=None, use_required_attribute=None, renderer=None):
self.is_bound = data is not None or files is not None
self.errors 是校验错误的函数
@property # 把 error 方法伪装成了属性,可以不加()便可以调用
def errors(self):
"""Return an ErrorDict for the data provided for the form."""
if self._errors is None:
self.full_clean()
return self._errors
其中 _errors 属性默认是 None
self._errors = None # Stores the errors after clean() has been called.
所以会调用 full_clean() 方法
def full_clean(self):
"""
Clean all of self.data and populate self._errors and self.cleaned_data.
"""
self._errors = ErrorDict() # 给 _errors 属性赋予了一个对象(错误字典)
if not self.is_bound: # Stop further processing.
return
self.cleaned_data = {} # 定义了 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()
在 full_clean() 方法中定义了 cleaned_data 字典
并调用了 _clean_fields()、_clean_form()、 _post_clean() 三个方法
_clean_fields():
def _clean_fields(self):
for name, bf in self._bound_items(): # 循环获取字段名和字段对象
field = bf.field
# 获取字段对应的用户数据
value = bf.initial if field.disabled else bf.data
try:
if isinstance(field, FileField):
value = field.clean(value, bf.initial)
else:
value = field.clean(value) # 校验字段方法 clean
self.cleaned_data[name] = value # 将合法的字段添加到 clean_data 中
if hasattr(self, 'clean_%s' % name): # 在此判断 clean_字段名 局部钩子是否存在
value = getattr(self, 'clean_%s' % name)() # 加括号调用 clean_字段名 局部钩子函数,并且需要返回值
self.cleaned_data[name] = value # 将局部钩子的返回值 value 赋值给 clean_data 字典
except ValidationError as e:
self.add_error(name, e) # 添加错误信息的方法
由此可见,有两种加入错误信息的方法
- 导入模块 from django.core.exceptions import ValidationError,直接 ValidationError('错误信息') 添加
- self.add_error('字段名', '错误信息'),按照查找顺序,会先调用 self 的类中的 .add_error() 方法添加错误信息
_clean_form():
def _clean_form(self):
try:
cleaned_data = self.clean() # 在此处调用了 clean() 全局钩子函数,并且全局钩子要有返回值 cleaned_data
except ValidationError as e:
self.add_error(None, e)
else:
if cleaned_data is not None: # 把全局钩子的放回值 cleaned_data 赋值给 self.cleaned_data 本身的字典
self.cleaned_data = cleaned_data