上一章讲了使用formalchemy进行服务端验证,但要开发一个用户体验良好的网站,客户端验证是必不可少的,用户可以在填写的时候就获取到错误信息,不必等待提交的过程,大大节省了用户的时间。
Formalchemy进行客户端验证,其实就是使用前端的js插件定义验证方法,然后在formalchemy中为字段赋予特殊的属性,让验证插件从属性了解某个字段需要进行什么验证,例如,我们在User表中定义email字段必须为email形式数据,那么formalchemy会在输出的表单中设置email字段的type为email,前台使用的插件能识别type="email"的input,然后进行email格式验证。
这里介绍一个我常用的jquery验证插件validation,这个插件本身已内置了很多常用的验证方法,如果有特殊的验证需求,还可以进行自定义,有兴趣的同学可以谷歌下。下面我们就以jquery的validation为验证插件,结合formalchemy来介绍下如何进行客户端验证。
首先我们下载validation,放入static中,并在模板中引用,form.html模板修改如下
form.html
<!DOCTYPE html>
<html>
<head>
<title></title>
<link type="text/css" href="/static/css/bootstrap.min.css" rel="stylesheet">
<script type="text/javascript" src="/static/js/jquery.min.js"></script>
<script type="text/javascript" src="/static/js/bootstrap.min.js"></script>
<script type="text/javascript" src="/static/js/jquery.metadata.js"></script><!--引入该插件可以在input中用metadata的形式设置验证规则-->
<script type="text/javascript" src="/static/js/jquery.validate.min.js"></script>
<script type="text/javascript" src="/static/js/additional-methods.min.js"></script><!--validation的内置验证规则-->
<script type="text/javascript" src="/static/js/messages_zh.js"></script><!--validation的中文多语言包-->
<script type="text/javascript" src="/static/js/base.js"></script>
</head>
<body>
<div class="container">
<div class="row">
<div class="span6 offset3">
<form class="form-horizontal validate" action="" method="post">
{{ form.render() }}
<div style="text-align: center;">
<input type="submit" class="btn btn-large" value="Submit" />
</div>
</form>
</div>
</div>
</div>
</body>
</html>
其中base.js是我们将要进行验证设置的js文件,代码如下
base.js
$(function(){
//重定义validation的remote验证方法,让其传递的值字段名全部为val,并使用post方法
$.validator.methods.remote = function(value, element, param) {
if ( this.optional(element) ) {
return "dependency-mismatch";
}
var previous = this.previousValue(element);
if (!this.settings.messages[element.name] ) {
this.settings.messages[element.name] = {};
}
previous.originalMessage = this.settings.messages[element.name].remote;
this.settings.messages[element.name].remote = previous.message;
param = typeof param === "string" && {url:param} || param;
if ( this.pending[element.name] ) {
return "pending";
}
if ( previous.old === value ) {
return previous.valid;
}
previous.old = value;
var validator = this;
this.startRequest(element);
var data = {};
data["val"] = value;
$.ajax($.extend(true, {
url: param,
mode: "abort",
port: "validate" + element.name,
dataType: "json",
data: data,
type: "post",
success: function(response) {
validator.settings.messages[element.name].remote = previous.originalMessage;
var valid = response === true || response === "true";
if ( valid ) {
var submitted = validator.formSubmitted;
validator.prepareElement(element);
validator.formSubmitted = submitted;
validator.successList.push(element);
delete validator.invalid[element.name];
validator.showErrors();
} else {
var errors = {};
var message = response || validator.defaultMessage( element, "remote" );
errors[element.name] = previous.message = $.isFunction(message) ? message(value) : message;
validator.invalid[element.name] = true;
validator.showErrors(errors);
}
previous.valid = valid;
validator.stopRequest(element, valid);
}
}, param));
return "pending";
};
var validate_form = $("form.validate");//定义class为validate的表单需要进行验证
validate_form.each(function(){
$(this).validate({
errorClass: "help-inline",//设置错误信息使用的class,可以自定义该class的样式
errorElement: "span",//设置错误信息包括在什么元素中
//设置用户输入错误时事件回调
errorPlacement: function(error, element){
element.after(error);
}
});
});
});
接下来我们重温下User表单的验证需求:
- name字段必填且数据库内唯一,长度不小于6,不大于20。
- email字段必填且数据库内唯一,且为email格式,长度不大于32。
- password必填,长度不小于6,不大于32。
- confirm password必填且必须与password相等,长度不小于6,不大于32。
class unique
class unique(BaseView):
def __init__(self):
super(unique, self).__init__()
#设置白名单,让需要进行验证的表和字段才可以在该视图中查询
self.allow = [
("User", "name"),
("User", "email"),
]
def POST(self, model, field, action):
if (model, field) not in self.allow:
raise web.notfound()
data = web.input()
m = globals()[model]
try:
id = int(action)
except:
query = self.db.query(m).filter(getattr(m, field)==data.val).first()
else:
query = self.db.query(m).filter(
(getattr(m, field)==data.val) &
(m.id!=id)
).first()
if not query:
return "true"
else:
return "false"
并在urls改为以下:
urls = (
"/unique/(.*)/(.*)/(.*)/", "unique",
"/", "index",
"/form/", "showform",
)
最后我们在forms.py中进行with_html的定义,就能在输出的表单中设置需要告诉验证插件的信息了,forms.py修改如下:
forms.py
#-*- coding:utf-8 -*-
from formalchemy import config, validators, Field, FieldSet
from customEngine import Jinja2Engine
from models import *
from customValidators import *
class UserForm:
def __init__(self):
#这里的directories是指表单模板存放的地方,我们在第二章提到的templates下创建一个文件夹,命名为form
config.engine = Jinja2Engine(directories=["templates/form"])
#为表单设置label
def setLabel(self):
self.name = self.fs.name.label("User Name")
self.email = self.fs.email.label("Email Address")
self.password = self.fs.password.label("Password")
self.superuser = self.fs.superuser.label("Admin?")
#定义编辑模式下通用的设置,编辑模式包括:新增,修改
def wmode(self, password=None):
self.setLabel()
#因为新增和修改中都需要用户重新确认密码,所以要为表单加入Confirm Password
#如果有指定password的值,说明用户是在修改记录,那么Confirm Password也必须有值
if not password:
self.fs.insert_after(self.fs.password, Field("confirm_password"))
else:
self.fs.insert_after(self.fs.password, Field("confirm_password", value=password))
self.confirm_password = self.fs.confirm_password.label("Re-enter Password")
self.name = self.name.required().validate(
validators.length(min=6, max=20)
).with_html(
minlength_=6,#定义客户端验证限制该字段最小长度为6
class_="{messages:{remote:'该用户名已存在'}}"#定义客户端唯一验证错误的提示语
)
self.email = self.email.required().email().validate(
validators.email
).validate(
validators.maxlength(32)
).with_html(
class_="{messages:{remote:'该邮箱已存在'}}"#定义客户端唯一验证错误的提示语
)
self.password = self.password.required().password().validate(
validators.length(min=6, max=32)
).with_html(
minlength_=6,#定义客户端验证限制该字段最小长度为6
)
self.confirm_password = self.confirm_password.required().password().validate(
validators.length(min=6, max=32)
).validate(
customEqual("password", "密码前后不一致")
).with_html(
minlength_=6,#定义客户端验证限制该字段最小长度为6
)
#定义新增用户时调用的方法
def write_render(self, cls):
#设置Fieldset对象,指定要绑定的sqlalchemy中的表类,并赋予sqlalchemy的session
self.fs = FieldSet(User, session=cls.db)
self.wmode()
#配置表单信息
self.fs.configure(
#表单包含的字段
include=[
self.name.validate(customUnique(cls.db)).with_html(
remote_="/unique/User/name/create/",#定义客户端远程验证的地址
),
self.email.validate(customUnique(cls.db)).with_html(
remote_="/unique/User/email/create/",#定义客户端远程验证的地址
),
self.password,
self.confirm_password.with_html(
class_="{equalTo:'#User--password'}"#设置客户端验证该字段值与另一个字段值是否相等
)
]
)
return self.fs
修改完成后运行main.py,进入/form/,输入已经注册过的name和email,看看客户端远程验证是否实现了:)