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'>]}
这里两个最重要的形式族(出于所有实际目的)是 base 和 edition .大多数时候,人们想要改变 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
,分别通过 concerns
和 done_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)
但是,我们必须给出更多:如果票据与一个项目相关,我们应该提供这个项目的所有未发布版本。 (Version 和 Project 可以通过 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_mindate 和 get_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 对象)。