# HG changeset patch # User Sylvain Thénault # Date 1310378207 -7200 # Node ID f3e3892fc7e3ca6d4880df2351f0a0f7357e5a66 # Parent 64eee2a83bfaa792dcb6ecb2882728fd7a39b88d [book, form] include complete example of self-posted form with custom field/widget + minor other changes diff -r 64eee2a83bfa -r f3e3892fc7e3 doc/book/en/devweb/edition/dissection.rst --- a/doc/book/en/devweb/edition/dissection.rst Mon Jul 11 09:21:44 2011 +0200 +++ b/doc/book/en/devweb/edition/dissection.rst Mon Jul 11 11:56:47 2011 +0200 @@ -1,8 +1,8 @@ .. _form_dissection: -Dissection of a form --------------------- +Dissection of an entity form +---------------------------- This is done (again) with a vanilla instance of the `tracker`_ cube. We will populate the database with a bunch of entities and see diff -r 64eee2a83bfa -r f3e3892fc7e3 doc/book/en/devweb/edition/form.rst --- a/doc/book/en/devweb/edition/form.rst Mon Jul 11 09:21:44 2011 +0200 +++ b/doc/book/en/devweb/edition/form.rst Mon Jul 11 11:56:47 2011 +0200 @@ -48,9 +48,10 @@ 'sparql': []} -The two most important form families here (for all pracitcal purposes) -are `base` and `edition`. Most of the time one wants alterations of -the AutomaticEntityForm (from the `edition` category). +The two most important form families here (for all practical purposes) are `base` +and `edition`. Most of the time one wants alterations of the +:class:`AutomaticEntityForm` to generate custom forms to handle edition of an +entity. The Automatic Entity Form ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -214,6 +215,158 @@ unpublished versions defined in the project (sorted by number) for which the current user is allowed to establish the relation. + +Building self-posted form with custom fields/widgets +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes you want a form that is not related to entity edition. For those, +you'll have to handle form posting by yourself. Here is a complete example on how +to achieve this (and more). + +Imagine you want a form that selects a month period. There are no proper +field/widget to handle this in CubicWeb, so let's start by defining them: + +.. sourcecode:: python + + # 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.selectors 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 + + +Here we first define a widget that will be used to select the beginning and the +end of the period, displaying months like ' YYYY' but using 'YYYY/mm' as +actual value. + +We then define a field that will actually hold two fields, one for the beginning +and another for the end of the period. Each subfield uses the widget we defined +earlier, and the outer field itself uses the standard +:class:`IntervalWidget`. The field adds some logic: + +* a vocabulary generation function `get_range`, used to populate each sub-field + +* two 'value' functions `get_mindate` and `get_maxdate`, used to tell to + subfields which value they should consider on form initialization + +* overriding of `process_posted`, called when the form is being posted, so that + the end of the period is properly set to the last day of the month. + +Now, we can define a very simple form: + +.. sourcecode:: python + + class MonthPeriodSelectorForm(forms.FieldsForm): + __regid__ = 'myform' + __select__ = match_kwargs('mindate', 'maxdate') + + form_buttons = [wdgs.SubmitButton()] + form_renderer_id = 'onerowtable' + period = MonthPeriodField() + + +where we simply add our field, set a submit button and use a very simple renderer +(try others!). Also we specify a selector that ensures form will have arguments +necessary to our field. + +Now, we need a view that will wrap the form and handle post when it occurs, +simply displaying posted values in the page: + +.. sourcecode:: python + + 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'

posted values %s

' % xml_escape(repr(posted))) + except RequestError: # no specified period asked + pass + form.render(w=self.w, formvalues=self._cw.form) + + +Notice usage of the :meth:`process_posted` method, that will return a dictionary +of typed values (because they have been processed by the field). In our case, when +the form is posted you should see a dictionary with 'begin_month' and 'end_month' +as keys with the selected dates as value (as a python `date` object). + + APIs ~~~~