wtforms作用:
用于对python web框架做表单验证.
前端页面上不能这么写,原因是正确的时候form.errors中最开始是是没有错误信息的,如果这么写{{form.errors.user.0}}来显示错误信息,就会报下面截图中的错误,
{{form.errors.user.0}} 需要将它修改为{{form.user.errors[0]}},form.user它的内部会自动把没有错误信息时报错的这个机制给处理掉,所以
通过form.user.errors可以取出user这个字段的错误信息
小项目
from flask import Blueprint,request,render_template,session,redirect
from uuid import uuid4
import pymysql
from ..utils.sql import SQLHelper
account = Blueprint('account',__name__)
from wtforms import Form
from wtforms.fields import core
from wtforms.fields import html5
from wtforms.fields import simple
from wtforms import validators
from wtforms import widgets
class LoginForm(Form):
user = simple.StringField(
label='用户名',
validators=[
validators.DataRequired(message='用户名不能为空.'),
# validators.Length(min=6, max=18, message='用户名长度必须大于%(min)d且小于%(max)d')
],
widget=widgets.TextInput(),
render_kw={'class': 'form-control'}
)
pwd = simple.PasswordField(
label='密码',
validators=[
validators.DataRequired(message='密码不能为空.'),
# validators.Length(min=8, message='用户名长度必须大于%(min)d'),
# validators.Regexp(regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]{8,}",
# message='密码至少8个字符,至少1个大写字母,1个小写字母,1个数字和1个特殊字符')
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control'}
)
@account.route('/login',methods=['GET','POST'])
def login():
if request.method == "GET":
form = LoginForm()
return render_template('login.html',form=form)
user = request.form.get('user')
pwd = request.form.get('pwd')
form = LoginForm(formdata=request.form) #将提交上来的数据在form中进行校验
if not form.validate():
print(form.errors)
return render_template('login.html', form=form)
print('用户提交数据通过格式验证,提交的值为:',form.data)
obj = SQLHelper.fetch_one('select id,name from user where name=%(user)s and pwd=%(pwd)s',form.data)
if obj:
session.permanent = True
session['user_info'] = {'id':obj['id'],'name':obj['name']}
return redirect('/index')
else:
return render_template('login.html',msg="用户名或密码错误",form=form)
class RegisterForm(Form):
name = simple.StringField(
label='用户名',
validators=[
validators.DataRequired()
],
widget=widgets.TextInput(),
render_kw={'class': 'form-control'},
default='alex'
)
pwd = simple.PasswordField(
label='密码',
validators=[
validators.DataRequired(message='密码不能为空.')
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control'}
)
pwd_confirm = simple.PasswordField(
label='重复密码',
validators=[
validators.DataRequired(message='重复密码不能为空.'),
validators.EqualTo('pwd', message="两次密码输入不一致")
],
widget=widgets.PasswordInput(),
render_kw={'class': 'form-control'}
)
email = html5.EmailField(
label='邮箱',
validators=[
validators.DataRequired(message='邮箱不能为空.'),
validators.Email(message='邮箱格式错误')
],
widget=widgets.TextInput(input_type='email'),
render_kw={'class': 'form-control'}
)
gender = core.RadioField(
label='性别',
choices=(
(1, '男'),
(2, '女'),
),
coerce=int #相当于是把值拿过来做一个强转int(值),如果choices=(('1','男')),里面的1是字符串类型的,那就不需要这个强转了,否则要记得用coerce给它变成相应的类型,必须要保证数据库中的和提交上来的是一样的(包括数据类型)
)
city = core.SelectField(
label='城市',
choices=SQLHelper.fetch_all('select id,name from city',{},None)
)
hobby = core.SelectMultipleField(
label='爱好',
choices=(
(1, '篮球'),
(2, '足球'),
),
coerce=int
)
favor = core.SelectMultipleField(
label='喜好',
choices=(
(1, '篮球'),
(2, '足球'),
),
widget=widgets.ListWidget(prefix_label=False),
option_widget=widgets.CheckboxInput(),
coerce=int,
default=[1, 2]
)
def __init__(self, *args, **kwargs):
'''
作用就是解决下拉框选的时候,如果给表中新增数据,在项目没有重启的情况下, 前端页面不能显示新增的信息
原因是form中的字段都是静态字段,静态字段是在第一次执行的时候只加载一次,如果下次使用,用的
时候还是这一次的查询,就不会到数据库中再次查询,所以就造成了在对表总新增数据后,如果项目已经被加载,数据库中新增的数据就不会显示在下
拉框中,即使页面刷新也不会显示,解决方法就是重写一个构造方法,它每一次实例化的时候都找到city这个字段,让city的choices每一次都更新一次
'''
super(RegisterForm, self).__init__(*args, **kwargs) #执行父类的构造方法,如果没有下面的那一行代码,就相当于什么都没写,还是使用的父类中的构造方法
self.city.choices = SQLHelper.fetch_all('select id,name from city',{},None) # #表中每新增一条记录,就重新到数据库中查询一次,把city.choices中的值改成最新的查询结果
def validate_name(self, field): #name是要验证的字段
"""
钩子函数是以validate开头的,后面的是字段名
自定义pwd_confirm字段规则,例:与pwd字段是否一致
:param field:
:return:
"""
# 最开始初始化时,self.data中是当前传过来的所有的值:name,pwd....
print(field.data) # #field.data其实就是当前name传过来的值
print(self.data) #self.data中是当前传过来的所有的值:name,pwd....
obj = SQLHelper.fetch_one('select id from user where name=%s',[field.data,])
if obj:
# 如果用户名已经存在,就要抛出异常,下面两个哪个都行,但两者有区别
# raise validators.ValidationError("密码不一致") # 继续后续验证
raise validators.StopValidation("密码不一致") # 不再继续后续验证
@account.route('/register',methods=['GET','POST'])
def register():
if request.method == 'GET':
form = RegisterForm()
return render_template('register.html',form=form)
form = RegisterForm(formdata=request.form)#将提交上来的input框中输入的值进行验证
if form.validate():
print(form.data)
else:
print(form.errors)
return 'dalskj'
views/account.py
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>用户注册</title>
</head>
<body>
<h1>用户注册</h1>
<form method="post" >
{% for item in form %}
<p>{{item.label}}: {{item}} {{item.errors[0] }}</p>
{% endfor %}
<input type="submit" value="提交">
</form>
</body>
</html>
register.html
from datetime import timedelta
from redis import Redis
import pymysql
import threading
from DBUtils.PooledDB import PooledDB, SharedDBConnection
class Config(object):
DEBUG = True
SECRET_KEY = "dsjlajfla"
PERMANENT_SESSION_LIFETIME = timedelta(minutes=20) #设置session超时时间是20分钟,20分钟之后就失效了
# 如果写上下面这一句还需要在用户登录成功之后写上session.permanent=True,才能对session的二级数据进行修改
SESSION_REFRESH_EACH_REQUEST = True #每次请求过来都刷新session,就不会存在session['user_info']['k'] =v不能修改的问题了,如果不写这一句,需要在修改之后加上一个session['modified']= True
SESSION_TYPE = 'redis'
PYMYSQL_POOL = PooledDB(
creator=pymysql, # 使用链接数据库的模块
maxconnections=6, # 连接池允许的最大连接数,0和None表示不限制连接数
mincached=2, # 初始化时,链接池中至少创建的空闲的链接,0表示不创建
maxcached=5, # 链接池中最多闲置的链接,0和None不限制
maxshared=3, # 链接池中最多共享的链接数量,0和None表示全部共享。PS: 无用,因为pymysql和MySQLdb等模块的 threadsafety都为1,所有值无论设置为多少,_maxcached永远为0,所以永远是所有链接都共享。
blocking=True, # 连接池中如果没有可用连接后,是否阻塞等待。True,等待;False,不等待然后报错
maxusage=None, # 一个链接最多被重复使用的次数,None表示无限制
setsession=[], # 开始会话前执行的命令列表。如:["set datestyle to ...", "set time zone ..."]
ping=0,
# ping MySQL服务端,检查是否服务可用。# 如:0 = None = never, 1 = default = whenever it is requested, 2 = when a cursor is created, 4 = when a query is executed, 7 = always
host='127.0.0.1',
port=3306,
user='root',
password='123456',
database='user_info',
charset='utf8'
)
# 正式环境连接redis
class ProductionConfig(Config):
SESSION_REDIS = Redis(host='192.168.0.11',port='6379')
#开发环境
class DevelopmentConfig(Config):
SESSION_REDIS = Redis(host='127.0.0.1',port='6379')
class TestingConfig(Config):
pass
settings
from flask_pro_pymysql import create_app
app = create_app()
if __name__ == '__main__':
app.run()
manage.py
源码流程: 以下面一段代码示例
from flask import Flask,render_template,request,redirect
from wtforms import Form,validators,widgets
from wtforms.fields import simple,core,html5
app = Flask(__name__,template_folder='templates')
app.debug = True
class LoginForm(Form):
xxx = 123
name = simple.StringField(
label = '用户名',
validators = [
validators.DataRequired(message='用户名不能为空'),
validators.Length(min=6,max=18,message='用户名长度必须大于%(min)d且小于%(max)d')
],
widget = widgets.TextInput(),
render_kw={'class':'form-control'} #给这个input标签添加属性class以及它的属性值
)
pwd = simple.PasswordField(
label = '密码',
validators = [
validators.DataRequired(message='密码不能为空'),
validators.Length(min=8,message='用户名或密码错误'),
validators.Regexp(regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]{8,}",
message='密码至少8个字符,1个小写字母,1个数字和1个特殊字符')
],
widget = widgets.PasswordInput(),
render_kw={'class':'form-control'}
)
@app.route('/login',methods=['GET','POST'])
def login():
if request.method == 'GET':
form = LoginForm()
return render_template('login.html',form = form)
else:
form = LoginForm(formdata=request.form)
if form.validate():
print('用户提交数据通过格式验证,提交的值为:', form.data)
else:
print(form.errors)
return render_template('login.html',form = form)
if __name__ == '__main__':
app.run()
上面代码是从上到下执行的,当执行到class LoginForm(Form)这行代码时,它会找该类中有没有metaclass,如果有就使用metaclass指定的类来创建LoginForm类,如果它的基类是通过metaclass来创建的类,那么LoginForm也是通过metaclass来创建的,如果它的内部也是会使用type来创建的类.在LoginForm的父类Form中,是通过执行了一个函数,这个函数的返回值 是FormMeta('NewBase',(BaseForm,),{}) ,FormMeta是继承的type.
Form(with_metaclass(FormMeta, BaseForm))
def with_metaclass(meta, base=object):
return meta("NewBase", (base,), {})
with_metaclass的返回值是FormMeta('NewBase',(BaseForm,),{})
FormMeta(type)
关于metaclass的相关内容查看,
所以当代码执行到class LoginForm(Form),会先执行FormMeta类的__init__方法,它里面定义了两个变量,此时FormMeta中的cls就指的是LoginForm类
class FormMeta(type):
def __init__(cls, name, bases, attrs):
type.__init__(cls, name, bases, attrs)
cls._unbound_fields = None
cls._wtforms_meta = None
所以,此时LoginForm中就多了两个变量
LoginForm._unbound_fields = None
LoginForm._wtforms_meta = None
另外这个类中还有另外一个变量
LoginForm.xxx = 123
LoginForm.name=UnboundField(simple.StringField,*args,*kwargs,creation_counter=1)
LoginForm.pwd=UnboundField(simple.PasswordField,*args,*kwargs,creation_counter=2)
在login视图中当执行到form = LoginForm()时,会执行FormMeta中的__call__方法
def __call__(cls, *args, **kwargs):
if cls._unbound_fields is None:
fields = []
for name in dir(cls):
if not name.startswith('_'):
unbound_field = getattr(cls, name)
if hasattr(unbound_field, '_formfield'):
fields.append((name, unbound_field))
fields.sort(key=lambda x: (x[1].creation_counter, x[0]))
cls._unbound_fields = fields
# Create a subclass of the 'class Meta' using all the ancestors.
if cls._wtforms_meta is None:
bases = []
for mro_class in cls.__mro__:
if 'Meta' in mro_class.__dict__:
bases.append(mro_class.Meta)
cls._wtforms_meta = type('Meta', tuple(bases), {})
return type.__call__(cls, *args, **kwargs)
开始时LoginForm._unbound_fields = None,所以会走判断,里面定义了fields=[],dir(LoginForm)=[ _unbound_fields,_wtforms_meta,xxx ,name,pwd],name就是这个dir(LoginForm)中的一个个的元素,如果name不是以_开头的,通过反射找到name和pwd这两个对象,通过hasattr后此时fields=[('name',UnboundField(simple.StringField,*args,*kwargs,creation_counter=1),('pwd',UnboundField(simple.PasswordField,*args,*kwargs,creation_counter=2)].源码中的
fields.sort(key=lambda x: (x[1].creation_counter, x[0])) #如果没有creation_counter,按照x[0]就是按照fields中的索引进行排序;creation_counter的值和它写的顺序有关
cls._unbound_fields = fields
是将fields进行排序,然后将fields赋值给LoginForm._unbound_fields.和_unbound_fields的一样,_wtforms_meta的起始值也是None,所以在上面的_unbound_fields的的判断执行完了之后就执行下面的代码
if cls._wtforms_meta is None:
bases = []
for mro_class in cls.__mro__:
if 'Meta' in mro_class.__dict__:
bases.append(mro_class.Meta)
cls._wtforms_meta = type('Meta', tuple(bases), {})
return type.__call__(cls, *args, **kwargs)
cls.__mro__是LoginForm的继承顺序(LoginForm,Form,BaseForm,object),如果Meta在前面元祖的类属性中,就把这个类.Meta添加到Base列表中,Meta只存在于Form类中.所以Base=[DefaultMeta,]
class Form(with_metaclass(FormMeta, BaseForm)):
Meta = DefaultMeta
此时LoginForm.wtforms_meta=type('Meta', tuple(bases), {}),如果在LoginForm中定义了Meta那就会优先找LoginForm中定义的Meta,如果找不到才会找到这里的Meta
type('Meta', tuple(DefaultMeta), {}) 就相当于是创建了一个Meta类,Meta类继承了DefaultMeta类
Meta(DefaultMeta):
pass
最终FormMeta的__call__方法执行完之后,此时
LoginForm._unbound_fields = [('name',UnboundField(simple.StringField,*args,*kwargs,creation_counter=1),('pwd',UnboundField(simple.PasswordField,*args,*kwargs,creation_counter=2)]
LoginForm._wtforms_meta = type('Meta', tuple(DefaultMeta), {}).
接着就会执行LoginForm中的__new__方法,它里面没有到__mro__的继承顺序中去找,我找了一下里面都没有__new__方法,那就继续执行__init__方法,找到form类中就有__init__方法
class Form(with_metaclass(FormMeta, BaseForm)):
Meta = DefaultMeta
def __init__(self, formdata=None, obj=None, prefix='', data=None, meta=None, **kwargs):
meta_obj = self._wtforms_meta()
if meta is not None and isinstance(meta, dict):
meta_obj.update_values(meta)
super(Form, self).__init__(self._unbound_fields, meta=meta_obj, prefix=prefix)
for name, field in iteritems(self._fields):
# Set all the fields to attributes so that they obscure the class
# attributes with the same names.
setattr(self, name, field)
self.process(formdata, obj, data=data, **kwargs)
super(Form, self).__init__(self._unbound_fields, meta=meta_obj, prefix=prefix)中是将LoginForm._unbound_fields传进去,super继承的是BaseForm类中的__init__方法,BaseForm.__init__中的
下面一段代码的作用就是实例化simple.StringField().此时_fields={'name':simple.StringField(),'pwd':simple.PasswordField()}
for name, unbound_field in itertools.chain(fields, extra_fields):
options = dict(name=name, prefix=prefix, translations=translations)
field = meta.bind_field(self, unbound_field, options)
self._fields[name] = field
在form类中继续走for循环,for循环中将LoginForm()._firlds设置成LoginForm.name=simple.StringField(),LoginForm.pwd=simple.PasswordField().
self.process(formdata, obj, data=data, **kwargs)是给生成的input框设置默认值.StringField中的返回值的执行流程如下图
wtforms执行流程图
验证阶段:
用户提交的是post请求,login视图在做处理时执行到
...
form = LoginForm(formdata=request.form)
if form.validate():
...
form在对其提交的数据进行校验,也就是执行了.validate方法,它首先会在LoginForm中找validate方法,找不到回到父类中找,在Form中找到有validate方法
def validate(self):
"""
Validates the form by calling `validate` on each field, passing any
extra `Form.validate_<fieldname>` validators to the field validator.
"""
extra = {}
for name in self._fields:
inline = getattr(self.__class__, 'validate_%s' % name, None) #self.__class__ 获取的是当前的类,这一行代码是去当前类中找钩子函数
if inline is not None:
extra[name] = [inline]
return super(Form, self).validate(extra)
这个方法中的self._fields = [name,pwd],其中的self就是form对象,列表中的元素是LoginForm中定义的字段.inline是找到的钩子函数,如果找不到钩子函数就是None,如果inline不为None,就把它添加到extra这个字典中.super(Form, self).validate(extra)是执行了它父类中的validate方法,Form的父类是BaseForm.
def validate(self, extra_validators=None): #extra_validators是传进来的钩子函数
self._errors = None
success = True
for name, field in iteritems(self._fields): #iteritems(self._fields)相当于是.items, name就是每个字段的字段名name,pwd,field就是对象StringField和PasswordField
if extra_validators is not None and name in extra_validators:
extra = extra_validators[name]
else:
extra = tuple()
if not field.validate(self, extra): #
success = False
return success
self._errors = None 表示是没错误,如果有钩子函数,field.validate(self, extra)是对某个对象设定的钩子函数,就对哪个对象中的validate中进行校验. 假如是对StringField加了一个钩子函数,就要在StringField中执行validate中进行校验,但是StringField中没有这个方法,StringField的父类Field中有这个方法,所以就执行父类的validate方法.
if not stop_validation:
chain = itertools.chain(self.validators, extra_validators)
stop_validation = self._run_validation_chain(form, chain)
是将LoginForm中的validators的验证规则和定义的钩子函数的验证规则拼接到一起.执行顺序是先执行自己定义的验证规则,然后再执行钩子函数的验证规则.chain就是所有的验证规则,self._run_validation_chain(form,chain)中的form是
用户提交过来的数据,
def _run_validation_chain(self, form, validators):
for validator in validators:
try:
validator(form, self)
except StopValidation as e:
if e.args and e.args[0]:
self.errors.append(e.args[0])
return True
except ValueError as e:
self.errors.append(e.args[0])
return False
如果执行validator(form, self)没有校验成功就会抛出一个异常StopValidation它返回True,那么这个for循环就会终止,也就是自己定义的这个规则中如果抛异常是StopValidation,那么后面的钩子函数中的校验规则就不会再去校验了,如果是
ValueError就会继续执行,把错误信息添加到errors中.