doc/book/en/devweb/edition/form.rst
changeset 7643 f3e3892fc7e3
parent 5869 8a129b3a5aff
child 7901 bdb81b1a8243
equal deleted inserted replaced
7642:64eee2a83bfa 7643:f3e3892fc7e3
    46   'massmailing': [<class 'cubicweb.web.views.massmailing.MassMailingForm'>],
    46   'massmailing': [<class 'cubicweb.web.views.massmailing.MassMailingForm'>],
    47   'muledit': [<class 'cubicweb.web.views.editforms.TableEditForm'>],
    47   'muledit': [<class 'cubicweb.web.views.editforms.TableEditForm'>],
    48   'sparql': [<class 'cubicweb.web.views.sparql.SparqlForm'>]}
    48   'sparql': [<class 'cubicweb.web.views.sparql.SparqlForm'>]}
    49 
    49 
    50 
    50 
    51 The two most important form families here (for all pracitcal purposes)
    51 The two most important form families here (for all practical purposes) are `base`
    52 are `base` and `edition`. Most of the time one wants alterations of
    52 and `edition`. Most of the time one wants alterations of the
    53 the AutomaticEntityForm (from the `edition` category).
    53 :class:`AutomaticEntityForm` to generate custom forms to handle edition of an
       
    54 entity.
    54 
    55 
    55 The Automatic Entity Form
    56 The Automatic Entity Form
    56 ~~~~~~~~~~~~~~~~~~~~~~~~~
    57 ~~~~~~~~~~~~~~~~~~~~~~~~~
    57 
    58 
    58 .. automodule:: cubicweb.web.views.autoform
    59 .. automodule:: cubicweb.web.views.autoform
   212 
   213 
   213 Here, given a project eid, we complete the vocabulary with all
   214 Here, given a project eid, we complete the vocabulary with all
   214 unpublished versions defined in the project (sorted by number) for
   215 unpublished versions defined in the project (sorted by number) for
   215 which the current user is allowed to establish the relation.
   216 which the current user is allowed to establish the relation.
   216 
   217 
       
   218 
       
   219 Building self-posted form with custom fields/widgets
       
   220 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   221 
       
   222 Sometimes you want a form that is not related to entity edition. For those,
       
   223 you'll have to handle form posting by yourself. Here is a complete example on how
       
   224 to achieve this (and more).
       
   225 
       
   226 Imagine you want a form that selects a month period. There are no proper
       
   227 field/widget to handle this in CubicWeb, so let's start by defining them:
       
   228 
       
   229 .. sourcecode:: python
       
   230 
       
   231     # let's have the whole import list at the beginning, even those necessary for
       
   232     # subsequent snippets
       
   233     from logilab.common import date
       
   234     from logilab.mtconverter import xml_escape
       
   235     from cubicweb.view import View
       
   236     from cubicweb.selectors import match_kwargs
       
   237     from cubicweb.web import RequestError, ProcessFormError
       
   238     from cubicweb.web import formfields as fields, formwidgets as wdgs
       
   239     from cubicweb.web.views import forms, calendar
       
   240 
       
   241     class MonthSelect(wdgs.Select):
       
   242         """Custom widget to display month and year. Expect value to be given as a
       
   243         date instance.
       
   244         """
       
   245 
       
   246         def format_value(self, form, field, value):
       
   247             return u'%s/%s' % (value.year, value.month)
       
   248 
       
   249         def process_field_data(self, form, field):
       
   250             val = super(MonthSelect, self).process_field_data(form, field)
       
   251             try:
       
   252                 year, month = val.split('/')
       
   253                 year = int(year)
       
   254                 month = int(month)
       
   255                 return date.date(year, month, 1)
       
   256             except ValueError:
       
   257                 raise ProcessFormError(
       
   258                     form._cw._('badly formated date string %s') % val)
       
   259 
       
   260 
       
   261     class MonthPeriodField(fields.CompoundField):
       
   262         """custom field composed of two subfields, 'begin_month' and 'end_month'.
       
   263 
       
   264         It expects to be used on form that has 'mindate' and 'maxdate' in its
       
   265         extra arguments, telling the range of month to display.
       
   266         """
       
   267 
       
   268         def __init__(self, *args, **kwargs):
       
   269             kwargs.setdefault('widget', wdgs.IntervalWidget())
       
   270             super(MonthPeriodField, self).__init__(
       
   271                 [fields.StringField(name='begin_month',
       
   272                                     choices=self.get_range, sort=False,
       
   273                                     value=self.get_mindate,
       
   274                                     widget=MonthSelect()),
       
   275                  fields.StringField(name='end_month',
       
   276                                     choices=self.get_range, sort=False,
       
   277                                     value=self.get_maxdate,
       
   278                                     widget=MonthSelect())], *args, **kwargs)
       
   279 
       
   280         @staticmethod
       
   281         def get_range(form, field):
       
   282             mindate = date.todate(form.cw_extra_kwargs['mindate'])
       
   283             maxdate = date.todate(form.cw_extra_kwargs['maxdate'])
       
   284             assert mindate <= maxdate
       
   285             _ = form._cw._
       
   286             months = []
       
   287             while mindate <= maxdate:
       
   288                 label = '%s %s' % (_(calendar.MONTHNAMES[mindate.month - 1]),
       
   289                                    mindate.year)
       
   290                 value = field.widget.format_value(form, field, mindate)
       
   291                 months.append( (label, value) )
       
   292                 mindate = date.next_month(mindate)
       
   293             return months
       
   294 
       
   295         @staticmethod
       
   296         def get_mindate(form, field):
       
   297             return form.cw_extra_kwargs['mindate']
       
   298 
       
   299         @staticmethod
       
   300         def get_maxdate(form, field):
       
   301             return form.cw_extra_kwargs['maxdate']
       
   302 
       
   303         def process_posted(self, form):
       
   304             for field, value in super(MonthPeriodField, self).process_posted(form):
       
   305                 if field.name == 'end_month':
       
   306                     value = date.last_day(value)
       
   307                 yield field, value
       
   308 
       
   309 
       
   310 Here we first define a widget that will be used to select the beginning and the
       
   311 end of the period, displaying months like '<month> YYYY' but using 'YYYY/mm' as
       
   312 actual value.
       
   313 
       
   314 We then define a field that will actually hold two fields, one for the beginning
       
   315 and another for the end of the period. Each subfield uses the widget we defined
       
   316 earlier, and the outer field itself uses the standard
       
   317 :class:`IntervalWidget`. The field adds some logic:
       
   318 
       
   319 * a vocabulary generation function `get_range`, used to populate each sub-field
       
   320 
       
   321 * two 'value' functions `get_mindate` and `get_maxdate`, used to tell to
       
   322   subfields which value they should consider on form initialization
       
   323 
       
   324 * overriding of `process_posted`, called when the form is being posted, so that
       
   325   the end of the period is properly set to the last day of the month.
       
   326 
       
   327 Now, we can define a very simple form:
       
   328 
       
   329 .. sourcecode:: python
       
   330 
       
   331     class MonthPeriodSelectorForm(forms.FieldsForm):
       
   332         __regid__ = 'myform'
       
   333         __select__ = match_kwargs('mindate', 'maxdate')
       
   334 
       
   335         form_buttons = [wdgs.SubmitButton()]
       
   336         form_renderer_id = 'onerowtable'
       
   337         period = MonthPeriodField()
       
   338 
       
   339 
       
   340 where we simply add our field, set a submit button and use a very simple renderer
       
   341 (try others!). Also we specify a selector that ensures form will have arguments
       
   342 necessary to our field.
       
   343 
       
   344 Now, we need a view that will wrap the form and handle post when it occurs,
       
   345 simply displaying posted values in the page:
       
   346 
       
   347 .. sourcecode:: python
       
   348 
       
   349     class SelfPostingForm(View):
       
   350         __regid__ = 'myformview'
       
   351 
       
   352         def call(self):
       
   353             mindate, maxdate = date.date(2010, 1, 1), date.date(2012, 1, 1)
       
   354             form = self._cw.vreg['forms'].select(
       
   355                 'myform', self._cw, mindate=mindate, maxdate=maxdate, action='')
       
   356             try:
       
   357                 posted = form.process_posted()
       
   358                 self.w(u'<p>posted values %s</p>' % xml_escape(repr(posted)))
       
   359             except RequestError: # no specified period asked
       
   360                 pass
       
   361             form.render(w=self.w, formvalues=self._cw.form)
       
   362 
       
   363 
       
   364 Notice usage of the :meth:`process_posted` method, that will return a dictionary
       
   365 of typed values (because they have been processed by the field). In our case, when
       
   366 the form is posted you should see a dictionary with 'begin_month' and 'end_month'
       
   367 as keys with the selected dates as value (as a python `date` object).
       
   368 
       
   369 
   217 APIs
   370 APIs
   218 ~~~~
   371 ~~~~
   219 
   372 
   220 .. automodule:: cubicweb.web.formfields
   373 .. automodule:: cubicweb.web.formfields
   221 .. automodule:: cubicweb.web.formwidgets
   374 .. automodule:: cubicweb.web.formwidgets