十:Forms组件的学习
前言
写一个注册功能,在后端来判断用户名或者密码是否符合一定的要求:
(1)用户名中不能还有敏感字符串"#";
(2)密码的长度不能小于3位
后端代码:
def ab_form(request):
back_dic = {'username': '', 'password': ''}
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
if '#' in username:
back_dic['username'] = "名字含有非法字符'#'"
if len(password) < 3:
back_dic['password'] = "密码的长度不小于3位"
return render(request,'ab_form.html',locals())
"""
无论是post请求还是get请求:
页面都能够获取到字典,只不过get请求来的时候,字典值都是空的
而post请求来之后,字典可能有值
"""
前端逻辑:
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<form action="" method="post">
<p>UserName:
<input type="text" name="username" class="form-control" placeholder="UserName~">
<span style="color: red">{{ back_dic.username }}</span>
</p>
<p>Password:
<input type="password" name="password" class="form-control" placeholder="Passeord~">
<span style="color: red">{{ back_dic.password }}</span>
</p>
<div class="text-center">
<input type="submit" class="btn btn-success btn-md">
</div>
</form>
</div>
</div>
</div>
为什么校验数据非得去后端校验了?
(1)前端的数据校验是弱不禁风的,可有可以无
(2)你可以直接修改或者利用爬虫程序绕过前端页面直接朝后端提交数据
forms组件能够完成的几件事情。
(1)渲染html代码
(2)校验数据
(3)展示提示信息
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=8,label='密码')
re_password = forms.CharField(min_length=6,max_length=8,label='确认密码')
email = forms.EmailField(label='邮箱')
校验数据的两种方式:
(1)自己准备测试环境(test.py),拷贝代码;
(2)使用pycharm自带的测试环境python console
使用第二种方式来测试
from app01 import form
form_obj = form.MyForm({'username':'surpass#','password':'123456'})
form_obj.is_valid()
False
form_obj.cleaned_data
{'username': 'surpass#', 'password': '123456'}
form_obj.errors
{'re_password': ['This field is required.'], 'email': ['This field is required.']}
form_obj.has_error('email')
True
总结:
给MyForm传值实例化对象,传值方式:将带校验的字段和数据组织成字典的形式
is_valid() 该方法只有在所有的数据全部合法的情况下才会返回True
cleaned_data 查看所有校验通过的数据
errors 查看所有不符合校验规则以及不符合的原因,{key: ['']}
has_error() 判断某一个字段是否不合法,不合法返回True
校验数据只校验类中出现的字段,多传不影响,多传的字段直接忽略
校验数据 默认情况下 类里面所有的字段都必须传值,即少传肯定不合法
渲染标签
forms组件只会自动渲染标签(input select radio checkbox),不能帮你渲染提交按钮。
def index(request):
from app01 import form
form_obj = form.MyForm() # 首先先创建一个空对象
return render(request, 'index.html', locals()) # 将空的form对象传递给html页面
渲染标签的三种方式
第一种方式
特点:代码书写极少,封装程度太高 不便于后续的扩展 一般情况下只在本地测试使用
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
{{ form_obj.as_p }}
{{ form_obj.as_ul}}
{{ form_obj.as_table }}
</div>
</div>
</div>
第二种方式
特点:可扩展性很强 但是需要书写的代码太多 一般情况下不用
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<p>{{ form_obj.username.label }}:{{ form_obj.username }}</p>
<p>{{ form_obj.password.label }}:{{ form_obj.password }}</p>
<p>{{ form_obj.re_password.label }}:{{ form_obj.re_password }}</p>
<p>{{ form_obj.email.label }}:{{ form_obj.email }}</p>
</div>
</div>
</div>
第三种渲染方式
特点:代码书写简单 并且扩展性也高,推荐使用
<div class="container-fluid">
<div class="row">
<div class="col-md-8 col-md-offset-2">
{% for form in form_obj %}
<p>{{ form.label }}:{{ form }}</p>
{% endfor %}
</div>
</div>
</div>
"""
label属性默认展示的是类中定义的字段首字母大写的形式
也可以自己修改 直接给字段对象加label属性即可
username = forms.CharField(min_length=3, max_length=8, label='用户名')
"""
展示提示信息
禁止浏览器自动校验的设置
# form表单中增加参数novalidate
<div class="col-md-8 col-md-offset-2">
<form action="{% url 'app01:index' %}" method="post" novalidate>
{% for form in form_obj %}
<p>{{ form.label }}:{{ form }}
<span style="color: red">{{ form.errors.0 }}</span>
</p>
{% endfor %}
<input type="submit" class="btn btn-success">
</form>
</div>
后端逻辑判断
def index(request):
from app01 import form
form_obj = form.MyForm() # 首先先创建一个空对象
if request.method == 'POST':
form_obj = form.MyForm(request.POST)
if form_obj.is_valid():
return HttpResponse('it is ok')
return render(request, 'index.html', locals()) # 将空的form对象传递给html页面
总结:
"""
总结:
不论是get请求还是post请求,前端页面都可以接收名字为 form_obj的对象;
区别在于get请求时该对象时空对象,没有值和错误信息,所以此时前端页面什么只有标签没有任何数据和提示信息。
post请求后,前端页面收到的时form_obj可能携带数据,以及非法字段的提示信息。
这样有两个好处:前端post提交失败时保留数据;并显示提示信息。
"""
自定义错误信息
class MyForm(forms.Form):
# username字符串类型最小5位最大12位
username = forms.CharField(
min_length=5,
max_length=12,
label='用户名',
error_messages={
'min_length':'用户名字段不得小于5位',
'max_length':'用户名字段不得大于12位',
'required':'用户名字段是必须的',
}
)
# password字符串类型最小6位最大8位
password = forms.CharField(
min_length=6,
max_length=8,
label='密码',
error_messages={
'min_length': '密码字段不得小于5位',
'max_length': '密码字段不得大于8位',
'required': '密码字段是必须的',
}
)
re_password = forms.CharField(min_length=6,max_length=8,label='确认密码')
# email字段必须符合邮箱格式 xxx@xx.com
email = forms.EmailField(
label='邮箱',
error_messages={
'invalid':'邮箱格式不正确',
'required': '邮箱字段是必须的',
}
)
error_messages
参数需要构造成字典的数据结构,key是校验条件,value是校验失败时的提示信息
钩子函数(hook)
"""
上述校验是forms的第一道校验,如果希望自定一些特殊的校验规则,可以使用钩子函数。
钩子函数在forms组件中类似于第二道关卡,能够让我们自定义校验规则。
校验流程通过第一道关卡后就会来到钩子函数,我们可以在钩子函数里面进一步定制校验规则。
"""
钩子分为两种:局部钩子、全局钩子
局部钩子:给单个字段添加校验规则
全局钩子:给多个字段添加校验规则
局部钩子的使用案例
(1) 邮箱不能含有字符'surpass'
# 局部钩子
def clean_email(self):
email = self.cleaned_data.get('email')
if 'surpass' in email:
self.add_error('email', '邮箱字段中不能含有特殊字符surpass')
return email
# 注意
定义方法:clean_字段()
该方法中在cleaned_data中获取该字段的数据,局部钩子中只能拿到当前字段的数据
校验数据失败时,通过add_error方法给字段添加错误信息,最后一定要返回该字段
局部钩子取出的字段数据一定要返回出去
全局钩子的使用案例
两次密码输入一致检测
# 全局钩子
def clean(self):
password = self.cleaned_data.get('password')
re_password = self.cleaned_data.get('re_password')
if re_password != password:
self.add_error('re_password', '两次输入密码不一致')
return self.cleaned_data
form组件以及其他参数的补充
常用的参数
min_length 最少几位字符
max_length 最多几位字符
label 字段名
required 控制字段是否必填(默认required=True)
error_messages 自定义报错信息,字典的结构
initial 初始值,input框里面的初始值
widget参数
增加校验字段的样式属性,修改input标签的type类型,通过参数widget
class MyForm(forms.Form):
# username字符串类型最小5位最大12位
username = forms.CharField(
min_length=5,
max_length=12,
label='用户名',
error_messages={
'min_length': '用户名字段不得小于5位',
'max_length': '用户名字段不得大于12位',
'required': '用户名字段是必须的',
},
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:空格隔开即可。
"""
validators参数
第一道关卡里面还支持正则校验, 通过参数validators
from django.core.validators import RegexValidator
phone = forms.CharField(
label='手机号',
error_messages={
'required':'必须输入手机号',
},
validators=[
RegexValidator(r'^[0-9]+$', '请输入数字'),
RegexValidator(r'^183[0-9]+$', '手机号必须以183开头')
]
)
总结:
validators的值是一个列表,烈面可以更多个正则表校验条件
RegexValidator第一个参数是正则表校验条件,第二个是校验失败是的提示信息
其他类型的渲染
# radio单选
gender = forms.ChoiceField(
choices=((1, '男'), (2, '女'), (3, '保密')),
error_messages={
'required': '必须选择性别'
},
label='性别',
initial=1, # 默认值男性,相当于default,checked
widget=forms.widgets.RadioSelect()
)
# checkbox多选
hobby = forms.MultipleChoiceField(
choices=((1, '篮球'), (2, '足球'), (3, '排球')),
error_messages={
'required': '必须选择爱好',
},
label='爱好',
initial=[1, 2],
widget=forms.widgets.CheckboxSelectMultiple()
)
# 下拉单选
hometown = forms.ChoiceField(
choices=((1, '上海'), (2, '江苏'), (3, '浙江')),
error_messages={
'required': '必须选择家乡',
},
label='家乡',
initial=2,
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()
)
form组件源码分析
切入点:form_obj.is_valid()
def is_valid(self):
"""
Returns True if the form has no errors. Otherwise, False. If errors are
being ignored, returns False.
"""
return self.is_bound and not self.errors
self.is_bound = data is not None or files is not None
"""
data就是我们传入的字典数据
form_obj = form.MyForm({'username':'surpass','password':'123'})
或者
form_obj = form.MyForm(request.POST)
"""
(1)没有错误,返回值为True
,否则,返回False
;
(2)如果错误正在被忽略,返回False
。
从上面分析中可以得出,只要传入数据data,即传入的字典数据不为空,返回值取决于self.errors
。
再看self.errors
@property 将方法伪装成类属性
def errors(self):
"Returns an ErrorDict for the data provided for the form"
if self._errors is None:
self.full_clean()
return self._errors
self._errors = None # Stores the errors after clean() has been called
"""
在钩子函数已经被回调之后,存储错误errors,默认值是None
"""
它返回的是一个包含错误信息的字典,字典的数据是由表单form提供。
所以,此处if self._errors is None
是成立的,我们看self.full_clean()
def full_clean(self):
"""
Cleans all of self.data and populates self._errors and
self.cleaned_data.
"""
self._errors = ErrorDict() # 创建一个空字典对象
if not self.is_bound: # 如果没有传数据data返回
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._clean_fields()
和self._clean_form()
self._clean_fields()
def _clean_fields(self):
for name, field in self.fields.items(): # 遍历self.fields字典
# 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:
# 如果字段的属性是diabled,值从初始化字段中获取;否则,从datadict中获取
value = self.get_initial_for_field(field, name)
else:
value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))
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 #给clean_data添加数据
if hasattr(self, 'clean_%s' % name):
# 反射,判断form对象是否有局部钩子函数
# 从这里看出局部钩子函数,是需要返回值的
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e: # 添加错误提示信息的第二种方式
self.add_error(name, e)
self._clean_form()
def _clean_form(self):
try:
cleaned_data = self.clean()
#cleaned_data = self.cleaned_data
except ValidationError as e:
self.add_error(None, e)
else:
if cleaned_data is not None:
self.cleaned_data = cleaned_data
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 # 全局钩子需要一个返回值
通过学习源码,我们收获以下几点:
(1)局部钩子函数和全局钩子函数必须要有返回值
(2)我们可以通过主动raise ValidationError来为字段添加错误提示信息
示例:
from django.core.exceptions import ValidationError
def clean_email(self):
email = self.cleaned_data.get('email')
if 'surpass' in email:
raise ValidationError('邮箱中不能含有特殊字符surpass')
return email
# 全局钩子
def clean(self):
password = self.cleaned_data.get('password')
re_password = self.cleaned_data.get('re_password')
if re_password != password:
self.add_error('re_password', '两次输入密码不一致')
return self.cleaned_data