跳转到主要内容

Python API 用于创建和处理表单

项目描述

w20e.forms包提供了一个强大的API,用于创建和处理电子表单。该包松散地基于XForms概念(以及Socrates QE,一个Java实现)。该包旨在提供标准plone、pyramid和django解决方案的即插即用替代品,但可以在任何框架(或没有框架的情况下)使用。

核心概念

核心概念如下(这样您可以快速决定是否喜欢这种方法)

表单是以下四件事的容器/组合

  1. 数据

  2. 模型

  3. 视图

  4. 提交

这清楚地分离了数据、数据的属性(数据类型、是否必需等)和表单的可渲染部分。这也是本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 (49.6 kB 查看散列)

上传时间

支持者