Flask组件之wtforms
1、简介
WTForms是一个支持多个web框架的form组件,主要用于对用户请求数据进行验证。
安装:
pip3 install wtforms
2、用户登录注册示例
2.1、用户登录
当用户登录时候,需要对用户提交的用户名和密码进行多种格式校验。如:
用户不能为空;用户长度必须大于6;
密码不能为空;密码长度必须大于12;密码必须包含 字母、数字、特殊字符等(自定义正则);
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 from flask import Flask, render_template, request, redirect 4 from wtforms import Form 5 from wtforms.fields import core 6 from wtforms.fields import html5 7 from wtforms.fields import simple 8 from wtforms import validators 9 from wtforms import widgets 10 11 app = Flask(__name__, template_folder='templates') 12 app.debug = True 13 14 15 class LoginForm(Form): 16 name = simple.StringField( 17 label='用户名', 18 validators=[ 19 validators.DataRequired(message='用户名不能为空.'), 20 validators.Length(min=6, max=18, message='用户名长度必须大于%(min)d且小于%(max)d') 21 ], 22 widget=widgets.TextInput(), 23 render_kw={'class': 'form-control'} 24 25 ) 26 pwd = simple.PasswordField( 27 label='密码', 28 validators=[ 29 validators.DataRequired(message='密码不能为空.'), 30 validators.Length(min=8, message='用户名长度必须大于%(min)d'), 31 validators.Regexp(regex="^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!%*?&])[A-Za-z\d$@$!%*?&]{8,}", 32 message='密码至少8个字符,至少1个大写字母,1个小写字母,1个数字和1个特殊字符') 33 34 ], 35 widget=widgets.PasswordInput(), 36 render_kw={'class': 'form-control'} 37 ) 38 39 40 41 @app.route('/login', methods=['GET', 'POST']) 42 def login(): 43 if request.method == 'GET': 44 form = LoginForm() 45 return render_template('login.html', form=form) 46 else: 47 form = LoginForm(formdata=request.form) 48 if form.validate(): 49 print('用户提交数据通过格式验证,提交的值为:', form.data) 50 else: 51 print(form.errors) 52 return render_template('login.html', form=form) 53 54 if __name__ == '__main__': 55 app.run()
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 </head> 7 <body> 8 <h1>登录</h1> 9 <form method="post"> 10 <!--<input type="text" name="name">--> 11 <p>{{form.name.label}} {{form.name}} {{form.name.errors[0] }}</p> 12 13 <!--<input type="password" name="pwd">--> 14 <p>{{form.pwd.label}} {{form.pwd}} {{form.pwd.errors[0] }}</p> 15 <input type="submit" value="提交"> 16 </form> 17 </body> 18 </html>
2.2、用户注册
注册页面需要让用户输入:用户名、密码、密码重复、性别、爱好等
1 from flask import Flask, render_template, request, redirect 2 from wtforms import Form 3 from wtforms.fields import core 4 from wtforms.fields import html5 5 from wtforms.fields import simple 6 from wtforms import validators 7 from wtforms import widgets 8 9 app = Flask(__name__, template_folder='templates') 10 app.debug = True 11 12 13 14 class RegisterForm(Form): 15 name = simple.StringField( 16 label='用户名', 17 validators=[ 18 validators.DataRequired() 19 ], 20 widget=widgets.TextInput(), 21 render_kw={'class': 'form-control'}, 22 default='alex' 23 ) 24 25 pwd = simple.PasswordField( 26 label='密码', 27 validators=[ 28 validators.DataRequired(message='密码不能为空.') 29 ], 30 widget=widgets.PasswordInput(), 31 render_kw={'class': 'form-control'} 32 ) 33 34 pwd_confirm = simple.PasswordField( 35 label='重复密码', 36 validators=[ 37 validators.DataRequired(message='重复密码不能为空.'), 38 validators.EqualTo('pwd', message="两次密码输入不一致") 39 ], 40 widget=widgets.PasswordInput(), 41 render_kw={'class': 'form-control'} 42 ) 43 44 email = html5.EmailField( 45 label='邮箱', 46 validators=[ 47 validators.DataRequired(message='邮箱不能为空.'), 48 validators.Email(message='邮箱格式错误') 49 ], 50 widget=widgets.TextInput(input_type='email'), 51 render_kw={'class': 'form-control'} 52 ) 53 54 gender = core.RadioField( 55 label='性别', 56 choices=( 57 (1, '男'), 58 (2, '女'), 59 ), 60 coerce=int 61 ) 62 city = core.SelectField( 63 label='城市', 64 choices=( 65 ('bj', '北京'), 66 ('sh', '上海'), 67 ) 68 ) 69 70 hobby = core.SelectMultipleField( 71 label='爱好', 72 choices=( 73 (1, '篮球'), 74 (2, '足球'), 75 ), 76 coerce=int 77 ) 78 79 favor = core.SelectMultipleField( 80 label='喜好', 81 choices=( 82 (1, '篮球'), 83 (2, '足球'), 84 ), 85 widget=widgets.ListWidget(prefix_label=False), 86 option_widget=widgets.CheckboxInput(), 87 coerce=int, 88 default=[1, 2] 89 ) 90 91 def __init__(self, *args, **kwargs): 92 super(RegisterForm, self).__init__(*args, **kwargs) 93 self.favor.choices = ((1, '篮球'), (2, '足球'), (3, '羽毛球')) 94 95 def validate_pwd_confirm(self, field): 96 """ 97 自定义pwd_confirm字段规则,例:与pwd字段是否一致 98 :param field: 99 :return: 100 """ 101 # 最开始初始化时,self.data中已经有所有的值 102 103 if field.data != self.data['pwd']: 104 # raise validators.ValidationError("密码不一致") # 继续后续验证 105 raise validators.StopValidation("密码不一致") # 不再继续后续验证 106 107 108 @app.route('/register', methods=['GET', 'POST']) 109 def register(): 110 if request.method == 'GET': 111 form = RegisterForm(data={'gender': 1}) 112 return render_template('register.html', form=form) 113 else: 114 form = RegisterForm(formdata=request.form) 115 if form.validate(): 116 print('用户提交数据通过格式验证,提交的值为:', form.data) 117 else: 118 print(form.errors) 119 return render_template('register.html', form=form) 120 121 122 123 if __name__ == '__main__': 124 app.run()
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 </head> 7 <body> 8 <h1>用户注册</h1> 9 <form method="post" novalidate style="padding:0 50px"> 10 {% for item in form %} 11 <p>{{item.label}}: {{item}} {{item.errors[0] }}</p> 12 {% endfor %} 13 <input type="submit" value="提交"> 14 </form> 15 </body> 16 </html>
2.3、meta
1 #!/usr/bin/env python 2 # -*- coding:utf-8 -*- 3 from flask import Flask, render_template, request, redirect, session 4 from wtforms import Form 5 from wtforms.csrf.core import CSRF 6 from wtforms.fields import core 7 from wtforms.fields import html5 8 from wtforms.fields import simple 9 from wtforms import validators 10 from wtforms import widgets 11 from hashlib import md5 12 13 app = Flask(__name__, template_folder='templates') 14 app.debug = True 15 16 17 class MyCSRF(CSRF): 18 """ 19 Generate a CSRF token based on the user's IP. I am probably not very 20 secure, so don't use me. 21 """ 22 23 def setup_form(self, form): 24 self.csrf_context = form.meta.csrf_context() 25 self.csrf_secret = form.meta.csrf_secret 26 return super(MyCSRF, self).setup_form(form) 27 28 def generate_csrf_token(self, csrf_token): 29 gid = self.csrf_secret + self.csrf_context 30 token = md5(gid.encode('utf-8')).hexdigest() 31 return token 32 33 def validate_csrf_token(self, form, field): 34 print(field.data, field.current_token) 35 if field.data != field.current_token: 36 raise ValueError('Invalid CSRF') 37 38 39 class TestForm(Form): 40 name = html5.EmailField(label='用户名') 41 pwd = simple.StringField(label='密码') 42 43 class Meta: 44 # -- CSRF 45 # 是否自动生成CSRF标签 46 csrf = True 47 # 生成CSRF标签name 48 csrf_field_name = 'csrf_token' 49 50 # 自动生成标签的值,加密用的csrf_secret 51 csrf_secret = 'xxxxxx' 52 # 自动生成标签的值,加密用的csrf_context 53 csrf_context = lambda x: request.url 54 # 生成和比较csrf标签 55 csrf_class = MyCSRF 56 57 # -- i18n 58 # 是否支持本地化 59 # locales = False 60 locales = ('zh', 'en') 61 # 是否对本地化进行缓存 62 cache_translations = True 63 # 保存本地化缓存信息的字段 64 translations_cache = {} 65 66 67 @app.route('/index/', methods=['GET', 'POST']) 68 def index(): 69 if request.method == 'GET': 70 form = TestForm() 71 else: 72 form = TestForm(formdata=request.form) 73 if form.validate(): 74 print(form) 75 return render_template('index.html', form=form) 76 77 78 if __name__ == '__main__': 79 app.run()
3、其他
3.1、metaclass
1 class MyType(type): 2 def __init__(self, *args, **kwargs): 3 print('MyType创建类',self) 4 super(MyType, self).__init__(*args, **kwargs) 5 6 def __call__(self, *args, **kwargs): 7 obj = super(MyType, self).__call__(*args, **kwargs) 8 print('类创建对象', self, obj) 9 return obj 10 11 12 class Foo(object,metaclass=MyType): 13 user = 'wupeiqi' 14 age = 18 15 16 obj = Foo()
1 class MyType(type): 2 def __init__(self, *args, **kwargs): 3 super(MyType, self).__init__(*args, **kwargs) 4 5 def __call__(cls, *args, **kwargs): 6 v = dir(cls) 7 obj = super(MyType, cls).__call__(*args, **kwargs) 8 return obj 9 10 11 class Foo(MyType('MyType', (object,), {})): 12 user = 'wupeiqi' 13 age = 18 14 15 16 obj = Foo()
1 class MyType(type): 2 def __init__(self, *args, **kwargs): 3 super(MyType, self).__init__(*args, **kwargs) 4 5 def __call__(cls, *args, **kwargs): 6 v = dir(cls) 7 obj = super(MyType, cls).__call__(*args, **kwargs) 8 return obj 9 10 11 def with_metaclass(arg,base): 12 return MyType('MyType', (base,), {}) 13 14 15 class Foo(with_metaclass(MyType,object)): 16 user = 'wupeiqi' 17 age = 18 18 19 20 obj = Foo()
3.2. 实例化流程分析
1 # 源码流程 2 1. 执行type的 __call__ 方法,读取字段到静态字段 cls._unbound_fields 中; meta类读取到cls._wtforms_meta中 3 2. 执行构造方法 4 5 a. 循环cls._unbound_fields中的字段,并执行字段的bind方法,然后将返回值添加到 self._fields[name] 中。 6 即: 7 _fields = { 8 name: wtforms.fields.core.StringField(), 9 } 10 11 PS:由于字段中的__new__方法,实例化时:name = simple.StringField(label='用户名'),创建的是UnboundField(cls, *args, **kwargs),当执行完bind之后,才变成执行 wtforms.fields.core.StringField() 12 13 b. 循环_fields,为对象设置属性 14 for name, field in iteritems(self._fields): 15 # Set all the fields to attributes so that they obscure the class 16 # attributes with the same names. 17 setattr(self, name, field) 18 c. 执行process,为字段设置默认值:self.process(formdata, obj, data=data, **kwargs) 19 优先级:obj,data,formdata; 20 21 再循环执行每个字段的process方法,为每个字段设置值: 22 for name, field, in iteritems(self._fields): 23 if obj is not None and hasattr(obj, name): 24 field.process(formdata, getattr(obj, name)) 25 elif name in kwargs: 26 field.process(formdata, kwargs[name]) 27 else: 28 field.process(formdata) 29 30 执行每个字段的process方法,为字段的data和字段的raw_data赋值 31 def process(self, formdata, data=unset_value): 32 self.process_errors = [] 33 if data is unset_value: 34 try: 35 data = self.default() 36 except TypeError: 37 data = self.default 38 39 self.object_data = data 40 41 try: 42 self.process_data(data) 43 except ValueError as e: 44 self.process_errors.append(e.args[0]) 45 46 if formdata: 47 try: 48 if self.name in formdata: 49 self.raw_data = formdata.getlist(self.name) 50 else: 51 self.raw_data = [] 52 self.process_formdata(self.raw_data) 53 except ValueError as e: 54 self.process_errors.append(e.args[0]) 55 56 try: 57 for filter in self.filters: 58 self.data = filter(self.data) 59 except ValueError as e: 60 self.process_errors.append(e.args[0]) 61 62 d. 页面上执行print(form.name) 时,打印标签 63 64 因为执行了: 65 字段的 __str__ 方法 66 字符的 __call__ 方法 67 self.meta.render_field(self, kwargs) 68 def render_field(self, field, render_kw): 69 other_kw = getattr(field, 'render_kw', None) 70 if other_kw is not None: 71 render_kw = dict(other_kw, **render_kw) 72 return field.widget(field, **render_kw) 73 执行字段的插件对象的 __call__ 方法,返回标签字符串
3.3.验证流程分析
1 a. 执行form的validate方法,获取钩子方法 2 def validate(self): 3 extra = {} 4 for name in self._fields: 5 inline = getattr(self.__class__, 'validate_%s' % name, None) 6 if inline is not None: 7 extra[name] = [inline] 8 9 return super(Form, self).validate(extra) 10 b. 循环每一个字段,执行字段的 validate 方法进行校验(参数传递了钩子函数) 11 def validate(self, extra_validators=None): 12 self._errors = None 13 success = True 14 for name, field in iteritems(self._fields): 15 if extra_validators is not None and name in extra_validators: 16 extra = extra_validators[name] 17 else: 18 extra = tuple() 19 if not field.validate(self, extra): 20 success = False 21 return success 22 c. 每个字段进行验证时候 23 字段的pre_validate 【预留的扩展】 24 字段的_run_validation_chain,对正则和字段的钩子函数进行校验 25 字段的post_validate【预留的扩展】