doc/book/en/devweb/edition/form.rst
branchstable
changeset 5459 6e561796804c
parent 5418 4f0047cfecb5
equal deleted inserted replaced
5458:a5b96b98242c 5459:6e561796804c
       
     1 HTML form construction
       
     2 ----------------------
       
     3 
       
     4 CubicWeb provides the somewhat usual form / field / widget / renderer abstraction
       
     5 to provide generic building blocks which will greatly help you in building forms
       
     6 properly integrated with CubicWeb (coherent display, error handling, etc...),
       
     7 while keeping things as flexible as possible.
       
     8 
       
     9 A ``form`` basically only holds a set of ``fields``, and has te be bound to a
       
    10 ``renderer`` which is responsible to layout them. Each field is bound to a
       
    11 ``widget`` that will be used to fill in value(s) for that field (at form
       
    12 generation time) and 'decode' (fetch and give a proper Python type to) values
       
    13 sent back by the browser.
       
    14 
       
    15 The ``field`` should be used according to the type of what you want to edit.
       
    16 E.g. if you want to edit some date, you'll have to use the
       
    17 :class:`cubicweb.web.formfields.DateField`. Then you can choose among multiple
       
    18 widgets to edit it, for instance :class:`cubicweb.web.formwidgets.TextInput` (a
       
    19 bare text field), :class:`~cubicweb.web.formwidgets.DateTimePicker` (a simple
       
    20 calendar) or even :class:`~cubicweb.web.formwidgets.JQueryDatePicker` (the JQuery
       
    21 calendar).  You can of course also write your own widget.
       
    22 
       
    23 
       
    24 .. automodule:: cubicweb.web.views.autoform
       
    25 
       
    26 
       
    27 Example of ad-hoc fields form
       
    28 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
    29 
       
    30 We want to define a form doing something else than editing an entity. The idea is
       
    31 to propose a form to send an email to entities in a resultset which implements
       
    32 :class:`IEmailable`.  Let's take a simplified version of what you'll find in
       
    33 :mod:`cubicweb.web.views.massmailing`.
       
    34 
       
    35 Here is the source code:
       
    36 
       
    37 .. sourcecode:: python
       
    38 
       
    39     def sender_value(form):
       
    40 	return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email())
       
    41 
       
    42     def recipient_choices(form, field):
       
    43 	return [(e.get_email(), e.eid)
       
    44                  for e in form.cw_rset.entities()
       
    45 		 if e.get_email()]
       
    46 
       
    47     def recipient_value(form):
       
    48 	return [e.eid for e in form.cw_rset.entities()
       
    49                 if e.get_email()]
       
    50 
       
    51     class MassMailingForm(forms.FieldsForm):
       
    52 	__regid__ = 'massmailing'
       
    53 
       
    54 	needs_js = ('cubicweb.widgets.js',)
       
    55 	domid = 'sendmail'
       
    56 	action = 'sendmail'
       
    57 
       
    58 	sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
       
    59 				label=_('From:'),
       
    60 				value=sender_value)
       
    61 
       
    62 	recipient = ff.StringField(widget=CheckBox(),
       
    63 	                           label=_('Recipients:'),
       
    64 				   choices=recipient_choices,
       
    65 				   value=recipients_value)
       
    66 
       
    67 	subject = ff.StringField(label=_('Subject:'), max_length=256)
       
    68 
       
    69 	mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
       
    70 						    inputid='mailbody'))
       
    71 
       
    72 	form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
       
    73 				  _('send email'), 'SEND_EMAIL_ICON'),
       
    74 			ImgButton('cancelbutton', "javascript: history.back()",
       
    75 				  stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')]
       
    76 
       
    77 Let's detail what's going on up there. Our form will hold four fields:
       
    78 
       
    79 * a sender field, which is disabled and will simply contains the user's name and
       
    80   email
       
    81 
       
    82 * a recipients field, which will be displayed as a list of users in the context
       
    83   result set with checkboxes so user can still choose who will receive his mailing
       
    84   by checking or not the checkboxes. By default all of them will be checked since
       
    85   field's value return a list containing same eids as those returned by the
       
    86   vocabulary function.
       
    87 
       
    88 * a subject field, limited to 256 characters (hence we know a
       
    89   :class:`~cubicweb.web.formwidgets.TextInput` will be used, as explained in
       
    90   :class:`~cubicweb.web.formfields.StringField`)
       
    91 
       
    92 * a mailbody field. This field use an ajax widget, defined in `cubicweb.widgets.js`,
       
    93   and whose definition won't be shown here. Notice though that we tell this form
       
    94   need this javascript file by using `needs_js`
       
    95 
       
    96 Last but not least, we add two buttons control: one to post the form using
       
    97 javascript (`$('#sendmail')` being the jQuery call to get the element with DOM id
       
    98 set to 'sendmail', which is our form DOM id as specified by its `domid`
       
    99 attribute), another to cancel the form which will go back to the previous page
       
   100 using another javascript call. Also we specify an image to use as button icon as a
       
   101 resource identifier (see :ref:`external_resources`) given as last argument to
       
   102 :class:`cubicweb.web.formwidgets.ImgButton`.
       
   103 
       
   104 To see this form, we still have to wrap it in a view. This is pretty simple:
       
   105 
       
   106 .. sourcecode:: python
       
   107 
       
   108     class MassMailingFormView(form.FormViewMixIn, EntityView):
       
   109 	__regid__ = 'massmailing'
       
   110 	__select__ = implements(IEmailable) & authenticated_user()
       
   111 
       
   112 	def call(self):
       
   113 	    form = self._cw.vreg['forms'].select('massmailing', self._cw,
       
   114 	                                         rset=self.cw_rset)
       
   115 	    self.w(form.render())
       
   116 
       
   117 As you see, we simply define a view with proper selector so it only apply to a
       
   118 result set containing :class:`IEmailable` entities, and so that only users in the
       
   119 managers or users group can use it. Then in the `call()` method for this view we
       
   120 simply select the above form and write what its `.render()` method returns.
       
   121 
       
   122 When this form is submitted, a controller with id 'sendmail' will be called (as
       
   123 specified using `action`). This controller will be responsible to actually send
       
   124 the mail to specified recipients.
       
   125 
       
   126 Here is what it looks like:
       
   127 
       
   128 .. sourcecode:: python
       
   129 
       
   130    class SendMailController(Controller):
       
   131        __regid__ = 'sendmail'
       
   132        __select__ = (authenticated_user() &
       
   133                      match_form_params('recipient', 'mailbody', 'subject'))
       
   134 
       
   135        def publish(self, rset=None):
       
   136            body = self._cw.form['mailbody']
       
   137            subject = self._cw.form['subject']
       
   138            eids = self._cw.form['recipient']
       
   139            # eids may be a string if only one recipient was specified
       
   140            if isinstance(eids, basestring):
       
   141                rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
       
   142            else:
       
   143                rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
       
   144            recipients = list(rset.entities())
       
   145            msg = format_mail({'email' : self._cw.user.get_email(),
       
   146                               'name' : self._cw.user.dc_title()},
       
   147                              recipients, body, subject)
       
   148            if not self._cw.vreg.config.sendmails([(msg, recipients]):
       
   149                msg = self._cw._('could not connect to the SMTP server')
       
   150            else:
       
   151                msg = self._cw._('emails successfully sent')
       
   152            raise Redirect(self._cw.build_url(__message=msg))
       
   153 
       
   154 
       
   155 The entry point of a controller is the publish method. In that case we simply get
       
   156 back post values in request's `form` attribute, get user instances according
       
   157 to eids found in the 'recipient' form value, and send email after calling
       
   158 :func:`format_mail` to get a proper email message. If we can't send email or
       
   159 if we successfully sent email, we redirect to the index page with proper message
       
   160 to inform the user.
       
   161 
       
   162 Also notice that our controller has a selector that deny access to it
       
   163 to anonymous users (we don't want our instance to be used as a spam
       
   164 relay), but also checks if the expected parameters are specified in
       
   165 forms. That avoids later defensive programming (though it's not enough
       
   166 to handle all possible error cases).
       
   167 
       
   168 To conclude our example, suppose we wish a different form layout and that existent
       
   169 renderers are not satisfying (we would check that first of course :). We would then
       
   170 have to define our own renderer:
       
   171 
       
   172 .. sourcecode:: python
       
   173 
       
   174     class MassMailingFormRenderer(formrenderers.FormRenderer):
       
   175         __regid__ = 'massmailing'
       
   176 
       
   177         def _render_fields(self, fields, w, form):
       
   178             w(u'<table class="headersform">')
       
   179             for field in fields:
       
   180                 if field.name == 'mailbody':
       
   181                     w(u'</table>')
       
   182                     w(u'<div id="toolbar">')
       
   183                     w(u'<ul>')
       
   184                     for button in form.form_buttons:
       
   185                         w(u'<li>%s</li>' % button.render(form))
       
   186                     w(u'</ul>')
       
   187                     w(u'</div>')
       
   188                     w(u'<div>')
       
   189                     w(field.render(form, self))
       
   190                     w(u'</div>')
       
   191                 else:
       
   192                     w(u'<tr>')
       
   193                     w(u'<td class="hlabel">%s</td>' % self.render_label(form, field))
       
   194                     w(u'<td class="hvalue">')
       
   195                     w(field.render(form, self))
       
   196                     w(u'</td></tr>')
       
   197 
       
   198         def render_buttons(self, w, form):
       
   199             pass
       
   200 
       
   201 We simply override the `_render_fields` and `render_buttons` method of the base form renderer
       
   202 to arrange fields as we desire it: here we'll have first a two columns table with label and
       
   203 value of the sender, recipients and subject field (form order respected), then form controls,
       
   204 then a div containing the textarea for the email's content.
       
   205 
       
   206 To bind this renderer to our form, we should add to our form definition above:
       
   207 
       
   208 .. sourcecode:: python
       
   209 
       
   210     form_renderer_id = 'massmailing'
       
   211 
       
   212 API
       
   213 ~~~
       
   214 
       
   215 .. automodule:: cubicweb.web.formfields
       
   216 .. automodule:: cubicweb.web.formwidgets
       
   217 .. automodule:: cubicweb.web.views.forms
       
   218 .. automodule:: cubicweb.web.views.formrenderers
       
   219 
       
   220 .. Example of entity fields form