WTForms是一个支持多个web框架的form组件,主要用于对用户请求数据进行验证。
安装:
pip install wtforms
提供多种注册表单方式
from wtforms.fields import simple from wtforms.fields import core from wtforms.fields import html5 from wtforms import validators # 校验规则 from wtforms import widgets # 输入框类别:下拉框、输入框、长文本等等
使用:
简单的注册
from wtforms import Form from wtforms.fields import simple from wtforms import validators class RegFrom(Form): name = simple.StringField(label="用户名", validators=[validators.DataRequired(message="用户名不能为空"), validators.Length(min=2, max=8, message="用户名最小两位,最大8位")]) pwd = simple.PasswordField(label="密码", validators=[validators.DataRequired(message="密码不能为空"), validators.Regexp(regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[$@$!%*?&])[A-Za-zd$@$!%*?&]{8,}", message='密码至少8个字符,至少1个大写字母,1个小写字母,1个数字和1个特殊字符')]) # pwd = simple.PasswordField(label="密码", validators=[validators.DataRequired(message="密码不能为空"), ]) rep_pwd = simple.PasswordField(label="再次输入密码", validators=[validators.DataRequired(message="重复密码不能为空"), validators.EqualTo("pwd", message="两次密码不一致")])
from flask import Flask, render_template, current_app from flask import Blueprint from flask import request from flask import session from flask_wtforms.forms import register_froms reg = Blueprint('reg', __name__) @reg.route("/register", methods=["GET", "POST"]) def register(): if request.method == "GET": forms = register_froms.RegFrom() # 注册form表单 return render_template("register.html", forms=forms) forms = register_froms.RegFrom(formdata=request.form) # 将post数据传入 if forms.validate(): # 校验post请求数据 return "ok" return render_template("register.html", forms=forms)
<!DOCTYPE html> <html lang="zn-CH"> <head> <meta charset="UTF-8"> <title>注册</title> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <div> <form action="" method="post" novalidate> <div> {% for from in forms %} <p>{{ from.label }}: {{ from }} {{ from.errors[0] }}</p> {% endfor %} <input type="submit"> </div> </form> </div> </body> </html>
复杂的注册:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
from wtforms import Form from wtforms.fields import simple from wtforms.fields import core from wtforms.fields import html5 from wtforms import validators from wtforms import widgets class RegFrom(Form): # name = simple.StringField(label="用户名", # validators=[validators.DataRequired(message="用户名不能为空"), # validators.Length(min=2, max=8, message="用户名最小两位,最大8位")]) # pwd = simple.PasswordField(label="密码", # validators=[validators.DataRequired(message="密码不能为空"), # validators.Regexp(regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*d)(?=.*[$@$!%*?&])[A-Za-zd$@$!%*?&]{8,}", # message='密码至少8个字符,至少1个大写字母,1个小写字母,1个数字和1个特殊字符')]) # # pwd = simple.PasswordField(label="密码", validators=[validators.DataRequired(message="密码不能为空"), ]) # rep_pwd = simple.PasswordField(label="再次输入密码", # validators=[validators.DataRequired(message="重复密码不能为空"), # validators.EqualTo("pwd", message="两次密码不一致")]) name = simple.StringField( label='用户名', validators=[ validators.DataRequired() ], widget=widgets.TextInput(), # 输入框type 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类型 default=1 ) city = core.SelectField( label='城市', choices=( ('bj', '北京'), ('sh', '上海'), ) ) 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): super(RegFrom, self).__init__(*args, **kwargs) self.favor.choices = ((1, '篮球'), (2, '足球'), (3, '羽毛球')) def validate_pwd_confirm(self, field): """ 自定义pwd_confirm字段规则,例:与pwd字段是否一致 :param field: :return: """ # 最开始初始化时,self.data中已经有所有的值 if field.data != self.data['pwd']: # raise validators.ValidationError("密码不一致") # 继续后续验证 raise validators.StopValidation("密码不一致") # 不再继续后续验证
在复杂注册的时候,单选或者多选框的值可能是从数据库中取到的,但是在数据库中添加一个选项时,页面并不会跟随更新,只有重启服务才会显示。
解决办法:请求过来执行视图,视图中实例forms表单类对象,在forms表单类中重写__init__方法,实现每次请求都重新去数据库中取值,但是别忘了设置pymysql返回元祖类型的数据
from wtforms import Form from wtforms.fields import simple from wtforms.fields import core from wtforms.fields import html5 from wtforms import validators from wtforms import widgets import helper class UserForm(Form): city = core.SelectField( label='城市', choices=(), # 在__init__中从数据库中获取 coerce=int ) name = simple.StringField(label='姓名') def __init__(self,*args,**kwargs): super(UserForm,self).__init__(*args,**kwargs) self.city.choices=helper.fetch_all('select id,name from tb1',[],type=None)
import pymysql from DBUtils.PooledDB import PooledDB, SharedDBConnection import 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='s9day119', charset='utf8' ) def connect(type): conn = POOL.connection() cursor = conn.cursor(cursor=type) return conn,cursor def connect_close(conn,cursor): cursor.close() conn.close() def fetch_all(sql,args,type=pymysql.cursors.DictCursor): # type=pymysql.cursors.DictCursor返回的数据为字典,传入None时,返回元祖 conn,cursor = connect(type) cursor.execute(sql, args) record_list = cursor.fetchall() connect_close(conn,cursor) return record_list def fetch_one(sql, args): conn, cursor = connect() cursor.execute(sql, args) result = cursor.fetchone() connect_close(conn, cursor) return result def insert(sql, args): conn, cursor = connect() row = cursor.execute(sql, args) conn.commit() connect_close(conn, cursor) return row
3. meta
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#!/usr/bin/env python # -*- coding:utf-8 -*- from flask import Flask, render_template, request, redirect, session from wtforms import Form from wtforms.csrf.core import CSRF from wtforms.fields import core from wtforms.fields import html5 from wtforms.fields import simple from wtforms import validators from wtforms import widgets from hashlib import md5 app = Flask(__name__, template_folder='templates') app.debug = True class MyCSRF(CSRF): """ Generate a CSRF token based on the user's IP. I am probably not very secure, so don't use me. """ def setup_form(self, form): self.csrf_context = form.meta.csrf_context() self.csrf_secret = form.meta.csrf_secret return super(MyCSRF, self).setup_form(form) def generate_csrf_token(self, csrf_token): gid = self.csrf_secret + self.csrf_context token = md5(gid.encode('utf-8')).hexdigest() return token def validate_csrf_token(self, form, field): print(field.data, field.current_token) if field.data != field.current_token: raise ValueError('Invalid CSRF') class TestForm(Form): name = html5.EmailField(label='用户名') pwd = simple.StringField(label='密码') class Meta: # -- CSRF # 是否自动生成CSRF标签 csrf = True # 生成CSRF标签name csrf_field_name = 'csrf_token' # 自动生成标签的值,加密用的csrf_secret csrf_secret = 'xxxxxx' # 自动生成标签的值,加密用的csrf_context csrf_context = lambda x: request.url # 生成和比较csrf标签 csrf_class = MyCSRF # -- i18n # 是否支持本地化 # locales = False locales = ('zh', 'en') # 是否对本地化进行缓存 cache_translations = True # 保存本地化缓存信息的字段 translations_cache = {} @app.route('/index/', methods=['GET', 'POST']) def index(): if request.method == 'GET': form = TestForm() else: form = TestForm(formdata=request.form) if form.validate(): print(form) return render_template('index.html', form=form) if __name__ == '__main__': app.run()
其他:
1. metaclass
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
class MyType(type): def __init__(self, *args, **kwargs): print('MyType创建类',self) super(MyType, self).__init__(*args, **kwargs) def __call__(self, *args, **kwargs): obj = super(MyType, self).__call__(*args, **kwargs) print('类创建对象', self, obj) return obj class Foo(object,metaclass=MyType): user = 'wupeiqi' age = 18 obj = Foo() 示例一
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
class MyType(type): def __init__(self, *args, **kwargs): super(MyType, self).__init__(*args, **kwargs) def __call__(cls, *args, **kwargs): v = dir(cls) obj = super(MyType, cls).__call__(*args, **kwargs) return obj class Foo(MyType('MyType', (object,), {})): user = 'wupeiqi' age = 18 obj = Foo() 示例二
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
class MyType(type): def __init__(self, *args, **kwargs): super(MyType, self).__init__(*args, **kwargs) def __call__(cls, *args, **kwargs): v = dir(cls) obj = super(MyType, cls).__call__(*args, **kwargs) return obj def with_metaclass(arg,base): return MyType('MyType', (base,), {}) class Foo(with_metaclass(MyType,object)): user = 'wupeiqi' age = 18 obj = Foo() 示例三
2. 实例化流程分析
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 源码流程 1. 执行type的 __call__ 方法,读取字段到静态字段 cls._unbound_fields 中; meta类读取到cls._wtforms_meta中 2. 执行构造方法 a. 循环cls._unbound_fields中的字段,并执行字段的bind方法,然后将返回值添加到 self._fields[name] 中。 即: _fields = { name: wtforms.fields.core.StringField(), } PS:由于字段中的__new__方法,实例化时:name = simple.StringField(label='用户名'),创建的是UnboundField(cls, *args, **kwargs),当执行完bind之后,才变成执行 wtforms.fields.core.StringField() b. 循环_fields,为对象设置属性 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) c. 执行process,为字段设置默认值:self.process(formdata, obj, data=data, **kwargs) 优先级:obj,data,formdata; 再循环执行每个字段的process方法,为每个字段设置值: for name, field, in iteritems(self._fields): if obj is not None and hasattr(obj, name): field.process(formdata, getattr(obj, name)) elif name in kwargs: field.process(formdata, kwargs[name]) else: field.process(formdata) 执行每个字段的process方法,为字段的data和字段的raw_data赋值 def process(self, formdata, data=unset_value): self.process_errors = [] if data is unset_value: try: data = self.default() except TypeError: data = self.default self.object_data = data try: self.process_data(data) except ValueError as e: self.process_errors.append(e.args[0]) if formdata: try: if self.name in formdata: self.raw_data = formdata.getlist(self.name) else: self.raw_data = [] self.process_formdata(self.raw_data) except ValueError as e: self.process_errors.append(e.args[0]) try: for filter in self.filters: self.data = filter(self.data) except ValueError as e: self.process_errors.append(e.args[0]) d. 页面上执行print(form.name) 时,打印标签 因为执行了: 字段的 __str__ 方法 字符的 __call__ 方法 self.meta.render_field(self, kwargs) def render_field(self, field, render_kw): other_kw = getattr(field, 'render_kw', None) if other_kw is not None: render_kw = dict(other_kw, **render_kw) return field.widget(field, **render_kw) 执行字段的插件对象的 __call__ 方法,返回标签字符串
3. 验证流程分析
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
a. 执行form的validate方法,获取钩子方法 def validate(self): extra = {} for name in self._fields: inline = getattr(self.__class__, 'validate_%s' % name, None) if inline is not None: extra[name] = [inline] return super(Form, self).validate(extra) b. 循环每一个字段,执行字段的 validate 方法进行校验(参数传递了钩子函数) def validate(self, extra_validators=None): self._errors = None success = True for name, field in iteritems(self._fields): 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 c. 每个字段进行验证时候 字段的pre_validate 【预留的扩展】 字段的_run_validation_chain,对正则和字段的钩子函数进行校验 字段的post_validate【预留的扩展】