一、forms组件介绍
在我们只做项目的过程中,比如注册功能,登录功能等肯定是需要校验的。校验通常在前端和后端都会进行,前端校验可以做一些简单的逻辑判断,减少服务器压力,且对于一些非法数据直接过滤。后端的校验可以说是安全的保障,因为对于专业人士来说,完全可以自己模拟http请求,绕过前端的校验。比如直接用postman发送数据,我们的前端就没办法校验其数据。
所以,不论前端有没有校验,后端的数据校验是必须的有的。如果是传统的校验,那么会在后端写很多的if判断语句,django帮我们封装了一个forms组件,可以快速字段校验。
# 借助于forms组件,可以快速实现字段的校验
from django.forms import Form
二、forms校验字段
1.常见的检验属性
字段名字 | 表达含义 |
---|---|
max_length | 最大长度 |
min_length | 最小长度 |
required | 是否必传(默认为True,若设为False可传可不传,不传不会报错,若传了也需要经过检验) |
label | 前端自动渲染标签的标签名字 |
widget | 定制class样式属性,需要从django.forms导入 |
error_message | 错误信息自定义,将一个字典赋值给他 |
2.forms组件的常用属性和方法
.is_valid() # 返回布尔值,检验是否通过
.cleaned_data # 检验通过的字段,如果所有字段都通过,那么和原数据相同,这个属性不论校验是否通过,里面都会存放着校验通过的字段
.errors # 校验失败的信息,通过源码可知通过重写了__str__转成了ul标签的格式,可读性差
.errors.as_data() # 校验失败的信息
.errors.as_data() # 校验失败的信息,json格式
3.示例
使用步骤
1.新建一个py文件,自定义一个类继承django.forms下的Form类
2.在类中书写要校验的字段
3.在视图函数中导入类,实例化得到对象使用
4.若要自动渲染模板,在前端拿到视图函数传的对象后,套下面的渲染方案(在自定义类中最好写上标签的样式不然很丑)
# 写一个类,类里写要校验的字段
from django import forms
class MyForm(forms.Form):
name = forms.CharField(min_length=3, max_length=16, label='用户名',
error_messages={"min_length": "用户名过短", "min_length": '用户名超出限制'},
widget=widgets.TextInput(attrs={'class': 'form-control'}))
passwd = forms.CharField(min_length=3, max_length=16, label='密码',
error_messages={"min_length": "密码太短,请至少三位", "min_length": '密码超出限制'},
widget=widgets.PasswordInput(attrs={'class': 'form-control'}))
re_passwd = forms.CharField(min_length=3, max_length=16, label='确认密码',
error_messages={"min_length": "密码太短,请至少三位", "min_length": '密码超出限制'},
widget=widgets.PasswordInput(attrs={'class': 'form-control'}))
profile = forms.CharField(max_length=200, label='个人简介',
error_messages={"max_length": '个人简介超出限制'},
widget=widgets.Textarea(attrs={'class': 'form-control'}))
email = forms.EmailField(label='邮箱', error_messages={'required': '必填属性'},
widget=widgets.TextInput(attrs={'class': 'form-control'}))
# 视图函数中
def register(request):
# 获取前端数据
data=request.POST
# 校验数据是否合法,实例化得到form对象,把要校验的数据传入
obj=myforms.MyForm(data)
# 校验数据:obj.is_valid() 返回布尔类型
if obj.is_valid():
print('校验通过')
# 校验通过的数据
print(obj.cleaned_data) # 不一定是上面传入的数据
else:
print(obj.cleaned_data)
print('校验失败')
# 哪个字段失败了?失败的原因是什么
print(obj.errors) # 打印出来一个标签的格式,通过下面两句去查看了源码,发现重写了__str__
print(type(obj.errors))
from django.forms.utils import ErrorDict
print(obj.errors.as_json())
print(obj.errors.as_data())
# obj.errors.as_ul()
return HttpResponse('ok')
三、forms渲染模板
1.自动渲染模板
传统的模板中,需要我们自己写form表单然后写样式,forms组件可以帮助我们直接生成标签。
使用步骤:
1.视图函数不变
2.在模板文件中按照指定语法使用form对象
生成的标签:<input type="text" name="name" maxlength="32" minlength="3" id="id_name" />
自动生成的id:是id_字段名
自定生成是没有样式的,我们可以在类的widget属性中自定义
在前端利用form组件自动生成的标签使用如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<hr>
<h1>手动创建模板</h1>
<form action="" method="post">
<p>用户名:<input type="text" name="name"></p>
<p>邮箱:<input type="text" name="email"></p>
<p>年龄:<input type="text" name="age"></p>
<p><input type="submit" value="提交"></p>
</form>
<hr>
<h1>半自动渲染模板1</h1>
<form action="" method="post">
<p>用户名:{{ form.name }}</p>
<p>邮箱:{{ form.email }}</p>
<p>年龄:{{ form.age }}</p>
<p><input type="submit" value="提交"></p>
</form>
<h1>半自动渲染模板2(用的最多)</h1>
<form action="" method="post">
<p>{{ form.name.label }}:{{ form.name }}</p>
<p>{{ form.email.label }}:{{ form.email }}</p>
<p>{{ form.age.label }}:{{ form.age }}</p>
<p><input type="submit" value="提交"></p>
</form>
<h1>半自动渲染模板3(用的最多)</h1>
<form action="" method="post">
{% for foo in form %}
<p>{{ foo.label }} :{{ foo }}</p>
{% endfor %}
<p><input type="submit" value="提交"></p>
</form>
<h1>全自动(了解)</h1>
<form action="" method="post">
{{ form.as_ul }}
{{ form.as_p }}
<table>
{{ form.as_table }}
</table>
<p><input type="submit" value="提交"></p>
</form>
</body>
</html>
2.渲染错误信息
错误信息来自两个位置,一般情况下我们都是使用对象自己的errors,因为forms对象的错误信息的所有的,且会一起打印出来,一般情况下我们都是希望各自打印出错误信息,然后不同的错误信息显示在不同的位置。
1 form对象.errors # 是一个字典
2 name对象.errors
# 视图函数
# 若检验成功,重定向到百度,失败则返回原页面,此时form对象中已经包含了错误信息
def register(request):
if request.method=='GET':
form=myforms.MyForm()
return render(request, 'register.html',{'form':form})
else:
form=myforms.MyForm(request.POST)
if form.is_valid():
return redirect('http://www.baidu.com')
else:
return render(request, 'register.html',{'form':form})
# 模板
<form action="" method="post" novalidate>
{% for foo in form %}
<div class="form-group">
<label for="">{{ foo.label }}</label>
{{ foo }}
# 打印错误信息,因为我们接受的是form对象,当还没校验的时候没有错误信息,则不会打印错误信息
<span class="text-danger pull-right">{{ foo.errors }}</span>
</div>
{% endfor %}
<div class="text-center">
<input type="submit" value="提交" class="btn btn-danger">
</div>
</form>
四、局部钩子和全局钩子
局部钩子和全局钩子都是为了进行更复杂的检验,比如名字不能以sb开头,在注册时,两个输入的密码必须一致。
局部钩子和全局钩子的使用地方都是在自己写的那个form类中。
1.局部钩子
# 局部钩子的触发时间在字段检验后面,全局钩子之前
# 局部钩子的使用
1 在自定义的Form类中写一个方法,名字必须是clean_字段名
2 取出字段的真正值,name=self.cleaned_data.get('name') # 因为触发时间在检验之后,所以cleaned_data中肯定有值
3 判断自己的规则,如果判断失败,抛出ValidationError
4 如果通过,return name
# 局部钩子
def clean_name(self):
# name对应的值,如何取到?
name = self.cleaned_data.get('name')
if name.startswith('sb'):
# 不让校验通过
raise ValidationError('不能以sb开头')
else:
# 校验通过,返回name
return name
2.全局钩子
# 全局钩子的触发时间在字段检验和局部钩子之后
# 全局钩子的使用
1 在自定义的Form类中写一个方法,名字必须是clean
2 从cleaned中取出字段的真正值
3 判断自己的规则,如果判断失败,抛出ValidationError
4 如果通过,self.cleaned_data,在这必须返回self.cleaned_data,因为这是检验的最后一步,返回什么那么我们对象拿到的数据就是什么。
且此处如果要自己定制,那么就是return自己的字典,如果写return None,那么默认也会返回self.cleaned_data
# 全局钩子
def clean(self):
# name=self.cleaned_data.get('name')
# print(name)
password = self.cleaned_data.get('password')
re_password = self.cleaned_data.get('re_password')
if password == re_password:
return self.cleaned_data
# return {'yang':"nb"}
else:
raise ValidationError('两次密码不一致')
五、Form组件源码分析
1.Form组件执行流程
form组件的执行顺序是,当执行了is_valid(),先检验字段1,再检验字段1的局部钩子,再字段2,然后字段2的局部钩子,最后再全局钩子。
所以我们在使用局部钩子的时候,在后面的字段可以在cleaned_data里拿到前面的字段值,而在前面的字段无法拿到后面的值
passwd = forms.CharField(min_length=3, max_length=16, label='密码',
error_messages={"min_length": "密码太短,请至少三位", "min_length": '密码超出限制'},
widget=widgets.PasswordInput(attrs={'class': 'form-control'}))
re_passwd = forms.CharField(min_length=3, max_length=16, label='确认密码',
error_messages={"min_length": "密码太短,请至少三位", "min_length": '密码超出限制'},
widget=widgets.PasswordInput(attrs={'class': 'form-control'}))
def clean_name(self):
name = self.cleaned_data.get('name')
if name.startswith == 'sb':
raise ValidationError('用户名不能以sb开头')
else:
return name
def clean_passwd(self):
passwd = self.cleaned_data.get('passwd')
re_passwd = self.cleaned_data.get('re_passwd')
print(passwd)
print(re_passwd)
return passwd
---------------------
123
None
123
456
2.源码分析
我们调用form组件的入口是is_valid,
1 form.is_valid()->
2 self.errors(BaseForm类)->
3 self.full_clean()(BaseForm类)->
4 self._clean_fields(局部数据校验),self._clean_form(全局数据校验)
3.局部钩子和全局钩子部分源码
self._clean_fields(BaseForm类)
for name, field in self.fields.items():
try:
# 字段自己的校验(最大值,最小值,是不是邮箱格式)
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name): # 反射判断有没有clean_字段名
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self.add_error(name, e)
self._clean_form(BaseForm类) # 全局钩子
try:
cleaned_data = self.clean() # self.clean执行的是自己类的clean方法
except ValidationError as e:
self.add_error(None, e)
4.源码总结
form组件的源码给了我们一种思路。自己写一个类,如果别人要用,他继承了类之后如果要写了一个方法,我们就执行,不写就不执行,如form类中,我们如果写了clean_xxx或者clean,他就会执行,不写就不执行,也不会报错,我们可以利用反射判断其方法是否存在来实现。这种编程思想叫做面向切面编程AOP,面向对象编程叫OOP