django表单校验组件forms
前戏
# 需求:写一个注册功能
获取用户名和密码,利用form表单提交数据
在后端判断用户名和密码是否符合一定的条件:
用户名中不能含有'金瓶子'
密码不能少于三位
如何不符合条件需要你将提示信息展示到前端页面
前端显示
<form action="" method="post">
<p>username:
<input type="text" name="username">
<span style="color: red">{{ back_dic.username }}</span>
</p>
<p>password:
<input type="text" name="password">
<span style="color: red">{{ back_dic.password }}</span>
</p>
<input type="submit" class="btn btn-info">
</form>
后端逻辑
def ab_form(request):
response_dict = {'username': '', 'password': ''}
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
if '金瓶子' in username:
response_dict['username'] = '不符合社会主义核心价值观'
if len(password) < 3:
response_dict['password'] = '不能太短 不好!'
return render(request,'ab_form.html',locals())
"""
总结:
不论是get请求还是post请求,前端页面在渲染的时候都会收到response_dict中的数据。
get请求时,两个key对应的value每数据,所以前端页面渲染时没有任何数据显示
post请求时,两个key对应的value有数据,所以前端位置就显示了提示信息。
"""
总结:
- 上面注册验证功能我们
手写获取用户数据的html代码
、后端手动通过逻辑判断做表单校验
、对不符合条件的数据手动将提示信息展示在前端页面
。 - 这种操作是繁琐、低效且容易出错的。
- django自带的表单校验组件
forms
会帮我们完成上述三个需求。
数据一定要在后端做校验,前端校验是可有可无的(弱不禁风的,或者利用爬虫程序绕过前端页面直接朝后端提交数据)
forms组件基本使用
创建Form
from django import forms
class MyForm(forms.Form):
username = forms.CharField(min_length=5, max_length=12, label='用户名')
password = forms.CharField(min_length=6, max_length=12, label='密码')
re_password = forms.CharField(min_length=6, max_length=12, label='确认密码')
email = forms.EmailField(label='邮箱')
"""
总结:
# username字符串类型最小3位最大8位
# password字符串类型最小3位最大8位
# email字段必须符合邮箱格式 xxx@xx.com
# label属性用来指定字段展示信息
"""
校验数据
# 测试环境
- 1.测试环境的准备 可以自己拷贝代码准备
- 2.其实在pycharm里面已经帮你准备一个测试环境(python console)
使用第二种测试方式
from app01 import views
form_obj = views.MyForm({'username': 'the3times', 'password': '123456'})
form_obj.is_valid()
False
form_obj.cleaned_data
{'username': 'the3times', 'password': '123456'}
form_obj.errors
{'re_password': ['This field is required.'], 'email': ['This field is required.']}
form_obj.has_error('email')
True
"""
# 总结1:
给MyForm传值实例化对象,传值方式:将带校验的字段和数据组织成字典的形式
is_valid() 该方法只有在所有的数据全部合法的情况下才会返回True
cleaned_data 查看所有校验通过的数据
errors 查看所有不符合校验规则以及不符合的原因,{key: ['']}
has_error() 判断某一个字段是否不合法,不合法返回True
# 总结2:
校验数据只校验类中出现的字段,多传不影响,多传的字段直接忽略
校验数据 默认情况下 类里面所有的字段都必须传值,即少传肯定不合法
"""
渲染标签
forms组件只会自动渲染标签(input select radio checkbox),不能帮你渲染提交按钮。
后端视图函数
def index(request):
form_obj = MyForm() # 1 先产生一个空对象
return render(request,'index.html',locals()) # 2 直接将该空对象传递给html页面
渲染标签的三种方式
<p>第一种渲染方式:代码书写极少,封装程度太高 不便于后续的扩展 一般情况下只在本地测试使用</p>
{{ form_obj.as_p }}
{{ form_obj.as_ul }}
{{ form_obj.as_table }}
<p>第二种渲染方式:可扩展性很强 但是需要书写的代码太多 一般情况下不用</p>
<p>{{ form_obj.username.label }}:{{ form_obj.username }}</p>
<p>{{ form_obj.password.label }}:{{ form_obj.password }}</p>
<p>{{ form_obj.email.label }}:{{ form_obj.email }}</p>
<p>第三种渲染方式(推荐使用):代码书写简单 并且扩展性也高</p>
{% for form in form_obj %}
<p>{{ form.label }}:{{ form }}</p>
{% endfor %}
"""
label属性默认展示的是类中定义的字段首字母大写的形式
也可以自己修改 直接给字段对象加label属性即可
username = forms.CharField(min_length=3, max_length=8, label='用户名')
"""
展示提示信息
禁止浏览器自动校验的设置
<form action="" method="post" novalidate> # 增加参数 novalidate
前端页面预留显示提示信息的span
标签
<form action="" method="post" novalidate>
{% for form in form_obj %}
{{ form.label }}:
<p>
{{ form }}
<span style="color: red">{{ form.errors.0 }}</span>
</p>
{% endfor %}
<input type="submit" value="提交">
</form>
后端逻辑判断
def register(request):
form_obj = MyForm()
if request.method == 'POST':
form_obj = MyForm(request.POST)
if form_obj.is_valid():
return HttpResponse('ok')
return render(request, 'register.html', locals())
'''
总结:
不论是get请求还是post请求,前端页面都可以接收名字为 form_obj的对象;
区别在于get请求时该对象时空对象,没有值和错误信息,所以此时前端页面什么只有标签没有任何数据和提示信息。
post请求后,前端页面收到的时有数据的form_obj,以及非法字段的提示信息。
这样有两个好处:前端post提交失败时保留数据;并显示提示信息。
'''
自定制错误信息
forms默认的错误信息是英文的,可以通过参数error_messages
自定制错误信息。
class MyForm(forms.Form):
username = forms.CharField(min_length=5,
max_length=12,
label='用户名',
error_messages={
'min_length': '用户名不能少于5位',
'max_length': '用户名不能超多12位',
'required': '用户名不能为空',
}
)
password = forms.CharField(min_length=6, max_length=12, label='密码')
re_password = forms.CharField(min_length=6, max_length=12, label='确认密码')
email = forms.EmailField(label='邮箱')
"""
error_messages参数需要构造成字典的数据结构,key是校验条件,value是校验失败时的提示信息
"""
钩子函数(hook)
上述校验是forms的第一道校验,如果希望自定一些特殊的校验规则,可以使用钩子函数。
钩子函数在forms组件中类似于第二道关卡,能够让我们自定义校验规则。
校验流程通过第一道关卡后就会来到钩子函数,我们可以在钩子函数里面进一步定制校验规则。
钩子函数分两种:局部钩子、全局钩子
# 局部钩子:给单个字段添加校验规则
# 全局钩子:给多个字段添加校验规则
局部钩子使用:用户名不能包含'666'
class MyForm(forms.Form):
username = forms.CharField(min_length=5,
max_length=12,
label='用户名',
error_messages={
'min_length':'用户名不能少于5位',
'max_length':'用户名不能超多12位',
'required':'用户名不能为空',
})
def clean_username(self):
username = self.cleaned_data.get('username')
if '666' in username:
self.add_error('username', '不要666的没完没了')
return username
"""
总结:
定义方法:clean_字段()
该方法中在cleaned_data中获取该字段的数据,局部钩子中只能拿到当前字段的数据
校验数据失败时,通过add_error方法给字段添加错误信息,最后一定要返回该字段
局部钩子取出的字段数据一定要返回出去
"""
全局钩子的使用:两次输入密码一致
class MyForm(forms.Form):
password = forms.CharField(min_length=6, max_length=12, label='密码')
re_password = forms.CharField(min_length=6, max_length=12, label='确认密码')
def clean(self):
password = self.cleaned_data.get('password')
re_password = self.cleaned_data.get('re_password')
if password != re_password:
self.add_error('re_password', '两次密码输入不一致')
return self.cleaned_data
"""
总结:
全局钩子内可以在cleaned_data内获得任意字段的数据,
全局钩子取出的cleaned_data一定要返回出去
"""
其他参数及知识点
常用参数
min_length 最少几位字符
max_length 最多几位字符
label 字段名
required 控制字段是否必填(默认required=True)
error_messages 自定义报错信息,字典的结构
initial 初始值,input框里面的初始值value
widget参数
增加校验字段的样式属性,修改input标签的type类型,通过参数widget
class MyForm(forms.Form):
username = forms.CharField(min_length=5,
max_length=12,
widget=forms.widgets.TextInput(attrs={'class': 'form-control'}))
"""
总结:
widget=forms.widgets.TextInput()默认是TextInput(及input[type='text'])
widget=forms.widgets.PasswordInput()密码格式
widget=forms.widgets.EmailInput()邮箱格式
attrs提供字段的属性,可以是内置的也可以是自定义的;如有多个class:空格隔开即可。
"""
补充:每个字段都通过widget参数增加设置属性很繁琐,可以通过类的构造方法循环设置每个字段的属性。
class MyForm(forms.Form): username = forms.CharField(min_length=5, max_length=12, widget=forms.widgets.TextInput()) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # 批量增加属性 for field in self.fields: self.fields[field].widget.attrs.update({'class': 'form-control'})
validators参数
第一道关卡里面还支持正则校验, 通过参数validators
from django import forms
from django.core.validators import RegexValidator
class MyForm(forms.Form):
phone = forms.CharField(label='手机号',
validators=[
RegexValidator(r'^[0-9]+$', '请输入数字'),
RegexValidator(r'^159[0-9]+$', '数字必须以159开头')
],)
"""
总结:
validators的值是一个列表,烈面可以更多个正则表校验条件
RegexValidator第一个参数是正则表校验条件,第二个是校验失败是的提示信息
"""
其他类型渲染
# radio单选
gender = forms.ChoiceField(
choices=((1, "男"), (2, "女"), (3, "保密")),
label="性别",
initial=1,
widget=forms.widgets.RadioSelect()
)
# checkbox多选
hobby = forms.MultipleChoiceField(
choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
label="爱好",
initial=[1, 3],
widget=forms.widgets.CheckboxSelectMultiple()
)
# 下拉单选:即使添加attrs={'multiple': 'multiple'}任然为单选
hobby2 = forms.ChoiceField(
choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
label="爱好",
initial=3,
widget=forms.widgets.Select()
)
# 下拉多选
hobby3 = forms.MultipleChoiceField(
choices=((1, "篮球"), (2, "足球"), (3, "双色球"),),
label="爱好2",
initial=[1, 3],
widget=forms.widgets.SelectMultiple()
)
# 选择checkbox是否选择, initial空表示False, 只要有值就是True默认选中(任何值都可以)
keep = forms.ChoiceField(
choices=(('False', 0), ('True', 1)),
label="是否记住密码",
initial='',
widget=forms.widgets.CheckboxInput()
)
choices参数注意
如果choicee展示的数据是动态从数据库中取出的,需要额外的设置。
class BookAddForm(forms.Form):
name = forms.CharField(label='图书名称',
error_messages={'required': '图书名称不能为空'},
widget=forms.widgets.TextInput())
price = forms.DecimalField(label='价格',
error_messages={'required': '图书价格不能为空'},
widget=forms.widgets.TextInput())
publish_date = forms.DateField(label='出版日期',
error_messages={'required': '出版日期不能为空'},
widget=forms.widgets.DateInput())
publish_id = forms.ChoiceField(label='出版社',
error_messages={'required':'出版社不能为空'},
widget=forms.widgets.Select())
author = forms.MultipleChoiceField(label='作者',
error_messages={'required':'作者不能为空'},
widget=forms.widgets.SelectMultiple())
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# choices字段的数据动态来自数据库
self.fields['publish_id'].choices = models.Publish.objects.values_list('pk', 'name')
self.fields['author'].choices = models.Author.objects.values_list('pk', 'name')
# 批量增加属性
for field in self.fields:
self.fields[field].widget.attrs.update({'class': 'form-control'})
钩子函数源码
原本想自己记录总结这部分内容,无意间发现一篇介绍非常详细的博客,点击查看我就不写了。
关于源码的阅读,需要注意两点:知道自己想看什么;只看自己能看懂的。
总结和补充:
- 自己定义的局部钩子和全局钩怎么被调用的
# 从is_valid()函数出发进入self.errors再到self.full_clean()
在full_clean()方法内,会依次调用三个方法
self._clean_fields()
self._clean_form()
self._post_clean()
第1个方法内会通过反射调用我们自己定义的局部钩子函数;
第2个方法内会调用self.clean()全局钩子函数。
- 为什么局部钩子要返回校验字段的数据,全局钩子要返回
self.cleaned_data
"""
self._clean_fields()内通过反射动态判断form_obj对象是否定义了局部钩子函数,如果定义了就立即加括号执行并且通过一个value变量接收执行的返回值。该value变量会再赋值为该字段对应的数据值。
这就是为啥要要校验之后还需要再将数据值返回的原因,如果该局部钩子没有返回值则默认返回None,这样就将该字段的数据值丢失了。
全局钩子函数也是一致的,self._clean_form()方法内,直接调用self.clean()。并且clean()其实是一个接口,这个接口内部仅仅返回了self.cleaned_data,外部需要这个返回值赋值给self.cleaned_data.
所以我写全局钩子是也要返回这个self.cleaned_data, 否则通过form_obj.cleaned_data就拿不到数据了。
"""
- 钩子函数是第二道关卡,只有经过了第一道管卡才会来到钩子函数。那么在钩子函数内校验失败时,
cleaned_data
里面就不应该有这个字段及其数据。解决这个问题的关键在于add_error()
方法。
"""
我们在钩子函数内做校验时,校验失败是通过add_error(field, error_info)方法添加错误字段和错误提示信息的。
add_error()内部添加错误信息后会再判断该字段是否在clened_data这个字典内,如果存在则会将其从该字典中删除。
"""
- 添加校验错误方式,主动抛出异常
"""
看源码之后发现,forms源码也使用了异常捕获或添加错误信息。
所以其实我们也可以主动抛出异常来将错误信息添加到errors中,但这种方式操作起来不简洁,不如add_error()用的方便。
"""