doc/book/en/development/devweb/form.rst
branchstable
changeset 5394 105011657405
parent 5393 875bdc0fe8ce
child 5395 e0ab7433e640
equal deleted inserted replaced
5393:875bdc0fe8ce 5394:105011657405
     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.formfields
       
    25 .. automodule:: cubicweb.web.formwidgets
       
    26 .. automodule:: cubicweb.web.views.forms
       
    27 .. automodule:: cubicweb.web.views.autoform
       
    28 .. automodule:: cubicweb.web.views.formrenderers
       
    29 
       
    30 
       
    31 Now what ? Example of bare fields form
       
    32 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
    33 
       
    34 We want to define a form doing something else than editing an entity. The idea is
       
    35 to propose a form to send an email to entities in a resultset which implements
       
    36 :class:`IEmailable`.  Let's take a simplified version of what you'll find in
       
    37 :mod:`cubicweb.web.views.massmailing`.
       
    38 
       
    39 Here is the source code:
       
    40 
       
    41 .. sourcecode:: python
       
    42 
       
    43     def sender_value(form):
       
    44 	return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email())
       
    45 
       
    46     def recipient_choices(form, field):
       
    47 	return [(e.get_email(), e.eid) for e in form.cw_rset.entities()
       
    48 		 if e.get_email()]
       
    49 
       
    50     def recipient_value(form):
       
    51 	return [e.eid for e in form.cw_rset.entities() if e.get_email()]
       
    52 
       
    53     class MassMailingForm(forms.FieldsForm):
       
    54 	__regid__ = 'massmailing'
       
    55 
       
    56 	needs_js = ('cubicweb.widgets.js',)
       
    57 	domid = 'sendmail'
       
    58 	action = 'sendmail'
       
    59 
       
    60 	sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
       
    61 				label=_('From:'),
       
    62 				value=sender_value)
       
    63 
       
    64 	recipient = ff.StringField(widget=CheckBox(),
       
    65 	                           label=_('Recipients:'),
       
    66 				   choices=recipient_choices,
       
    67 				   value=recipients_value)
       
    68 
       
    69 	subject = ff.StringField(label=_('Subject:'), max_length=256)
       
    70 
       
    71 	mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
       
    72 						    inputid='mailbody'))
       
    73 
       
    74 	form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
       
    75 				  _('send email'), 'SEND_EMAIL_ICON'),
       
    76 			ImgButton('cancelbutton', "javascript: history.back()",
       
    77 				  stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')]
       
    78 
       
    79 Let's detail what's going on up there. Our form will hold four fields:
       
    80 
       
    81 * a sender field, which is disabled and will simply contains the user's name and
       
    82   email
       
    83 
       
    84 * a recipients field, which will be displayed as a list of users in the context
       
    85   result set with checkboxes so user can still choose who will receive his mailing
       
    86   by checking or not the checkboxes. By default all of them will be checked since
       
    87   field's value return a list containing same eids as those returned by the
       
    88   vocabulary function.
       
    89 
       
    90 * a subject field, limited to 256 characters (hence we know a
       
    91   :class:`~cubicweb.web.formwidgets.TextInput` will be used, as explained in
       
    92   :class:`~cubicweb.web.formfields.StringField`)
       
    93 
       
    94 * a mailbody field. This field use an ajax widget, defined in `cubicweb.widgets.js`,
       
    95   and whose definition won't be shown here. Notice though that we tell this form
       
    96   need this javascript file by using `needs_js`
       
    97 
       
    98 Last but not least, we add two buttons control: one to post the form using
       
    99 javascript (`$('#sendmail')` being the jQuery call to get the element with DOM id
       
   100 set to 'sendmail', which is our form DOM id as specified by its `domid`
       
   101 attribute), another to cancel the form which will go back to the previous page
       
   102 using another javascript call. Also we specify an image to use as button icon as a
       
   103 resource identifier (see :ref:`external_resources`) given as last argument to
       
   104 :class:`cubicweb.web.formwidgets.ImgButton`.
       
   105 
       
   106 To see this form, we still have to wrap it in a view. This is pretty simple:
       
   107 
       
   108 .. sourcecode:: python
       
   109 
       
   110     class MassMailingFormView(form.FormViewMixIn, EntityView):
       
   111 	__regid__ = 'massmailing'
       
   112 	__select__ = implements(IEmailable) & authenticated_user()
       
   113 
       
   114 	def call(self):
       
   115 	    form = self._cw.vreg['forms'].select('massmailing', self._cw,
       
   116 	                                         rset=self.cw_rset)
       
   117 	    self.w(form.render())
       
   118 
       
   119 As you see, we simply define a view with proper selector so it only apply to a
       
   120 result set containing :class:`IEmailable` entities, and so that only users in the
       
   121 managers or users group can use it. Then in the `call()` method for this view we
       
   122 simply select the above form and write what its `.render()` method returns.
       
   123 
       
   124 When this form is submitted, a controller with id 'sendmail' will be called (as
       
   125 specified using `action`). This controller will be responsible to actually send
       
   126 the mail to specified recipients.
       
   127 
       
   128 Here is what it looks like:
       
   129 
       
   130 .. sourcecode:: python
       
   131 
       
   132     class SendMailController(Controller):
       
   133         __regid__ = 'sendmail'
       
   134         __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject')
       
   135 
       
   136         def publish(self, rset=None):
       
   137             body = self._cw.form['mailbody']
       
   138             subject = self._cw.form['subject']
       
   139             eids = self._cw.form['recipient']
       
   140             # eids may be a string if only one recipient was specified
       
   141             if isinstance(eids, basestring):
       
   142                 rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
       
   143             else:
       
   144                 rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
       
   145             recipients = list(rset.entities())
       
   146             msg = format_mail({'email' : self._cw.user.get_email(),
       
   147                                'name' : self._cw.user.dc_title()},
       
   148                               recipients, body, subject)
       
   149             if not self._cw.vreg.config.sendmails([(msg, recipients]):
       
   150                 msg = self._cw._('could not connect to the SMTP server')
       
   151             else:
       
   152                 msg = self._cw._('emails successfully sent')
       
   153             raise Redirect(self._cw.build_url(__message=msg))
       
   154 
       
   155 
       
   156 The entry point of a controller is the publish method. In that case we simply get
       
   157 back post values in request's `form` attribute, get user instances according
       
   158 to eids found in the 'recipient' form value, and send email after calling
       
   159 :func:`format_mail` to get a proper email message. If we can't send email or
       
   160 if we successfully sent email, we redirect to the index page with proper message
       
   161 to inform the user.
       
   162 
       
   163 Also notice that our controller has a selector that deny access to it to
       
   164 anonymous users (we don't want our instance to be used as a spam relay), but also
       
   165 check expected parameters are specified in forms. That avoids later defensive
       
   166 programming (though it's not enough 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 
       
   213 .. Example of entity fields form