HTML表单结构

CubicWeb提供了一些常见的表单/字段/小部件/呈现器抽象,以提供通用的构建块,这将极大地帮助您构建与CubicWeb适当集成的表单(一致显示、错误处理等),同时尽可能保持灵活性。

A form 基本上只有一组 fields 以及是否绑定到 renderer 负责布置。每个字段都绑定到 widget 它将用于填充该字段的值(在表单生成时)和浏览器返回的“解码”(获取并提供适当的python类型)。

这个 field 应根据要编辑的内容的类型使用。例如,如果要编辑某个日期,则必须使用 cubicweb.web.formfields.DateField .然后您可以在多个小部件中进行选择以编辑它,例如 cubicweb.web.formwidgets.TextInput (纯文本字段) DateTimePicker (简单的日历)甚至 JQueryDatePicker (jquery日历)。当然,您也可以编写自己的小部件。

浏览可用表单

一次小小的旅行 CubicWeb shell是发现可用表单(或一般的应用程序对象)的最快方法。

>>> from pprint import pprint
>>> pprint( session.vreg['forms'] )
{'base': [<class 'cubicweb.web.views.forms.FieldsForm'>,
          <class 'cubicweb.web.views.forms.EntityFieldsForm'>],
 'changestate': [<class 'cubicweb.web.views.workflow.ChangeStateForm'>,
                 <class 'cubicweb_tracker.views.forms.VersionChangeStateForm'>],
 'composite': [<class 'cubicweb.web.views.forms.CompositeForm'>,
               <class 'cubicweb.web.views.forms.CompositeEntityForm'>],
 'deleteconf': [<class 'cubicweb.web.views.editforms.DeleteConfForm'>],
 'edition': [<class 'cubicweb.web.views.autoform.AutomaticEntityForm'>,
             <class 'cubicweb.web.views.workflow.TransitionEditionForm'>,
             <class 'cubicweb.web.views.workflow.StateEditionForm'>],
 'logform': [<class 'cubicweb.web.views.basetemplates.LogForm'>],
 'massmailing': [<class 'cubicweb.web.views.massmailing.MassMailingForm'>],
 'muledit': [<class 'cubicweb.web.views.editforms.TableEditForm'>],
 'sparql': [<class 'cubicweb.web.views.sparql.SparqlForm'>]}

这里两个最重要的形式族(出于所有实际目的)是 baseedition .大多数时候,人们想要改变 AutomaticEntityForm 生成自定义窗体以处理实体的版本。

自动实体窗体

选择函数的解剖

让我们看看 ticket_done_in_choices function given to the choices parameter of the relation tag that is applied to the ('Ticket', 'done_in', '*') relation definition, as it is both typical and sophisticated enough. This is a code snippet from the tracker 立方体。

这个 Ticket 实体类型可以与 Project 和A Version ,分别通过 concernsdone_in 关系。当用户要编辑票据时,我们希望在组合框中填充 done_in 与上下文相关的值的关系。这里的重要背景是:

  • 创建或修改(在任何情况下,我们都不能以相同的方式获取值)

  • __linkto 创建上下文中给定的URL参数

from cubicweb.web import formfields

def ticket_done_in_choices(form, field):
    entity = form.edited_entity
    # first see if its specified by __linkto form parameters
    linkedto = form.linked_to[('done_in', 'subject')]
    if linkedto:
        return linkedto
    # it isn't, get initial values
    vocab = field.relvoc_init(form)
    veid = None
    # try to fetch the (already or pending) related version and project
    if not entity.has_eid():
        peids = form.linked_to[('concerns', 'subject')]
        peid = peids and peids[0]
    else:
        peid = entity.project.eid
        veid = entity.done_in and entity.done_in[0].eid
    if peid:
        # we can complete the vocabulary with relevant values
        rschema = form._cw.vreg.schema['done_in'].rdef('Ticket', 'Version')
        rset = form._cw.execute(
            'Any V, VN ORDERBY version_sort_value(VN) '
            'WHERE V version_of P, P eid %(p)s, V num VN, '
            'V in_state ST, NOT ST name "published"', {'p': peid}, 'p')
        vocab += [(v.view('combobox'), v.eid) for v in rset.entities()
                  if rschema.has_perm(form._cw, 'add', toeid=v.eid)
                  and v.eid != veid]
    return vocab

我们首先要做的是从 __linkto 通常在实体创建上下文中找到的URL参数(创建操作为此类参数提供一个预定值;例如,在这种情况下,可以在 Version 实体)。这个 RelationField Field类提供 relvoc_linkedto() 方法,该方法获取一个适当地填充词汇值的列表。

linkedto = field.relvoc_linkedto(form)
if linkedto:
    return linkedto

那么,如果没有 __linkto 给出了参数,我们必须用一个初始空值准备词汇表(因为 done_in 不是强制性的,我们必须允许用户不选择verson)和已经链接的值。这是用 relvoc_init() 方法。

vocab = field.relvoc_init(form)

但是,我们必须给出更多:如果票据与一个项目相关,我们应该提供这个项目的所有未发布版本。 (VersionProject 可以通过 version_of 关系)。相反,如果我们还不知道这个项目,那么建议所有现有的版本是没有意义的,因为它可能导致不一致。即使这些会被一些RqConstraint捕获,也不要用错误诱导的候选值来引诱用户是明智的。

“票据与项目相关”部分必须分解为:

  • 这是一个新的通知单,它是作为项目上下文创建的。

  • 这是一个已存在的通知单,链接到一个项目(通过 concerns 关系)

  • 没有相关的项目(考虑到 concerns relation, so it can only mean that we are creating a new ticket, and a project is about to be selected but there is no ``_ _链接到``参数)

注解

最后一种情况可能以多种方式发生,但在一个经过优化的应用程序中,应该控制票据创建的路径,以避免出现不理想的最终用户体验。

因此,我们试图获取相关的项目。

veid = None
if not entity.has_eid():
    peids = form.linked_to[('concerns', 'subject')]
    peid = peids and peids[0]
else:
    peid = entity.project.eid
    veid = entity.done_in and entity.done_in[0].eid

我们使用 Entity.has_eid() 方法,返回 False on creation. At creation time the only way to get a project is through the ``_ _ linkto``参数。请注意,我们获取了票据所在的版本 done_in 如果有的话,以后再说。

注解

上面的实现假设如果 __linkto 参数,它只与项目有关。虽然大多数时候它是有意义的,但它不是绝对的。根据实体创建操作URL的构建方式,可能会有多个结果

如果票据已经链接到一个项目,那么获取它是很简单的。然后我们将相关版本添加到初始词汇表中。

if peid:
    rschema = form._cw.vreg.schema['done_in'].rdef('Ticket', 'Version')
    rset = form._cw.execute(
        'Any V, VN ORDERBY version_sort_value(VN) '
        'WHERE V version_of P, P eid %(p)s, V num VN, '
        'V in_state ST, NOT ST name "published"', {'p': peid})
    vocab += [(v.view('combobox'), v.eid) for v in rset.entities()
              if rschema.has_perm(form._cw, 'add', toeid=v.eid)
              and v.eid != veid]

警告

我们必须保护自己免受缺乏项目EID的影响。给定的基数 concerns 关系,那里 must 是一个项目,但此规则只能在验证时执行,当然只有在表单子任务之后才会执行。

在这里,给定一个项目ID,我们使用项目中定义的所有未发布版本(按编号排序)来完成词汇表,当前用户可以为此建立关系。

使用自定义字段/小部件构建自发布表单

有时您需要一个与实体版本无关的表单。对于这些,您必须自己处理表单发布。下面是一个关于如何实现这一点(以及更多)的完整示例。

假设您需要一个选择月份期间的表单。在CubicWeb中没有合适的字段/小部件来处理这个问题,所以我们先定义它们:

# let's have the whole import list at the beginning, even those necessary for
# subsequent snippets
from logilab.common import date
from logilab.mtconverter import xml_escape
from cubicweb.view import View
from cubicweb.predicates import match_kwargs
from cubicweb.web import RequestError, ProcessFormError
from cubicweb.web import formfields as fields, formwidgets as wdgs
from cubicweb.web.views import forms, calendar

class MonthSelect(wdgs.Select):
    """Custom widget to display month and year. Expect value to be given as a
    date instance.
    """

    def format_value(self, form, field, value):
        return u'%s/%s' % (value.year, value.month)

    def process_field_data(self, form, field):
        val = super(MonthSelect, self).process_field_data(form, field)
        try:
            year, month = val.split('/')
            year = int(year)
            month = int(month)
            return date.date(year, month, 1)
        except ValueError:
            raise ProcessFormError(
                form._cw._('badly formated date string %s') % val)


class MonthPeriodField(fields.CompoundField):
    """custom field composed of two subfields, 'begin_month' and 'end_month'.

    It expects to be used on form that has 'mindate' and 'maxdate' in its
    extra arguments, telling the range of month to display.
    """

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('widget', wdgs.IntervalWidget())
        super(MonthPeriodField, self).__init__(
            [fields.StringField(name='begin_month',
                                choices=self.get_range, sort=False,
                                value=self.get_mindate,
                                widget=MonthSelect()),
             fields.StringField(name='end_month',
                                choices=self.get_range, sort=False,
                                value=self.get_maxdate,
                                widget=MonthSelect())], *args, **kwargs)

    @staticmethod
    def get_range(form, field):
        mindate = date.todate(form.cw_extra_kwargs['mindate'])
        maxdate = date.todate(form.cw_extra_kwargs['maxdate'])
        assert mindate <= maxdate
        _ = form._cw._
        months = []
        while mindate <= maxdate:
            label = '%s %s' % (_(calendar.MONTHNAMES[mindate.month - 1]),
                               mindate.year)
            value = field.widget.format_value(form, field, mindate)
            months.append( (label, value) )
            mindate = date.next_month(mindate)
        return months

    @staticmethod
    def get_mindate(form, field):
        return form.cw_extra_kwargs['mindate']

    @staticmethod
    def get_maxdate(form, field):
        return form.cw_extra_kwargs['maxdate']

    def process_posted(self, form):
        for field, value in super(MonthPeriodField, self).process_posted(form):
            if field.name == 'end_month':
                value = date.last_day(value)
            yield field, value

在这里,我们首先定义一个小部件,用于选择周期的开始和结束,显示月份,如“<month>yyyy”,但使用“yy/mm”作为实际值。

然后我们定义一个字段,它实际包含两个字段,一个用于开始,另一个用于结束期间。每个子字段使用前面定义的小部件,外部字段本身使用标准 IntervalWidget .该字段添加了一些逻辑:

  • 词汇生成函数 get_range ,用于填充每个子字段

  • 两个“值”函数 get_mindateget_maxdate ,用于告诉子字段在窗体初始化时应考虑哪些值

  • 覆盖 process_posted ,以便将期间的结束日期正确设置为月份的最后一天时调用。

现在,我们可以定义一个非常简单的形式:

class MonthPeriodSelectorForm(forms.FieldsForm):
    __regid__ = 'myform'
    __select__ = match_kwargs('mindate', 'maxdate')

    form_buttons = [wdgs.SubmitButton()]
    form_renderer_id = 'onerowtable'
    period = MonthPeriodField()

我们只需添加字段,设置提交按钮并使用非常简单的渲染器(尝试其他方法!)。此外,我们还指定了一个选择器,以确保表单具有字段所需的参数。

现在,我们需要一个视图,在表单发生时将其包装并处理发布,只需在页面中显示发布的值:

class SelfPostingForm(View):
    __regid__ = 'myformview'

    def call(self):
        mindate, maxdate = date.date(2010, 1, 1), date.date(2012, 1, 1)
        form = self._cw.vreg['forms'].select(
            'myform', self._cw, mindate=mindate, maxdate=maxdate, action='')
        try:
            posted = form.process_posted()
            self.w(u'<p>posted values %s</p>' % xml_escape(repr(posted)))
        except RequestError: # no specified period asked
            pass
        form.render(w=self.w, formvalues=self._cw.form)

注意使用 process_posted() 方法,它将返回类型值的字典(因为它们已由字段处理)。在我们的例子中,当表单发布时,您应该看到一个字典,其中“begin_month”和“end_month”作为键,所选日期作为值(作为python date 对象)。

APIs