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