Python API 用于创建和处理表单
项目描述
w20e.forms包提供了一个强大的API,用于创建和处理电子表单。该包松散地基于XForms概念(以及Socrates QE,一个Java实现)。该包旨在提供标准plone、pyramid和django解决方案的即插即用替代品,但可以在任何框架(或没有框架的情况下)使用。
核心概念
核心概念如下(这样您可以快速决定是否喜欢这种方法)
表单是以下四件事的容器/组合
数据
模型
视图
提交
这清楚地分离了数据、数据的属性(数据类型、是否必需等)和表单的可渲染部分。这也是本API与其他解决方案(据我所知)的主要区别。通常的方法是根据模式或其他以数据为中心的概念来定义表单,如下所示
foo = String("id", "label", validator=somefunction)
w20e.forms的方法是定义
foo = Field("fid") props = FieldProperties("pid", ["fid"], required=True, datatype=int) ctrl = Input("cid", "label", bind="fid")
其中属性和控制被绑定到变量。这使您的表单中的控件不绑定到您希望收集的任何数据,共享属性等。
另一个重要的区别是,API提供了一种结构化的方式来定义数据属性,而不是必须定义自己的验证。有关详细信息,请参阅1.2节。
数据
数据包含您希望通过此表单收集的变量。一个变量仅有一个id,并且如果您喜欢一个默认值。
模型
模型包含给定表单的所有属性,如只读、必填、变量的数据类型、相关性等。所有这些属性都是通过表达式计算的,在这种情况下是Python表达式,因此必填性不仅可以是真或假,还可以基于一个表达式进行计算,该表达式可以包含您数据中的其他变量。通过'data'字典,所有表单数据中的变量都在表达式中可用,因此如果变量'foo'在变量'bar'设置为666时是必填的,这可以在绑定到'foo'的属性中如此表示
… required="data['bar'] == 666" …
通常,所有表达式都会被评估为真或假。模型提供了以下属性
required:变量是必填的吗?
relevant:变量相关吗?例如,当性别为男性时, maiden name 就不相关。通常,对于不相关的变量,相关的控件/小部件不会显示。
readonly:一个人能否更改变量的值?
calculate:而不是让一个人设置变量的值,变量将被计算。
constraint:检查表达式是否评估为真。
datatype:变量的数据类型,如int、string或更复杂的变量。
属性通过bind属性绑定到变量。可以绑定一组属性到一系列变量。
视图
视图(或FormView)是表单的实际可见部分(或者说是可听的部分)。视图可以被渲染,并包含一组绑定到变量的控件或控件。多个控件可以绑定到同一个变量。为了布局目的,控件可以分组,如流动布局或卡片布局(标签页)。
在控件标签和提示文本中,您可以通过使用表达式${}来使用变量的词汇值。这样,您可以从标签和提示中引用其他变量中给出的值。
基本使用
好的,理论已经足够了,让我们来做一些实际的事情。
表单是手工制作,或使用工厂:这应该负责生成包含必要内容的表单。
让我们完成导入…
>>> import sys >>> from interfaces import * >>> from zope.interface import implements >>> from formdata import FormData >>> from formview import FormView >>> from formmodel import FormModel >>> from data.field import Field >>> from model.fieldproperties import FieldProperties >>> from rendering.control import Input, Select, Option >>> from rendering.group import FlowGroup >>> from form import Form, FormValidationError >>> from rendering.html.renderer import HTMLRenderer >>> from submission.attrstorage import AttrStorage
创建表单
现在让我们创建一个工厂类
>>> class FormFactory(): ... implements(IFormFactory) ... def createForm(self): ... data = FormData() ... data.addField(Field("field0")) ... data.addField(Field("field1", "foo")) ... data.addField(Field("field2", "bar")) ... data.addField(Field("field3")) ... view = FormView() ... grp = FlowGroup("grp0", label="Group 0") ... grp.addRenderable(Input("input2", "Input 2", bind="field0")) ... view.addRenderable(Input("input0", "First name", bind="field0")) ... view.addRenderable(Input("input1", "Last name", bind="field1")) ... view.addRenderable(Select("select0", "Select me!", options=[], bind="field2", with_empty=True)) ... view.addRenderable(grp) ... model = FormModel() ... model.addFieldProperties(FieldProperties("prop0", ["field0"], required="True")) ... model.addFieldProperties(FieldProperties("prop1", ["field1", "field2"], relevant="data['field0']")) ... submission = AttrStorage(attr_name="_data") ... return Form("test", data, model, view, submission)>>> ff = FormFactory() >>> form = ff.createForm()
到目前为止,我们应该有一个表单,其中field0是必填的,而field1和field2只有在field0填写的情况下才相关。
>>> print len(form.data.getFields()) 4>>> props = form.model.getFieldProperties("field0") >>> props[0].id 'prop0'>>> len(props) 1>>> field0 = form.data.getField("field0") >>> field0.id 'field0'>>> field0.value
同时,由于field0没有值,field1和field2应该是不相关的。
>>> form.model.isRelevant("field1", form.data) False >>> form.model.isRelevant("field2", form.data) False
由于field0是必填的,验证应该失败。
>>> try: ... form.validate() ... except FormValidationError: ... print sys.exc_info()[1].errors['field0'] ['required']>>> form.data.getField("field0").value = "pipo" >>> form.validate() True>>> field0.value 'pipo'
到目前为止,field1和field2也应该相关。
>>> form.model.isRelevant("field1", form.data) True >>> form.model.isRelevant("field2", form.data) True
显示
下一节将假设渲染到HTML。这可能会涵盖近100%的使用案例…现在是一些显示部分。不相关的控件不应该有'class' 'relevant',否则它应该有…这使您可以进行特定的样式设计,如'display: none'。
>>> form.data.getField('field0').value = None >>> field = form.view.getRenderable('input1') >>> renderer = HTMLRenderer() >>> renderer.render(form, field, sys.stdout) <div id="input1" class="control input "> <label for="input-input1">Last name</label> <div class="alert"></div> <div class="hint"></div> <input id="input-input1" type="text" name="input1" value="foo" size="20"/> </div>>>> form.data.getField('field0').value = 'pipo' >>> field = form.view.getRenderable('input1') >>> renderer = HTMLRenderer() >>> renderer.render(form, field, sys.stdout) <div id="input1" class="control input relevant"> <label for="input-input1">Last name</label> <div class="alert"></div> <div class="hint"></div> <input id="input-input1" type="text" name="input1" value="foo" size="20"/> </div>>>> field = form.view.getRenderable('input0') >>> renderer.render(form, field, sys.stdout) <div id="input0" class="control input relevant required"> <label for="input-input0">First name</label> <div class="alert"></div> <div class="hint"></div> <input id="input-input0" type="text" name="input0" value="pipo" size="20"/> </div>
那些额外的类怎么样…
>>> renderer.render(form, field, sys.stdout, extra_classes="card") <div id="input0" class="control input card relevant required"> <label for="input-input0">First name</label> <div class="alert"></div> <div class="hint"></div> <input id="input-input0" type="text" name="input0" value="pipo" size="20"/> </div>>>> select = form.view.getRenderable('select0') >>> renderer.render(form, select, sys.stdout) <div id="select0" class="control select relevant"> <label for="input-select0">Select me!</label> <div class="alert"></div> <div class="hint"></div> <select id="input-select0" name="select0" size="1"> <option value="" >Maak een keuze</option> </select> </div>
我们实际上得到了分组的控件吗?
>>> nested_input = form.view.getRenderable('input2') >>> nested_input.id 'input2'
提交
最后,当表单被渲染、被某人填写并验证后,数据通常会发送到某个地方。这是通过提交来实现的。我们定义提交为AttrStorage,这是一种在某个上下文中存储数据的属性。这是一个可以用在很多框架中的案例,至少是plone和pyramid。
让我们看看它做了什么
>>> class Context: ... """ some context """ >>> ctx = Context() >>> form.submission.submit(form, ctx)
现在上下文应该持有数据属性。我们指定属性名为'_data',让我们检查
>>> ctx._data.getField('field0').value 'pipo'
基础知识之外
嗯,这很简单,您很可能会希望它更有用。表单的所有部分都可以扩展。例如,FormView。开发者(或最终用户)应该能够
创建一个完整的HTML表单;
使用生成的HTML表单(这是基本实现所做的事情);
创建一个PDF表单。
工厂也是表单处理过程的重要部分。工厂可以想象成以下之一
由Schema(内容类型)生成;
由XML定义生成,例如OpenOffice中的XForms实例。
一般而言,表单应该是
可提交给一系列处理器,如电子邮件、数据库存储、内容类型存储;
易于实时验证;
支持多页。
更详细的测试
我们想检查通过绑定查找控件是否有效,以便能够将值处理成词汇值。当使用选择时,这特别有趣:我们预计会看到标签而不是词汇空间中的值。
>>> data = FormData() >>> data.addField(Field("f0", "opt0")) >>> view = FormView() >>> opts = [Option("opt0", "Option 0"), Option("opt1", "Option 1")] >>> view.addRenderable(Select("sel0", "Select 0", bind="f0", options=opts)) >>> ctl = view.getRenderableByBind("f0") >>> ctl.lexVal("opt0") 'Option 0'
我们能否在标签和提示中使用变量替换?是的,我们可以!
>>> data = FormData() >>> data.addField(Field("f0", "Pipo")) >>> data.addField(Field("f1")) >>> view = FormView() >>> view.addRenderable(Input("in0", "First name", bind="f0")) >>> view.addRenderable(Input("in1", "Last name for ${f0}", bind="f1")) >>> model = FormModel() >>> form = Form("test", data, model, view, None) >>> renderer = HTMLRenderer() >>> field = form.view.getRenderable('in1') >>> renderer.render(form, field, sys.stdout) <div id="in1" class="control input relevant"> <label for="input-in1">Last name for Pipo</label> <div class="alert"></div> <div class="hint"></div> <input id="input-in1" type="text" name="in1" value="" size="20"/> </div>
让我们深入探讨一下输入处理……一个简单的输入应该只返回它的自身值
>>> data = {'pipo': 'lala'} >>> ctl = Input("pipo", "f0", "Some input") >>> ctl.processInput(data) 'lala'
注册自己的内容
w20e.forms不是完整的表单库,它永远不会是,因为大多数人都有非常具体的需求,比如特定的小部件、自定义的输入字段版本等。API通过使用全局注册表来注册扩展来简化这一点。
全局注册表可以这样使用
>>> from w20e.forms.registry import Registry
并提供了许多类方法来注册各种东西。
例如,让我们注册一个新的输入渲染器
词汇表
w20e.forms允许使用词汇表来限制给定列表的可能答案。这是一个通常与选择小部件一起使用的功能。词汇表是一个“命名”的工厂,它创建一个选项列表。
可以这样注册
>>> def make_vocab(): ... return [Option('0', 'Opt 0'), Option('1', 'Opt 1')] ... Registry.register_vocab('foovocab', make_vocab) ... sel = Select("select0", "Select me!", vocab=make_vocab, ... bind="field2", with_empty=True))
必需的、相关的、只读的
在一个表单中,你通常会想要说类似这样的话:这个控件只有在那个问题的答案是“x”时才需要显示,或者那个问题需要在其他问题的答案是“y”时是必填的。
w20e.forms通过使用表达式来实现这一点。这些表达式通过它们的“bind”属性设置为变量的属性。因此,在表单模型中,你可能有一个名为“req”的属性集合,它使得变量“foo”必须填写,如下所示
model.addFieldProperties(FieldProperties("req", ["foo"], required="True"))
显然,在一般情况下,你想要比这更灵活一些,比如检查已经输入的其他数据。所有表单数据都通过“data”变量(这是一个字典)提供给表达式。所以检查其他变量就像这样
- model.addFieldProperties(FieldProperties("req", ["foo"],
required="data['bar'] == 42"))
所以只有当“bar”的答案是42时,“foo”才是必填的。相关性、必填性和只读性都以此类推。
你甚至可以向引擎添加自己的表达式上下文,以调用对象的方法等。
像这样进行,假设你的对象是obj
>>> registry.register_expr_context('mycontext', obj) ... model.addFieldProperties(FieldProperties("req", ["foo"], ... relevant="mycontext.some_method())
XML
w20e.forms包的xml命名空间提供了一个基于XML的w20e.forms API实现。这允许从XML文件定义和序列化。提供了用于定义w20e.forms的XML的DTD。这与xForms非常相似。
使用XML作为表单的定义提供了一种更声明性的创建表单的方法,就像你在HTML中创建表单一样。此外,XML是一种易于存储和传输的格式。
开始使用XML工厂
>>> from factory import XMLFormFactory
现在让我们创建一个工厂类
>>> xml = """ ... <form id="test"> ... ... <!-- The data part, a.k.a. the variables you wish to collect --> ... <data> ... <foo/> ... <bar value="666"/> ... </data> ... ... <model> ... <properties id="required"> ... <bind>foo</bind> ... <bind>bar</bind> ... <required>True</required> ... </properties> ... <properties id="int"> ... <bind>bar</bind> ... <datatype>int</datatype> ... </properties> ... </model> ... ... <view> ... <input id="fooctl" bind="foo"> ... <label>Foo?</label> ... <hint>Well, foo or no?</hint> ... </input> ... <select id="barctl" bind="bar"> ... <property name="multiple">False</property> ... <label>Bar</label> ... <option value="1">One</option> ... <option value="2">Two</option> ... </select> ... <select bind="bar" id="barctl2"> ... <label>Bar2</label> ... <option value="3">Three</option> ... <option value="4">Four</option> ... </select> ... <select bind="bar" id="barctl3"> ... <property name="vocab">some_vocab</property> ... <label>Bar3</label> ... </select> ... <group layout="flow" id="groupie"> ... <label>GruppoSportivo</label> ... <text id="txt">Moi</text> ... </group> ... </view> ... ... <submission type="none"> ... <property name="action">@@save</property> ... </submission> ... ... </form>"""我们在xml中使用了一个词汇表,所以注册它… >>> from w20e.forms.registry import Registry … def some_vocab(): … return [Option(0, 0), Option(1, 1)] … Registry.register_vocab('some_vocab', some_vocab)
>>> xmlff = XMLFormFactory(xml) >>> form = xmlff.create_form() >>> print len(form.data.getFields()) 2>>> print form.data.getField("foo").id foo>>> print form.data.getField("bar").value 666设置值
>>> form.data.getField("bar").value = 777 >>> print form.data.getField("bar").value 777好的,到目前为止一切顺利。现在让我们看看我们有什么属性。
>>> props = form.model.getFieldProperties("bar") >>> len(props) 2>>> intprop = [prop for prop in props if prop.id == "int"][0] >>> reqprop = [prop for prop in props if prop.id == "required"][0] >>> reqprop.getRequired() 'True'>>> intprop.getDatatype() 'int'最后,检查可查看的部分,或控件 >>> ctrl = form.view.getRenderable("fooctl") >>> ctrl.label ‘Foo?’
>>> ctrl.__class__.__name__ 'Input'>>> ctrl.hint 'Well, foo or no?'>>> ctrl.id 'fooctl'>>> ctrl.bind 'foo'>>> ctrl = form.view.getRenderable("barctl") >>> ctrl.multiple 'False'>>> len(ctrl.options) 2
- 我们能否得到嵌套的内容?
>>> ctrl = form.view.getRenderable("txt") >>> ctrl.id 'txt'
序列化
你可以轻松地将表单序列化为XML。让我们试试…
>>> from serializer import XMLSerializer >>> serializer = XMLSerializer() >>> print serializer.serialize(form) <form id="test"> <data> <foo/> <bar value="777"/> </data> <model> <properties id="int"> <bind>bar</bind> <datatype>int</datatype> </properties> <properties id="required"> <bind>foo</bind> <bind>bar</bind> <required>True</required> </properties> </model> <view> <input bind="foo" id="fooctl"> <label>Foo?</label> <hint>Well, foo or no?</hint> </input> <select bind="bar" id="barctl"> <label>Bar</label> <property name="multiple">False</property> <option value="1">One</option> <option value="2">Two</option> </select> <select bind="bar" id="barctl2"> <label>Bar2</label> <option value="3">Three</option> <option value="4">Four</option> </select> <flowgroup id="groupie"> <label>GruppoSportivo</label> <text id="txt"/> </flowgroup> </view> <submission type="none"> <property name="action">@@save</property> </submission> </form> <BLANKLINE>
注意,变量foo现在持有值777。遗憾的是,很难保证所有XML都与输入XML完全相同。
Pyramid
pyramid包提供了一个简单的方法,可以在pyramid应用中使用w20e.forms。该包为pyramid提供了一个特定的“文件”字段,以从POST/GET请求中的文件提取文件名和内容,以及一个基本视图。
您希望使用w20e.forms吗?那么
将w20e.forms添加到您的应用依赖项的eggs中(当然...)
对于您希望显示实际表单的视图,覆盖w20e.forms.pyramid.pyramidformview。让我们首先进行一些导入。请注意,使用XML实现更方便,如后文所示。另外,如果您坚持使用Python实现,最好创建一个工厂来创建表单,这样您就可以直接从视图调用工厂。无论如何,让我们先采取不那么聪明的办法
>>> from w20e.forms.form import Form >>> from w20e.forms.formdata import FormData >>> from w20e.forms.formmodel import FormModel >>> from w20e.forms.formview import FormView >>> from w20e.forms.submission.attrstorage import AttrStorage >>> from w20e.forms.data.field import Field >>> from w20e.forms.rendering.control import Input >>> from w20e.forms.pyramid.formview import formview as pyramidformview哇,导入了一大堆。现在进行实际的视图类。这是一个相当简单的表单,但你应该明白这个意思。
>>> class yourformview(pyramidformview): ... def __init__(self, context, request): ... data = FormData() ... data.addField(Field("foo", "some default value")) ... data.addField(Field("bar")) ... model = FormModel() ... view = FormView() ... # We'll leave the poperties out for now, check the main ... # README for details ... view.addRenderable(Input("input0", "Input foo", bind="foo")) ... view.addRenderable(Input("input1", "Input bar here", bind="bar")) ... submission = AttrStorage(attr_name="_data") ... form = Form("test", data, model, view, submission) ... pyramidformview.__init__(self, context, request, form)现在,pyramid的视图只需要一个上下文和一个请求,所以让我们创建视图实例
>>> class Context: ... """ nothing needed here, but we'll store the data in here """ >>> class Request: ... def __init__(self, params=None): ... self.params = params >>> ctx = Context() >>> req = Request() >>> view = yourformview(ctx, req)好了,我们现在可以开始操作了。让我们尝试渲染表单。
>>> print view.renderform() <form class="w20e-form" method="post" action="" enctype="multipart/form-data"> <input type="hidden" name="formprocess" value="1"/> <div class="alert"></div> <div id="input0" class="control input relevant"> <label for="input-input0">Input foo</label> <div class="alert"></div> <div class="hint"></div> <input id="input-input0" type="text" name="input0" value="some default value" size="20"/> </div> <div id="input1" class="control input relevant"> <label for="input-input1">Input bar here</label> <div class="alert"></div> <div class="hint"></div> <input id="input-input1" type="text" name="input1" value="" size="20"/> </div> </form> <BLANKLINE>不错。现在让我们给请求一些内容,让视图处理提交。这将导致上下文中的表单数据存储在_data属性中。formprocess是w20e.forms用来假定表单已提交的标记。
>>> req = Request({'formprocess': 1, 'input0': 6, 'input1': 'whatever'}) >>> view = yourformview(ctx, req) >>> view() {'status': 'stored', 'errors': {}} >>> ctx._data.getField('foo').value 6 >>> ctx._data.getField('bar').value 'whatever'
XML实现
使用XML实现使生活更加简单
from w20e.forms.pyramid.formview import xmlformview as pyramidformview from w20e.forms.xml.formfile import FormFile class yourformview(pyramidformview): def __init__(self, context, request): pyramidformview.__init__(self, context, request, FormFile("forms/yourform.xml"))您有一个名为“forms”的目录,其中包含名为yourform.xml的XML定义。有关XML定义的详细信息,请参阅w20e.forms.xml模块。
创建一个模板(例如form.pt),它调用视图的render方法
<p tal:content="structure python:view.renderform()"></p>将内容连接到zcml中(假设您正在使用它),如下所示
<view context=".models.YourModel" view=".views.yourformview" renderer="templates/form.pt" name="yourform" />
项目详情
w20e.forms-1.0.2b.tar.gz的散列
算法 | 散列摘要 | |
---|---|---|
SHA256 | 2c06945271aa0dca4f540bc0a093a01e06db718e54192ef0f353b1f6ceb7b06a |
|
MD5 | fe34744c9fc3e7e00898b49ef886b7c8 |
|
BLAKE2b-256 | 4b5152e615cc75761725acae43cc602ddc53723dad78d35812e8ad3b111b7e05 |