examples.rst 9.64 KB
Newer Older
1
2
3
Examples
--------

4
5
6
7
8
9
10
11
12
(Automatic) Entity form
~~~~~~~~~~~~~~~~~~~~~~~

Looking at some cubes available on the `cubicweb forge`_ we find some
with form manipulation. The following example comes from the the
`conference`_ cube. It extends the change state form for the case
where a ``Talk`` entity is getting into ``submitted`` state. The goal
is to select reviewers for the submitted talk.

13
14
.. _`cubicweb forge`:  https://forge.extranet.logilab.fr/cubicweb/cubicweb
.. _`conference`: https://forge.extranet.logilab.fr/cubicweb/cubes/conference
15
16
17
18
19
20

.. sourcecode:: python

 from cubicweb.web import formfields as ff, formwidgets as fwdgs
 class SendToReviewerStatusChangeView(ChangeStateFormView):
     __select__ = (ChangeStateFormView.__select__ &
21
                   is_instance('Talk') &
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
                   rql_condition('X in_state S, S name "submitted"'))

     def get_form(self, entity, transition, **kwargs):
         form = super(SendToReviewerStatusChangeView, self).get_form(entity, transition, **kwargs)
         relation = ff.RelationField(name='reviews', role='object',
                                     eidparam=True,
                                     label=_('select reviewers'),
                                     widget=fwdgs.Select(multiple=True))
         form.append_field(relation)
         return form

Simple extension of a form can be done from within the `FormView`
wrapping the form. FormView instances have a handy ``get_form`` method
that returns the form to be rendered. Here we add a ``RelationField``
to the base state change form.

One notable point is the ``eidparam`` argument: it tells both the
field and the ``edit controller`` that the field is linked to a
specific entity.

It is hence entirely possible to add ad-hoc fields that will be
processed by some specialized instance of the edit controller.


Ad-hoc fields form
~~~~~~~~~~~~~~~~~~
48
49
50
51
52
53
54
55
56
57

We want to define a form doing something else than editing an entity. The idea is
to propose a form to send an email to entities in a resultset which implements
:class:`IEmailable`.  Let's take a simplified version of what you'll find in
:mod:`cubicweb.web.views.massmailing`.

Here is the source code:

.. sourcecode:: python

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
69
70
                if e.get_email()]

    class MassMailingForm(forms.FieldsForm):
71
        __regid__ = 'massmailing'
72

73
74
75
        needs_js = ('cubicweb.widgets.js',)
        domid = 'sendmail'
        action = 'sendmail'
76

77
78
79
        sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
                                label=_('From:'),
                                value=sender_value)
80

81
82
83
84
        recipient = ff.StringField(widget=CheckBox(),
                                   label=_('Recipients:'),
                                   choices=recipient_choices,
                                   value=recipients_value)
85

86
        subject = ff.StringField(label=_('Subject:'), max_length=256)
87

88
89
        mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
                                                    inputid='mailbody'))
90

91
92
93
94
        form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
                                  _('send email'), 'SEND_EMAIL_ICON'),
                        ImgButton('cancelbutton', "javascript: history.back()",
                                  stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')]
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

Let's detail what's going on up there. Our form will hold four fields:

* a sender field, which is disabled and will simply contains the user's name and
  email

* a recipients field, which will be displayed as a list of users in the context
  result set with checkboxes so user can still choose who will receive his mailing
  by checking or not the checkboxes. By default all of them will be checked since
  field's value return a list containing same eids as those returned by the
  vocabulary function.

* a subject field, limited to 256 characters (hence we know a
  :class:`~cubicweb.web.formwidgets.TextInput` will be used, as explained in
  :class:`~cubicweb.web.formfields.StringField`)

* a mailbody field. This field use an ajax widget, defined in `cubicweb.widgets.js`,
  and whose definition won't be shown here. Notice though that we tell this form
  need this javascript file by using `needs_js`

Last but not least, we add two buttons control: one to post the form using
javascript (`$('#sendmail')` being the jQuery call to get the element with DOM id
set to 'sendmail', which is our form DOM id as specified by its `domid`
attribute), another to cancel the form which will go back to the previous page
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
122
123
124
125
126
127
:class:`cubicweb.web.formwidgets.ImgButton`.

To see this form, we still have to wrap it in a view. This is pretty simple:

.. sourcecode:: python

    class MassMailingFormView(form.FormViewMixIn, EntityView):
128
129
        __regid__ = 'massmailing'
        __select__ = is_instance(IEmailable) & authenticated_user()
130

131
132
133
134
        def call(self):
            form = self._cw.vreg['forms'].select('massmailing', self._cw,
                                                 rset=self.cw_rset)
            form.render(w=self.w)
135
136
137
138

As you see, we simply define a view with proper selector so it only apply to a
result set containing :class:`IEmailable` entities, and so that only users in the
managers or users group can use it. Then in the `call()` method for this view we
139
140
simply select the above form and call its `.render()` method with our output
stream as argument.
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167

When this form is submitted, a controller with id 'sendmail' will be called (as
specified using `action`). This controller will be responsible to actually send
the mail to specified recipients.

Here is what it looks like:

.. sourcecode:: python

   class SendMailController(Controller):
       __regid__ = 'sendmail'
       __select__ = (authenticated_user() &
                     match_form_params('recipient', 'mailbody', 'subject'))

       def publish(self, rset=None):
           body = self._cw.form['mailbody']
           subject = self._cw.form['subject']
           eids = self._cw.form['recipient']
           # eids may be a string if only one recipient was specified
           if isinstance(eids, basestring):
               rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids})
           else:
               rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids)))
           recipients = list(rset.entities())
           msg = format_mail({'email' : self._cw.user.get_email(),
                              'name' : self._cw.user.dc_title()},
                             recipients, body, subject)
168
           if not self._cw.vreg.config.sendmails([(msg, recipients)]):
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
               msg = self._cw._('could not connect to the SMTP server')
           else:
               msg = self._cw._('emails successfully sent')
           raise Redirect(self._cw.build_url(__message=msg))


The entry point of a controller is the publish method. In that case we simply get
back post values in request's `form` attribute, get user instances according
to eids found in the 'recipient' form value, and send email after calling
:func:`format_mail` to get a proper email message. If we can't send email or
if we successfully sent email, we redirect to the index page with proper message
to inform the user.

Also notice that our controller has a selector that deny access to it
to anonymous users (we don't want our instance to be used as a spam
relay), but also checks if the expected parameters are specified in
forms. That avoids later defensive programming (though it's not enough
to handle all possible error cases).

To conclude our example, suppose we wish a different form layout and that existent
renderers are not satisfying (we would check that first of course :). We would then
have to define our own renderer:

.. sourcecode:: python

    class MassMailingFormRenderer(formrenderers.FormRenderer):
        __regid__ = 'massmailing'

        def _render_fields(self, fields, w, form):
            w(u'<table class="headersform">')
            for field in fields:
                if field.name == 'mailbody':
                    w(u'</table>')
                    w(u'<div id="toolbar">')
                    w(u'<ul>')
                    for button in form.form_buttons:
                        w(u'<li>%s</li>' % button.render(form))
                    w(u'</ul>')
                    w(u'</div>')
                    w(u'<div>')
                    w(field.render(form, self))
                    w(u'</div>')
                else:
                    w(u'<tr>')
                    w(u'<td class="hlabel">%s</td>' %
                      self.render_label(form, field))
                    w(u'<td class="hvalue">')
                    w(field.render(form, self))
                    w(u'</td></tr>')

        def render_buttons(self, w, form):
            pass

We simply override the `_render_fields` and `render_buttons` method of the base form renderer
to arrange fields as we desire it: here we'll have first a two columns table with label and
value of the sender, recipients and subject field (form order respected), then form controls,
then a div containing the textarea for the email's content.

To bind this renderer to our form, we should add to our form definition above:

.. sourcecode:: python

    form_renderer_id = 'massmailing'