doc/book/en/devweb/edition/form.rst
changeset 10491 c67bcee93248
parent 10490 76ab3c71aff2
child 10492 68c13e0c0fc5
--- a/doc/book/en/devweb/edition/form.rst	Mon Jul 06 17:39:35 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,377 +0,0 @@
-.. _webform:
-
-HTML form construction
-----------------------
-
-CubicWeb provides the somewhat usual form / field / widget / renderer abstraction
-to provide generic building blocks which will greatly help you in building forms
-properly integrated with CubicWeb (coherent display, error handling, etc...),
-while keeping things as flexible as possible.
-
-A ``form`` basically only holds a set of ``fields``, and has te be bound to a
-``renderer`` which is responsible to layout them. Each field is bound to a
-``widget`` that will be used to fill in value(s) for that field (at form
-generation time) and 'decode' (fetch and give a proper Python type to) values
-sent back by the browser.
-
-The ``field`` should be used according to the type of what you want to edit.
-E.g. if you want to edit some date, you'll have to use the
-:class:`cubicweb.web.formfields.DateField`. Then you can choose among multiple
-widgets to edit it, for instance :class:`cubicweb.web.formwidgets.TextInput` (a
-bare text field), :class:`~cubicweb.web.formwidgets.DateTimePicker` (a simple
-calendar) or even :class:`~cubicweb.web.formwidgets.JQueryDatePicker` (the JQuery
-calendar).  You can of course also write your own widget.
-
-Exploring the available forms
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-A small excursion into a |cubicweb| shell is the quickest way to
-discover available forms (or application objects in general).
-
-.. sourcecode:: python
-
- >>> 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 'cubes.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'>]}
-
-
-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
-~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. automodule:: cubicweb.web.views.autoform
-
-Anatomy of a choices function
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Let's have a look at the `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`_
-cube.
-
-.. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker
-
-The ``Ticket`` entity type can be related to a ``Project`` and a
-``Version``, respectively through the ``concerns`` and ``done_in``
-relations. When a user is about to edit a ticket, we want to fill the
-combo box for the ``done_in`` relation with values pertinent with
-respect to the context. The important context here is:
-
-* creation or modification (we cannot fetch values the same way in
-  either case)
-
-* ``__linkto`` url parameter given in a creation context
-
-.. sourcecode:: python
-
-    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
-
-The first thing we have to do is fetch potential values from the ``__linkto`` url
-parameter that is often found in entity creation contexts (the creation action
-provides such a parameter with a predetermined value; for instance in this case,
-ticket creation could occur in the context of a `Version` entity). The
-:class:`~cubicweb.web.formfields.RelationField` field class provides a
-:meth:`~cubicweb.web.formfields.RelationField.relvoc_linkedto` method that gets a
-list suitably filled with vocabulary values.
-
-.. sourcecode:: python
-
-        linkedto = field.relvoc_linkedto(form)
-        if linkedto:
-            return linkedto
-
-Then, if no ``__linkto`` argument was given, we must prepare the vocabulary with
-an initial empty value (because `done_in` is not mandatory, we must allow the
-user to not select a verson) and already linked values. This is done with the
-:meth:`~cubicweb.web.formfields.RelationField.relvoc_init` method.
-
-.. sourcecode:: python
-
-        vocab = field.relvoc_init(form)
-
-But then, we have to give more: if the ticket is related to a project,
-we should provide all the non published versions of this project
-(`Version` and `Project` can be related through the `version_of`
-relation). Conversely, if we do not know yet the project, it would not
-make sense to propose all existing versions as it could potentially
-lead to incoherences. Even if these will be caught by some
-RQLConstraint, it is wise not to tempt the user with error-inducing
-candidate values.
-
-The "ticket is related to a project" part must be decomposed as:
-
-* this is a new ticket which is created is the context of a project
-
-* this is an already existing ticket, linked to a project (through the
-  `concerns` relation)
-
-* there is no related project (quite unlikely given the cardinality of
-  the `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
-  ``__linkto`` argument)
-
-.. note::
-
-   the last situation could happen in several ways, but of course in a
-   polished application, the paths to ticket creation should be
-   controlled so as to avoid a suboptimal end-user experience
-
-Hence, we try to fetch the related project.
-
-.. sourcecode:: python
-
-        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
-
-We distinguish between entity creation and entity modification using
-the ``Entity.has_eid()`` method, which returns `False` on creation. At
-creation time the only way to get a project is through the
-``__linkto`` parameter. Notice that we fetch the version in which the
-ticket is `done_in` if any, for later.
-
-.. note::
-
-  the implementation above assumes that if there is a ``__linkto``
-  parameter, it is only about a project. While it makes sense most of
-  the time, it is not an absolute. Depending on how an entity creation
-  action action url is built, several outcomes could be possible
-  there
-
-If the ticket is already linked to a project, fetching it is
-trivial. Then we add the relevant version to the initial vocabulary.
-
-.. sourcecode:: python
-
-        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]
-
-.. warning::
-
-   we have to defend ourselves against lack of a project eid. Given
-   the cardinality of the `concerns` relation, there *must* be a
-   project, but this rule can only be enforced at validation time,
-   which will happen of course only after form subsmission
-
-Here, given a project eid, we complete the vocabulary with all
-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.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
-
-
-Here we first define a widget that will be used to select the beginning and the
-end of the period, displaying months like '<month> 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'<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)
-
-
-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
-~~~~
-
-.. automodule:: cubicweb.web.formfields
-.. automodule:: cubicweb.web.formwidgets
-.. automodule:: cubicweb.web.views.forms
-.. automodule:: cubicweb.web.views.formrenderers
-
-