|
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 |