doc/book/en/development/devweb/form.rst
branchstable
changeset 5368 d321e4b62a10
parent 5350 49c065ae225e
child 5388 9167751463d4
equal deleted inserted replaced
5367:4176a50c81c9 5368:d321e4b62a10
     1 Form construction
     1 HTML form construction
     2 ------------------
     2 ----------------------
     3 
     3 
     4 CubicWeb provides usual form/field/widget/renderer abstraction to
     4 CubicWeb provides the somewhat usual form / field / widget / renderer abstraction
     5 provide some generic building blocks which will greatly help you in
     5 to provide generic building blocks which will greatly help you in building forms
     6 building forms properly integrated with CubicWeb (coherent display,
     6 properly integrated with CubicWeb (coherent display, error handling, etc...),
     7 error handling, etc...).
     7 while keeping things as flexible as possible.
     8 
     8 
     9 A form basically only holds a set of fields, and has te be bound to a
     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
    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
    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)
    12 generation time) and 'decode' (fetch and give a proper Python type to) values
    13 values sent back by the browser.
    13 sent back by the browser.
    14 
    14 
    15 The Field class and basic fields
    15 The **field** should be used according to the type of what you want to edit.
    16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    16 E.g. if you want to edit some date, you'll have to use the
    17 
    17 :class:`~cubicweb.web.formfields.DateField`. Then you can choose among multiple
    18 .. autoclass:: cubicweb.web.formfields.Field
    18 widgets to edit it, for instance :class:`~cubicweb.web.formwidgets.TextInput` (a
    19 
    19 bare text field), :class:`~cubicweb.web.formwidgets.DateTimePicker` (a simple
    20 Existing field types are:
    20 calendar) or even :class:`~cubicweb.web.formwidgets.JQueryDatePicker` (the JQuery
    21 
    21 calendar).  You can of course also write your own widget.
    22 .. autoclass:: cubicweb.web.formfields.StringField
    22 
    23 .. autoclass:: cubicweb.web.formfields.PasswordField
    23 
    24 .. autoclass:: cubicweb.web.formfields.RichTextField
    24 .. automodule:: cubicweb.web.formfields
    25 .. autoclass:: cubicweb.web.formfields.FileField
    25 .. automodule:: cubicweb.web.formwidgets
    26 .. autoclass:: cubicweb.web.formfields.EditableFileField
    26 .. automodule:: cubicweb.web.views.forms
    27 .. autoclass:: cubicweb.web.formfields.IntField
    27 .. automodule:: cubicweb.web.views.autoform
    28 .. autoclass:: cubicweb.web.formfields.BooleanField
    28 .. automodule:: cubicweb.web.views.formrenderers
    29 .. autoclass:: cubicweb.web.formfields.FloatField
    29 
    30 .. autoclass:: cubicweb.web.formfields.DateField
    30 
    31 .. autoclass:: cubicweb.web.formfields.DateTimeField
    31 Now what ? Example of bare fields form
    32 .. autoclass:: cubicweb.web.formfields.TimeField
    32 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    33 .. autoclass:: cubicweb.web.formfields.RelationField
    33 
    34 .. autoclass:: cubicweb.web.formfields.CompoundField
    34 We want to define a form doing something else than editing an entity. The idea is
    35 
    35 to propose a form to send an email to entities in a resultset which implements
    36 
    36 :class:`IEmailable`.  Let's take a simplified version of what you'll find in
    37 Widgets
    37 :mod:`cubicweb.web.views.massmailing`.
    38 ~~~~~~~
    38 
    39 Base class for widget is :class:cubicweb.web.formwidgets.FieldWidget class.
    39 Here is the source code:
    40 
    40 
    41 Existing widget types are:
    41 .. sourcecode:: python
    42 
    42 
    43 .. autoclass:: cubicweb.web.formwidgets.HiddenInput
    43     def sender_value(form):
    44 .. autoclass:: cubicweb.web.formwidgets.TextInput
    44 	return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email())
    45 .. autoclass:: cubicweb.web.formwidgets.PasswordInput
    45 
    46 .. autoclass:: cubicweb.web.formwidgets.PasswordSingleInput
    46     def recipient_choices(form, field):
    47 .. autoclass:: cubicweb.web.formwidgets.FileInput
    47 	return [(e.get_email(), e.eid) for e in form.cw_rset.entities()
    48 .. autoclass:: cubicweb.web.formwidgets.ButtonInput
    48 		 if e.get_email()]
    49 .. autoclass:: cubicweb.web.formwidgets.TextArea
    49 
    50 .. autoclass:: cubicweb.web.formwidgets.FCKEditor
    50     def recipient_value(form):
    51 .. autoclass:: cubicweb.web.formwidgets.Select
    51 	return [e.eid for e in form.cw_rset.entities() if e.get_email()]
    52 .. autoclass:: cubicweb.web.formwidgets.CheckBox
    52 
    53 .. autoclass:: cubicweb.web.formwidgets.Radio
    53     class MassMailingForm(forms.FieldsForm):
    54 .. autoclass:: cubicweb.web.formwidgets.DateTimePicker
    54 	__regid__ = 'massmailing'
    55 .. autoclass:: cubicweb.web.formwidgets.JQueryDateTimePicker
    55 
    56 .. autoclass:: cubicweb.web.formwidgets.JQueryDatePicker
    56 	needs_js = ('cubicweb.widgets.js',)
    57 .. autoclass:: cubicweb.web.formwidgets.JQueryTimePicker
    57 	domid = 'sendmail'
    58 .. autoclass:: cubicweb.web.formwidgets.AjaxWidget
    58 	action = 'sendmail'
    59 .. autoclass:: cubicweb.web.formwidgets.AutoCompletionWidget
    59 
    60 .. autoclass:: cubicweb.web.formwidgets.EditableURLWidget
    60 	sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
    61 
    61 				label=_('From:'),
    62 Other classes in this module, which are not proper widget (they are not associated to
    62 				value=sender_value)
    63 field) but are used as form controls, may also be useful: Button, SubmitButton,
    63 
    64 ResetButton, ImgButton,
    64 	recipient = ff.StringField(widget=CheckBox(),
    65 
    65 	                           label=_('Recipients:'),
    66 
    66 				   choices=recipient_choices,
    67 Of course you can not use any widget with any field...
    67 				   value=recipients_value)
    68 
    68 
    69 Renderers
    69 	subject = ff.StringField(label=_('Subject:'), max_length=256)
    70 ~~~~~~~~~
    70 
    71 
    71 	mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
    72 .. autoclass:: cubicweb.web.views.formrenderers.BaseFormRenderer
    72 						    inputid='mailbody'))
    73 .. autoclass:: cubicweb.web.views.formrenderers.HTableFormRenderer
    73 
    74 .. autoclass:: cubicweb.web.views.formrenderers.EntityCompositeFormRenderer
    74 	form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
    75 .. autoclass:: cubicweb.web.views.formrenderers.EntityFormRenderer
    75 				  _('send email'), 'SEND_EMAIL_ICON'),
    76 .. autoclass:: cubicweb.web.views.formrenderers.EntityInlinedFormRenderer
    76 			ImgButton('cancelbutton', "javascript: history.back()",
    77 
    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 image to used as button icon 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 avoid 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