doc/book/devweb/edition/examples.rst
changeset 10491 c67bcee93248
parent 9423 76c0c96557f8
child 12879 7347715bf0ee
equal deleted inserted replaced
10490:76ab3c71aff2 10491:c67bcee93248
       
     1 Examples
       
     2 --------
       
     3 
       
     4 (Automatic) Entity form
       
     5 ~~~~~~~~~~~~~~~~~~~~~~~
       
     6 
       
     7 Looking at some cubes available on the `cubicweb forge`_ we find some
       
     8 with form manipulation. The following example comes from the the
       
     9 `conference`_ cube. It extends the change state form for the case
       
    10 where a ``Talk`` entity is getting into ``submitted`` state. The goal
       
    11 is to select reviewers for the submitted talk.
       
    12 
       
    13 .. _`cubicweb forge`: http://www.cubicweb.org/view?rql=Any+P+ORDERBY+N+WHERE+P+name+LIKE+%22cubicweb-%25%22%2C+P+is+Project%2C+P+name+N
       
    14 .. _`conference`: http://www.cubicweb.org/project/cubicweb-conference
       
    15 
       
    16 .. sourcecode:: python
       
    17 
       
    18  from cubicweb.web import formfields as ff, formwidgets as fwdgs
       
    19  class SendToReviewerStatusChangeView(ChangeStateFormView):
       
    20      __select__ = (ChangeStateFormView.__select__ &
       
    21                    is_instance('Talk') &
       
    22                    rql_condition('X in_state S, S name "submitted"'))
       
    23 
       
    24      def get_form(self, entity, transition, **kwargs):
       
    25          form = super(SendToReviewerStatusChangeView, self).get_form(entity, transition, **kwargs)
       
    26          relation = ff.RelationField(name='reviews', role='object',
       
    27                                      eidparam=True,
       
    28                                      label=_('select reviewers'),
       
    29                                      widget=fwdgs.Select(multiple=True))
       
    30          form.append_field(relation)
       
    31          return form
       
    32 
       
    33 Simple extension of a form can be done from within the `FormView`
       
    34 wrapping the form. FormView instances have a handy ``get_form`` method
       
    35 that returns the form to be rendered. Here we add a ``RelationField``
       
    36 to the base state change form.
       
    37 
       
    38 One notable point is the ``eidparam`` argument: it tells both the
       
    39 field and the ``edit controller`` that the field is linked to a
       
    40 specific entity.
       
    41 
       
    42 It is hence entirely possible to add ad-hoc fields that will be
       
    43 processed by some specialized instance of the edit controller.
       
    44 
       
    45 
       
    46 Ad-hoc fields form
       
    47 ~~~~~~~~~~~~~~~~~~
       
    48 
       
    49 We want to define a form doing something else than editing an entity. The idea is
       
    50 to propose a form to send an email to entities in a resultset which implements
       
    51 :class:`IEmailable`.  Let's take a simplified version of what you'll find in
       
    52 :mod:`cubicweb.web.views.massmailing`.
       
    53 
       
    54 Here is the source code:
       
    55 
       
    56 .. sourcecode:: python
       
    57 
       
    58     def sender_value(form, field):
       
    59 	return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email())
       
    60 
       
    61     def recipient_choices(form, field):
       
    62 	return [(e.get_email(), e.eid)
       
    63                  for e in form.cw_rset.entities()
       
    64 		 if e.get_email()]
       
    65 
       
    66     def recipient_value(form, field):
       
    67 	return [e.eid for e in form.cw_rset.entities()
       
    68                 if e.get_email()]
       
    69 
       
    70     class MassMailingForm(forms.FieldsForm):
       
    71 	__regid__ = 'massmailing'
       
    72 
       
    73 	needs_js = ('cubicweb.widgets.js',)
       
    74 	domid = 'sendmail'
       
    75 	action = 'sendmail'
       
    76 
       
    77 	sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
       
    78 				label=_('From:'),
       
    79 				value=sender_value)
       
    80 
       
    81 	recipient = ff.StringField(widget=CheckBox(),
       
    82 	                           label=_('Recipients:'),
       
    83 				   choices=recipient_choices,
       
    84 				   value=recipients_value)
       
    85 
       
    86 	subject = ff.StringField(label=_('Subject:'), max_length=256)
       
    87 
       
    88 	mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
       
    89 						    inputid='mailbody'))
       
    90 
       
    91 	form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
       
    92 				  _('send email'), 'SEND_EMAIL_ICON'),
       
    93 			ImgButton('cancelbutton', "javascript: history.back()",
       
    94 				  stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')]
       
    95 
       
    96 Let's detail what's going on up there. Our form will hold four fields:
       
    97 
       
    98 * a sender field, which is disabled and will simply contains the user's name and
       
    99   email
       
   100 
       
   101 * a recipients field, which will be displayed as a list of users in the context
       
   102   result set with checkboxes so user can still choose who will receive his mailing
       
   103   by checking or not the checkboxes. By default all of them will be checked since
       
   104   field's value return a list containing same eids as those returned by the
       
   105   vocabulary function.
       
   106 
       
   107 * a subject field, limited to 256 characters (hence we know a
       
   108   :class:`~cubicweb.web.formwidgets.TextInput` will be used, as explained in
       
   109   :class:`~cubicweb.web.formfields.StringField`)
       
   110 
       
   111 * a mailbody field. This field use an ajax widget, defined in `cubicweb.widgets.js`,
       
   112   and whose definition won't be shown here. Notice though that we tell this form
       
   113   need this javascript file by using `needs_js`
       
   114 
       
   115 Last but not least, we add two buttons control: one to post the form using
       
   116 javascript (`$('#sendmail')` being the jQuery call to get the element with DOM id
       
   117 set to 'sendmail', which is our form DOM id as specified by its `domid`
       
   118 attribute), another to cancel the form which will go back to the previous page
       
   119 using another javascript call. Also we specify an image to use as button icon as a
       
   120 resource identifier (see :ref:`uiprops`) given as last argument to
       
   121 :class:`cubicweb.web.formwidgets.ImgButton`.
       
   122 
       
   123 To see this form, we still have to wrap it in a view. This is pretty simple:
       
   124 
       
   125 .. sourcecode:: python
       
   126 
       
   127     class MassMailingFormView(form.FormViewMixIn, EntityView):
       
   128 	__regid__ = 'massmailing'
       
   129 	__select__ = is_instance(IEmailable) & authenticated_user()
       
   130 
       
   131 	def call(self):
       
   132 	    form = self._cw.vreg['forms'].select('massmailing', self._cw,
       
   133 	                                         rset=self.cw_rset)
       
   134 	    form.render(w=self.w)
       
   135 
       
   136 As you see, we simply define a view with proper selector so it only apply to a
       
   137 result set containing :class:`IEmailable` entities, and so that only users in the
       
   138 managers or users group can use it. Then in the `call()` method for this view we
       
   139 simply select the above form and call its `.render()` method with our output
       
   140 stream as argument.
       
   141 
       
   142 When this form is submitted, a controller with id 'sendmail' will be called (as
       
   143 specified using `action`). This controller will be responsible to actually send
       
   144 the mail to specified recipients.
       
   145 
       
   146 Here is what it looks like:
       
   147 
       
   148 .. sourcecode:: python
       
   149 
       
   150    class SendMailController(Controller):
       
   151        __regid__ = 'sendmail'
       
   152        __select__ = (authenticated_user() &
       
   153                      match_form_params('recipient', 'mailbody', 'subject'))
       
   154 
       
   155        def publish(self, rset=None):
       
   156            body = self._cw.form['mailbody']
       
   157            subject = self._cw.form['subject']
       
   158            eids = self._cw.form['recipient']
       
   159            # eids may be a string if only one recipient was specified
       
   160            if isinstance(eids, basestring):
       
   161                rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
       
   162            else:
       
   163                rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
       
   164            recipients = list(rset.entities())
       
   165            msg = format_mail({'email' : self._cw.user.get_email(),
       
   166                               'name' : self._cw.user.dc_title()},
       
   167                              recipients, body, subject)
       
   168            if not self._cw.vreg.config.sendmails([(msg, recipients)]):
       
   169                msg = self._cw._('could not connect to the SMTP server')
       
   170            else:
       
   171                msg = self._cw._('emails successfully sent')
       
   172            raise Redirect(self._cw.build_url(__message=msg))
       
   173 
       
   174 
       
   175 The entry point of a controller is the publish method. In that case we simply get
       
   176 back post values in request's `form` attribute, get user instances according
       
   177 to eids found in the 'recipient' form value, and send email after calling
       
   178 :func:`format_mail` to get a proper email message. If we can't send email or
       
   179 if we successfully sent email, we redirect to the index page with proper message
       
   180 to inform the user.
       
   181 
       
   182 Also notice that our controller has a selector that deny access to it
       
   183 to anonymous users (we don't want our instance to be used as a spam
       
   184 relay), but also checks if the expected parameters are specified in
       
   185 forms. That avoids later defensive programming (though it's not enough
       
   186 to handle all possible error cases).
       
   187 
       
   188 To conclude our example, suppose we wish a different form layout and that existent
       
   189 renderers are not satisfying (we would check that first of course :). We would then
       
   190 have to define our own renderer:
       
   191 
       
   192 .. sourcecode:: python
       
   193 
       
   194     class MassMailingFormRenderer(formrenderers.FormRenderer):
       
   195         __regid__ = 'massmailing'
       
   196 
       
   197         def _render_fields(self, fields, w, form):
       
   198             w(u'<table class="headersform">')
       
   199             for field in fields:
       
   200                 if field.name == 'mailbody':
       
   201                     w(u'</table>')
       
   202                     w(u'<div id="toolbar">')
       
   203                     w(u'<ul>')
       
   204                     for button in form.form_buttons:
       
   205                         w(u'<li>%s</li>' % button.render(form))
       
   206                     w(u'</ul>')
       
   207                     w(u'</div>')
       
   208                     w(u'<div>')
       
   209                     w(field.render(form, self))
       
   210                     w(u'</div>')
       
   211                 else:
       
   212                     w(u'<tr>')
       
   213                     w(u'<td class="hlabel">%s</td>' %
       
   214                       self.render_label(form, field))
       
   215                     w(u'<td class="hvalue">')
       
   216                     w(field.render(form, self))
       
   217                     w(u'</td></tr>')
       
   218 
       
   219         def render_buttons(self, w, form):
       
   220             pass
       
   221 
       
   222 We simply override the `_render_fields` and `render_buttons` method of the base form renderer
       
   223 to arrange fields as we desire it: here we'll have first a two columns table with label and
       
   224 value of the sender, recipients and subject field (form order respected), then form controls,
       
   225 then a div containing the textarea for the email's content.
       
   226 
       
   227 To bind this renderer to our form, we should add to our form definition above:
       
   228 
       
   229 .. sourcecode:: python
       
   230 
       
   231     form_renderer_id = 'massmailing'
       
   232