|
1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
3 # |
|
4 # This file is part of CubicWeb. |
|
5 # |
|
6 # CubicWeb is free software: you can redistribute it and/or modify it under the |
|
7 # terms of the GNU Lesser General Public License as published by the Free |
|
8 # Software Foundation, either version 2.1 of the License, or (at your option) |
|
9 # any later version. |
|
10 # |
|
11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT |
|
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|
13 # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more |
|
14 # details. |
|
15 # |
|
16 # You should have received a copy of the GNU Lesser General Public License along |
|
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
|
18 """ |
|
19 Renderers |
|
20 --------- |
|
21 |
|
22 .. Note:: |
|
23 Form renderers are responsible to layout a form to HTML. |
|
24 |
|
25 Here are the base renderers available: |
|
26 |
|
27 .. autoclass:: cubicweb.web.views.formrenderers.FormRenderer |
|
28 .. autoclass:: cubicweb.web.views.formrenderers.HTableFormRenderer |
|
29 .. autoclass:: cubicweb.web.views.formrenderers.EntityCompositeFormRenderer |
|
30 .. autoclass:: cubicweb.web.views.formrenderers.EntityFormRenderer |
|
31 .. autoclass:: cubicweb.web.views.formrenderers.EntityInlinedFormRenderer |
|
32 |
|
33 """ |
|
34 |
|
35 __docformat__ = "restructuredtext en" |
|
36 from cubicweb import _ |
|
37 |
|
38 from warnings import warn |
|
39 |
|
40 from six import text_type |
|
41 |
|
42 from logilab.mtconverter import xml_escape |
|
43 from logilab.common.registry import yes |
|
44 |
|
45 from cubicweb import tags, uilib |
|
46 from cubicweb.appobject import AppObject |
|
47 from cubicweb.predicates import is_instance |
|
48 from cubicweb.utils import json_dumps, support_args |
|
49 from cubicweb.web import eid_param, formwidgets as fwdgs |
|
50 |
|
51 |
|
52 def checkbox(name, value, attrs='', checked=None): |
|
53 if checked is None: |
|
54 checked = value |
|
55 checked = checked and 'checked="checked"' or '' |
|
56 return u'<input type="checkbox" name="%s" value="%s" %s %s />' % ( |
|
57 name, value, checked, attrs) |
|
58 |
|
59 def field_label(form, field): |
|
60 if callable(field.label): |
|
61 return field.label(form, field) |
|
62 # XXX with 3.6 we can now properly rely on 'if field.role is not None' and |
|
63 # stop having a tuple for label |
|
64 if isinstance(field.label, tuple): # i.e. needs contextual translation |
|
65 return form._cw.pgettext(*field.label) |
|
66 return form._cw._(field.label) |
|
67 |
|
68 |
|
69 |
|
70 class FormRenderer(AppObject): |
|
71 """This is the 'default' renderer, displaying fields in a two columns table: |
|
72 |
|
73 +--------------+--------------+ |
|
74 | field1 label | field1 input | |
|
75 +--------------+--------------+ |
|
76 | field2 label | field2 input | |
|
77 +--------------+--------------+ |
|
78 |
|
79 +---------+ |
|
80 | buttons | |
|
81 +---------+ |
|
82 """ |
|
83 __registry__ = 'formrenderers' |
|
84 __regid__ = 'default' |
|
85 |
|
86 _options = ('display_label', 'display_help', |
|
87 'display_progress_div', 'table_class', 'button_bar_class', |
|
88 # add entity since it may be given to select the renderer |
|
89 'entity') |
|
90 display_label = True |
|
91 display_help = True |
|
92 display_progress_div = True |
|
93 table_class = u'attributeForm' |
|
94 button_bar_class = u'formButtonBar' |
|
95 |
|
96 def __init__(self, req=None, rset=None, row=None, col=None, **kwargs): |
|
97 super(FormRenderer, self).__init__(req, rset=rset, row=row, col=col) |
|
98 if self._set_options(kwargs): |
|
99 raise ValueError('unconsumed arguments %s' % kwargs) |
|
100 |
|
101 def _set_options(self, kwargs): |
|
102 for key in self._options: |
|
103 try: |
|
104 setattr(self, key, kwargs.pop(key)) |
|
105 except KeyError: |
|
106 continue |
|
107 return kwargs |
|
108 |
|
109 # renderer interface ###################################################### |
|
110 |
|
111 def render(self, w, form, values): |
|
112 self._set_options(values) |
|
113 form.add_media() |
|
114 data = [] |
|
115 _w = data.append |
|
116 _w(self.open_form(form, values)) |
|
117 self.render_content(_w, form, values) |
|
118 _w(self.close_form(form, values)) |
|
119 errormsg = self.error_message(form) |
|
120 if errormsg: |
|
121 data.insert(0, errormsg) |
|
122 # NOTE: we call unicode because `tag` objects may be found within data |
|
123 # e.g. from the cwtags library |
|
124 w(''.join(text_type(x) for x in data)) |
|
125 |
|
126 def render_content(self, w, form, values): |
|
127 if self.display_progress_div: |
|
128 w(u'<div id="progress">%s</div>' % self._cw._('validating...')) |
|
129 w(u'\n<fieldset>\n') |
|
130 self.render_fields(w, form, values) |
|
131 self.render_buttons(w, form) |
|
132 w(u'\n</fieldset>\n') |
|
133 |
|
134 def render_label(self, form, field): |
|
135 if field.label is None: |
|
136 return u'' |
|
137 label = field_label(form, field) |
|
138 attrs = {'for': field.dom_id(form)} |
|
139 if field.required: |
|
140 attrs['class'] = 'required' |
|
141 return tags.label(label, **attrs) |
|
142 |
|
143 def render_help(self, form, field): |
|
144 help = [] |
|
145 descr = field.help |
|
146 if callable(descr): |
|
147 descr = descr(form, field) |
|
148 if descr: |
|
149 help.append('<div class="helper">%s</div>' % self._cw._(descr)) |
|
150 example = field.example_format(self._cw) |
|
151 if example: |
|
152 help.append('<div class="helper">(%s: %s)</div>' |
|
153 % (self._cw._('sample format'), example)) |
|
154 return u' '.join(help) |
|
155 |
|
156 # specific methods (mostly to ease overriding) ############################# |
|
157 |
|
158 def error_message(self, form): |
|
159 """return formatted error message |
|
160 |
|
161 This method should be called once inlined field errors has been consumed |
|
162 """ |
|
163 req = self._cw |
|
164 errex = form.form_valerror |
|
165 # get extra errors |
|
166 if errex is not None: |
|
167 errormsg = req._('please correct the following errors:') |
|
168 errors = form.remaining_errors() |
|
169 if errors: |
|
170 if len(errors) > 1: |
|
171 templstr = u'<li>%s</li>\n' |
|
172 else: |
|
173 templstr = u' %s\n' |
|
174 for field, err in errors: |
|
175 if field is None: |
|
176 errormsg += templstr % err |
|
177 else: |
|
178 errormsg += templstr % '%s: %s' % (req._(field), err) |
|
179 if len(errors) > 1: |
|
180 errormsg = '<ul>%s</ul>' % errormsg |
|
181 return u'<div class="errorMessage">%s</div>' % errormsg |
|
182 return u'' |
|
183 |
|
184 def open_form(self, form, values, **attrs): |
|
185 if form.needs_multipart: |
|
186 enctype = u'multipart/form-data' |
|
187 else: |
|
188 enctype = u'application/x-www-form-urlencoded' |
|
189 attrs.setdefault('enctype', enctype) |
|
190 attrs.setdefault('method', 'post') |
|
191 attrs.setdefault('action', form.form_action() or '#') |
|
192 if form.domid: |
|
193 attrs.setdefault('id', form.domid) |
|
194 if form.onsubmit: |
|
195 attrs.setdefault('onsubmit', form.onsubmit) |
|
196 if form.cssstyle: |
|
197 attrs.setdefault('style', form.cssstyle) |
|
198 if form.cssclass: |
|
199 attrs.setdefault('class', form.cssclass) |
|
200 if form.cwtarget: |
|
201 attrs.setdefault('target', form.cwtarget) |
|
202 if not form.autocomplete: |
|
203 attrs.setdefault('autocomplete', 'off') |
|
204 return '<form %s>' % uilib.sgml_attributes(attrs) |
|
205 |
|
206 def close_form(self, form, values): |
|
207 """seems dumb but important for consistency w/ close form, and necessary |
|
208 for form renderers overriding open_form to use something else or more than |
|
209 and <form> |
|
210 """ |
|
211 out = u'</form>' |
|
212 if form.cwtarget: |
|
213 attrs = {'name': form.cwtarget, 'id': form.cwtarget, |
|
214 'width': '0px', 'height': '0px', |
|
215 'src': 'javascript: void(0);'} |
|
216 out = (u'<iframe %s></iframe>\n' % uilib.sgml_attributes(attrs)) + out |
|
217 return out |
|
218 |
|
219 def render_fields(self, w, form, values): |
|
220 fields = self._render_hidden_fields(w, form) |
|
221 if fields: |
|
222 self._render_fields(fields, w, form) |
|
223 self.render_child_forms(w, form, values) |
|
224 |
|
225 def render_child_forms(self, w, form, values): |
|
226 # render |
|
227 for childform in getattr(form, 'forms', []): |
|
228 self.render_fields(w, childform, values) |
|
229 |
|
230 def _render_hidden_fields(self, w, form): |
|
231 fields = form.fields[:] |
|
232 for field in form.fields: |
|
233 if not field.is_visible(): |
|
234 w(field.render(form, self)) |
|
235 w(u'\n') |
|
236 fields.remove(field) |
|
237 return fields |
|
238 |
|
239 def _render_fields(self, fields, w, form): |
|
240 byfieldset = {} |
|
241 for field in fields: |
|
242 byfieldset.setdefault(field.fieldset, []).append(field) |
|
243 if form.fieldsets_in_order: |
|
244 fieldsets = form.fieldsets_in_order |
|
245 else: |
|
246 fieldsets = byfieldset |
|
247 for fieldset in list(fieldsets): |
|
248 try: |
|
249 fields = byfieldset.pop(fieldset) |
|
250 except KeyError: |
|
251 self.warning('no such fieldset: %s (%s)', fieldset, form) |
|
252 continue |
|
253 w(u'<fieldset>\n') |
|
254 if fieldset: |
|
255 w(u'<legend>%s</legend>' % self._cw.__(fieldset)) |
|
256 w(u'<table class="%s">\n' % self.table_class) |
|
257 for field in fields: |
|
258 w(u'<tr class="%s_%s_row">\n' % (field.name, field.role)) |
|
259 if self.display_label and field.label is not None: |
|
260 w(u'<th class="labelCol">%s</th>\n' % self.render_label(form, field)) |
|
261 w(u'<td') |
|
262 if field.label is None: |
|
263 w(u' colspan="2"') |
|
264 error = form.field_error(field) |
|
265 if error: |
|
266 w(u' class="error"') |
|
267 w(u'>\n') |
|
268 w(field.render(form, self)) |
|
269 w(u'\n') |
|
270 if error: |
|
271 self.render_error(w, error) |
|
272 if self.display_help: |
|
273 w(self.render_help(form, field)) |
|
274 w(u'</td></tr>\n') |
|
275 w(u'</table></fieldset>\n') |
|
276 if byfieldset: |
|
277 self.warning('unused fieldsets: %s', ', '.join(byfieldset)) |
|
278 |
|
279 def render_buttons(self, w, form): |
|
280 if not form.form_buttons: |
|
281 return |
|
282 w(u'<table class="%s">\n<tr>\n' % self.button_bar_class) |
|
283 for button in form.form_buttons: |
|
284 w(u'<td>%s</td>\n' % button.render(form)) |
|
285 w(u'</tr></table>') |
|
286 |
|
287 def render_error(self, w, err): |
|
288 """return validation error for widget's field, if any""" |
|
289 w(u'<span class="errorMsg">%s</span>' % err) |
|
290 |
|
291 |
|
292 |
|
293 class BaseFormRenderer(FormRenderer): |
|
294 """use form_renderer_id = 'base' if you want base FormRenderer layout even |
|
295 when selected for an entity |
|
296 """ |
|
297 __regid__ = 'base' |
|
298 |
|
299 |
|
300 |
|
301 class HTableFormRenderer(FormRenderer): |
|
302 """The 'htable' form renderer display fields horizontally in a table: |
|
303 |
|
304 +--------------+--------------+---------+ |
|
305 | field1 label | field2 label | | |
|
306 +--------------+--------------+---------+ |
|
307 | field1 input | field2 input | buttons | |
|
308 +--------------+--------------+---------+ |
|
309 """ |
|
310 __regid__ = 'htable' |
|
311 |
|
312 display_help = False |
|
313 def _render_fields(self, fields, w, form): |
|
314 w(u'<table border="0" class="htableForm">') |
|
315 w(u'<tr>') |
|
316 for field in fields: |
|
317 if self.display_label: |
|
318 w(u'<th class="labelCol">%s</th>' % self.render_label(form, field)) |
|
319 if self.display_help: |
|
320 w(self.render_help(form, field)) |
|
321 # empty slot for buttons |
|
322 w(u'<th class="labelCol"> </th>') |
|
323 w(u'</tr>') |
|
324 w(u'<tr>') |
|
325 for field in fields: |
|
326 error = form.field_error(field) |
|
327 if error: |
|
328 w(u'<td class="error">') |
|
329 self.render_error(w, error) |
|
330 else: |
|
331 w(u'<td>') |
|
332 w(field.render(form, self)) |
|
333 w(u'</td>') |
|
334 w(u'<td>') |
|
335 for button in form.form_buttons: |
|
336 w(button.render(form)) |
|
337 w(u'</td>') |
|
338 w(u'</tr>') |
|
339 w(u'</table>') |
|
340 |
|
341 def render_buttons(self, w, form): |
|
342 pass |
|
343 |
|
344 |
|
345 class OneRowTableFormRenderer(FormRenderer): |
|
346 """The 'htable' form renderer display fields horizontally in a table: |
|
347 |
|
348 +--------------+--------------+--------------+--------------+---------+ |
|
349 | field1 label | field1 input | field2 label | field2 input | buttons | |
|
350 +--------------+--------------+--------------+--------------+---------+ |
|
351 """ |
|
352 __regid__ = 'onerowtable' |
|
353 |
|
354 display_help = False |
|
355 def _render_fields(self, fields, w, form): |
|
356 w(u'<table border="0" class="oneRowTableForm">') |
|
357 w(u'<tr>') |
|
358 for field in fields: |
|
359 if self.display_label: |
|
360 w(u'<th class="labelCol">%s</th>' % self.render_label(form, field)) |
|
361 if self.display_help: |
|
362 w(self.render_help(form, field)) |
|
363 error = form.field_error(field) |
|
364 if error: |
|
365 w(u'<td class="error">') |
|
366 self.render_error(w, error) |
|
367 else: |
|
368 w(u'<td>') |
|
369 w(field.render(form, self)) |
|
370 w(u'</td>') |
|
371 w(u'<td>') |
|
372 for button in form.form_buttons: |
|
373 w(button.render(form)) |
|
374 w(u'</td>') |
|
375 w(u'</tr>') |
|
376 w(u'</table>') |
|
377 |
|
378 def render_buttons(self, w, form): |
|
379 pass |
|
380 |
|
381 |
|
382 class EntityCompositeFormRenderer(FormRenderer): |
|
383 """This is a specific renderer for the multiple entities edition form |
|
384 ('muledit'). |
|
385 |
|
386 Each entity form will be displayed in row off a table, with a check box for |
|
387 each entities to indicate which ones are edited. Those checkboxes should be |
|
388 automatically updated when something is edited. |
|
389 """ |
|
390 __regid__ = 'composite' |
|
391 |
|
392 _main_display_fields = None |
|
393 |
|
394 def render_fields(self, w, form, values): |
|
395 if form.parent_form is None: |
|
396 w(u'<table class="listing">') |
|
397 # get fields from the first subform with something to display (we |
|
398 # may have subforms with nothing editable that will simply be |
|
399 # skipped later) |
|
400 for subform in form.forms: |
|
401 subfields = [field for field in subform.fields |
|
402 if field.is_visible()] |
|
403 if subfields: |
|
404 break |
|
405 if subfields: |
|
406 # main form, display table headers |
|
407 w(u'<tr class="header">') |
|
408 w(u'<th align="left">%s</th>' % |
|
409 tags.input(type='checkbox', |
|
410 title=self._cw._('toggle check boxes'), |
|
411 onclick="setCheckboxesState('eid', null, this.checked)")) |
|
412 for field in subfields: |
|
413 w(u'<th>%s</th>' % field_label(form, field)) |
|
414 w(u'</tr>') |
|
415 super(EntityCompositeFormRenderer, self).render_fields(w, form, values) |
|
416 if form.parent_form is None: |
|
417 w(u'</table>') |
|
418 if self._main_display_fields: |
|
419 super(EntityCompositeFormRenderer, self)._render_fields( |
|
420 self._main_display_fields, w, form) |
|
421 |
|
422 def _render_fields(self, fields, w, form): |
|
423 if form.parent_form is not None: |
|
424 entity = form.edited_entity |
|
425 values = form.form_previous_values |
|
426 qeid = eid_param('eid', entity.eid) |
|
427 cbsetstate = "setCheckboxesState('eid', %s, 'checked')" % \ |
|
428 xml_escape(json_dumps(entity.eid)) |
|
429 w(u'<tr class="%s">' % (entity.cw_row % 2 and u'even' or u'odd')) |
|
430 # XXX turn this into a widget used on the eid field |
|
431 w(u'<td>%s</td>' % checkbox('eid', entity.eid, |
|
432 checked=qeid in values)) |
|
433 for field in fields: |
|
434 error = form.field_error(field) |
|
435 if error: |
|
436 w(u'<td class="error">') |
|
437 self.render_error(w, error) |
|
438 else: |
|
439 w(u'<td>') |
|
440 if isinstance(field.widget, (fwdgs.Select, fwdgs.CheckBox, |
|
441 fwdgs.Radio)): |
|
442 field.widget.attrs['onchange'] = cbsetstate |
|
443 elif isinstance(field.widget, fwdgs.Input): |
|
444 field.widget.attrs['onkeypress'] = cbsetstate |
|
445 # XXX else |
|
446 w(u'<div>%s</div>' % field.render(form, self)) |
|
447 w(u'</td>\n') |
|
448 w(u'</tr>') |
|
449 else: |
|
450 self._main_display_fields = fields |
|
451 |
|
452 |
|
453 class EntityFormRenderer(BaseFormRenderer): |
|
454 """This is the 'default' renderer for entity's form. |
|
455 |
|
456 You can still use form_renderer_id = 'base' if you want base FormRenderer |
|
457 layout even when selected for an entity. |
|
458 """ |
|
459 __regid__ = 'default' |
|
460 # needs some additional points in some case (XXX explain cases) |
|
461 __select__ = is_instance('Any') & yes() |
|
462 |
|
463 _options = FormRenderer._options + ('main_form_title',) |
|
464 main_form_title = _('main informations') |
|
465 |
|
466 def open_form(self, form, values): |
|
467 attrs_fs_label = '' |
|
468 if self.main_form_title: |
|
469 attrs_fs_label += ('<div class="iformTitle"><span>%s</span></div>' |
|
470 % self._cw._(self.main_form_title)) |
|
471 attrs_fs_label += '<div class="formBody">' |
|
472 return attrs_fs_label + super(EntityFormRenderer, self).open_form(form, values) |
|
473 |
|
474 def close_form(self, form, values): |
|
475 """seems dumb but important for consistency w/ close form, and necessary |
|
476 for form renderers overriding open_form to use something else or more than |
|
477 and <form> |
|
478 """ |
|
479 return super(EntityFormRenderer, self).close_form(form, values) + '</div>' |
|
480 |
|
481 def render_buttons(self, w, form): |
|
482 if len(form.form_buttons) == 3: |
|
483 w("""<table width="100%%"> |
|
484 <tbody> |
|
485 <tr><td align="center"> |
|
486 %s |
|
487 </td><td style="align: right; width: 50%%;"> |
|
488 %s |
|
489 %s |
|
490 </td></tr> |
|
491 </tbody> |
|
492 </table>""" % tuple(button.render(form) for button in form.form_buttons)) |
|
493 else: |
|
494 super(EntityFormRenderer, self).render_buttons(w, form) |
|
495 |
|
496 |
|
497 class EntityInlinedFormRenderer(EntityFormRenderer): |
|
498 """This is a specific renderer for entity's form inlined into another |
|
499 entity's form. |
|
500 """ |
|
501 __regid__ = 'inline' |
|
502 fieldset_css_class = 'subentity' |
|
503 |
|
504 def render_title(self, w, form, values): |
|
505 w(u'<div class="iformTitle">') |
|
506 w(u'<span>%(title)s</span> ' |
|
507 '#<span class="icounter">%(counter)s</span> ' % values) |
|
508 if values['removejs']: |
|
509 values['removemsg'] = self._cw._('remove-inlined-entity-form') |
|
510 w(u'[<a href="javascript: %(removejs)s;$.noop();">%(removemsg)s</a>]' |
|
511 % values) |
|
512 w(u'</div>') |
|
513 |
|
514 def render(self, w, form, values): |
|
515 form.add_media() |
|
516 self.open_form(w, form, values) |
|
517 self.render_title(w, form, values) |
|
518 # XXX that stinks |
|
519 # cleanup values |
|
520 for key in ('title', 'removejs', 'removemsg'): |
|
521 values.pop(key, None) |
|
522 self.render_fields(w, form, values) |
|
523 self.close_form(w, form, values) |
|
524 |
|
525 def open_form(self, w, form, values): |
|
526 try: |
|
527 w(u'<div id="div-%(divid)s" onclick="%(divonclick)s">' % values) |
|
528 except KeyError: |
|
529 w(u'<div id="div-%(divid)s">' % values) |
|
530 else: |
|
531 w(u'<div id="notice-%s" class="notice">%s</div>' % ( |
|
532 values['divid'], self._cw._('click on the box to cancel the deletion'))) |
|
533 w(u'<div class="iformBody">') |
|
534 |
|
535 def close_form(self, w, form, values): |
|
536 w(u'</div></div>') |
|
537 |
|
538 def render_fields(self, w, form, values): |
|
539 w(u'<fieldset id="fs-%(divid)s">' % values) |
|
540 fields = self._render_hidden_fields(w, form) |
|
541 w(u'</fieldset>') |
|
542 w(u'<fieldset class="%s">' % self.fieldset_css_class) |
|
543 if fields: |
|
544 self._render_fields(fields, w, form) |
|
545 self.render_child_forms(w, form, values) |
|
546 w(u'</fieldset>') |